CF 102433I - Error Correction

We have a collection of (N) distinct words. Every word uses exactly the same set of letters, so each word is simply a different permutation of the same letters. No letter appears twice inside one word.

CF 102433I - Error Correction

Rating: -
Tags: -
Solve time: 1m 30s
Verified: yes

Solution

Problem Understanding

We have a collection of (N) distinct words. Every word uses exactly the same set of letters, so each word is simply a different permutation of the same letters. No letter appears twice inside one word.

Two words conflict if one can be obtained from the other by swapping exactly two positions. The two positions do not have to be adjacent. We need to keep as many words as possible while keeping every pair of retained words conflict-free.

A useful way to think about the problem is as a graph. Each word is a vertex, and two vertices are connected when their words differ by one swap. The answer is the maximum independent set of this graph.

The constraint (N \le 500) is small enough for graph algorithms whose running time is around (O(N^2)), but it is far too large for enumerating all subsets. There are (2^{500}) possible subsets, so a direct maximum-independent-set search is completely infeasible. The words contain only lowercase letters, hence their length is at most 26, because no letter can occur twice. This small word length gives us another useful bound: from any word there are at most (\binom{26}{2}=325) different single-swap results.

There are several edge cases that a careless implementation can mishandle. If (N=1), for example, the input

1
a

has answer 1. There is no other word with which a can conflict. A solution that assumes every vertex has an opposite-side neighbor can incorrectly return 0.

A word of length two demonstrates that swapping adjacent positions is not a special operation. For

2
ab
ba

the answer is 1 because the two words are connected by swapping their only two positions. A solution that only checks some restricted set of swaps would miss this edge.

The other subtle case is when many words exist but none of them are one swap apart. For example,

3
abc
bca
cab

has answer 3. All three permutations have the same permutation parity, while one swap always changes parity. A solution that assumes the graph must contain many edges would unnecessarily remove vertices.

Finally, the fact that all words are unique matters. We never need to handle an edge from a word to itself. Swapping two distinct positions changes the word because every letter is distinct.

The official sample outputs are 3 for Sample 1, 8 for Sample 2, and 4 for Sample 3.

Approaches

The most direct brute-force idea is to construct the conflict graph and then try every subset of words, checking whether that subset contains a conflicting pair. This is correct because every possible candidate answer is explicitly considered. For a subset of (N) vertices, checking all pairs costs (O(N^2)), and there are (2^N) subsets, giving (O(2^N N^2)) time in the worst case. At (N=500), this is roughly (2^{500}\cdot250000) pair checks, which is not remotely practical.

Even constructing the graph by comparing every pair of words is already (O(N^2L)), where (L\le26). At the maximum constraints that means up to

[ \binom{500}{2}\cdot26 = 3,243,500 ]

character comparisons in the worst case. That part is manageable, but the general maximum-independent-set problem remains exponential.

The key observation is that every edge has a very specific effect on permutation parity. Assign every word a parity according to whether its permutation of the letters is even or odd. Swapping two positions changes the permutation parity exactly once, so every conflict edge connects an even word with an odd word.

The conflict graph is consequently bipartite.

This changes the problem completely. For a bipartite graph, the size of a maximum independent set is

[ |V|-\text{minimum vertex cover}. ]

By König's theorem, the minimum vertex cover in a bipartite graph has the same size as a maximum matching. Thus the answer is simply

[ N-\text{maximum matching}. ]

The other useful observation is that we do not need to compare every pair of words to find the edges. From a word of length (L), every possible one-swap neighbor can be generated by choosing two positions and swapping them. There are only (\binom L2) such candidates. A hash table tells us in constant expected time whether the resulting word is actually present in the input.

The brute force works because checking every pair gives the exact conflict graph, but fails when we try to solve maximum independent set directly. The parity observation turns that graph into a bipartite graph, where matching gives the answer in polynomial time. Generating neighbors by actual swaps also avoids the unnecessary (N^2) word comparisons.

Approach Time Complexity Space Complexity Verdict
Brute Force (O(2^N N^2)) (O(N^2)) Too slow
Optimal (O(NL^2 + E\sqrt N)) (O(NL^2 + N)) Accepted

