Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
81 changes: 81 additions & 0 deletions problems/3559-number-of-ways-to-assign-edge-weights-ii/problem.md
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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<<j) != 0 {
u = up[j][u]
}
}
if u == v {
return u
}
// Lift both until just below the LCA.
for j := LOG - 1; j >= 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
}
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}