CF 1249B1 - Books Exchange (easy version)

Each child starts with exactly one book, and every day all books are passed simultaneously according to a fixed permutation. If a child i gives their book to p[i], then after one day the book moves one step along this directed edge.

CF 1249B1 - Books Exchange (easy version)

Rating: 1000
Tags: dsu, math
Solve time: 5m 26s
Verified: no

Solution

Problem Understanding

Each child starts with exactly one book, and every day all books are passed simultaneously according to a fixed permutation. If a child i gives their book to p[i], then after one day the book moves one step along this directed edge. After two days it moves two steps along the permutation, and so on.

From the perspective of a single starting book, we are repeatedly applying the same permutation. The question for each starting position i is: after how many applications of the permutation does the book that started at i return to i for the first time.

This is equivalent to asking for the length of the cycle that contains node i in a directed graph where each node has exactly one outgoing edge. Since p is a permutation, the graph is a disjoint union of cycles.

The constraints are small: n is at most 200 and there are at most 200 queries. This immediately tells us that even an O(n²) or O(n³) approach per test case is safe. We do not need heavy optimization or data structures beyond simple traversal and marking.

A subtle edge case is when a node maps to itself, meaning p[i] = i. In this case, the cycle length is 1 and the answer must be 1. Another case is a long cycle where no node repeats until the full cycle completes, for example a permutation like 2 3 4 5 1, where every node shares the same answer 5. Any incorrect solution that tries to simulate day-by-day movement separately for each node may still work here due to small constraints, but risks inefficiency or redundant recomputation.

Approaches

A direct simulation approach is to take each starting index i and repeatedly apply the permutation until we return to i, counting steps along the way. This is correct because each step exactly follows the rules of book passing. However, in the worst case, a single cycle of length n forces O(n) steps per starting node, leading to O(n²) per test case. With n up to 200 and q up to 200, this still passes comfortably (about 8 million operations worst-case), but it repeats work heavily since every node in a cycle recomputes the same traversal.

The key structural insight is that the permutation decomposes into disjoint cycles. Every node in the same cycle shares the same return time, equal to the cycle length. Instead of simulating separately, we traverse each cycle once, compute its size, and assign that size to all nodes in it. This reduces redundant work and turns the solution into a clean graph traversal problem.

We can find cycles by iterating over all nodes and starting a walk whenever we encounter an unvisited node. We follow p[i], p[p[i]], and so on until we return to the starting point, counting how many nodes are in that loop.

Approach Time Complexity Space Complexity Verdict
Brute Force Simulation O(n²) per query O(1) extra Accepted
Cycle Decomposition O(n) per query O(n) Accepted

Algorithm Walkthrough

We process each query independently and build the answer array.

  1. Maintain a visited array of size n initialized to false, and an answer array initialized to zeros. The visited array ensures we do not process the same cycle multiple times.
  2. Iterate over each node i from 1 to n. If i is already visited, skip it because it belongs to a cycle already processed.
  3. When we find an unvisited node i, we start traversing the cycle beginning at i. We store the start node and move through the permutation repeatedly using p[current].
  4. While traversing, we collect all nodes in the current cycle into a list and mark them as visited. We also count how many steps we take until we return to the starting node.
  5. Once we close the cycle (we reach the start again), we assign the computed cycle length to every node in the collected list.

The reason we explicitly collect nodes rather than assigning on the fly is that we only know the cycle length after completing the traversal.

Why it works

Every node in a permutation belongs to exactly one directed cycle. The traversal from any unvisited node follows deterministic edges and must eventually return to the starting node because the graph is finite and each node has exactly one outgoing edge. This guarantees that each traversal discovers a complete cycle without branching or ambiguity. Since the answer is defined as the first return time, which is exactly the cycle length, assigning the cycle size to all nodes in that component is correct.

Python Solution

import sys
input = sys.stdin.readline