Here (L\le26) is the word length and (E) is the number of conflict edges. Since a vertex has at most (\binom L2\le325) neighbors, (E\le O(NL^2)).

Algorithm Walkthrough

  1. Store every input word in a dictionary mapping the word to its vertex index. This lets us test whether a generated one-swap word belongs to the input in expected constant time.
  2. Choose the letters of the first word in sorted order and assign each letter a rank. Replace every word by the corresponding sequence of ranks. Since all words contain exactly the same distinct letters, every word is now a permutation of the same sequence of ranks.
  3. Compute the parity of each permutation by counting inversions. A word with an even number of inversions belongs to the left side of the bipartite graph, and a word with an odd number belongs to the right side.
  4. For every even-parity word, try every pair of positions (i<j). Swap those two characters and look up the resulting word in the dictionary. If it exists, add the corresponding vertex to the current word's adjacency list.

Swapping any two positions changes permutation parity, so a generated neighbor is automatically on the opposite side. We only generate edges from one side, which gives the standard representation needed for bipartite matching. 5. Run Hopcroft-Karp on this bipartite graph. Maintain a matching for every left vertex and every right vertex. Breadth-first search finds layers of possible augmenting paths, and depth-first search sends several augmenting paths through those layers. 6. Let the resulting matching size be (M). Return (N-M). König's theorem says that (M) is also the size of a minimum vertex cover, and removing such a cover leaves an independent set of exactly (N-M) vertices.

Why it works

The central invariant is that every conflict edge joins words of opposite permutation parity. A single transposition changes the parity of a permutation, so the conflict graph is bipartite with parity as its two sides.

In any graph, the complement of a vertex cover is an independent set, so a minimum vertex cover of size (C) gives an independent set of size (N-C). Conversely, the complement of any independent set is a vertex cover, so the largest independent set has size exactly (N-C), where (C) is the minimum vertex-cover size.

For bipartite graphs, König's theorem gives (C=M), where (M) is the maximum matching size. The algorithm constructs exactly the conflict graph, finds its maximum matching, and returns (N-M), which is consequently the largest possible swap-free set.

Python Solution

import sys
input = sys.stdin.readline

def solve():
    n = int(input())
    words = [input().strip() for _ in range(n)]

    index = {word: i for i, word in enumerate(words)}

    # Every word is a permutation of the same distinct letters.
    base = sorted(words[0])
    rank = {ch: i for i, ch in enumerate(base)}

    parity = [0] * n
    left = []

    for u, word in enumerate(words):
        a = [rank[ch] for ch in word]
        inv_parity = 0

        for i in range(len(a)):
            for j in range(i + 1, len(a)):
                inv_parity ^= (a[i] > a[j])

        parity[u] = inv_parity
        if inv_parity == 0:
            left.append(u)

    # adj[u] contains only vertices on the odd-parity side.
    adj = [[] for _ in range(n)]

    length = len(words[0])

    for u in left:
        s = list(words[u])

        for i in range(length):
            for j in range(i + 1, length):
                s[i], s[j] = s[j], s[i]
                v = index.get(''.join(s))

                if v is not None:
                    adj[u].append(v)

                s[i], s[j] = s[j], s[i]

    # Hopcroft-Karp maximum matching.
    pair_u = [-1] * n
    pair_v = [-1] * n
    dist = [-1] * n

    from collections import deque

    def bfs():
        q = deque()
        found = False

        for u in left:
            if pair_u[u] == -1:
                dist[u] = 0
                q.append(u)
            else:
                dist[u] = -1

        while q:
            u = q.popleft()

            for v in adj[u]:
                pu = pair_v[v]

                if pu == -1:
                    found = True
                elif dist[pu] == -1:
                    dist[pu] = dist[u] + 1
                    q.append(pu)

        return found

    sys.setrecursionlimit(2000)

    def dfs(u):
        for v in adj[u]:
            pu = pair_v[v]

            if pu == -1 or (
                dist[pu] == dist[u] + 1 and dfs(pu)
            ):
                pair_u[u] = v
                pair_v[v] = u
                return True

        dist[u] = -1
        return False

    matching = 0

    while bfs():
        for u in left:
            if pair_u[u] == -1 and dfs(u):
                matching += 1

    print(n - matching)

