Skip to content
Ethan Li
Go back

An Original Problem: Minimum Swap Distance for Pattern Elimination

Edit page

Problem

Given a string SS and a pattern TT, where m=Tm=|T|, one operation chooses any two positions in SS and swaps their characters. Find the minimum number of operations needed so that SS no longer contains TT as a substring.

Constraints: T3|T|\ge 3, adjacent characters in TT are distinct (TiTi+1T_i\ne T_{i+1}), and SS may contain arbitrary additional characters.

Because swapping preserves the multiset of characters, the problem is equivalent to finding, among all rearrangements of SS that avoid TT, one with minimum swap distance from SS (the Cayley distance in the permutation case).


Two properties

The solution repeatedly uses two facts that follow directly from the statement:

  1. One swap changes at most two positions. Every lower bound below comes from this fact.
  2. Adjacent characters in TT are distinct. Therefore, any segment containing two equal adjacent characters cannot match TT. This lets us destroy target occurrences without creating new ones.

1. Example

T=abababa(m=7)T=\texttt{abababa}\qquad(m=7)

It satisfies the constraint, and its first and last characters are equal: T0=T6=aT_0=T_6=\texttt{a}. This will matter later.

S=ababababababazzzzzzzzzzzzzzzzzabababazzzabababazzzabababazzzS=\texttt{ababababababazzzzzzzzzzzzzzzzzabababazzzabababazzzabababazzz}

The character z does not appear in TT and only separates groups of occurrences.

Use KMP to find every starting position of TT in SS. There are seven:

a=[0,2,4,6overlapping prefix group,  30,40,50three isolated occurrences]a=[\,\underbrace{0,\,2,\,4,\,6}_{\text{overlapping prefix group}},\ \ \underbrace{30,\,40,\,50}_{\text{three isolated occurrences}}\,]

Visualized on SS:

Seven occurrences of T in S: #0-#3 overlap into one cluster, while #4-#6 are isolated

Number these occurrences from left to right as #0, #1, ..., #6. Occurrence ii starts at aia_i and occupies [ai,ai+m1][a_i,a_i+m-1].

Because adjacent characters in TT differ, the starting positions of two occurrences differ by at least two. The prefix group starts at 0,2,4,60,2,4,6, exactly two apart.


2. Shared positions and blocks

Destroying an occurrence requires changing at least one position inside it.

The first four occurrences overlap. If one position belongs to several occurrences, changing that single position destroys all of them. This suggests the following reformulation:

Partition the occurrences from left to right into blocks such that all occurrences in each block share at least one common position. Changing one shared position destroys the entire block.

For occurrences #i through #j, their common intersection is [aj,ai+m1][a_j,a_i+m-1]. It is nonempty exactly when

ajaim1.a_j-a_i\le m-1.

Its width is m(ajai)m-(a_j-a_i). This width gives two kinds of blocks.


3. Flexible and rigid blocks

Flexible block: common width at least 2

The condition is ajaim2a_j-a_i\le m-2.

Consider #0,#1,#2, starting at 0,2,40,2,4 with m=7m=7:

Flexible block: #0, #1, and #2 share positions 4, 5, and 6

Their common intersection has three positions. The formula gives 7(40)=37-(4-0)=3. Any of these shared positions can be changed, so the block is flexible.

Rigid block: common width exactly 1

The condition is ajai=m1a_j-a_i=m-1.

Consider #0 and #3, whose starts differ by 6=m16=m-1:

Rigid block: #0 and #3 share only position 6

The only shared position is p=6p=6, so it must be changed. This position is simultaneously:

Therefore a rigid block can exist only if T0=Tm1T_0=T_{m-1}. Its shared character is fixed as

c=T0=Tm1.c=T_0=T_{m-1}.

For abababa, both are a, so rigid blocks are possible. If the endpoints of TT differ, the algorithm immediately takes the simpler branch with no rigid blocks.

A flexible block offers several shared positions to change. A rigid block offers exactly one, whose original character must be cc. All later complexity comes from this distinction.


4. The partition DAG

There are several ways to partition the occurrences. They produce different numbers of flexible and rigid blocks, and therefore different costs. We represent every valid partition in a directed acyclic graph.

4.1 State order

Process occurrences from left to right. A state ii means that occurrence #i is the leftmost occurrence not yet covered by a block. The next block must begin with #i.

4.2 Two maximal choices

At state ii, only two maximal choices matter.

(A) Maximum flexible block. Starting from #i, include as many following occurrences as possible while the first and last starts differ by at most m2m-2. This choice always exists.

