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.

CF 104252F - Favorite Tree

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$?”.

  1. 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.
  2. Process nodes of $T_2$ in increasing order of subtree size (postorder). This ensures that when we compute match[v][u], all children states match[v'][u'] are already known.
  3. 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.
  4. 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.
  5. 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 output Y, otherwise N.

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.