if __name__ == "__main__":
    solve()

The dictionary index is the bridge between permutation generation and graph construction. After swapping two characters in a word, index.get() immediately tells us whether that word belongs to the given set.

The inversion calculation uses only parity, not the full inversion count. The XOR operation is enough because only whether the count is odd or even matters. Since the word length is at most 26, the simple quadratic calculation is comfortably small.

The temporary list s is modified in place for each candidate swap. The characters are swapped back immediately after the dictionary lookup, so every pair of positions starts from the original word. Forgetting the second swap-back would make later candidates depend on earlier candidates and corrupt the graph.

The matching arrays have size (N), even though only vertices on the appropriate parity side are iterated as left vertices. Using the original vertex indices keeps the dictionary, adjacency lists, and matching arrays consistent.

The BFS builds distance layers from all currently unmatched left vertices. The DFS only follows edges that respect those layers, so each successful DFS augments the matching along a shortest available augmenting path. When BFS can no longer find an unmatched right endpoint reachable from a free left vertex, no augmenting path remains and the matching is maximum.

No integer can overflow in Python, and the answer is always between 0 and 500.

Worked Examples

Sample 1

The six words are all permutations of abc. Their inversion parities split them into two groups.

Word Parity Generated conflict neighbors Matching state
abc even acb, bac, cba matched with acb
acb odd abc, cab, bca matched with abc
cab even cba, abc, bca matched with cba
cba odd cab, bac, abc matched with cab
bac even bca, cba, acb unmatched
bca odd bac, acb, cab unmatched

The maximum matching has size 3. The graph is balanced between the two parity classes, and three disjoint conflict edges can be selected. Hence the maximum swap-free set has size (6-3=3), matching the sample output.

The trace demonstrates the central parity invariant. Every listed neighbor lies on the opposite side, so the original conflict graph really is bipartite.

Sample 2

For the eleven alerts permutations, the parity split is uneven. The matching algorithm does not need to find an independent set explicitly. It only needs to find how many vertices must be removed to eliminate every conflict.

Quantity Value
Number of words 11
Maximum matching 3
Minimum vertex cover 3
Maximum swap-free set 8

The matching contains three disjoint conflict edges. Once a minimum vertex cover of three vertices is removed, the remaining eight words contain no conflict edge. Thus the answer is (11-3=8), which agrees with the official sample.

This example shows why simply taking the smaller parity class is not always enough. An arbitrary subset of one parity is always swap-free, but the optimum can contain vertices from both parity classes when the conflict graph does not match all of one side.

Complexity Analysis

Measure Complexity Explanation
Time (O(NL^2 + E\sqrt N)) Each word generates (O(L^2)) swaps, and Hopcroft-Karp takes (O(E\sqrt N))
Space (O(NL^2 + N)) The adjacency graph stores at most (O(NL^2)) edges

Here (N\le500) and (L\le26). A vertex has at most 325 possible one-swap neighbors, so the graph has at most roughly (81{,}250) directed adjacency entries in the one-sided representation. This is easily within the intended limits, while the matching phase is polynomial and avoids the exponential (2^N) search.

Test Cases

import sys
import io
from itertools import permutations

