3558. Number of Ways to Assign Edge Weights I
題目 / Problem
中文:有一棵無向樹,節點編號 1 到 n,以節點 1 為根。樹由二維陣列 edges 表示,edges[i] = [uᵢ, vᵢ] 代表節點 uᵢ 和 vᵢ 之間有一條邊。一開始每條邊的權重都是 0,你必須把每條邊的權重指定為 1 或 2。兩節點 u、v 之間路徑的花費是該路徑上所有邊權重的總和。請選一個最大深度的節點 x,回傳「從節點 1 到 x 的路徑上,使總花費為奇數的指定方法數」,結果對 10⁹ + 7 取模。不在這條路徑上的邊一律忽略。
English: You have an undirected tree with n nodes labeled 1..n, rooted at node 1. The array edges lists the n − 1 edges. Every edge starts with weight 0; you must set each edge's weight to either 1 or 2. The cost of the path between two nodes is the sum of edge weights along it. Pick any node x at the maximum depth, and return the number of weight assignments on the path from node 1 to x whose total cost is odd, modulo 10⁹ + 7. Ignore all edges not on that path.
Constraints / 限制:
- 2 <= n <= 10⁵
- edges.length == n - 1
- 1 <= uᵢ, vᵢ <= n
- edges 是一棵合法的樹 / forms a valid tree.
Example / 範例:
edges = [[1,2],[1,3],[3,4],[3,5]] → 輸出 2。最大深度為 2(節點 4、5)。到節點 4 的路徑有 2 條邊,權重 (1,2) 或 (2,1) 的花費為奇數,共 2 種。
名詞解釋 / Glossary
- 樹 / Tree:一種特殊的圖,有
n個節點、恰好n − 1條邊,且任意兩節點之間只有唯一一條路徑、沒有環。A connected graph withnnodes and exactlyn − 1edges, no cycles, and a unique path between any two nodes. - 根 / Root:被指定為「頂端」的節點(這裡是節點 1),深度的計算從它開始。The node chosen as the top (node 1 here); depths are measured from it.
- 深度 / Depth:某節點到根所經過的邊數。根的深度為 0,根的子節點深度為 1,以此類推。The number of edges from a node up to the root; the root has depth 0.
- 鄰接表 / Adjacency list:用「每個節點對應一個鄰居清單」的方式儲存圖,比鄰接矩陣省空間,適合稀疏的樹。A way to store a graph as, for each node, the list of its neighbors — memory-efficient for sparse trees.
- BFS(廣度優先搜尋)/ Breadth-First Search:用佇列(queue)一層一層地擴展走訪節點,最後一層走到的就是最大深度。Explores the tree level by level using a queue; the last level reached gives the maximum depth.
- 取模 / Modulo (
%):取除法的餘數。因為答案可能很大,全程對10⁹ + 7取模避免溢位。The remainder after division; we keep everything mod10⁹ + 7so numbers never overflow. - 快速冪 / Fast exponentiation:用平方的方式在
O(log e)時間算出base^e,比一個一個乘快得多。Computesbase^einO(log e)by repeated squaring instead ofemultiplications.
思路
先看最關鍵的觀察:權重只能是 1 或 2,而我們只在乎總和的奇偶性。權重 2 是偶數,加上去不改變奇偶;只有權重 1(奇數)才會翻轉奇偶。所以一條路徑的花費是奇數,等價於「路徑上被指定為 1 的邊有奇數條」。每條邊都是 1 或 2 兩種選擇,互相獨立。假設路徑上有 d 條邊(d 就是 x 的深度),問題變成:在 d 條邊中,選出奇數條當作 1,有幾種選法。從組合數學看,這是 C(d,1)+C(d,3)+C(d,5)+… 。一個經典結論是:在 d 個元素裡選出奇數個的子集數量正好是 2^(d-1)(因為奇數子集和偶數子集各佔一半)。所以我們根本不需要列舉所有邊,只要算出最大深度 d,答案就是 2^(d-1) mod (10⁹+7)。題目說「任選一個最大深度的節點」,而所有最大深度節點的深度都一樣是 d,所以答案只跟 d 有關,跟選哪個節點無關。步驟:用 BFS(或 DFS)從根算出每個節點深度,取最大值 d,再用快速冪算 2^(d-1)。
The crucial observation is that weights are only 1 or 2, and we only care about the parity of the sum. Adding a 2 (even) never changes parity; only a 1 (odd) flips it. So a path's cost is odd exactly when an odd number of its edges are assigned weight 1. Each edge independently picks 1 or 2. If the path has d edges (where d is the depth of x), the question reduces to: in how many ways can we pick an odd-sized subset of the d edges to be the 1s? That count is C(d,1)+C(d,3)+… = 2^(d-1), a classic identity — half of all 2^d subsets have odd size and half have even size. So we never enumerate assignments; we just need the maximum depth d and return 2^(d-1) mod (10⁹+7). Since every node at maximum depth shares the same depth d, the answer depends only on d, not on which node we choose. The plan: run BFS/DFS from the root to get each node's depth, take the maximum d, then use fast exponentiation to compute 2^(d-1).
逐步走查 / Walkthrough
以範例 edges = [[1,2],[1,3],[3,4],[3,5]],n = 5,根為節點 1。我們用 BFS 從根逐層算深度。
Using edges = [[1,2],[1,3],[3,4],[3,5]], n = 5, root = node 1. BFS computes depth level by level.
鄰接表 / Adjacency: 1: [2,3], 2: [1], 3: [1,4,5], 4: [3], 5: [3]
| 步驟 / Step | 取出節點 / Dequeue | 其深度 / depth | 加入佇列的鄰居 / Enqueue (child=depth+1) | max_depth |
|---|---|---|---|---|
| 初始 / init | — | — | 1 (depth 0) | 0 |
| 1 | 1 | 0 | 2 (depth 1), 3 (depth 1) | 1 |
| 2 | 2 | 1 | (無新節點 / none) | 1 |
| 3 | 3 | 1 | 4 (depth 2), 5 (depth 2) | 2 |
| 4 | 4 | 2 | (無 / none) | 2 |
| 5 | 5 | 2 | (無 / none) | 2 |
最後 max_depth = 2。答案 = 2^(2-1) = 2^1 = 2。✅
Final max_depth = 2, so the answer is 2^(2-1) = 2. ✅
Solution — C
// 演算法 / Algorithm:
// 1) 用 BFS 從根節點 1 算出最大深度 d。
// BFS from root (node 1) to find the maximum depth d.
// 2) 答案是 2^(d-1) mod 1e9+7(路徑上選奇數條邊為權重 1 的方法數)。
// Answer is 2^(d-1) mod 1e9+7, the number of ways to pick an odd subset of d edges.
#define MOD 1000000007L // 取模常數 / the modulus 10^9 + 7
// 快速冪:在 O(log e) 時間算出 base^e mod MOD。
// Fast exponentiation: computes base^e mod MOD in O(log e).
long power(long base, long e) {
long result = 1; // 累積結果,從 1 開始 / running product, starts at 1
base %= MOD; // 先取模,避免 base 過大 / reduce base mod MOD first
while (e > 0) { // 逐個處理指數的二進位位元 / process each bit of the exponent
if (e & 1) // 若當前最低位是 1(e 為奇數)/ if the lowest bit is set
result = result * base % MOD; // 把這個 base 的次方乘進結果 / fold this power into result
base = base * base % MOD; // base 平方,對應下一個位元 / square base for the next bit
e >>= 1; // 指數右移一位 / shift exponent right by one bit
}
return result;
}
int assignEdgeWeights(int** edges, int edgesSize, int* edgesColSize) {
int n = edgesSize + 1; // 樹有 n 個節點,邊數 = n-1 / n nodes, since edges = n-1
// ---- 建立鄰接表 / Build adjacency list ----
// adjCount[u] 記錄節點 u 有幾個鄰居 / number of neighbors of node u
int* adjCount = (int*)calloc(n + 1, sizeof(int)); // calloc 會把記憶體清為 0 / calloc zeroes memory
for (int i = 0; i < edgesSize; i++) { // 先數每個節點的鄰居數 / first count neighbors per node
adjCount[edges[i][0]]++; // 邊的一端 +1 / one endpoint
adjCount[edges[i][1]]++; // 另一端 +1(無向邊雙向)/ the other endpoint (undirected)
}
// adj[u] 是一個動態陣列,存節點 u 的所有鄰居 / adj[u] holds u's neighbors
int** adj = (int**)malloc((n + 1) * sizeof(int*)); // 指標陣列 / array of pointers
int* idx = (int*)calloc(n + 1, sizeof(int)); // 每個 u 目前已填入幾個鄰居 / fill position per node
for (int u = 1; u <= n; u++)
adj[u] = (int*)malloc(adjCount[u] * sizeof(int)); // 為每個節點配置剛好夠用的空間 / exact-size buffer
for (int i = 0; i < edgesSize; i++) { // 第二趟:真正填入鄰居 / second pass: fill neighbors
int u = edges[i][0], v = edges[i][1];
adj[u][idx[u]++] = v; // 把 v 加到 u 的清單,idx[u] 後移 / append v to u, advance write index
adj[v][idx[v]++] = u; // 把 u 加到 v 的清單 / append u to v
}
// ---- BFS 求最大深度 / BFS to find max depth ----
int* depth = (int*)malloc((n + 1) * sizeof(int)); // 每個節點的深度 / depth of each node
char* visited = (char*)calloc(n + 1, sizeof(char)); // 標記是否已走訪,避免回頭 / visited flag
int* queue = (int*)malloc(n * sizeof(int)); // 用陣列當佇列 / array used as a queue
int head = 0, tail = 0; // head 指向待取出位置,tail 指向待寫入位置 / read/write cursors
queue[tail++] = 1; // 從根節點 1 開始 / start BFS at root node 1
visited[1] = 1; // 標記根已走訪 / mark root visited
depth[1] = 0; // 根的深度為 0 / root depth is 0
int maxDepth = 0; // 目前看到的最大深度 / max depth seen so far
while (head < tail) { // 佇列還有節點就繼續 / while queue is not empty
int u = queue[head++]; // 取出隊首節點 / dequeue front node
if (depth[u] > maxDepth) // 更新最大深度 / update the maximum depth
maxDepth = depth[u];
for (int j = 0; j < adjCount[u]; j++) { // 走訪 u 的每個鄰居 / iterate neighbors of u
int w = adj[u][j];
if (!visited[w]) { // 沒走訪過才處理(樹不會有環,但仍要防回到父節點)/ skip the parent
visited[w] = 1; // 標記已走訪 / mark visited
depth[w] = depth[u] + 1; // 子節點深度 = 父深度 + 1 / child depth = parent + 1
queue[tail++] = w; // 加入佇列 / enqueue child
}
}
}
// ---- 釋放記憶體 / Free memory ----
for (int u = 1; u <= n; u++) free(adj[u]); // 釋放每個鄰居清單 / free each neighbor list
free(adj); free(adjCount); free(idx);
free(depth); free(visited); free(queue);
// 答案 = 2^(maxDepth - 1) mod 1e9+7 / answer = 2^(maxDepth-1)
return (int)power(2, maxDepth - 1);
}
Solution — C++
// 演算法 / Algorithm:
// 1) 用 BFS 從根節點 1 求最大深度 d。 BFS from root to find max depth d.
// 2) 回傳 2^(d-1) mod 1e9+7。 Return 2^(d-1) mod 1e9+7.
class Solution {
static const long MOD = 1000000007L; // 取模常數 / the modulus 10^9 + 7
// 快速冪 / fast exponentiation: base^e mod MOD in O(log e)
long power(long base, long e) {
long result = 1; // 累積乘積 / running product
base %= MOD;
while (e > 0) { // 逐位處理指數 / process exponent bit by bit
if (e & 1) result = result * base % MOD; // 此位為 1 就乘入 / fold in when bit set
base = base * base % MOD; // base 平方 / square the base
e >>= 1; // 指數右移 / shift exponent
}
return result;
}
public:
int assignEdgeWeights(vector<vector<int>>& edges) {
int n = edges.size() + 1; // 節點數 = 邊數 + 1 / nodes = edges + 1
// 鄰接表:adj[u] 是 vector,存 u 的所有鄰居。
// Adjacency list: adj[u] is a vector holding u's neighbors.
vector<vector<int>> adj(n + 1); // 索引 0 不用,1..n / index 0 unused
for (auto& e : edges) { // range-for:依序取出每條邊 / range-for over edges
adj[e[0]].push_back(e[1]); // push_back:往 vector 尾端加元素 / append neighbor
adj[e[1]].push_back(e[0]); // 無向邊兩端都要加 / undirected: both directions
}
// BFS 求最大深度 / BFS to find max depth
vector<int> depth(n + 1, 0); // 每個節點的深度,初始 0 / depths, all 0
vector<bool> visited(n + 1, false);// 走訪標記 / visited flags
queue<int> q; // STL 佇列,先進先出 / FIFO queue
q.push(1); // 從根節點 1 出發 / start at root
visited[1] = true;
int maxDepth = 0;
while (!q.empty()) { // 佇列非空就繼續 / until queue drains
int u = q.front(); q.pop(); // front 取隊首、pop 移除 / read and remove front
maxDepth = max(maxDepth, depth[u]); // 更新最大深度 / track the deepest level
for (int w : adj[u]) { // 走訪鄰居 / visit each neighbor
if (!visited[w]) { // 跳過已走訪(即父節點)/ skip the parent
visited[w] = true;
depth[w] = depth[u] + 1; // 子深度 = 父深度 + 1 / child = parent + 1
q.push(w); // 入列 / enqueue
}
}
}
// 答案 / answer = 2^(maxDepth-1) mod 1e9+7
return (int)power(2, maxDepth - 1);
}
};
複雜度 / Complexity
- Time: O(n) — 建鄰接表掃過
n − 1條邊是 O(n);BFS 每個節點與每條邊各走訪一次,也是 O(n);快速冪是 O(log n),被 O(n) 蓋過。n指節點數。Building the adjacency list and the BFS each touch every node and edge once, and fast exponentiation is only O(log n), so the linear BFS dominates. Herenis the number of nodes. - Space: O(n) — 鄰接表存所有邊共 O(n),加上
depth、visited、佇列各 O(n)。The adjacency list stores all edges (O(n)), plus the depth array, visited array, and queue each take O(n).
Pitfalls & Edge Cases
- 誤以為要枚舉所有權重組合 / Trying to enumerate all weight assignments:路徑長
d時組合有2^d種,d可達 ~10⁵,直接枚舉會超時。看穿「只關乎奇偶」才能化簡為一個公式。Enumerating2^dassignments times out; the parity insight collapses it to one formula. - 取模時的溢位 / Overflow during modular arithmetic:
result * base可能超過 32 位元整數範圍,必須用long(64 位元)相乘再取模。本文power全程用long。result * basecan exceed 32-bit range, so multiply using 64-bitlongbefore taking% MOD. - 指數為 0 的情況 / Exponent could be zero:當
maxDepth = 1(例如edges=[[1,2]])時算的是2^0 = 1,power函式在e = 0時直接回傳result = 1,正確。題目保證n ≥ 2,所以maxDepth ≥ 1,不會出現2^(-1)。WhenmaxDepth = 1, we compute2^0 = 1; the loop returns 1 correctly. Sincen ≥ 2,maxDepth ≥ 1, so the exponent is never negative. - 把樹當有向圖、忘了雙向加邊 / Forgetting edges are undirected:必須在
adj[u]和adj[v]兩邊都加,否則 BFS 走不到某些節點。Add each edge in both directions, or BFS won't reach all nodes. - BFS 回到父節點 / Revisiting the parent in BFS:用
visited標記避免在無向圖上沿原路折返而重複計數。Thevisitedarray stops BFS from walking back to the parent and looping.