备注
发表时间:2023-06-17 21:51
前言
yny
学长来 cdqz
讲课,写一篇讲课的题的题解纪念一下。
题意
给你一个 01
序列,有以下操作:
- 选择一段区间
设 \(cnt_0,cnt_1\) 分别表示该区间中0
和1
的数量。
花费 \(|cnt_0-cnt_1|+1\) 的代价对区间进行升序排序。
求最小代价。
思路
肯定是使每次操作的区间中 01
个数差越小越好,所以考虑每次都选择尽可能长且 01
个数差最小的区间。
如果最前面一段都是 0
,那就不用管前面。相当于每次都从第一个 1
开始考虑(注意不是操作区间的左端点)。
题解
因为每次操作必须是 \(cnt_0 = cnt_1\) 才最优。
所以现在考虑如何找最优区间。
为了简化思维,我们可以先只考虑整个序列 0
比 1
多时如何求解,因为这样我们可以加 0
进行贪心。
弱化版
首先每次从区间最左端的 1
开始考虑,我们可以贪心去找最优区间。
因为 0
永远比 1
多,所以如果后面的 0
少了可以直接从前面拿 0
来补齐。
所以现在问题就在如何求最优区间。
此时,本题最妙的点来了。我们可以给 0
和 1
赋值为 \(1\) 和 \(- 1\),将这个区间的变化用图表示。
我们假设要找序列 \(S_l\) 的最优区间右端点 \(S_r\),就可以在图中做一条与横轴平行的直线。
其中曲线与直线最右端的交点就是 \(S_r\)。
而图中的曲线就只用记录与直线 \(y = i \left \{i \in [1,n] \mid i \in Z^+\right \}\) 的最右端交点。
最后,如何将方法一般化呢?
我们可以将原来 0
比 1
多的序列变成上面这种特殊序列。
因为原序列中 1
要移到后面、0
要移到前面,所以其实我们只用将原序列翻转,再给 0
1
都取反就行了(读者可自行思考)。
注意
-
特判序列是否已为升序。
-
0
和1
赋值。
代码
/** @Author: H.F.Y* @Date: 2023-06-17 16:40:55* @Last Modified by: H.F.Y* @Last Modified time: 2023-06-17 17:41:39*/
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 2e5 + 5;
int n, a[N], pre_max[N];
char c[N];
signed main(){//freopen(,stdin);//freopen(,stdout);ios::sync_with_stdio(false);cin.tie(nullptr);int T;cin >> T;while(T--){cin >> n; int cnt1 = 0, cnt0 = 0;memset(pre_max, 0, sizeof pre_max);memset(a, 0, sizeof a);for(int i = 1; i <= n; ++i){cin >> c[i]; a[i] = ((c[i] - '0') ? 1 : - 1);if(a[i] == 1)++cnt1;else ++cnt0;}bool opt = true;for(int i = n - 1; i; --i)if(a[i] > a[i + 1])opt = false;if(opt){cout << 0 << '\n';continue;}if(cnt1 == cnt0){cout << 1 << '\n';continue;}else if(cnt1 > cnt0){swap(cnt1, cnt0);for(int i = 1; i <= n; ++i)a[i] = ((c[n - i + 1] - '0') ? - 1 : 1);}for(int i = 1, sum = 0; i <= n; ++i){sum -= a[i];if(~ sum)pre_max[sum] = i;}int ans = 1, i = 0, cnt = cnt0 - cnt1;while(a[i + 1] == - 1 and i < n)++i;while(i < cnt){++ans;int t = pre_max[i] - i + 1; t /= 2;i += t;}cout << ans << '\n';}return 0;
}