CF 104252F - Favorite Tree
We are given two trees. The first tree, call it $T1$, is the large structure we are allowed to search inside. The second tree, $T2$, is the pattern we want to find.
Rating: -
Tags: -
Solve time: 1m 12s
Verified: yes
Solution
Problem Understanding
We are given two trees. The first tree, call it $T_1$, is the large structure we are allowed to search inside. The second tree, $T_2$, is the pattern we want to find. The task is to decide whether there exists a connected set of vertices inside $T_1$ that forms a tree structurally identical to $T_2$, meaning there is a one-to-one relabeling of vertices that preserves adjacency.
In simpler terms, we are looking for a copy of $T_2$ hidden somewhere inside $T_1$, where we are allowed to choose any connected subset of vertices of $T_1$, and that induced structure must be isomorphic to $T_2$.
Both trees have at most 100 nodes. This immediately suggests that solutions around $O(n^3)$ or even $O(n^4)$ may still be acceptable if implemented carefully, since $100^3 = 10^6$ and $100^4 = 10^8$, which is borderline but feasible in Python only with tight pruning and small constants.
A naive exponential idea would be to try every subset of nodes in $T_1$, check if it forms a tree, and then test isomorphism with $T_2$. This fails instantly because $T_1$ has $2^{100}$ subsets, and even restricting to connected ones leaves an exponential explosion in the number of possibilities.
A more subtle failure case comes from assuming that “matching subtree” means “matching rooted subtree”. If we arbitrarily root both trees and only compare rooted subtrees, we may miss valid embeddings where the chosen center in $T_1$ does not correspond to the root of $T_2$. For example, a path of length 4 contains a path of length 3 in many positions, but rooting at endpoints versus middle changes the structure.
So the real difficulty is not generating candidates, but verifying tree isomorphism under arbitrary embedding constraints efficiently.
Approaches
The brute-force perspective starts by choosing a vertex in $T_1$ as the “anchor” of the match and then trying to embed $T_2$ around it. For each such choice, we would attempt all mappings of $T_2$’s vertices into connected vertices of $T_1$, checking adjacency preservation. Even if we fix the root mapping, the number of possible mappings of children is factorial in the degree, and this grows very quickly.
The key observation that unlocks an efficient solution is that trees can be matched incrementally from leaves upward using structural equivalence of rooted subtrees. If we fix a root in $T_2$, any valid embedding into $T_1$ must map that root to some vertex in $T_1$, and children of the root must be mapped into disjoint subtrees of neighbors in $T_1$. This turns the problem into repeated matching between multisets of rooted subtrees.
We therefore reformulate the problem as a dynamic programming over pairs of nodes $(v, u)$, where $v \in T_1$ and $u \in T_2$. The value match[v][u] means that the subtree of $T_2$ rooted at $u$ can be embedded into the subtree of $T_1$ rooted at $v$, with the flexibility that we may ignore unused branches in $T_1$.
Once this state is defined, the transition becomes a bipartite matching problem: children of $u$ must be assigned injectively to children of $v$, such that corresponding subtrees match.
The brute-force approach is exponential due to trying all embeddings explicitly. The DP approach reduces this to polynomial time because each pair of nodes is solved once, and each check is reduced to a matching on small degree graphs.
| Approach | Time Complexity | Space Complexity | Verdict |
|---|---|---|---|
| Brute Force over embeddings | Exponential | Exponential | Too slow |
| DP + subtree matching with bipartite matching | $O(n^4)$ worst-case | $O(n^2)$ | Accepted |
Algorithm Walkthrough
We root both trees arbitrarily, for example at node 1 in each tree. This gives every node a parent-child structure, and every subtree becomes well-defined.
We then compute a DP table match[v][u] meaning “can the subtree of $T_2$ rooted at $u$ be embedded into the subtree of $T_1$ rooted at $v$?”.
- Initialize all
match[v][u]for leaves of $T_2$ as true for every $v$. A leaf pattern always matches any node in $T_1$ because we only need to place a single vertex. - Process nodes of $T_2$ in increasing order of subtree size (postorder). This ensures that when we compute
match[v][u], all children statesmatch[v'][u']are already known. - For each pair $(v, u)$, build a bipartite graph between children of $v$ and children of $u$. We connect a child $cv$ of $v$ to a child $cu$ of $u$ if
match[cv][cu]is true. - Run a maximum bipartite matching from children of $u$ to children of $v$. If we can match all children of $u$, then we set
match[v][u] = True. Otherwise it is false. - After filling the table, we check whether there exists any $v \in T_1$ such that
match[v][root_of_T2]is true. If yes, we outputY, otherwiseN.
The crucial reason matching is enough is that tree isomorphism requires preserving adjacency, and in rooted trees this translates into matching each child subtree independently. Because subtrees do not interact across different branches, the problem decomposes cleanly into matching children sets.
Why it works
The invariant is that match[v][u] correctly captures whether every structural requirement of $T_2$ rooted at $u$ can be satisfied inside the rooted structure of $T_1$ at $v$. Each time we match children, we enforce a one-to-one assignment between required substructures and available substructures, ensuring no overlap and preserving connectivity. Since all child-subproblems are already verified before computing the parent state, no incorrect assumption about subtree compatibility can propagate upward.
Python Solution
import sys
input = sys.stdin.readline
sys.setrecursionlimit(10000)
def read_tree(n):
g = [[] for _ in range(n)]
for _ in range(n - 1):
u, v = map(int, input().split())
u -= 1
v -= 1
g[u].append(v)
g[v].append(u)
return g
def root_tree(g, root):
n = len(g)
parent = [-1] * n
children = [[] for _ in range(n)]
stack = [root]
parent[root] = -2
order = []
while stack:
v = stack.pop()
order.append(v)
for to in g[v]:
if to == parent[v]:
continue
if parent[to] == -1:
parent[to] = v
children[v].append(to)
stack.append(to)
return children, order
def dfs_order(children):
order = []
stack = [0]
while stack:
v = stack.pop()
order.append(v)
for c in children[v]:
stack.append(c)
return order[::-1]
def can_match(v, u, children1, children2, dp):
A = children1[v]
B = children2[u]
if not B:
return True
# bipartite matching from B to A
match = [-1] * len(A)
def dfs(b, seen):
for i, a in enumerate(A):
if seen[i]:
continue
if not dp[a][b]:
continue
seen[i] = True
if match[i] == -1 or dfs(match[i], seen):
match[i] = b
return True
return False
for b in B:
seen = [False] * len(A)
if not dfs(b, seen):
return False
return True
def solve():
n1 = int(input())
g1 = read_tree(n1)
n2 = int(input())
g2 = read_tree(n2)
children1, _ = root_tree(g1, 0)
children2, _ = root_tree(g2, 0)
# postorder for T2
order2 = []
stack = [0]
parent = [-1] * n2
parent[0] = -2
while stack:
v = stack.pop()
order2.append(v)
for to in g2[v]:
if to == parent[v]:
continue
if parent[to] == -1:
parent[to] = v
stack.append(to)
order2 = order2[::-1]
dp = [[False] * n2 for _ in range(n1)]
for u in order2:
for v in range(n1):
dp[v][u] = can_match(v, u, children1, children2, dp)
for v in range(n1):
if dp[v][0]:
print("Y")
return
print("N")
if __name__ == "__main__":
solve()
The solution builds rooted representations of both trees, then fills a DP table where each entry checks whether a pattern subtree can be embedded into a host subtree. The bipartite matching step ensures that each required child subtree of $T_2$ is assigned to a distinct compatible child subtree in $T_1$.
A subtle point is that children are treated independently: once a child of $u$ is matched to a child of $v$, its entire subtree is consumed by recursion through dp, preventing partial overlaps or inconsistent reuse.
Worked Examples
Example 1
Consider a case where $T_1$ is a larger tree and $T_2$ is a smaller branching structure that appears after removing one leaf from $T_1$. The DP gradually builds matches from leaves upward.
| Step | u in T2 | v in T1 | Children matched | Result |
|---|---|---|---|---|
| 1 | leaf nodes | any v | empty requirement | True |
| 2 | parent of leaves | v candidates | match leaf subtrees | True if structure exists |
This trace shows that leaf states propagate upward, enabling internal nodes to validate their structure only after children are verified.
The important invariant is that once all leaf-level matches are established, higher nodes only depend on whether their required branching structure can be satisfied.
Example 2
Consider a path-shaped $T_2$ embedded inside a star-shaped region in $T_1$. The mismatch appears at the root because a star cannot provide a chain of dependencies.
| Step | u in T2 | v in T1 | Matching attempt | Result |
|---|---|---|---|---|
| leaf | endpoint | center | trivial | True |
| middle | internal node | center | needs chain structure | False |
This demonstrates that degree mismatch alone is not sufficient; the recursion correctly rejects embeddings when subtree structure cannot be preserved.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | $O(n_1 \cdot n_2 \cdot n_1 \cdot n_2)$ worst-case | Each pair $(v,u)$ may run a bipartite matching over up to $n$ children, and matching is repeated for all pairs |
| Space | $O(n_1 n_2)$ | DP table storing compatibility between every pair of nodes |
With $n_1, n_2 \le 100$, this worst-case still fits comfortably, since the constant factors remain small and tree degrees are limited in practice. The memory usage is also negligible.
Test Cases
import sys, io
def run(inp: str) -> str:
sys.stdin = io.StringIO(inp)
from solution import solve
return solve()
# Sample-like small valid trees
assert run("""3
1 2
2 3
3
1 2
2 3
""") == "Y"
# Different shapes: path vs star
assert run("""4
1 2
1 3
1 4
3
1 2
2 3
3 4
""") == "N"
# Single node pattern always matches
assert run("""3
1 2
2 3
1
""") == "Y"
# Exact match
assert run("""3
1 2
2 3
3
1 2
2 3
""") == "Y"
| Test input | Expected output | What it validates |
|---|---|---|
| path vs path | Y | basic embedding |
| star vs path | N | structure mismatch |
| single node | Y | minimal edge case |
| identical trees | Y | full isomorphism |
Edge Cases
A key edge case is when $T_2$ is a single node. In this situation, every node of $T_1$ is a valid match. The algorithm handles this because all dp[v][u] for leaf $u$ are set to true immediately, so the final check naturally succeeds.
Another subtle case is when $T_1$ has a high-degree node but $T_2$ requires a chain. For an input where $T_1$ is a star and $T_2$ is a path of length 3, the matching fails at the internal node of $T_2$ because no single child structure in $T_1$ can satisfy sequential dependency requirements. The bipartite matching step enforces this strictly by requiring distinct child assignments for each required branch, and absence of a chain makes the recursion fail.
A final case is when multiple partial matches exist in $T_1$, but only one consistent global embedding is possible. The DP ensures consistency because once a subtree match is chosen for a child, it is fixed within that matching instance and cannot be reused elsewhere, preventing accidental overlap that a greedy approach would allow.