← 題庫 / Archive
2026-06-11 Daily Medium MathTreeDepth-First Search

3558. Number of Ways to Assign Edge Weights I

題目 / Problem

中文:有一棵無向樹,節點編號 1 到 n,以節點 1 為根。樹由二維陣列 edges 表示,edges[i] = [uᵢ, vᵢ] 代表節點 uᵢvᵢ 之間有一條邊。一開始每條邊的權重都是 0,你必須把每條邊的權重指定為 12。兩節點 uv 之間路徑的花費是該路徑上所有邊權重的總和。請選一個最大深度的節點 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 with n nodes and exactly n − 1 edges, 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 mod 10⁹ + 7 so numbers never overflow.
  • 快速冪 / Fast exponentiation:用平方的方式在 O(log e) 時間算出 base^e,比一個一個乘快得多。Computes base^e in O(log e) by repeated squaring instead of e multiplications.

思路

先看最關鍵的觀察:權重只能是 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. Here n is the number of nodes.
  • Space: O(n) — 鄰接表存所有邊共 O(n),加上 depthvisited、佇列各 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⁵,直接枚舉會超時。看穿「只關乎奇偶」才能化簡為一個公式。Enumerating 2^d assignments times out; the parity insight collapses it to one formula.
  • 取模時的溢位 / Overflow during modular arithmeticresult * base 可能超過 32 位元整數範圍,必須用 long(64 位元)相乘再取模。本文 power 全程用 longresult * base can exceed 32-bit range, so multiply using 64-bit long before taking % MOD.
  • 指數為 0 的情況 / Exponent could be zero:當 maxDepth = 1(例如 edges=[[1,2]])時算的是 2^0 = 1power 函式在 e = 0 時直接回傳 result = 1,正確。題目保證 n ≥ 2,所以 maxDepth ≥ 1,不會出現 2^(-1)。When maxDepth = 1, we compute 2^0 = 1; the loop returns 1 correctly. Since n ≥ 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 標記避免在無向圖上沿原路折返而重複計數。The visited array stops BFS from walking back to the parent and looping.