(B) Rigid block. If the immediately following occurrence outside the maximum flexible block starts exactly at ai+(m1)a_i+(m-1), include it as well. The common width shrinks to one, producing a rigid block.

Each choice becomes an edge in the DAG.

4.3 Enumerating the example

Here m=7m=7, so a flexible block permits a span at most five and a rigid block requires a span of six. The occurrence starts are a=[0,2,4,6,30,40,50]a=[0,2,4,6,30,40,50].

State iiMaximum flexible blockFlexible destinationRigid choice
00{#0,#1,#2}\{\#0,\#1,\#2\}state 33{#0,#1,#2,#3}4\{\#0,\#1,\#2,\#3\}\to4
33{#3}\{\#3\}state 44none
44{#4}\{\#4\}state 55none
55{#5}\{\#5\}state 66none
66{#6}\{\#6\}state 77none

States 11 and 22 never appear as starting states because the flexible edge from state 00 already covers them.

4.4 Paths

The states and choices form the following DAG, with rigid edges shown in red:

Partition DAG: every path from state 0 to the terminal state is a complete partition

There are two paths in the example:

Define

gi=min{jaj>ai+m2}.g_i=\min\{j\mid a_j>a_i+m-2\}.

The flexible edge is igii\to g_i. If agi=ai+(m1)a_{g_i}=a_i+(m-1), there is also a rigid edge igi+1i\to g_i+1.

The next task is to compute the cost of a path.


5. Cost of a path: C(u,f)C(u,f)

Suppose a path contains uu flexible blocks and ff rigid blocks. Its minimum number of swaps is

C(u,f)=max ⁣(u+f2, f).C(u,f)=\max\!\Big(\Big\lceil\tfrac{u+f}{2}\Big\rceil,\ f\Big).

The two terms are lower bounds, and both can be achieved simultaneously.

5.1 Pairing lower bound

One swap changes at most two positions. Every block needs at least one changed position, and distinct blocks use distinct positions. Therefore, for kk swaps,

u+f2kku+f2.u+f\le2k\quad\Longrightarrow\quad k\ge\Big\lceil\tfrac{u+f}{2}\Big\rceil.

Equivalently, one swap can destroy at most two blocks.

5.2 Rigid-block lower bound

The unique shared position of every rigid block initially contains c=T0=Tm1c=T_0=T_{m-1} and must be changed to another character.

Let AA be the set of positions whose original character is cc. Initially, no position in AA contains a non-cc character. One swap can increase that number by at most one. After kk swaps, at most kk originally-cc positions can contain non-cc characters. Since every rigid block needs one, fkf\le k.

5.3 Upper bound: pairing with safe swaps

We construct swaps that destroy only their target blocks and create no new occurrence of TT. This uses the second property: two equal adjacent characters make a match impossible.

In the common part of a flexible block, choose a position and change it to match a neighbor. For example, turn Tm2T_{m-2} into Tm1T_{m-1}. The resulting equal adjacent pair destroys every occurrence in the block. Pair this target position with a target position in another block, and one swap destroys both blocks. A rigid block works similarly, except its unique cc position is forced.

Consider isolated flexible blocks #4 and #5, both equal to abababa. Choose the penultimate character of #4 (position 35, b) and the final character of #5 (position 46, a) and swap them:

Safe swap: swapping positions 35 and 46 creates aaa in #4 and bb in #5

Both occurrences disappear in one swap, and no new abababa appears. The new aaa and bb segments cannot occur inside TT, whose adjacent characters always differ.

Combining such swaps gives the bound: pair rigid blocks with flexible ones when possible, pair remaining flexible blocks together, and handle leftover rigid blocks individually. The result is exactly

max ⁣(u+f2,f).\max\!\Big(\Big\lceil\tfrac{u+f}{2}\Big\rceil,f\Big).

Some useful construction details:

  1. A flexible block has several safe candidate positions near its ends. One block can use a change xyx\to y while another uses the complementary yxy\to x, allowing a direct swap.
  2. To keep multiple swaps from interfering, order flexible blocks by position, assign one change direction to the left half and the complementary direction to the right half, then pair across the halves.
  3. A rigid block simply changes its unique cc position. It may pair with a flexible block, while a leftover rigid or flexible block can be handled in one swap.

For path 2 in the example, u=3u=3 and f=1f=1:

Path 2 pairing: one rigid block pairs with #4, while #5 pairs with #6

5.4 The two example paths

PathBlocksuuffCost
all flexibleflex{#0,#1,#2}, flex{#3}, flex{#4}, flex{#5}, flex{#6}5033
one rigidrigid{#0..#3}, flex{#4}, flex{#5}, flex{#6}312\mathbf2

The optimum is two swaps. The rigid edge merges two flexible blocks into one, reducing the total block count from five to four without making ff the bottleneck.


6. Minimizing C(u,f)C(u,f)

We now need to minimize

C(u,f)=max ⁣(u+f2,f)C(u,f)=\max\!\Big(\Big\lceil\tfrac{u+f}{2}\Big\rceil,f\Big)

over all paths from 00 to rr.

6.1 Reduction to FbF_b

For a fixed total number of blocks b=u+fb=u+f, only the minimum achievable number of rigid blocks matters. Define

Fb=minimum rigid blocks among partitions with exactly b blocks.F_b=\text{minimum rigid blocks among partitions with exactly }b\text{ blocks}.

Then

answer=minbmax ⁣(b2,Fb).\boxed{\text{answer}=\min_b\max\!\Big(\Big\lceil\tfrac b2\Big\rceil,F_b\Big).}

6.2 An O(r2)O(r^2) DP

Run dynamic programming on the DAG. Every edge contributes exactly one block; a rigid edge additionally contributes one rigid block.

Let dp[i] map a number of blocks bb to the minimum rigid count for a path from state ii to rr.

There are O(r)O(r) states and each table has size O(r)O(r), for O(r2)O(r^2) total time.

In the example,

F4=1,F5=0,F_4=1,\qquad F_5=0,

so

min(max(4/2,1),max(5/2,0))=min(2,3)=2.\min\big(\max(\lceil4/2\rceil,1),\max(\lceil5/2\rceil,0)\big)=\min(2,3)=\mathbf2.

This is sufficient for moderate input sizes. The remaining sections optimize for as many as 2×1062\times10^6 occurrences.

6.3 Convexity of FbF_b

FbF_b has two useful properties:

The answer becomes visible when the two bounds are plotted together:

The answer is the minimum over max(ceil(b/2), F_b)

The figure is illustrative; it uses a sample convex sequence to show the shape.

Instead of enumerating bb, assign cost λ\lambda to every flexible block and 2λ2-\lambda to every rigid block:

H(λ)=minpaths(λu+(2λ)f),0λ1.H(\lambda)=\min_{\text{paths}}\big(\lambda u+(2-\lambda)f\big),\qquad0\le\lambda\le1.

For fixed λ\lambda, this is a linear shortest-path DP on the DAG: flexible edges cost λ\lambda and rigid edges cost 2λ2-\lambda. A reverse scan evaluates it in O(r)O(r).

Convexity of FbF_b gives

answer=12max0λ1H(λ).\text{answer}=\left\lceil\frac12\max_{0\le\lambda\le1}H(\lambda)\right\rceil.

HH is concave and piecewise linear. Binary-search its slope to find the maximum. To avoid floating-point errors, search a grid with denominator 2502^{50} and compute the final intersection exactly with __int128.


7. Complete algorithm

  1. Use KMP to find all occurrence starts aia_i. If there are none, return zero.
  2. Use two pointers to compute every gig_i, the destination of the maximum flexible block.
  3. If T0Tm1T_0\ne T_{m-1}, no rigid block exists. Greedily follow flexible edges, count the blocks bb, and return b/2\lceil b/2\rceil.
  4. Otherwise maximize H(λ)H(\lambda), or use the O(r2)O(r^2) DP for smaller inputs, and return Hmax/2\lceil H_{\max}/2\rceil.

8. Complexity

KMP and DAG construction take O(S+T+r)O(|S|+|T|+r). Each fixed-λ\lambda DP takes O(r)O(r), and the binary search uses about 54 evaluations. The total is

O ⁣(S+T+occlogS),space O(T+r).O\!\Big(\sum|S|+\sum|T|+\sum\operatorname{occ}\log|S|\Big),\qquad\text{space }O(|T|+r).

The proof never requires SS and TT to use the same alphabet, so arbitrary extra characters in SS do not affect correctness.


9. Reference implementation

The core is a linear DP on the DAG plus slope binary search. For smaller inputs, the rigid branch can be replaced with the O(r2)O(r^2) table DP from Section 6.2.

#include <bits/stdc++.h>
using namespace std;
using i128 = __int128_t;

struct Line { int c; int d; }; // c = 2*rigid, d = flexible - rigid
struct Node { Line mn; Line mx; };

static inline i128 get_value(const Line& line, long long num, long long den) {
    return (i128)line.c * den + (i128)line.d * num;
}
static inline long long ceil_div(i128 a, i128 b) {
    return (long long)((a + b - 1) / b);
}

// Return every starting position where t occurs in s.
vector<int> find_occurrences(const string& s, const string& t) {
    const int n = (int)s.size(), m = (int)t.size();
    vector<int> pi(m), occ;
    for (int i = 1, j = 0; i < m; ++i) {
        while (j > 0 && t[i] != t[j]) j = pi[j - 1];
        if (t[i] == t[j]) ++j;
        pi[i] = j;
    }
    for (int i = 0, j = 0; i < n; ++i) {
        while (j > 0 && s[i] != t[j]) j = pi[j - 1];
        if (s[i] == t[j]) ++j;
        if (j == m) { occ.push_back(i - m + 1); j = pi[j - 1]; }
    }
    return occ;
}

long long solve_query(const string& s, const string& t) {
    const int m = (int)t.size();
    assert(m >= 3);

    vector<int> occ = find_occurrences(s, t);
    const int r = (int)occ.size();
    if (r == 0) return 0;

    // go[i] is the state reached after taking the maximum flexible block.
    vector<int> go(r);
    for (int i = 0, j = 0; i < r; ++i) {
        j = max(j, i + 1);
        const int limit = occ[i] + m - 2;
        while (j < r && occ[j] <= limit) ++j;
        go[i] = j;
    }

    // Different endpoints imply that rigid blocks cannot exist.
    if (t.front() != t.back()) {
        int blocks = 0;
        for (int i = 0; i < r; i = go[i]) ++blocks;
        return (blocks + 1LL) / 2;
    }

    // At each state, keep optimal lines with minimum and maximum slope.
    vector<int> c_min(r + 1), d_min(r + 1), c_max(r + 1), d_max(r + 1);

    auto evaluate = [&](long long num, long long den) -> Node {
        c_min[r] = d_min[r] = c_max[r] = d_max[r] = 0;
        for (int i = r - 1; i >= 0; --i) {
            const int j = go[i];
            Line flex_min{ c_min[j], d_min[j] + 1 };
            Line flex_max{ c_max[j], d_max[j] + 1 };
            Line result_min = flex_min, result_max = flex_max;
            i128 best = get_value(flex_min, num, den);

            // A rigid edge exists exactly when the next start is occ[i]+m-1.
            if (j < r && occ[j] == occ[i] + m - 1) {
                Line rigid_min{ c_min[j + 1] + 2, d_min[j + 1] - 1 };
                Line rigid_max{ c_max[j + 1] + 2, d_max[j + 1] - 1 };
                i128 cand = get_value(rigid_min, num, den);
                if (cand < best) {
                    best = cand;
                    result_min = rigid_min;
                    result_max = rigid_max;
                } else if (cand == best) {
                    if (rigid_min.d < result_min.d) result_min = rigid_min;
                    if (rigid_max.d > result_max.d) result_max = rigid_max;
                }
            }
            c_min[i] = result_min.c; d_min[i] = result_min.d;
            c_max[i] = result_max.c; d_max[i] = result_max.d;
        }
        return Node{ {c_min[0], d_min[0]}, {c_max[0], d_max[0]} };
    };

    constexpr long long DEN = 1LL << 50;

    Node at_zero = evaluate(0, DEN);
    if (at_zero.mn.d <= 0)
        return ceil_div(get_value(at_zero.mn, 0, DEN), (i128)2 * DEN);

    Node at_one = evaluate(DEN, DEN);
    if (at_one.mx.d >= 0)
        return ceil_div(get_value(at_one.mn, DEN, DEN), (i128)2 * DEN);

    long long left = 0, right = DEN;
    while (right - left > 1) {
        const long long mid = (left + right) >> 1;
        Node cur = evaluate(mid, DEN);
        if (cur.mn.d <= 0 && cur.mx.d >= 0)
            return ceil_div(get_value(cur.mn, mid, DEN), (i128)2 * DEN);
        if (cur.mn.d > 0) left = mid;
        else right = mid;
    }

    Node left_node = evaluate(left, DEN);
    Node right_node = evaluate(right, DEN);
    Line lhs = left_node.mn;
    Line rhs = right_node.mx;
    const long long den = lhs.d - rhs.d;
    const i128 h_num = (i128)lhs.d * rhs.c - (i128)rhs.d * lhs.c;
    return ceil_div(h_num, (i128)2 * den);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    int Q; cin >> Q;
    while (Q--) {
        string S, T; cin >> S >> T;
        cout << solve_query(S, T) << '\n';
    }
    return 0;
}

Why the restrictions matter


Edit page
Share this post:

Previous Post
Judgment Is Not a Moat, but a Water Source