q = int(input())
for _ in range(q):
    n = int(input())
    p = [0] + list(map(int, input().split()))
    
    visited = [False] * (n + 1)
    ans = [0] * (n + 1)
    
    for i in range(1, n + 1):
        if visited[i]:
            continue
        
        cycle = []
        cur = i
        
        while not visited[cur]:
            visited[cur] = True
            cycle.append(cur)
            cur = p[cur]
        
        length = len(cycle)
        for v in cycle:
            ans[v] = length
    
    print(*ans[1:])

The solution works by explicitly decomposing the permutation into cycles. The 1-indexing simplifies direct mapping from input to graph nodes. Each node is visited exactly once, so the traversal cost is linear per query.

A common implementation pitfall is forgetting to mark nodes as visited before moving to the next node in the cycle, which can lead to infinite loops in incorrect implementations. Another subtle issue is mixing up the termination condition of the cycle walk; we rely on revisiting a previously visited node within the current traversal, which is safe because every node has exactly one outgoing edge.

Worked Examples

Example 1

Input:

n = 5
p = [1, 2, 3, 4, 5]
Step Start Visited nodes Current node Cycle collected Cycle length
1 1 {} 1 [] -
2 1 {1} 1 [1] -
3 1 {1,2} 2 [1,2] -
4 1 {1,2,3} 3 [1,2,3] -
5 1 {1,2,3,4} 4 [1,2,3,4] -
6 1 {1,2,3,4,5} 5 [1,2,3,4,5] -
7 1 stop 1 full cycle 5

All nodes receive answer 5, which matches the fact that the entire permutation is one cycle.

This confirms that the algorithm correctly handles full-cycle permutations.

Example 2

Input:

n = 4
p = [2, 1, 4, 3]
Step Start Visited nodes Current node Cycle collected Cycle length
1 1 {} 1 [] -
2 1 {1} 1 [1] -
3 1 {1,2} 2 [1,2] 2
4 3 {} 3 [] -
5 3 {3} 3 [3] -
6 3 {3,4} 4 [3,4] 2

This shows two independent cycles, each of length 2, and confirms that visited tracking prevents recomputation.

Complexity Analysis

Measure Complexity Explanation
Time O(n) per query Each node is visited exactly once during cycle decomposition
Space O(n) Arrays for visited tracking and output storage

With n ≤ 200 and q ≤ 200, the total work is at most 40,000 node visits, which is trivial within limits.

Test Cases

import sys, io

def run(inp: str) -> str:
    sys.stdin = io.StringIO(inp)
    import sys
    input = sys.stdin.readline

    q = int(input())
    out_lines = []
    for _ in range(q):
        n = int(input())
        p = [0] + list(map(int, input().split()))
        
        visited = [False] * (n + 1)
        ans = [0] * (n + 1)
        
        for i in range(1, n + 1):
            if visited[i]:
                continue
            cycle = []
            cur = i
            while not visited[cur]:
                visited[cur] = True
                cycle.append(cur)
                cur = p[cur]
            for v in cycle:
                ans[v] = len(cycle)
        out_lines.append(" ".join(map(str, ans[1:])))
    
    return "\n".join(out_lines)

# provided samples
assert run("""6
5
1 2 3 4 5
3
2 3 1
6
4 6 2 1 5 3
1
1
4
3 4 1 2
5
5 1 2 4 3
""") == """1 1 1 1 1
3 3 3
2 3 3 2 1 3
1
2 2 2 2
4 4 4 1 4"""

# all self-loops
assert run("""1
3
1 2 3
""") == "1 1 1"

# two cycles
assert run("""1
6
2 1 4 3 6 5
""") == "2 2 2 2 2 2"

# single big cycle
assert run("""1
4
2 3 4 1
""") == "4 4 4 4"
Test input Expected output What it validates
identity permutation all 1s self-loop correctness
two disjoint swaps all 2s multiple cycles
single cycle all n full traversal correctness

Edge Cases

Self-loops such as p[i] = i are handled naturally because the traversal immediately returns to the starting node after visiting it once, producing cycle length 1. The visited marking ensures we stop correctly without infinite looping.

Large cycles where all nodes are connected in one loop are also handled correctly because the traversal continues until we revisit the start node, guaranteeing full coverage of the cycle before assignment.