def solve():
    n = int(input())
    words = [input().strip() for _ in range(n)]

    index = {word: i for i, word in enumerate(words)}

    base = sorted(words[0])
    rank = {ch: i for i, ch in enumerate(base)}

    parity = [0] * n
    left = []

    for u, word in enumerate(words):
        a = [rank[ch] for ch in word]
        p = 0

        for i in range(len(a)):
            for j in range(i + 1, len(a)):
                p ^= (a[i] > a[j])

        parity[u] = p
        if p == 0:
            left.append(u)

    length = len(words[0])
    adj = [[] for _ in range(n)]

    for u in left:
        s = list(words[u])

        for i in range(length):
            for j in range(i + 1, length):
                s[i], s[j] = s[j], s[i]

                v = index.get(''.join(s))
                if v is not None:
                    adj[u].append(v)

                s[i], s[j] = s[j], s[i]

    from collections import deque

    pair_u = [-1] * n
    pair_v = [-1] * n
    dist = [-1] * n

    def bfs():
        q = deque()
        found = False

        for u in left:
            if pair_u[u] == -1:
                dist[u] = 0
                q.append(u)
            else:
                dist[u] = -1

        while q:
            u = q.popleft()

            for v in adj[u]:
                pu = pair_v[v]

                if pu == -1:
                    found = True
                elif dist[pu] == -1:
                    dist[pu] = dist[u] + 1
                    q.append(pu)

        return found

    def dfs(u):
        for v in adj[u]:
            pu = pair_v[v]

            if pu == -1 or (
                dist[pu] == dist[u] + 1 and dfs(pu)
            ):
                pair_u[u] = v
                pair_v[v] = u
                return True

        dist[u] = -1
        return False

    matching = 0

    while bfs():
        for u in left:
            if pair_u[u] == -1 and dfs(u):
                matching += 1

    print(n - matching)

def run(inp: str) -> str:
    old_stdin = sys.stdin
    old_stdout = sys.stdout

    try:
        sys.stdin = io.StringIO(inp)
        sys.stdout = io.StringIO()
        solve()
        return sys.stdout.getvalue()
    finally:
        sys.stdin = old_stdin
        sys.stdout = old_stdout

assert run("""6
abc
acb
cab
cba
bac
bca
""") == "3\n", "sample 1"

assert run("""11
alerts
alters
artels
estral
laster
ratels
salter
slater
staler
stelar
talers
""") == "8\n", "sample 2"

assert run("""6
ates
east
eats
etas
sate
teas
""") == "4\n", "sample 3"

assert run("""1
a
""") == "1\n", "minimum size"

assert run("""2
ab
ba
""") == "1\n", "single conflict edge"

assert run("""3
abc
bca
cab
""") == "3\n", "all vertices have the same parity"

# Maximum N = 500.
# Every selected permutation is even, so no two selected words can be
# connected by one swap. The answer must be 500.
even_words = []

for p in permutations("abcdefgh"):
    inv = 0
    for i in range(8):
        for j in range(i + 1, 8):
            inv += p[i] > p[j]

    if inv % 2 == 0:
        even_words.append(''.join(p))

    if len(even_words) == 500:
        break

max_input = "500\n" + "\n".join(even_words) + "\n"
assert run(max_input) == "500\n", "maximum N"

print("all tests passed")
Test input Expected output What it validates
1 / a 1 Minimum (N), word length 1, and an isolated vertex
2 / ab, ba 1 The smallest possible conflict graph and exact one-swap detection
3 / abc, bca, cab 3 All vertices having the same parity and no conflict edges
500 even permutations of abcdefgh 500 Maximum (N), large graph input, and the fact that same-parity words are always compatible

Edge Cases

The minimum-size case is

1
a

The algorithm puts the only word on one parity side, generates no useful swap because the word has length one, and obtains a matching of size zero. The returned value is (1-0=1).

The smallest possible conflict is

2
ab
ba

The word ab has even parity and ba has odd parity. Swapping positions 0 and 1 in ab produces ba, so the graph contains one edge. Hopcroft-Karp matches those two vertices, giving matching size 1 and answer (2-1=1).

A case with many words but no conflicts is

3
abc
bca
cab

All three permutations are even. Every single swap changes parity, so none of these three words can be transformed into another listed word using one swap. The adjacency graph is empty, the matching has size zero, and the algorithm returns 3.

The maximum-(N) case uses 500 even permutations of abcdefgh. Every generated one-swap neighbor is odd, but no odd word is present in the input. Consequently the adjacency graph is empty despite having 500 vertices. The matching remains zero and the answer is 500. This catches implementations that accidentally assume a large input must contain conflicts.

The length boundary is also handled naturally. A word can contain at most 26 letters because letters cannot repeat and only lowercase English letters are available. The algorithm tries all (\binom L2) pairs, so for (L=26) it performs only 325 swap attempts per word. There is no off-by-one issue around the final character because both loops use range(length) and require (i<j).