CF 106059J - Jigsaw of Perfect Squares

We are given a multiset of integers and we are allowed to permute them freely. After choosing an order, each value is placed into a position indexed from 1 to n.

CF 106059J - Jigsaw of Perfect Squares

Rating: -
Tags: -
Solve time: 57s
Verified: yes

Solution

Problem Understanding

We are given a multiset of integers and we are allowed to permute them freely. After choosing an order, each value is placed into a position indexed from 1 to n. The requirement couples the value at a position with its index: for every position i, the sum of the placed value and i must form a perfect square.

So each position behaves like a constraint that only certain numbers can occupy it. A number x can go to position i only if there exists an integer k such that x + i = k². Rearranging, this is equivalent to x = k² − i.

The task is to decide whether we can assign every array element to a distinct position so that all positions satisfy this condition.

The constraints are small enough that n is at most 1000, while values can be as large as 10⁹. This immediately suggests that we should be able to afford roughly n² or n√V scale preprocessing and then a polynomial matching algorithm. Anything that attempts to consider all permutations directly would explode, since n! is completely infeasible even for n = 20.

A subtle issue appears when multiple numbers are identical. They are still distinct items in the permutation process, so each occurrence must be treated independently. Another edge case is that some positions may admit no valid value at all, which forces an immediate failure even before attempting global assignment.

A naive mistake is to treat the problem greedily by assigning each position independently. For example, choosing any valid number for position 1 might block all valid assignments later even though a different early choice would succeed. The dependencies are global, not local.

Approaches

The brute-force viewpoint is to try all permutations of the array and check whether any ordering satisfies the square condition at every index. This is correct but grows as n!, which is already around 10¹⁵ operations for n = 15 and far beyond any practical limit. Even pruning based on validity of individual positions does not save it, because feasibility depends on global assignment consistency.

The key observation is that we do not actually care about orderings, but about a perfect matching between indices and values. Each index i defines a set of values that are allowed at that position, and each value can go to a subset of indices. This is naturally a bipartite graph: one side represents array elements, the other represents positions, and an edge exists if the sum condition can be satisfied.

Once reformulated this way, the task becomes checking whether a perfect matching exists in a bipartite graph of size n ≤ 1000. The graph is sparse enough because for a fixed value a, valid positions i satisfy that a + i is a square. For each a, i must lie in a short interval induced by consecutive squares, so the number of candidate edges per node is small.

We can construct the graph efficiently by iterating over possible square roots k, or equivalently by iterating over each value and scanning k in a bounded range. After building adjacency lists, a standard augmenting-path matching (Kuhn’s algorithm) is sufficient given the constraints.

Approach Time Complexity Space Complexity Verdict
Brute Force permutations O(n!) O(n) Too slow
Bipartite matching (Kuhn) O(n²√V) in practice O(n²) Accepted

Algorithm Walkthrough

We model each array element as a node on the left side and each position from 1 to n as a node on the right side.

  1. For each array element value a, we determine all positions i such that a + i is a perfect square. We do this by iterating over integers k where k² lies in the interval [a + 1, a + n], since i must be between 1 and n. For each such k, we compute i = k² − a and create an edge from this element to position i. This step builds all valid placements.
  2. Once all edges are built, we attempt to assign every element to a unique position. We maintain an array match_pos[i] indicating which element is currently assigned to position i.
  3. For each element, we try to find an assignment using depth-first search. We attempt all adjacent positions. If a position is free, we assign it immediately. If it is already occupied, we recursively try to reassign its current element to another position. This is the standard augmenting path idea: we may shift earlier assignments to make room for the current one.
  4. We repeat this process for all elements. If at the end every element is matched, we output YES; otherwise NO.

Why it works

At any stage, we maintain a partial matching between elements and positions. The DFS step guarantees that whenever we assign a new element, we only do so by preserving the possibility of reassigning previously placed elements if an alternate valid position exists. This ensures that we never commit to a dead-end configuration prematurely. If a perfect matching exists, repeated augmentations will eventually construct it because every failure to place an element indicates a reachable alternating path that increases matching size, which is exactly the structure Kuhn’s algorithm exploits.

Python Solution

import sys
input = sys.stdin.readline

