Count Valid Paths in a Tree — Solution in Python, Java & C++
Count Valid Paths in a Tree solution explained: step-by-step approach, complexity and code in Python, Java, C++ and JavaScript. A hard problem.
Count Valid Paths in a Tree is a hard coding problem on dynamic programming, math and depth-first search, asked in Amazon and Google interviews.
- Difficulty: Hard
- Topics: Dynamic Programming, Math, Depth-First Search, Trees, Union Find, Number Theory
- Asked at: Amazon, Google
- Time limit: 2 s
- Memory limit: 256 MB
- Languages: JavaScript, TypeScript, Python, Java, C++, C, C#, Go, Kotlin, Swift, Rust, PHP and Ruby
Problem statement
An undirected tree has n nodes labelled 1 to n, joined by the n - 1 edges in edges.
A path between two different nodes a and b is valid if exactly one of the node labels on it (endpoints included) is a prime number. The path from a to b and the path from b to a count as the same path.
Return the number of valid paths.
CodeKairo bounds n at 10^4 so the count always fits in a 32-bit integer.
Example 1
Input: n = 5, edges = [[1,2],[1,3],[2,4],[2,5]]
Output: 4
Explanation: The valid paths are 1–2, 1–3, 1–2–4 and 2–4: each contains exactly one of the primes 2, 3, 5.
Example 2
Input: n = 6, edges = [[1,2],[1,3],[2,4],[3,5],[3,6]]
Output: 6
Explanation: 1–2, 1–3, 1–2–4, 2–4, 3–6 and 1–3–6.
Example 3
Input: n = 1, edges = []
Output: 0
Explanation: There is no pair of different nodes.
Constraints
1 <= n <= 10^4edges.length == n - 1edges[i].length == 21 <= u, v <= nedges represents a valid tree
Hints for Count Valid Paths in a Tree
Hint 1
Remove every prime node: the non-prime nodes fall apart into connected groups. A valid path contains exactly one prime, so everything else on it lies inside such groups.
Hint 2
Fix the prime
pon the path. Each end of the path is eitherpitself or a node in one of the non-prime groups touchingp— and the two ends must come from different groups.Hint 3
Find the groups with union-find over edges joining two non-primes. For a prime
pwhose neighbouring groups have sizess1, s2, …, adds_i(paths ending atp) pluss_i · (s1 + … + s_{i-1})(paths crossingp) for eachi.
How to solve Count Valid Paths in a Tree
A valid path has a single prime p; the rest of the path consists of non-prime nodes on either side of p, each side inside one connected group of non-primes adjacent to p. Precompute the size of every non-prime group, and count, for each prime, the pairs of ends it can join.
Approach
- Sieve the primes up to
n. - Union-find: join
uandvfor every edge whose ends are both non-prime; group sizes come from the roots. - For each prime
p, walk its non-prime neighbours; for a neighbour's group of sizes, adds · (1 + seen)to the answer, thenseen += s(seenstarts at 0 for each prime). - Return the total.
Why it works
Each valid path corresponds to exactly one prime p and one or two ends in distinct groups adjacent to p (two neighbours of p can never share a group, since the tree has no cycle). s · 1 counts paths from p into the group, and s · seen counts paths between this group and the earlier ones through p; every such path is counted once.
Complexity
- Time —
O(n α(n) + n log log n) - Space —
O(n)
Pitfalls
- 1 is not prime.
- Paths with zero primes or with two or more primes are both invalid — never count paths inside a single non-prime group.
- In the real problem
nreaches10^5and the count needs 64 bits; with CodeKairo's bound of10^4it fits in 32.
Reference solution
Python
from typing import List
def countPaths(n: int, edges: List[List[int]]) -> int:
is_prime = [False, False] + [True] * (n - 1)
for i in range(2, int(n ** 0.5) + 1):
if is_prime[i]:
for j in range(i * i, n + 1, i):
is_prime[j] = False
parent = list(range(n + 1))
size = [1] * (n + 1)
def find(x):
while parent[x] != x:
parent[x] = parent[parent[x]]
x = parent[x]
return x
adj = [[] for _ in range(n + 1)]
for u, v in edges:
adj[u].append(v)
adj[v].append(u)
if not is_prime[u] and not is_prime[v]:
a, b = find(u), find(v)
if a != b:
if size[a] < size[b]:
a, b = b, a
parent[b] = a
size[a] += size[b]
total = 0
for p in range(2, n + 1):
if not is_prime[p]:
continue
seen = 0
for v in adj[p]:
if not is_prime[v]:
s = size[find(v)]
total += s * (1 + seen)
seen += s
return totalJava
static int[] dsuParent;
static int dsuFind(int x) {
while (dsuParent[x] != x) { dsuParent[x] = dsuParent[dsuParent[x]]; x = dsuParent[x]; }
return x;
}
public static int countPaths(int n, int[][] edges) {
boolean[] composite = new boolean[n + 1];
composite[0] = true;
if (n >= 1) composite[1] = true;
for (int a = 2; (long) a * a <= n; a++) if (!composite[a]) for (int b = a * a; b <= n; b += a) composite[b] = true;
dsuParent = new int[n + 1];
int[] size = new int[n + 1];
List<List<Integer>> adj = new ArrayList<>();
for (int k = 0; k <= n; k++) { dsuParent[k] = k; size[k] = 1; adj.add(new ArrayList<>()); }
for (int[] e : edges) {
int u = e[0], v = e[1];
adj.get(u).add(v); adj.get(v).add(u);
if (composite[u] && composite[v]) {
int ru = dsuFind(u), rv = dsuFind(v);
if (ru != rv) {
if (size[ru] < size[rv]) { int t = ru; ru = rv; rv = t; }
dsuParent[rv] = ru;
size[ru] += size[rv];
}
}
}
long total = 0;
for (int p = 2; p <= n; p++) {
if (composite[p]) continue;
long seen = 0;
for (int w : adj.get(p)) {
if (!composite[w]) continue;
long s = size[dsuFind(w)];
total += s * (1 + seen);
seen += s;
}
}
return (int) total;
}C++
int countPaths(int n, vector<vector<int>>& edges) {
vector<bool> composite(n + 1, false);
composite[0] = true;
if (n >= 1) composite[1] = true;
for (int a = 2; (long long) a * a <= n; a++) if (!composite[a]) for (int b = a * a; b <= n; b += a) composite[b] = true;
vector<int> parent(n + 1), sz(n + 1, 1);
iota(parent.begin(), parent.end(), 0);
function<int(int)> find = [&](int x) {
while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
};
vector<vector<int>> adj(n + 1);
for (auto& e : edges) {
int u = e[0], v = e[1];
adj[u].push_back(v); adj[v].push_back(u);
if (composite[u] && composite[v]) {
int ru = find(u), rv = find(v);
if (ru != rv) {
if (sz[ru] < sz[rv]) swap(ru, rv);
parent[rv] = ru;
sz[ru] += sz[rv];
}
}
}
long long total = 0;
for (int p = 2; p <= n; p++) {
if (composite[p]) continue;
long long seen = 0;
for (int w : adj[p]) {
if (!composite[w]) continue;
long long s = sz[find(w)];
total += s * (1 + seen);
seen += s;
}
}
return (int) total;
}JavaScript
var countPaths = function(n, edges) {
var isPrime = new Array(n + 1);
for (var i = 0; i <= n; i++) isPrime[i] = i >= 2;
for (var a = 2; a * a <= n; a++) if (isPrime[a]) for (var b = a * a; b <= n; b += a) isPrime[b] = false;
var parent = new Array(n + 1), size = new Array(n + 1), adj = [];
for (var k = 0; k <= n; k++) { parent[k] = k; size[k] = 1; adj.push([]); }
var find = function(x) {
while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
};
for (var e = 0; e < edges.length; e++) {
var u = edges[e][0], v = edges[e][1];
adj[u].push(v); adj[v].push(u);
if (!isPrime[u] && !isPrime[v]) {
var ru = find(u), rv = find(v);
if (ru !== rv) {
if (size[ru] < size[rv]) { var t = ru; ru = rv; rv = t; }
parent[rv] = ru;
size[ru] += size[rv];
}
}
}
var total = 0;
for (var p = 2; p <= n; p++) {
if (!isPrime[p]) continue;
var seen = 0;
for (var j = 0; j < adj[p].length; j++) {
var w = adj[p][j];
if (isPrime[w]) continue;
var s = size[find(w)];
total += s * (1 + seen);
seen += s;
}
}
return total;
};Also on the editorial tab: C, C#, Go, Kotlin, PHP, Ruby, Rust, Swift, TypeScript.
All 402 dynamic programming problems · the whole catalogue
Learn the technique: Dynamic Programming · Depth-First Search (DFS)