LeetCode 3112 - Minimum Time to Visit Disappearing Nodes
The problem describes an undirected weighted graph with n vertices, represented by an array of edges. Each edge connects two vertices and has a weight.
Difficulty: 🟡 Medium
Topics: Array, Graph Theory, Heap (Priority Queue), Shortest Path
Solution
Problem Understanding
The problem describes an undirected weighted graph with n vertices, represented by an array of edges. Each edge connects two vertices and has a weight. A walk is a sequence of vertices and edges that may revisit vertices or edges, and the cost of a walk is defined as the bitwise AND of all edge weights along the walk. Given multiple queries asking for the minimum cost of a walk from si to ti, we must return an array of answers, where each element represents the smallest possible cost for the corresponding query, or -1 if no walk exists.
Key points to note include:
- Multiple edges between the same pair of vertices are possible.
- Walks may revisit edges or vertices, which allows traversing higher-weight edges multiple times to reduce the bitwise AND.
- The constraints (
nup to 10^5,edges.lengthup to 10^5, and edge weights ≤ 10^5) indicate that brute-force enumeration of all walks is infeasible. Efficient graph traversal with bit-level optimization is required.
Important edge cases are queries between disconnected nodes, graphs with zero-weight edges, and queries where the minimal bitwise AND requires revisiting nodes.
Approaches
The brute-force approach would be to enumerate all walks from si to ti for each query and compute the bitwise AND of edge weights along each walk. This guarantees correctness because it considers every possible path, but it is completely impractical due to the exponential number of possible walks and the high number of queries.
The optimal approach relies on the observation that the bitwise AND operation is monotonic under masking. For any two vertices u and v, if there exists a walk with cost c, then for each bit b that is not set in c, there is a way to avoid edges that would prevent that bit from being cleared. Therefore, we can apply bitmask-based BFS: we start from the highest bit and check connectivity using edges that have this bit set, iteratively considering lower bits. Alternatively, we can use a bitwise union-find approach for each bit, connecting nodes if the edge weight contains the bit, which allows fast queries for all pairs.
The key insight is that the minimum cost corresponds to the bitwise AND of a path constructed by repeatedly adding edges that do not eliminate bits unnecessarily, and we can efficiently find this by considering edges in terms of bit masks and connectivity.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(2^E * Q) | O(V+E) | Enumerates all walks for each query; impractical for large n and edges |
| Optimal (Bitwise BFS/Union-Find) | O(20 * (V + E + Q)) | O(V) | For each of the 17 bits (since max weight ≤ 10^5), check connectivity using BFS/Union-Find |
Algorithm Walkthrough
- Initialize a result array
answerof length equal to the number of queries, filled with -1. - For each bit from 16 down to 0 (since max weight ≤ 10^5 fits in 17 bits), create a Union-Find data structure of size
n. - Iterate over all edges and include an edge in the union-find only if the current bit is set in its weight.
- For each query, if the two vertices
siandtiare connected in the union-find structure, record the bit as part of the result mask for that query. - After processing all bits, each query will have accumulated the minimum possible AND cost as the combination of bits for which the nodes remained connected.
- Return the
answerarray.
This algorithm works because each bit in the final AND can only remain set if a path exists using edges with that bit set. By checking connectivity per bit, we construct the minimum AND path without enumerating all walks.
Python Solution
from typing import List
class UnionFind:
def __init__(self, n: int):
self.parent = list(range(n))
self.rank = [0] * n
def find(self, x: int) -> int:
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x])
return self.parent[x]
def union(self, x: int, y: int) -> None:
px, py = self.find(x), self.find(y)
if px == py:
return
if self.rank[px] < self.rank[py]:
self.parent[px] = py
else:
self.parent[py] = px
if self.rank[px] == self.rank[py]:
self.rank[px] += 1
class Solution:
def minimumCost(self, n: int, edges: List[List[int]], query: List[List[int]]) -> List[int]:
Q = len(query)
answer = [0] * Q
# Process bit by bit from highest to lowest
for bit in range(16, -1, -1):
uf = UnionFind(n)
mask = 1 << bit
for u, v, w in edges:
if w & mask:
uf.union(u, v)
for i in range(Q):
s, t = query[i]
if uf.find(s) == uf.find(t):
answer[i] |= mask
# Replace 0 with -1 if no path exists
for i in range(Q):
if answer[i] == 0:
# check if a path exists at all
uf_all = UnionFind(n)
for u, v, w in edges:
uf_all.union(u, v)
if uf_all.find(query[i][0]) != uf_all.find(query[i][1]):
answer[i] = -1
return answer
This Python implementation constructs the union-find structure per bit. For each query, it accumulates bits that can be preserved along a connected path. The final step ensures that queries with truly disconnected nodes are set to -1.
Go Solution
package main
type UnionFind struct {
parent []int
rank []int
}
func NewUnionFind(n int) *UnionFind {
uf := &UnionFind{
parent: make([]int, n),
rank: make([]int, n),
}
for i := 0; i < n; i++ {
uf.parent[i] = i
}
return uf
}
func (uf *UnionFind) Find(x int) int {
if uf.parent[x] != x {
uf.parent[x] = uf.Find(uf.parent[x])
}
return uf.parent[x]
}
func (uf *UnionFind) Union(x, y int) {
px, py := uf.Find(x), uf.Find(y)
if px == py {
return
}
if uf.rank[px] < uf.rank[py] {
uf.parent[px] = py
} else {
uf.parent[py] = px
if uf.rank[px] == uf.rank[py] {
uf.rank[px]++
}
}
}
func minimumCost(n int, edges [][]int, query [][]int) []int {
Q := len(query)
answer := make([]int, Q)
for bit := 16; bit >= 0; bit-- {
uf := NewUnionFind(n)
mask := 1 << bit
for _, e := range edges {
u, v, w := e[0], e[1], e[2]
if w&mask != 0 {
uf.Union(u, v)
}
}
for i := 0; i < Q; i++ {
s, t := query[i][0], query[i][1]
if uf.Find(s) == uf.Find(t) {
answer[i] |= mask
}
}
}
// Handle disconnected nodes
ufAll := NewUnionFind(n)
for _, e := range edges {
ufAll.Union(e[0], e[1])
}
for i := 0; i < Q; i++ {
if answer[i] == 0 && ufAll.Find(query[i][0]) != ufAll.Find(query[i][1]) {
answer[i] = -1
}
}
return answer
}
The Go implementation mirrors the Python logic, with attention to slices, struct-based union-find, and explicit initialization of parent arrays.
Worked Examples
Example 1: n = 5, edges = [[0,1,7],[1,3,7],[1,2,1]], query = [[0,3],[3,4]]
- Bit 2 (mask 4): edges
[0,1,7]and[1,3,7]are connected. Union-find shows 0 and 3 connected.answer[0] |= 4 - Bit 1 (mask 2): all edges connected, still path exists.
answer[0] |= 2→ 6 - Bit 0 (mask 1):
[1,2,1]included. 0 and 3 connected via 0->1->2->1->3.answer[0] |= 1→ 7 & path yields minimal AND 1 - For query
[3,4], nodes are disconnected, soanswer[1] = -1.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|