CF1530G What a Reversal 题解
题意简述
有两个长为 \(n\) 的 \(01\) 串 \(S\) 和 \(T\),你可以进行至多 \(4n\) 次操作,每次翻转(注意不是反转)\(S\) 的一个恰有 \(k\) 个 \(1\) 的子串,目标串是 \(T\)。判断无解或者构造方案。\(\sum n \leq 2000\)
题解
这个题太神了啊……想了好多天才想出来。(为啥小 t 的构造题都这么难
首先 \(k=0\) 或者 \(k \geq n\) 的情况是 trivial 的,特判一下就好了。接下来说 \(1 \leq k < n\) 的情况。
如果 \(S\) 和 \(T\) 中 \(1\) 的个数不一样我们也想都不用想。
结论:操作是可逆的。
这个结论的正确性是显然的。这意味着我们只要把 \(S\) 和 \(T\) 都通过某一操作变成同一个串就好了。接下来做的事情目标都是把 \(S,T\) 在 \(2n\) 步之内变成同一个串。
然后我们考虑 \(S\) 作为 \(01\) 串的形式不是很容易操作,进行一步转化:
令 \(p_i\) 表示 \(S\) 中第 \(i\) 个 \(1\) 之后有几个连续的 \(0\)。比如 \(S = 1001100011\) 可以表示成 \(p = [0, 2, 0, 3, 0, 0]\)。
令 \(m\) 表示 \(S\) 中有几个 \(1\),\(a_i\) 表示第 \(i\) 个 \(1\) 在 \(S\) 中的位置。令 \(a_0 = 0\),\(a_{m + 1} = n + 1\)。
结论:翻转 \(S\) 的一个包含第 \(l\) 个到第 \(r\) 个 \(1\) 的合法子串,相当于把 \(p_{l..r-1}\) 翻转,并且把 \(p_{l-1}\) 和 \(p_r\) 变成 \(p_{l-1}'\) 和 \(p_r'\) 满足 \(p_{l-1}' + p_r' = p_{l-1}+p_{r}\)。
这个结论的正确性也是显然的。
我们对于每一个 \(i > k\) 并且 \(p_i > 0\) 的 \(i\) 进行操作:翻转 \([a_{i - k + 1},a_{i + 1} - 1]\) 这个区间。这个操作做了什么?
直观上考虑,这个操作使得最后若干位都是 \(1\),把原本在后面的一段 \(0\) 放到前面去了。
从对 \(p\) 的贡献考虑,这个操作翻转了 \(p_{i - k + 1 .. i}\),并且把 \(p_{i + 1}\) 变成 \(0\),\(p_{i - k}\) 变成 \(p_{i - k} + p_{i + 1}\)。相当于把后面的 \(p\) 加到前面去了。
我们反复执行这个操作,就能使得所有的 \(0\) 都在第 \(k\) 个 \(1\) 之前,即 \(\forall i > k, p_i = 0\)。
这个至多会使用 \(m-k\) 个操作。
然后考虑处理接下来的情况。如果我们翻转 \(p_{1..k-1}\) 和 \(p_{2..k}\),则显然 \(p_1, p_2, p_3, \ldots, p_{k-2},p_{k-1}, p_k\) 就会变成 \(p_{k-1}, p_k, p_1, p_2, \ldots, p_{k-2}\)。
我们考虑在翻转 \(p_{1..k-1}\) 的时候把 \(p_k\) 全部加到 \(p_0\) 上面去,即翻转区间 \([a_1, a_{k+1}-1]\)。这样我们可以把 \(p_k\) 变成 \(0\)。
容易发现 \(p_{1..k}\) 发生了一个长度为 \(2\) 的循环移位。
Case 1
如果 \(k\) 是奇数,那么在 \(k\) 次移位之内 \(p_1, p_2, \ldots, p_k\) 中的每一个数都肯定到达过一次 \(p_k\) 的位置。(在每次操作时 \(p_k\) 位置上面的数分别是:\(p_k, p_{k-2}, p_{k-4}, p_{k-6}, \ldots, p_1, p_{k-1}, p_{k-3}, \ldots, p_2\)。)
因为每次移位把 \(p_k\) 位置上的数清零了,所以 \(k\) 次移位一定能够使 \(p_{1..k}\) 的数都是 \(0\),\(p_0\) 的值为 \(\sum _{i=1} ^k p_i\) 即 \(m\)。
所以我们可以在至多 \(2k + m - k = m + k \leq 2n\) 次操作内把 \(S\) 变成 \(0000..0011111\) 这种形式。(\(0\) 全部在 \(1\) 的前面)我们对于 \(T\) 进行同样的操作再把顺序倒过来就行了。
Case 2
如果 \(k\) 是偶数的话,我们明显并不能把所有的 \(p_i\) 都加到 \(p_0\) 上面。因为我们无论如何操作,循环移位的话总是有一半的位置不能到达 \(p_k\)。
为什么呢?
因为奇数总是管奇数,偶数总是管偶数。
那你不是把奇数都放到一个数,把偶数放到一个数就好了吗?
有道理啊/qd/qd/qd/qd/qd
结论:如果 \(k\) 是偶数,则 \(\sum_{x\text{ is even}} p_x\) 和 \(\sum _{x \text{ is odd}} p_x\) 是一个定值。
因为奇数总是管奇数,偶数总是管偶数。
那你就把奇数都放到一个数,把偶数放到一个数就好了。
具体的,我们在进行翻转 \(p_{1..k}\) 的时候把 \(p_k\) 加到 \(p_0\) 上面,在翻转 \(p_{2..k+1}\) 的时候把 \(p_1\) 加到 \(p_{k+1}\) 上。这样,在 \(k\) 次循环移位之后,我们一定可以把奇数位的都加到 \(p_{k+1}\),偶数位的都加到 \(p_0\)。
所以我们可以至多 \(m - k + 2k = m + k \leq 2n\) 次操作内把 \(S\) 变成 \(000..00111111111100..01111111\) 这种形式(里面第一个连续 \(1\) 段一定是 \(k\) 个 \(1\))。
实现有点细节,不知道为什么有些人写的很简单。
#include <bits/stdc++.h>
const int N = 2005;
int n, k, m, p[N], q[N];
char s[N], t[N];
std::vector<std::pair<int, int>> r1, r2;
// p[] & q[] describes the p array of string s & t.
// r1, r2 describes the steps that can make s & t the same intended string.
int s_eq_t() { for (int i = 1; i <= n; ++i) if (s[i] != t[i]) return 0; return 1; }
// s_eq_t() checks if s & t is the same string.
int get_k_eq_m() {
for (int i = 1 - n; i < n; ++i) {
int flg = 1;
for (int j = 1; j <= n; ++j)
if (s[j] != ((i + j > 0 && i + j <= n) ? t[i + j] : 0)) { flg = 0; break; }
if (flg) return i;
}
return n;
}
// get_k_eq_m() checks if s is a cyclic shift of t, and returns the steps if it is.
void sol_k_eq_m() {
int st, ed;
for (int i = 1; i <= n; ++i) if (s[i]) { st = i; break; }
for (int i = n; i; --i) if (s[i]) { ed = i; break; }
int k = get_k_eq_m(); // checks if s is a cyclic shift of t.
if (k < n) {
puts("2");
printf("%d %d\n", std::min(st, st + k), std::max(ed, ed + k));
printf("%d %d\n", st + k, ed + k);
return;
}
std::reverse(t + 1, t + n + 1);
k = get_k_eq_m(); // checks if s is a cyclic shift of t's reverse.
if (k < n) {
puts("3");
printf("%d %d\n", std::min(st, st + k), std::max(ed, ed + k));
printf("%d %d\n", st + k, ed + k);
printf("1 %d\n", n);
return;
}
puts("-1"); // if not, no solution
}
// sol_k_eq_m() solves the case of k equals m.
void get_p(char *s, int *p) {
int lst = 0, cnt = 0;
for (int i = 1; i <= n; ++i)
if (s[i]) p[cnt++] = i - lst - 1, lst = i;
p[cnt] = n - lst;
}
// get_p() calculates the array p according to s.
void sol(int *p, std::vector<std::pair<int, int>> &r) {
auto get_pos = [&](int x)->int {
if (x == 0) return 0;
if (x == m + 1) return n + 1;
int res = 0;
for (int i = 0; i < x; ++i) res += p[i];
return res + x;
}; // get the position of the xth 1 according to p[].
for (int i = m; i > k; --i)
if (p[i]) {
int st = get_pos(i - k + 1);
int ed = get_pos(i + 1) - 1;
if (st != ed) r.emplace_back(st, ed);
p[i - k] += p[i], p[i] = 0;
std::reverse(p + i - k + 1, p + i);
} // if i > k and p[i] has a value, add p[i] to p[i - k].
// This operation makes p[i] = 0 for all i > k.
for (int t = 0; t < k; ++t) {
int st = get_pos(1);
int ed = get_pos(k + 1);
if (st != ed - 1) r.emplace_back(st, ed - 1);
p[0] += p[k], p[k] = 0;
std::reverse(p + 1, p + k);
st = get_pos(2);
ed = get_pos(k + 1);
if (st != ed) r.emplace_back(st, ed);
std::reverse(p + 2, p + k + 1);
} // do k cyclic shifts of [1..k] such that all numbers has reached the position p[k] once. add p[k] to p[0].
// This operation makes p[i] = 0 for all i in [1, k].
// After this, p[i] = 0 stands for all i > 0.
// Therefore, applying sol() to both s and t makes them the same string with all 0s at the front of the string.
}
// sol() solves the case k is odd.
void sol2(int *p, std::vector<std::pair<int, int>> &r) {
auto get_pos = [&](int x) {
if (x == 0) return 0;
if (x == m + 1) return n + 1;
int res = 0;
for (int i = 0; i < x; ++i) res += p[i];
return res + x;
}; // get the position of the xth 1 according to p[].
for (int i = m; i > k; --i)
if (p[i]) {
int st = get_pos(i - k + 1);
int ed = get_pos(i + 1) - 1;
if (st != ed) r.emplace_back(st, ed);
p[i - k] += p[i], p[i] = 0;
std::reverse(p + i - k + 1, p + i);
} // if i > k and p[i] has a value, add p[i] to p[i - k].
// This operation makes p[i] = 0 for all i > k. (same as the one in sol())
for (int t = 0; t < k; ++t) {
int st = get_pos(1);
int ed = get_pos(k + 1);
if (st != ed - 1) r.emplace_back(st, ed - 1);
p[0] += p[k], p[k] = 0;
std::reverse(p + 1, p + k);
st = get_pos(1);
ed = get_pos(k + 1);
p[k + 1] += p[1], p[1] = 0;
if (st + 1 != ed) r.emplace_back(st + 1, ed);
std::reverse(p + 2, p + k + 1);
} // do k cyclic shifts of [1..k] such that all numbers has reached the position p[k] or p[1] once.
// This operation makes p[i] = 0 for all i in [1, k].
// After this, p[i] = 0 stands for all i in [1, k] \cup [k + 2, m]
// Therefore, applying sol() to both s and t makes them the same string 000..0001111..11(k 1s)000.0000111.11
}
// sol2() solves the case k is even.
int main() {
int T; scanf("%d", &T);
while (T--) {
scanf("%d%d%s%s", &n, &k, s + 1, t + 1);
int cnts = 0, cntt = 0;
for (int i = 1; i <= n; ++i) s[i] -= '0', t[i] -= '0';
for (int i = 1; i <= n; ++i) cnts += s[i], cntt += t[i];
if (cnts != cntt) { puts("-1"); continue; } else m = cnts;
if (s_eq_t()) { puts("0"); continue; }
if (k == 0 || k > m) { puts("-1"); continue; }
if (k == m) { sol_k_eq_m(); continue; }
get_p(s, p);
get_p(t, q);
r1.clear(), r2.clear();
if (k & 1) {
sol(p, r1);
sol(q, r2);
} else {
int tmp[2][2];
memset(tmp, 0, sizeof(tmp));
for (int i = 0; i <= m; ++i) tmp[0][i & 1] += p[i];
for (int i = 0; i <= m; ++i) tmp[1][i & 1] += q[i];
if (tmp[0][0] != tmp[1][0] || tmp[0][1] != tmp[1][1]) { puts("-1"); continue; }
sol2(p, r1);
sol2(q, r2);
}
printf("%d\n", (int)r1.size() + (int)r2.size());
for (unsigned i = 0; i < r1.size(); ++i) printf("%d %d\n", r1[i].first, r1[i].second);
for (unsigned i = r2.size() - 1; ~i; --i) printf("%d %d\n", r2[i].first, r2[i].second); // output in reverse order
}
return 0;
}