3559. Number of Ways to Assign Edge Weights II
題目 / Problem
中文:有一棵 n 個節點的無向樹,節點編號 1 到 n,以節點 1 為根。樹由 edges 陣列描述,edges[i] = [u, v] 表示節點 u 和 v 之間有一條邊。一開始每條邊權重都是 0,你必須把每條邊的權重指定為 1 或 2。兩個節點之間「路徑的成本」是這條路徑上所有邊權重的總和。給定 queries,對每個 queries[i] = [u, v],求有多少種「只對這條路徑上的邊」指定權重的方法,使得 u 到 v 路徑的成本是奇數。答案對 10^9 + 7 取模。
English: There is an undirected tree with n nodes (labeled 1..n), rooted at node 1, given by edges where edges[i] = [u, v] is an edge. Every edge must be assigned a weight of 1 or 2. The cost of a path is the sum of edge weights along it. For each query [u, v], count the number of ways to assign weights to the edges on that path so the path cost is odd. Return the answers modulo 10^9 + 7. Edges not on the path are ignored per query.
Constraints: 2 <= n <= 10^5, edges.length == n - 1, 1 <= queries.length <= 10^5, 1 <= u, v <= n, and edges is a valid tree.
Worked example: edges = [[1,2],[1,3],[3,4],[3,5]], queries = [[1,4],[3,4],[2,5]] → [2,1,4]. For [2,5] the path 2→1→3→5 has 3 edges; assignments (1,2,2),(2,1,2),(2,2,1),(1,1,1) give an odd total → 4 ways.
名詞解釋 / Glossary
- 樹 / Tree:一種無環的連通圖,
n個節點剛好有n-1條邊,任意兩點之間有唯一一條路徑。/ A connected acyclic graph;nnodes have exactlyn-1edges, and any two nodes have a unique path between them. - 根 / Root:被選為「最上層」的節點(這裡是節點 1),其他節點都掛在它下面。/ The designated top node (here node 1); every other node hangs below it.
- 深度 / Depth:某節點到根節點的邊數,根的深度為 0。/ The number of edges from a node up to the root; the root has depth 0.
- 路徑長度 / Path length:兩節點間路徑上的邊數,也就是它們的「距離」。/ The number of edges on the path between two nodes — their distance.
- 奇偶性 / Parity:一個數是奇數還是偶數。權重 1 是奇數、權重 2 是偶數,所以總和的奇偶性只取決於「選 1 的邊有幾條」。/ Whether a number is odd or even. Weight 1 is odd, weight 2 is even, so the total's parity depends only on how many edges got weight 1.
- 最近公共祖先 / Lowest Common Ancestor (LCA):兩個節點在樹上「最深的共同祖先」。距離公式:
dist(u,v) = depth[u] + depth[v] - 2*depth[lca]。/ The deepest node that is an ancestor of both nodes; distance isdepth[u] + depth[v] - 2*depth[lca]. - 倍增 / Binary lifting:預先存下「每個節點往上跳 2^k 步到哪」,讓我們能在
O(log n)內找到 LCA。/ Precomputing, for each node, its 2^k-th ancestor, so LCA can be found inO(log n). - 快速冪 / 預先算次方 / Precomputed powers:把
2^0, 2^1, ...先算好存進陣列,查表 O(1) 取用。/ Storing2^0, 2^1, ...in an array so each lookup is O(1). - 取模 / Modulo:
10^9+7是個質數,每次乘法後取餘數可避免溢位且保持答案正確。/ Taking the remainder by the prime10^9+7after each multiply keeps numbers small and correct.
思路
先觀察關鍵的數學事實。每條邊只能是 1(奇)或 2(偶)。路徑成本的奇偶只看「有幾條邊被指定為 1」:選了奇數條 1,總和才是奇數。設路徑有 k 條邊,每條邊獨立選 1 或 2,總共有 2^k 種指定方法,其中「恰好奇數條為 1」的方法數正好是一半,也就是 2^(k-1)(這是組合恆等式:長度 k 的 01 字串中,1 的個數為奇數的有 2^(k-1) 個)。特例:當 k = 0(u == v,路徑沒有邊)時,成本恆為 0(偶數),所以答案是 0。因此每個查詢的答案只取決於路徑邊數 k:k == 0 答 0,否則答 2^(k-1) mod p。問題就簡化成「快速求樹上兩點距離」。暴力地對每個查詢沿著父節點一步步走會是 O(n),最壞 O(n·q)=10^10 太慢。我們改用倍增法求最近公共祖先:先一次 BFS 算出每個節點的深度與直接父節點,再建好「往上跳 2^k 步」的表,之後每個查詢用 dist = depth[u]+depth[v]-2*depth[lca] 在 O(log n) 算出邊數,再查預先算好的 2 的次方表得到答案。
Start from the math. Each edge is either 1 (odd) or 2 (even), so the parity of the path cost depends only on how many edges are set to 1 — the cost is odd exactly when an odd number of edges get weight 1. If the path has k edges, there are 2^k total assignments, and exactly half of them have an odd count of 1's, namely 2^(k-1) (a standard identity: among length-k binary strings, 2^(k-1) have an odd number of ones). The special case k = 0 (u == v, no edges) always has cost 0, which is even, so the answer is 0. So every query reduces to: find the number of edges k on the path, then output 0 if k == 0 else 2^(k-1) mod p. The only real work is computing tree distance fast. Walking parent-by-parent per query is O(n), giving O(n·q) up to 10^10 — too slow. Instead we use binary-lifting LCA: one BFS computes each node's depth and immediate parent, we build a table of 2^k-th ancestors, and then each query computes dist = depth[u]+depth[v]-2*depth[lca] in O(log n) and reads the precomputed power of two.
逐步走查 / Walkthrough
用第一個範例 / Using the first example: edges = [[1,2]], queries = [[1,1],[1,2]]. 樹只有節點 1(根)和節點 2,邊 1–2。
Setup(BFS 從根 1 開始 / BFS from root 1):
| node | depth | parent up[0] |
|---|---|---|
| 1 | 0 | 0 (哨兵/sentinel) |
| 2 | 1 | 1 |
預算次方 / Precompute powers: pw[0]=1, pw[1]=2, ...
Query [1,1]:
- u=1, v=1。深度相同 / same depth, lca = 1。
- dist = depth[1]+depth[1]-2*depth[1] = 0+0-0 = 0。
- dist == 0 → 答案 0 / answer 0.
Query [1,2]:
- u=1, v=2。depth[1]=0 < depth[2]=1,先交換成 a=2,b=1,把較深的 a 往上跳 diff=1 步:a = up[0][2] = 1。
- 現在 a == b == 1 → lca = 1。
- dist = depth[1]+depth[2]-2*depth[1] = 0+1-0 = 1。
- dist != 0 → 答案 pw[dist-1] = pw[0] = 1 / answer 1.
輸出 / Output: [0, 1] ✓ 符合預期 / matches expected.
Solution — C
#include <stdlib.h>
// 演算法 / Algorithm:
// 1) 觀察:路徑長度 k 條邊時,使成本為奇數的方法數 = 2^(k-1)(k=0 時為 0)。
// For a path with k edges, the number of odd-cost assignments is 2^(k-1) (0 if k==0).
// 2) 用 BFS 求深度與父節點,倍增建表求 LCA,得到兩點距離 k。
// BFS for depth/parent, binary-lifting LCA to get distance k per query.
#define MOD 1000000007 // 模數,質數 / the prime modulus
#define LOG 17 // 2^17 = 131072 > 1e5,夠跳到根 / enough jump levels for n up to 1e5
int* assignEdgeWeights(int** edges, int edgesSize, int* edgesColSize,
int** queries, int queriesSize, int* queriesColSize,
int* returnSize) {
int n = edgesSize + 1; // 節點數 = 邊數 + 1 / nodes = edges + 1
// ---- 建鄰接表(用「頭+next」的陣列鏈結,避免動態 vector)----
// Build adjacency list with head/next arrays (linked list inside arrays).
int *head = malloc((n + 1) * sizeof(int)); // head[u] = u 的第一條邊索引 / first edge index of u
int *nxt = malloc(2 * edgesSize * sizeof(int)); // 無向邊存兩次 / each undirected edge stored twice
int *to = malloc(2 * edgesSize * sizeof(int)); // to[e] = 邊 e 的另一端 / the other endpoint of edge e
for (int i = 1; i <= n; i++) head[i] = -1; // -1 代表「沒有邊」/ -1 means "no edge"
int cnt = 0; // 已加入的有向邊數 / number of directed edges added
for (int i = 0; i < edgesSize; i++) {
int u = edges[i][0], v = edges[i][1]; // 取出這條邊的兩端 / the two endpoints
to[cnt] = v; nxt[cnt] = head[u]; head[u] = cnt++; // 把 u->v 插到 u 的鏈表頭 / push u->v
to[cnt] = u; nxt[cnt] = head[v]; head[v] = cnt++; // 把 v->u 插到 v 的鏈表頭 / push v->u
}
// ---- BFS 從根 1 算 depth 和 up[0](直接父節點)----
// BFS from root 1 to fill depth and up[0] (immediate parent).
int *depth = malloc((n + 1) * sizeof(int)); // depth[v] = v 到根的邊數 / edges from v to root
int **up = malloc(LOG * sizeof(int*)); // up[k][v] = v 往上 2^k 步的祖先 / 2^k-th ancestor
for (int k = 0; k < LOG; k++) up[k] = malloc((n + 1) * sizeof(int));
int *queue = malloc((n + 1) * sizeof(int)); // BFS 用的佇列 / the BFS queue
char *vis = calloc(n + 1, 1); // 是否已訪問,calloc 全設 0 / visited flags, zeroed
int qh = 0, qt = 0; // 佇列頭尾指標 / queue head & tail
queue[qt++] = 1; vis[1] = 1; depth[1] = 0; up[0][1] = 0; // 根的父節點用 0 當哨兵 / root parent = 0 sentinel
while (qh < qt) { // 佇列非空就繼續 / while queue not empty
int u = queue[qh++]; // 取出隊首 / pop front
for (int e = head[u]; e != -1; e = nxt[e]) { // 走訪 u 的每條邊 / iterate u's edges
int w = to[e]; // 鄰居 / neighbor
if (!vis[w]) { // 第一次看到才處理 / only if unvisited
vis[w] = 1;
depth[w] = depth[u] + 1; // 子節點深度 = 父深度 + 1 / child depth
up[0][w] = u; // w 的直接父節點是 u / parent of w is u
queue[qt++] = w; // 入列 / enqueue
}
}
}
depth[0] = 0; // 哨兵節點 0 的深度 / sentinel depth
// ---- 用倍增填出整張祖先表 / Fill the full ancestor table ----
for (int k = 1; k < LOG; k++) {
up[k][0] = 0; // 哨兵往上還是哨兵 / sentinel stays sentinel
for (int v = 1; v <= n; v++)
// 往上 2^k 步 = 先跳 2^(k-1),再跳 2^(k-1) / a 2^k jump is two 2^(k-1) jumps
up[k][v] = up[k - 1][ up[k - 1][v] ];
}
// ---- 預先算 2 的各次方 / Precompute powers of two ----
long long *pw = malloc((n + 1) * sizeof(long long));
pw[0] = 1; // 2^0 = 1
for (int i = 1; i <= n; i++)
pw[i] = pw[i - 1] * 2 % MOD; // 每步取模避免溢位 / mod each step to avoid overflow
// ---- 逐一回答查詢 / Answer each query ----
int* ans = malloc(queriesSize * sizeof(int));
for (int i = 0; i < queriesSize; i++) {
int a = queries[i][0], b = queries[i][1]; // 這次查詢的兩點 / the two query nodes
int u = a, v = b; // 留著原始值算距離 / keep originals for distance
if (depth[a] < depth[b]) { int t = a; a = b; b = t; } // 讓 a 比較深 / make a the deeper one
int diff = depth[a] - depth[b]; // 深度差 / depth difference
for (int k = 0; k < LOG; k++) // 把 a 往上拉到和 b 同深度 / lift a up to b's depth
if (diff & (1 << k)) a = up[k][a]; // diff 的第 k 個位元是 1 就跳 2^k / jump 2^k if bit k set
int lca;
if (a == b) {
lca = a; // b 本來就是 a 的祖先 / b was already an ancestor of a
} else {
for (int k = LOG - 1; k >= 0; k--) // 兩點一起往上跳到「父節點剛好相同」/ lift both together
if (up[k][a] != up[k][b]) { a = up[k][a]; b = up[k][b]; }
lca = up[0][a]; // 再上一步就是 LCA / one more step is the LCA
}
int dist = depth[u] + depth[v] - 2 * depth[lca]; // 路徑邊數 / number of edges on the path
ans[i] = dist == 0 ? 0 : (int)pw[dist - 1]; // k=0 答 0,否則 2^(k-1) / 0 if no edges else 2^(k-1)
}
*returnSize = queriesSize; // 回傳陣列長度 / set output length
// 釋放暫時用的記憶體(ans 要留給呼叫者)/ free scratch memory (ans is returned to caller)
free(head); free(nxt); free(to); free(depth);
for (int k = 0; k < LOG; k++) free(up[k]);
free(up); free(queue); free(vis); free(pw);
return ans;
}
Solution — C++
#include <vector>
using namespace std;
// 演算法 / Algorithm:
// 路徑有 k 條邊時,使成本為奇數的方法數 = 2^(k-1)(k=0 時為 0)。
// With k edges on the path, odd-cost assignments = 2^(k-1) (0 when k==0).
// 用 BFS 求深度/父節點,倍增 LCA 求兩點距離 k。
// BFS for depth/parent, binary-lifting LCA for the distance k.
class Solution {
public:
static const int MOD = 1000000007; // 模數 / the modulus
static const int LOG = 17; // 跳躍層數,2^17 > 1e5 / lifting levels
vector<int> assignEdgeWeights(vector<vector<int>>& edges,
vector<vector<int>>& queries) {
int n = edges.size() + 1; // 節點數 / number of nodes
// 鄰接表:adj[u] 是 u 所有鄰居的清單 / adjacency list: adj[u] holds u's neighbors
vector<vector<int>> adj(n + 1);
for (auto& e : edges) { // range-for 走訪每條邊 / range-based for over edges
adj[e[0]].push_back(e[1]); // 無向邊兩個方向都加 / add both directions
adj[e[1]].push_back(e[0]);
}
vector<int> depth(n + 1, 0); // 每點深度 / depth of each node
// up 是二維表,up[k][v] = v 往上 2^k 步的祖先 / up[k][v] = 2^k-th ancestor of v
vector<vector<int>> up(LOG, vector<int>(n + 1, 0));
// BFS 從根 1 / BFS from root 1
vector<char> vis(n + 1, 0); // 訪問標記 / visited flags
vector<int> q; // 用 vector 當佇列 / vector used as a queue
q.push_back(1); vis[1] = 1; up[0][1] = 0; // 根的父節點是哨兵 0 / root's parent = sentinel 0
for (size_t i = 0; i < q.size(); i++) { // i 隨 q 變長持續推進 / i advances as q grows
int u = q[i]; // 取出目前節點 / current node
for (int w : adj[u]) { // 走訪鄰居 / iterate neighbors
if (!vis[w]) { // 沒訪問過才展開 / expand only if unvisited
vis[w] = 1;
depth[w] = depth[u] + 1; // 子節點深度 / child depth
up[0][w] = u; // 直接父節點 / immediate parent
q.push_back(w); // 入列 / enqueue
}
}
}
// 倍增填表 / build the lifting table
for (int k = 1; k < LOG; k++)
for (int v = 1; v <= n; v++)
// 2^k 步 = 兩次 2^(k-1) 步 / a 2^k jump = two 2^(k-1) jumps
up[k][v] = up[k - 1][up[k - 1][v]];
// 預先算 2 的次方 / precompute powers of two
vector<long long> pw(n + 1);
pw[0] = 1;
for (int i = 1; i <= n; i++) pw[i] = pw[i - 1] * 2 % MOD;
// 求 LCA 的 lambda / a lambda computing the LCA of two nodes
auto lca = [&](int a, int b) {
if (depth[a] < depth[b]) swap(a, b); // 讓 a 較深 / make a the deeper one
int diff = depth[a] - depth[b]; // 深度差 / depth gap
for (int k = 0; k < LOG; k++) // 把 a 拉到和 b 同深度 / lift a to b's depth
if (diff & (1 << k)) a = up[k][a]; // 第 k 位元為 1 就跳 2^k / jump 2^k if bit set
if (a == b) return a; // b 已是祖先 / b is already the ancestor
for (int k = LOG - 1; k >= 0; k--) // 兩點一起往上 / lift both together
if (up[k][a] != up[k][b]) { a = up[k][a]; b = up[k][b]; }
return up[0][a]; // 再一步即 LCA / one step up is the LCA
};
vector<int> ans;
ans.reserve(queries.size()); // 預留空間避免重複擴容 / reserve to avoid reallocs
for (auto& qy : queries) {
int u = qy[0], v = qy[1]; // 結構化取兩端 / the two query endpoints
int l = lca(u, v); // 最近公共祖先 / their LCA
int dist = depth[u] + depth[v] - 2 * depth[l]; // 路徑邊數 / edges on the path
ans.push_back(dist == 0 ? 0 : (int)pw[dist - 1]); // 0 或 2^(k-1) / 0 or 2^(k-1)
}
return ans;
}
};
複雜度 / Complexity
- Time: O((n + q) log n) — 建鄰接表與 BFS 是
O(n);倍增建表是O(n log n)(LOG層、每層掃n個節點);每個查詢做一次 LCA 是O(log n),共q個查詢。n是節點數、q是查詢數。主導項是建表與查詢的log n因子。/ Building adjacency and BFS areO(n); the lifting table isO(n log n)(LOGlevels overnnodes); each query's LCA isO(log n)overqqueries. Thelog nfactor dominates. - Space: O(n log n) — 主要是倍增表
up[LOG][n+1];鄰接表、深度、次方表都是O(n)。/ Dominated by theuptable of sizeLOG × (n+1); adjacency, depth, and power arrays are eachO(n).
Pitfalls & Edge Cases
u == v的查詢 / Same-node query:路徑沒有邊,dist == 0,成本恆為偶數,答案必須是 0 而不是2^(-1)。程式碼用dist == 0 ? 0 : ...明確處理。/ The path has no edges, so the answer must be 0; we special-casedist == 0to avoid computing2^(-1).- 遞迴 DFS 會爆堆疊 / Recursive DFS overflows the stack:
n可達10^5,一條鏈狀樹會讓遞迴深度過深而崩潰。這裡改用迭代式 BFS。/ A chain-shaped tree withn=10^5would blow a recursive DFS stack; we use iterative BFS instead. - 整數溢位 / Integer overflow:
2^(k-1)在k大時極大,必須每步% MOD,且乘法用long long暫存(int * int在取模前可能溢位)。/2^(k-1)is huge; take% MODeach step and uselong longfor the multiply, sinceint * intcan overflow before the mod. - LCA 跳躍順序 / Lifting order:先把較深節點拉到相同深度,再「由大到小」的
k一起往上跳;若由小到大會跳過頭。/ First equalize depths, then lift both nodes from the largestkdown — going small-to-large overshoots. - 哨兵節點 0 / Sentinel parent 0:根節點沒有父節點,用 0 當哨兵並令
up[k][0]=0,避免越界存取。/ The root has no parent; node 0 is a sentinel withup[k][0]=0so jumps past the root stay in bounds. - 節點從 1 編號 / 1-based labels:陣列要開到
n+1,別誤用 0 號位置當真實節點。/ Nodes are 1-indexed; size arrays ton+1and don't treat index 0 as a real node.