Off-by-one on range boundaries
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.
Move from brute-force thinking to an efficient approach using array strategy.
You are given a tree rooted at node 0 that consists of n nodes numbered from 0 to n - 1. The tree is represented by an array parent of size n, where parent[i] is the parent of node i. Since node 0 is the root, parent[0] == -1.
You are also given a string s of length n, where s[i] is the character assigned to node i.
We make the following changes on the tree one time simultaneously for all nodes x from 1 to n - 1:
y to node x such that y is an ancestor of x, and s[x] == s[y].y does not exist, do nothing.x and its current parent and make node y the new parent of x by adding an edge between them.Return an array answer of size n where answer[i] is the size of the subtree rooted at node i in the final tree.
Example 1:
Input: parent = [-1,0,0,1,1,1], s = "abaabc"
Output: [6,3,1,1,1,1]
Explanation:
The parent of node 3 will change from node 1 to node 0.
Example 2:
Input: parent = [-1,0,4,0,1], s = "abbba"
Output: [5,2,1,1,1]
Explanation:
The following changes will happen at the same time:
Constraints:
n == parent.length == s.length1 <= n <= 1050 <= parent[i] <= n - 1 for all i >= 1.parent[0] == -1parent represents a valid tree.s consists only of lowercase English letters.Problem summary: You are given a tree rooted at node 0 that consists of n nodes numbered from 0 to n - 1. The tree is represented by an array parent of size n, where parent[i] is the parent of node i. Since node 0 is the root, parent[0] == -1. You are also given a string s of length n, where s[i] is the character assigned to node i. We make the following changes on the tree one time simultaneously for all nodes x from 1 to n - 1: Find the closest node y to node x such that y is an ancestor of x, and s[x] == s[y]. If node y does not exist, do nothing. Otherwise, remove the edge between x and its current parent and make node y the new parent of x by adding an edge between them. Return an array answer of size n where answer[i] is the size of the subtree rooted at node i in the final tree.
Start with the most direct exhaustive search. That gives a correctness anchor before optimizing.
Pattern signal: Array · Hash Map · Tree
[-1,0,0,1,1,1] "abaabc"
[-1,0,4,0,1] "abbba"
Source-backed implementations are provided below for direct study and interview prep.
// Accepted solution for LeetCode #3331: Find Subtree Sizes After Changes
class Solution {
private List<Integer>[] g;
private List<Integer>[] d;
private char[] s;
private int[] ans;
public int[] findSubtreeSizes(int[] parent, String s) {
int n = s.length();
g = new List[n];
d = new List[26];
this.s = s.toCharArray();
Arrays.setAll(g, k -> new ArrayList<>());
Arrays.setAll(d, k -> new ArrayList<>());
for (int i = 1; i < n; ++i) {
g[parent[i]].add(i);
}
ans = new int[n];
dfs(0, -1);
return ans;
}
private void dfs(int i, int fa) {
ans[i] = 1;
int idx = s[i] - 'a';
d[idx].add(i);
for (int j : g[i]) {
dfs(j, i);
}
int k = d[idx].size() > 1 ? d[idx].get(d[idx].size() - 2) : fa;
if (k >= 0) {
ans[k] += ans[i];
}
d[idx].remove(d[idx].size() - 1);
}
}
// Accepted solution for LeetCode #3331: Find Subtree Sizes After Changes
func findSubtreeSizes(parent []int, s string) []int {
n := len(s)
g := make([][]int, n)
for i := 1; i < n; i++ {
g[parent[i]] = append(g[parent[i]], i)
}
d := [26][]int{}
ans := make([]int, n)
var dfs func(int, int)
dfs = func(i, fa int) {
ans[i] = 1
idx := int(s[i] - 'a')
d[idx] = append(d[idx], i)
for _, j := range g[i] {
dfs(j, i)
}
k := fa
if len(d[idx]) > 1 {
k = d[idx][len(d[idx])-2]
}
if k != -1 {
ans[k] += ans[i]
}
d[idx] = d[idx][:len(d[idx])-1]
}
dfs(0, -1)
return ans
}
# Accepted solution for LeetCode #3331: Find Subtree Sizes After Changes
class Solution:
def findSubtreeSizes(self, parent: List[int], s: str) -> List[int]:
def dfs(i: int, fa: int):
ans[i] = 1
d[s[i]].append(i)
for j in g[i]:
dfs(j, i)
k = fa
if len(d[s[i]]) > 1:
k = d[s[i]][-2]
if k != -1:
ans[k] += ans[i]
d[s[i]].pop()
n = len(s)
g = [[] for _ in range(n)]
for i in range(1, n):
g[parent[i]].append(i)
d = defaultdict(list)
ans = [0] * n
dfs(0, -1)
return ans
// Accepted solution for LeetCode #3331: Find Subtree Sizes After Changes
// Rust example auto-generated from java reference.
// Replace the signature and local types with the exact LeetCode harness for this problem.
impl Solution {
pub fn rust_example() {
// Port the logic from the reference block below.
}
}
// Reference (java):
// // Accepted solution for LeetCode #3331: Find Subtree Sizes After Changes
// class Solution {
// private List<Integer>[] g;
// private List<Integer>[] d;
// private char[] s;
// private int[] ans;
//
// public int[] findSubtreeSizes(int[] parent, String s) {
// int n = s.length();
// g = new List[n];
// d = new List[26];
// this.s = s.toCharArray();
// Arrays.setAll(g, k -> new ArrayList<>());
// Arrays.setAll(d, k -> new ArrayList<>());
// for (int i = 1; i < n; ++i) {
// g[parent[i]].add(i);
// }
// ans = new int[n];
// dfs(0, -1);
// return ans;
// }
//
// private void dfs(int i, int fa) {
// ans[i] = 1;
// int idx = s[i] - 'a';
// d[idx].add(i);
// for (int j : g[i]) {
// dfs(j, i);
// }
// int k = d[idx].size() > 1 ? d[idx].get(d[idx].size() - 2) : fa;
// if (k >= 0) {
// ans[k] += ans[i];
// }
// d[idx].remove(d[idx].size() - 1);
// }
// }
// Accepted solution for LeetCode #3331: Find Subtree Sizes After Changes
function findSubtreeSizes(parent: number[], s: string): number[] {
const n = parent.length;
const g: number[][] = Array.from({ length: n }, () => []);
const d: number[][] = Array.from({ length: 26 }, () => []);
for (let i = 1; i < n; ++i) {
g[parent[i]].push(i);
}
const ans: number[] = Array(n).fill(1);
const dfs = (i: number, fa: number): void => {
const idx = s.charCodeAt(i) - 97;
d[idx].push(i);
for (const j of g[i]) {
dfs(j, i);
}
const k = d[idx].length > 1 ? d[idx].at(-2)! : fa;
if (k >= 0) {
ans[k] += ans[i];
}
d[idx].pop();
};
dfs(0, -1);
return ans;
}
Use this to step through a reusable interview workflow for this problem.
BFS with a queue visits every node exactly once — O(n) time. The queue may hold an entire level of the tree, which for a complete binary tree is up to n/2 nodes = O(n) space. This is optimal in time but costly in space for wide trees.
Every node is visited exactly once, giving O(n) time. Space depends on tree shape: O(h) for recursive DFS (stack depth = height h), or O(w) for BFS (queue width = widest level). For balanced trees h = log n; for skewed trees h = n.
Review these before coding to avoid predictable interview regressions.
Wrong move: Loop endpoints miss first/last candidate.
Usually fails on: Fails on minimal arrays and exact-boundary answers.
Fix: Re-derive loops from inclusive/exclusive ranges before coding.
Wrong move: Zero-count keys stay in map and break distinct/count constraints.
Usually fails on: Window/map size checks are consistently off by one.
Fix: Delete keys when count reaches zero.
Wrong move: Recursive traversal assumes children always exist.
Usually fails on: Leaf nodes throw errors or create wrong depth/path values.
Fix: Handle null/base cases before recursive transitions.