diff --git a/problems/3559-number-of-ways-to-assign-edge-weights-ii/analysis_daily_20260612.md b/problems/3559-number-of-ways-to-assign-edge-weights-ii/analysis_daily_20260612.md new file mode 100644 index 0000000..b6e4bdb --- /dev/null +++ b/problems/3559-number-of-ways-to-assign-edge-weights-ii/analysis_daily_20260612.md @@ -0,0 +1,109 @@ +# 3559. Number of Ways to Assign Edge Weights II + +[LeetCode Link](https://leetcode.com/problems/number-of-ways-to-assign-edge-weights-ii/) + +Difficulty: Hard +Topics: Array, Math, Dynamic Programming, Bit Manipulation, Tree, Depth-First Search +Acceptance Rate: 67.6% + +This problem looks intimidating because it combines trees, counting, and modular +arithmetic — but the counting half collapses to a tiny formula once you spot it. +The real work is answering many path-distance queries efficiently. Take it one +layer at a time and it becomes very approachable. + +## Hints + +### Hint 1 + +Forget the tree for a moment and look at a single path with exactly `k` edges. +Each edge is assigned either `1` or `2`. Only the *parity* of the total cost +matters. Notice that weight `1` flips the parity and weight `2` leaves it +unchanged. So really each edge is a binary choice: "flip" or "don't flip". How +many of the `2^k` total assignments end up with an *odd* number of flips? + +### Hint 2 + +The number of ways to choose an odd-sized subset out of `k` items is exactly +`2^(k-1)` whenever `k >= 1` (and `0` when `k = 0`, since an empty path has cost +`0`, which is even). That means every query's answer depends on **one number +only**: `k`, the number of edges on the path between `u` and `v`. The entire +problem reduces to: for each query, find the path length, then return +`2^(k-1) mod (1e9+7)`. + +### Hint 3 + +The number of edges on the path between `u` and `v` in a rooted tree is the +classic distance formula: + +``` +dist(u, v) = depth[u] + depth[v] - 2 * depth[lca(u, v)] +``` + +So the whole problem is really "answer up to `10^5` LCA / distance queries on a +tree of up to `10^5` nodes." Precompute depths and a **binary-lifting** table +(`up[k][node]` = the `2^k`-th ancestor) so each LCA is `O(log n)`, then plug the +distance into the `2^(k-1)` formula. Don't forget the `k = 0` case (a node paired +with itself, or two equal endpoints). + +## Approach + +There are two independent parts. + +**1. The counting formula.** +For a fixed path of `k` edges, an assignment makes the cost odd exactly when an +odd number of edges receive weight `1`. The count of odd-sized subsets of a +`k`-element set equals `2^(k-1)` for `k >= 1`. (Half of all `2^k` subsets are +odd-sized; pairing each subset with its complement-on-the-last-element gives a +bijection.) For `k = 0` the path is empty, cost is `0` (even), so the answer is +`0`. + +So `answer = (k == 0) ? 0 : 2^(k-1) mod M`, with `M = 1e9 + 7`. + +**2. Computing `k = dist(u, v)` fast.** +We build the tree from `edges`, root it at node `1`, and run one DFS (done +iteratively to avoid stack overflow at `n = 10^5`) to record `depth[node]` and +each node's immediate parent. We then build a binary-lifting table where +`up[j][v]` is the ancestor of `v` that is `2^j` steps above it. With it, the LCA +of `u` and `v` is found in `O(log n)`: + +1. Lift the deeper node up until both are at the same depth. +2. If they coincide, that node is the LCA. +3. Otherwise lift both in lockstep by the largest jumps that keep them distinct; + their common parent is the LCA. + +Finally `dist(u, v) = depth[u] + depth[v] - 2*depth[lca]`, and the answer is the +precomputed power of two. + +**Worked example** (`edges = [[1,2],[1,3],[3,4],[3,5]]`): +depths are `depth[1]=0, depth[2]=1, depth[3]=1, depth[4]=2, depth[5]=2`. +- Query `[1,4]`: `lca=1`, `dist = 0 + 2 - 0 = 2`, answer `2^1 = 2`. +- Query `[3,4]`: `lca=3`, `dist = 1 + 2 - 2 = 1`, answer `2^0 = 1`. +- Query `[2,5]`: `lca=1`, `dist = 1 + 2 - 0 = 3`, answer `2^2 = 4`. + +Output `[2, 1, 4]`, matching the expected result. + +We precompute all powers of two up to `n` once, so each query is just an +`O(log n)` LCA plus an array lookup. + +## Complexity Analysis + +Let `n` be the number of nodes and `q` the number of queries. + +Time Complexity: `O(n log n + q log n)` — building the binary-lifting table costs +`O(n log n)`, and each of the `q` queries costs `O(log n)` for the LCA. + +Space Complexity: `O(n log n)` for the binary-lifting table (plus `O(n)` for the +adjacency list, depths, and the table of powers of two). + +## Edge Cases + +- **Self query (`u == v`)**: distance is `0`, so the answer must be `0`. The + `2^(k-1)` formula is undefined at `k = 0`; guard it explicitly. +- **Single-edge tree / adjacent nodes**: `k = 1` gives `2^0 = 1`, the smallest + non-zero answer. Make sure exponent `k-1 = 0` is handled. +- **Deep / skewed (path-like) trees**: with `n = 10^5` a recursive DFS can blow + the call stack; use an iterative DFS or BFS to compute depths and parents. +- **Large distances and the modulus**: `k` can be up to `n-1 ≈ 10^5`, so + precompute `2^i mod (1e9+7)` rather than recomputing per query. +- **Node labels are 1-indexed**: keep arrays sized `n+1` and be consistent, or + off-by-one bugs creep into depth/parent lookups. diff --git a/problems/3559-number-of-ways-to-assign-edge-weights-ii/problem.md b/problems/3559-number-of-ways-to-assign-edge-weights-ii/problem.md new file mode 100644 index 0000000..2104815 --- /dev/null +++ b/problems/3559-number-of-ways-to-assign-edge-weights-ii/problem.md @@ -0,0 +1,81 @@ +--- +number: "3559" +frontend_id: "3559" +title: "Number of Ways to Assign Edge Weights II" +slug: "number-of-ways-to-assign-edge-weights-ii" +difficulty: "Hard" +topics: + - "Array" + - "Math" + - "Dynamic Programming" + - "Bit Manipulation" + - "Tree" + - "Depth-First Search" +acceptance_rate: 6755.3 +is_premium: false +created_at: "2026-06-12T05:12:48.052671+00:00" +fetched_at: "2026-06-12T05:12:48.052671+00:00" +link: "https://leetcode.com/problems/number-of-ways-to-assign-edge-weights-ii/" +date: "2026-06-12" +--- + +# 3559. Number of Ways to Assign Edge Weights II + +There is an undirected tree with `n` nodes labeled from 1 to `n`, rooted at node 1. The tree is represented by a 2D integer array `edges` of length `n - 1`, where `edges[i] = [ui, vi]` indicates that there is an edge between nodes `ui` and `vi`. + +Initially, all edges have a weight of 0. You must assign each edge a weight of either **1** or **2**. + +The **cost** of a path between any two nodes `u` and `v` is the total weight of all edges in the path connecting them. + +You are given a 2D integer array `queries`. For each `queries[i] = [ui, vi]`, determine the number of ways to assign weights to edges **in the path** such that the cost of the path between `ui` and `vi` is **odd**. + +Return an array `answer`, where `answer[i]` is the number of valid assignments for `queries[i]`. + +Since the answer may be large, apply **modulo** `109 + 7` to each `answer[i]`. + +**Note:** For each query, disregard all edges **not** in the path between node `ui` and `vi`. + + + +**Example 1:** + +![](https://assets.leetcode.com/uploads/2025/03/23/screenshot-2025-03-24-at-060006.png) + +**Input:** edges = [[1,2]], queries = [[1,1],[1,2]] + +**Output:** [0,1] + +**Explanation:** + + * Query `[1,1]`: The path from Node 1 to itself consists of no edges, so the cost is 0. Thus, the number of valid assignments is 0. + * Query `[1,2]`: The path from Node 1 to Node 2 consists of one edge (`1 -> 2`). Assigning weight 1 makes the cost odd, while 2 makes it even. Thus, the number of valid assignments is 1. + + + +**Example 2:** + +![](https://assets.leetcode.com/uploads/2025/03/23/screenshot-2025-03-24-at-055820.png) + +**Input:** edges = [[1,2],[1,3],[3,4],[3,5]], queries = [[1,4],[3,4],[2,5]] + +**Output:** [2,1,4] + +**Explanation:** + + * Query `[1,4]`: The path from Node 1 to Node 4 consists of two edges (`1 -> 3` and `3 -> 4`). Assigning weights (1,2) or (2,1) results in an odd cost. Thus, the number of valid assignments is 2. + * Query `[3,4]`: The path from Node 3 to Node 4 consists of one edge (`3 -> 4`). Assigning weight 1 makes the cost odd, while 2 makes it even. Thus, the number of valid assignments is 1. + * Query `[2,5]`: The path from Node 2 to Node 5 consists of three edges (`2 -> 1, 1 -> 3`, and `3 -> 5`). Assigning (1,2,2), (2,1,2), (2,2,1), or (1,1,1) makes the cost odd. Thus, the number of valid assignments is 4. + + + + + +**Constraints:** + + * `2 <= n <= 105` + * `edges.length == n - 1` + * `edges[i] == [ui, vi]` + * `1 <= queries.length <= 105` + * `queries[i] == [ui, vi]` + * `1 <= ui, vi <= n` + * `edges` represents a valid tree. diff --git a/problems/3559-number-of-ways-to-assign-edge-weights-ii/solution_daily_20260612.go b/problems/3559-number-of-ways-to-assign-edge-weights-ii/solution_daily_20260612.go new file mode 100644 index 0000000..eff5c45 --- /dev/null +++ b/problems/3559-number-of-ways-to-assign-edge-weights-ii/solution_daily_20260612.go @@ -0,0 +1,112 @@ +package main + +// 3559. Number of Ways to Assign Edge Weights II +// +// Approach: +// For a path of k edges, each edge is assigned weight 1 (flips parity) or 2 +// (keeps parity). The cost is odd exactly when an odd number of edges get +// weight 1, and the number of odd-sized subsets of k items is 2^(k-1) for +// k >= 1 (and 0 for k == 0). So every query reduces to: +// +// answer = (k == 0) ? 0 : 2^(k-1) mod (1e9+7), where k = dist(u, v). +// +// The distance in a rooted tree is depth[u] + depth[v] - 2*depth[lca(u, v)]. +// We compute depths/parents with an iterative DFS, build a binary-lifting +// table for O(log n) LCA queries, and precompute powers of two. + +const modAssignEdgeWeights = 1_000_000_007 + +func assignEdgeWeights(edges [][]int, queries [][]int) []int { + n := len(edges) + 1 + + // Build adjacency list (nodes are 1-indexed). + adj := make([][]int, n+1) + for _, e := range edges { + u, v := e[0], e[1] + adj[u] = append(adj[u], v) + adj[v] = append(adj[v], u) + } + + // Binary-lifting table dimensions. + LOG := 1 + for (1 << LOG) < n { + LOG++ + } + LOG++ // headroom so the highest jump covers the deepest path + + up := make([][]int, LOG) + for j := range up { + up[j] = make([]int, n+1) + } + depth := make([]int, n+1) + + // Iterative DFS from the root (node 1) to avoid stack overflow on deep + // trees. Record each node's immediate parent in up[0] and its depth. + visited := make([]bool, n+1) + stack := []int{1} + visited[1] = true + up[0][1] = 1 // root is its own 2^0 ancestor (sentinel) + depth[1] = 0 + for len(stack) > 0 { + node := stack[len(stack)-1] + stack = stack[:len(stack)-1] + for _, next := range adj[node] { + if !visited[next] { + visited[next] = true + up[0][next] = node + depth[next] = depth[node] + 1 + stack = append(stack, next) + } + } + } + + // Fill the rest of the binary-lifting table. + for j := 1; j < LOG; j++ { + for v := 1; v <= n; v++ { + up[j][v] = up[j-1][up[j-1][v]] + } + } + + lca := func(u, v int) int { + if depth[u] < depth[v] { + u, v = v, u + } + // Lift u up to the depth of v. + diff := depth[u] - depth[v] + for j := 0; j < LOG; j++ { + if diff&(1<= 0; j-- { + if up[j][u] != up[j][v] { + u = up[j][u] + v = up[j][v] + } + } + return up[0][u] + } + + // Precompute powers of two mod M up to n. + pow2 := make([]int, n+1) + pow2[0] = 1 + for i := 1; i <= n; i++ { + pow2[i] = pow2[i-1] * 2 % modAssignEdgeWeights + } + + ans := make([]int, len(queries)) + for i, q := range queries { + u, v := q[0], q[1] + k := depth[u] + depth[v] - 2*depth[lca(u, v)] + if k == 0 { + ans[i] = 0 + } else { + ans[i] = pow2[k-1] + } + } + return ans +} diff --git a/problems/3559-number-of-ways-to-assign-edge-weights-ii/solution_daily_20260612_test.go b/problems/3559-number-of-ways-to-assign-edge-weights-ii/solution_daily_20260612_test.go new file mode 100644 index 0000000..4db5e7e --- /dev/null +++ b/problems/3559-number-of-ways-to-assign-edge-weights-ii/solution_daily_20260612_test.go @@ -0,0 +1,62 @@ +package main + +import ( + "reflect" + "testing" +) + +func TestAssignEdgeWeights(t *testing.T) { + tests := []struct { + name string + edges [][]int + queries [][]int + expected []int + }{ + { + name: "example 1: single edge, self and adjacent queries", + edges: [][]int{{1, 2}}, + queries: [][]int{{1, 1}, {1, 2}}, + expected: []int{0, 1}, + }, + { + name: "example 2: branching tree with varied path lengths", + edges: [][]int{{1, 2}, {1, 3}, {3, 4}, {3, 5}}, + queries: [][]int{{1, 4}, {3, 4}, {2, 5}}, + expected: []int{2, 1, 4}, + }, + { + name: "edge case: every query is a self-pair (distance 0)", + edges: [][]int{{1, 2}, {2, 3}}, + queries: [][]int{{1, 1}, {2, 2}, {3, 3}}, + expected: []int{0, 0, 0}, + }, + { + name: "edge case: straight-line (path) tree, distances 1..4", + edges: [][]int{{1, 2}, {2, 3}, {3, 4}, {4, 5}}, + queries: [][]int{{1, 2}, {1, 3}, {1, 4}, {1, 5}}, + expected: []int{1, 2, 4, 8}, // 2^0, 2^1, 2^2, 2^3 + }, + { + name: "edge case: LCA is an interior node, not the root", + edges: [][]int{{1, 2}, {2, 3}, {2, 4}}, + queries: [][]int{{3, 4}, {1, 3}}, + expected: []int{2, 2}, // dist(3,4)=2 via node 2; dist(1,3)=2 via node 1 + }, + { + name: "edge case: longer path crossing the root", + edges: [][]int{{1, 2}, {1, 3}, {3, 4}, {3, 5}}, + queries: [][]int{{2, 4}}, + expected: []int{4}, // path 2-1-3-4 has 3 edges -> 2^2 = 4 + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := assignEdgeWeights(tt.edges, tt.queries) + if !reflect.DeepEqual(got, tt.expected) { + t.Errorf("assignEdgeWeights(%v, %v) = %v, want %v", + tt.edges, tt.queries, got, tt.expected) + } + }) + } +}