← 題庫 / Archive
2026-06-12 Daily Hard ArrayMathDynamic ProgrammingBit ManipulationTreeDepth-First Search

3559. Number of Ways to Assign Edge Weights II

題目 / Problem

中文:有一棵 n 個節點的無向樹,節點編號 1 到 n,以節點 1 為根。樹由 edges 陣列描述,edges[i] = [u, v] 表示節點 uv 之間有一條邊。一開始每條邊權重都是 0,你必須把每條邊的權重指定為 1 或 2。兩個節點之間「路徑的成本」是這條路徑上所有邊權重的總和。給定 queries,對每個 queries[i] = [u, v],求有多少種「只對這條路徑上的邊」指定權重的方法,使得 uv 路徑的成本是奇數。答案對 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; n nodes have exactly n-1 edges, 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 is depth[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 in O(log n).
  • 快速冪 / 預先算次方 / Precomputed powers:把 2^0, 2^1, ... 先算好存進陣列,查表 O(1) 取用。/ Storing 2^0, 2^1, ... in an array so each lookup is O(1).
  • 取模 / Modulo10^9+7 是個質數,每次乘法後取餘數可避免溢位且保持答案正確。/ Taking the remainder by the prime 10^9+7 after 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 = 0u == v,路徑沒有邊)時,成本恆為 0(偶數),所以答案是 0。因此每個查詢的答案只取決於路徑邊數 kk == 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=2depth[1]=0 < depth[2]=1,先交換成 a=2,b=1,把較深的 a 往上跳 diff=1 步:a = up[0][2] = 1。 - 現在 a == b == 1lca = 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 are O(n); the lifting table is O(n log n) (LOG levels over n nodes); each query's LCA is O(log n) over q queries. The log n factor dominates.
  • Space: O(n log n) — 主要是倍增表 up[LOG][n+1];鄰接表、深度、次方表都是 O(n)。/ Dominated by the up table of size LOG × (n+1); adjacency, depth, and power arrays are each O(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-case dist == 0 to avoid computing 2^(-1).
  • 遞迴 DFS 會爆堆疊 / Recursive DFS overflows the stackn 可達 10^5,一條鏈狀樹會讓遞迴深度過深而崩潰。這裡改用迭代式 BFS。/ A chain-shaped tree with n=10^5 would blow a recursive DFS stack; we use iterative BFS instead.
  • 整數溢位 / Integer overflow2^(k-1)k 大時極大,必須每步 % MOD,且乘法用 long long 暫存(int * int 在取模前可能溢位)。/ 2^(k-1) is huge; take % MOD each step and use long long for the multiply, since int * int can overflow before the mod.
  • LCA 跳躍順序 / Lifting order:先把較深節點拉到相同深度,再「由大到小」的 k 一起往上跳;若由小到大會跳過頭。/ First equalize depths, then lift both nodes from the largest k down — going small-to-large overshoots.
  • 哨兵節點 0 / Sentinel parent 0:根節點沒有父節點,用 0 當哨兵並令 up[k][0]=0,避免越界存取。/ The root has no parent; node 0 is a sentinel with up[k][0]=0 so jumps past the root stay in bounds.
  • 節點從 1 編號 / 1-based labels:陣列要開到 n+1,別誤用 0 號位置當真實節點。/ Nodes are 1-indexed; size arrays to n+1 and don't treat index 0 as a real node.