题解:CF1923F Shrink-Reverse

· · 题解

明天要讲 SA,提前写一道 SA 题。

简要题意

有一个长度为 n01S,你可以执行下列两种操作至多 k 次:

你需要使 S 表示的二进制数尽可能小,输出这个二进制数值的最小值对 10^9+7 取模。

## 思路 超级高明题,难点不在 SA 本身。 ### Part 1 难点在于意识到这些结论。 > **引理 1**:我们总是在执行所有交换后进行变换。 **证明**:假如我们在一次变换后进行交换,我们一定可以在找到这两个位置在变换前的下标,改为在变换前进行交换。也就是说在变换前交换是不劣的。 > **引理 2**:我们不会执行超过一次变换。 **证明**:不妨先研究一下变换的过程: $$ \textcolor{grey}{0\cdots0}\textcolor{red}{\text{AB}}\textcolor{blue}{0\cdots 0}\xrightarrow{\text{Shrink-Reverse}} \textcolor{grey}{0\cdots 0}\textcolor{red}{\text{BA}}\xrightarrow{\text{Shrink-Reverse}}\textcolor{red}{\text{AB}}\xrightarrow{\text{Shrink-Reverse}}\textcolor{red}{\text{BA}} $$ 在上面的示意图中,灰色的 $0$ 表示前导 $0$,蓝色的 $0$ 表示后导 $0$,中间红色的 $\mathrm{AB}$ 表示以 $1$ 开头和结尾的串,用 $\text{AB},\text{BA}$ 区分顺序。 当我们进行超过 $2$ 次变换时,便陷入 BA 与 AB 的循环,那么执行奇数次变换,不如执行一次变换(因为前导 $0$ 在最终比较时是忽略的),执行偶数次变换,不如执行两次变换。所以我们最多执行 $2$ 次变换。 进一步地,考察进行两次变换的影响,发现仅仅在原串的基础上去除了后导 $0$。那么有一种非常神奇的策略:仅执行一次变换,接下来进行调整。 具体的调整策略是,如果中间串全是 $1$,那么无需调整。否则将外侧的一个 $1$ 与内部的一个 $0$ 交换(当然,需要根据引理 1 将这个操作等效替代为变换前的操作),这样有效位数就减小了,比变换两次更优。 ### Part 2 由于引理 2,我们只能进行 $0$ 次或 $1$ 次变换。现在考虑进行 $0$ 次的情况。 很容易设计一个贪心,将高位的 $1$ 与低位的 $0$ 交换,正确性是显然的。这个贪心很容易用双指针实现。 同时观察到这个贪心的本质是将 $1$ 尽可能的靠拢在一个区间中。这个思路是可以沿用的。 ### Part 3 现在考虑进行 $1$ 次的情况。 根据前面的分析,我们很容易感受出操作的策略是什么,就是选定一个区间,保留其中的所有 $1$,然后将区间外的 $1$ 与区间内交换。 由于我们需要在长度相同的情况下,让字典序最小,于是我们希望 $1$ 在后面,所以需要从后往前填区间外的 $1$。 观察这样的区间 $[L,R]$ 的限制。首先需要将区间外的所有 $1$ 都填进去,所以 $R-L+1$ 要不小于整个序列的 $1$ 的数量。 另外我们有操作次数的限制,所以区间外的 $1$ 的数量必须不大于 $k-1$。 我们发现固定区间的一端,另一端对区间是否合法的影响是单调的,于是可以用双指针维护。 ### Part 4 最后我们可以用双指针得到若干个固定左端点,最短的合法区间。我们需要选择最优的。 观察到对于两个候选的子串 $a,b$,如果 $a$ 的长度比 $b$ 小,那么我们肯定选择 $a$。 如果 $a,b$ 长度相同,观察到我们需要从后往前将 $0$ 替换为 $1$。那么如果 $a$ 的字典序比 $b$ 小,那么最终替换后 $a$ 的字典序仍然不比 $b$ 大。这是因为我们定义字典序小,是因为 $a_i=0$ 而 $b_i=1$($[1,i-1]$ 都相同),而若 $a_i$ 未被替换为 $0$,则结论显然成立。否则 $a_i$ 后面的 $0$ 均是 $1$,由于 $a,b$ 的 $0/1$ 数量相同(都会是 $k-1$,因为 $1$ 的数量在离散意义上是连续的,根据介值定理可以得到本结论),所以 $b_i$ 后面也都是 $1$,此时 $a=b$。本结论得证。 于是现在的问题是,需要比较若干个区间翻转后的字典序的大小,将字符串翻转后使用后缀数组即可做到比较时 $O(1)$。 最终时间复杂度 $O(n\log^\mu(n))$,复杂度瓶颈在 SA 的构建。若使用倍增配合一般的排序求后缀数组,$\mu=2$,若改用基数排序,$\mu=1$,若利用 SA-IS 或 DC3 等科技,$\mu=0$。 ## 代码 我的实现 $\mu=2$,足以通过本题。 ```cpp #include <bits/stdc++.h> using namespace std; constexpr int mod = 1e9 + 7; int Add(int x, int y){ return (x + y) >= mod ? (x + y - mod) : (x + y); } int Sub(int x, int y){ return (x - y) < 0 ? (x - y + mod) : (x - y); } int Mul(int x, int y){ return 1ll * x * y % mod; } template<int N> struct SuffixArray { int sa[N], rk[N], tmp[N]; void build(const string &s){ int n = s.size() - 1; for(int i=1;i<=n;i++) sa[i] = i, rk[i] = s[i]; for(int k=1;k<n;k<<=1){ auto mk = [=](int x){ return make_pair(rk[x], rk[x + k]); }; sort(sa + 1, sa + n + 1, [mk](int x, int y){ return mk(x) < mk(y); }); for(int i=1,p=0;i<=n;i++){ if(mk(sa[i - 1]) == mk(sa[i])) tmp[sa[i]] = p; else tmp[sa[i]] = ++p; } copy(tmp + 1, tmp + n + 1, rk + 1); } } }; const int N = 5e5 + 5; int n, k; string s; SuffixArray<N> sa; string solve0(){ s = ' ' + s; int L = 1, R = n; string ret = s; for(int i=1;i<=k;i++){ while(L <= n && s[L] != '1') L++; while(R >= 1 && s[R] != '0') R--; if(L > n || R < 1 || L > R) break; swap(ret[L], ret[R]); L++, R--; } return ret; } struct node{ int pos, len; bool operator<(const node &rhs) const { return len == rhs.len ? sa.rk[pos] < sa.rk[rhs.pos] : len < rhs.len; } }; string solve1(){ reverse(s.begin(), s.end()); s = " " + s; sa.build(s); int R = 0, cnt = 0, total = count(s.begin(), s.end(), '1'); vector<node> vct; for(int L=1;L<=n;L++){ while(R <= n && !((R - L + 1 >= total) && (total - cnt <= k - 1))) cnt += s[++R] == '1'; if(R > n) break; vct.push_back({L, R - L + 1}); cnt -= s[L] == '1'; } auto [pos, len] = *min_element(vct.begin(), vct.end()); string ret = s.substr(pos, len); int ext = total - count(ret.begin(), ret.end(), '1'); reverse(ret.begin(), ret.end()); for(char& ch : ret){ if(ch == '0' && (--ext) >= 0) ch = '1'; } reverse(ret.begin(), ret.end()); return ret; } string wash(string s){ s = s.substr(s.find('1')); if(s.empty()) s = "0"; return s; } signed main(){ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); cin >> n >> k >> s; string ret1 = wash(solve0()), ret2 = wash(solve1()), ans; if(ret1.size() < ret2.size()) ans = ret1; else if(ret1.size() > ret2.size()) ans = ret2; else ans = min(ret1, ret2); int ret = 0; for(char ch : ans) ret = Add(Add(ret, ret), ch == '1'); cout << ret << '\n'; return 0; } // Written by xiezheyuan ```