Description
给定一个长度为 n 的 01 字符串 S。
你可以进行下列操作任意次:
- 选择 S 的一个连续子串 S[l,r]。
设 cnt0,cnt1 分别表示该子段中字符 0 和字符 1 的数量。
则你将花费 ∣cnt0−cnt1∣+1 枚金币并对 S[l,r] 进行升序排序。
你需要求出使 S 本身升序排序所需的最少金币数。
n≤2×105。
Solution
首先如果 cnt0<cnt1,则可以让所有数反转并翻转整个序列,容易发现答案是不变的。现在 cnt0≥cnt1。
经过手玩可以发现每次都是选择 cnt0=cnt1 的区间进行操作。
证明
设 d=cnt0−cnt1,现在需要证明可以通过不超过 d+1 次 cnt0=cnt1 操作将整个序列排序。
如果 a1=0,则把第一个数删掉后可归纳为 d−1 的问题。
如果 a1=1,由于 cnt0>cnt1,所以必然存在一个前缀使得其 0 和 1 的个数相等,操作这个前缀并把第一个 0 删掉也可归纳为 d−1 的问题。
得到这个结论后就可以贪心地选择以当前第一个 1 为左端点的最长区间进行操作。
实现时维护一个数组 lsti 表示最后一个前缀 cnt0−cnt1=i 的位置即可。
时间复杂度:O(n)。
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49
| #include <bits/stdc++.h>
const int kMaxN = 2e5 + 5;
int n; int a[kMaxN], lst[kMaxN]; std::string s;
void dickdreamer() { std::cin >> n >> s; int cnt[2] = {0}; for (int i = 1; i <= n; ++i) { a[i] = s[i - 1] - '0'; ++cnt[a[i]]; } if (cnt[0] < cnt[1]) { std::swap(cnt[0], cnt[1]); std::reverse(a + 1, a + 1 + n); for (int i = 1; i <= n; ++i) a[i] ^= 1; } for (int i = 0; i <= n; ++i) lst[i] = 0; for (int i = 1, sum = 0; i <= n; ++i) { sum += (!a[i] ? 1 : -1); if (sum >= 0) lst[sum] = i; } int ans = 0, pos = 0; for (; pos < n && a[pos + 1] == 0; ++pos) {} if (pos == cnt[0]) return void(std::cout << "0\n"); for (; pos < cnt[0] - cnt[1];) { if (!lst[pos]) break; pos += (lst[pos] - pos) / 2, ++ans; } std::cout << ans + 1 << '\n'; }
int32_t main() { #ifdef ORZXKR freopen("in.txt", "r", stdin); freopen("out.txt", "w", stdout); #endif std::ios::sync_with_stdio(0), std::cin.tie(0), std::cout.tie(0); int T = 1; std::cin >> T; while (T--) dickdreamer(); return 0; }
|