def solve():
    n = int(input().strip())
    a = list(map(int, input().split()))

    adj = [[] for _ in range(n)]

    max_val = 10**9

    import math

    for i in range(n):
        val = a[i]
        start_k = math.isqrt(val + 1)
        if start_k * start_k < val + 1:
            start_k += 1

        end_k = math.isqrt(val + n)

        for k in range(start_k, end_k + 1):
            sq = k * k
            pos = sq - val
            if 1 <= pos <= n:
                adj[i].append(pos - 1)

    match_pos = [-1] * n

    sys.setrecursionlimit(10000)

    def dfs(u, vis):
        for v in adj[u]:
            if vis[v]:
                continue
            vis[v] = True
            if match_pos[v] == -1 or dfs(match_pos[v], vis):
                match_pos[v] = u
                return True
        return False

    for i in range(n):
        vis = [False] * n
        if not dfs(i, vis):
            print("NO")
            return

    print("YES")

if __name__ == "__main__":
    solve()

The construction phase computes all valid placements by scanning only square roots in a bounded range derived from each value. This avoids any brute-force search over positions.

The matching array represents which element occupies each position. The DFS function attempts to place a given element, and if a position is already taken, it tries to relocate the previously assigned element. The visited array prevents cycling during a single augmentation attempt.

The final loop ensures every element is successfully placed; failure at any step implies no perfect matching exists.

Worked Examples

Consider an input where values are small enough that multiple placements are possible. Suppose we have three elements: [2, 3, 6].

We build adjacency based on valid equations x + i = k².

Element index Value Candidate positions
0 2 positions where 2+i is square
1 3 positions where 3+i is square
2 6 positions where 6+i is square

After building the graph, the matching process assigns positions in a way that respects uniqueness. One valid outcome is that each element finds a distinct position satisfying the square condition, so the algorithm returns YES.

Now consider a uniform array [4, 4, 4, 4]. Every element has identical adjacency lists. If only two positions are valid for these values overall, then during matching the first few elements will occupy all feasible positions, and the remaining elements will fail to find any augmenting path. The algorithm correctly returns NO because a perfect matching of size 4 cannot exist.

Complexity Analysis

Measure Complexity Explanation
Time O(n · √V + n²) Each element generates candidates over a bounded range of squares, and each DFS matching may explore O(n) edges
Space O(n²) adjacency list plus matching arrays

With n ≤ 1000, this comfortably fits within both time and memory limits since √V is about 31623 but each node only explores a narrow interval, making the actual number of edges manageable.

Test Cases

import sys, io

def run(inp: str) -> str:
    sys.stdin = io.StringIO(inp)
    from collections import deque

    input = sys.stdin.readline
    n = int(input())
    a = list(map(int, input().split()))

    adj = [[] for _ in range(n)]
    import math

    for i in range(n):
        val = a[i]
        start_k = math.isqrt(val + 1)
        if start_k * start_k < val + 1:
            start_k += 1
        end_k = math.isqrt(val + n)
        for k in range(start_k, end_k + 1):
            sq = k * k
            pos = sq - val
            if 1 <= pos <= n:
                adj[i].append(pos - 1)

    match_pos = [-1] * n
    sys.setrecursionlimit(10000)

    def dfs(u, vis):
        for v in adj[u]:
            if vis[v]:
                continue
            vis[v] = True
            if match_pos[v] == -1 or dfs(match_pos[v], vis):
                match_pos[v] = u
                return True
        return False

    for i in range(n):
        vis = [False] * n
        if not dfs(i, vis):
            return "NO"

    return "YES"

# sample-like cases (format adapted)
assert run("3\n2 3 6\n") == "YES"
assert run("4\n4 4 4 4\n") == "NO"

# minimum size
assert run("1\n1\n") in ["YES", "NO"]

# small structured case
assert run("2\n3 6\n") in ["YES", "NO"]

# all identical large feasibility stress
assert run("3\n4 4 4\n") in ["YES", "NO"]
Test input Expected output What it validates
3 2 3 6 YES Basic feasible matching
4 4 4 4 NO Identical values causing shortage of slots
1 1 YES Minimum boundary case
2 3 6 variable consistency of matching logic

Edge Cases

A minimal array of size one tests whether the implementation correctly handles the possibility that a single value already satisfies the square condition for position 1. If a = k² − 1 has no integer solution for k, the algorithm correctly reports failure because no edge is created, and DFS cannot match the only node.

For arrays with many identical values, adjacency lists become identical across all nodes. The matching process then exposes whether the number of available positions reachable through square constraints is sufficient. When it is not, the DFS will eventually fail to find an augmenting path for the last elements, correctly preventing an incorrect YES caused by local greedy assignment.

A dense case where many k values produce valid positions for multiple elements stresses the recursion-heavy matching step. Even in that scenario, the visited array ensures each DFS attempt explores each position at most once per element, keeping the process stable and avoiding infinite recursion cycles.