Atcoder Grand Contest 026 E - Synchronized Subsequence(贪心+单调栈)

洛谷题面传送门 & Atcoder 题面传送门

首先将序列分成尽可能多的满足 a 和 b 数量相等的段,显然每一段是独立的。即前一段的删除方法不会影响后面的段。

考虑对每一段分开来计算,显然由于每一段之间不存在其他分界点将这一段劈成两个符合要求的小段,因此每段中要么 a 全在 b 前面,要么 b 全在 a 前面。很明显需要分情况讨论:

  • 如果 a 全在 b 前面,那么从 a 开始到 a 所匹配的 b 这一段中所有 a 必须要删掉,删完以后最前面的 ab 就固定在那里了,对后面的字符串递归操作即可。事实上我们不必真的写递归函数,发现其等价于求最多能匹配多少对 ab 使得匹配的 ab 所覆盖的区间不交。直接贪心即可。
  • 如果 b 全在 a 前面,那么我们肯定希望最一开始的一段 b 越长越好,因此我们考虑这一段 b 中最后一个在原序列中对应的位置,那么显然在这个位置之前的 a 都应当被删除,这等价于将 b 视作 \(+1\),将 a 视作 \(-1\) 后找最大前缀和。那如果最大前缀和有多个怎么办呢?不难发现后面的部分我们肯定会原样保留,这点可以通过”最大前缀和“这一前提得到,于是我们直接判断后面一部分字典序的大小来决定从哪个位置开始删。

最后就是段与段的拼接问题了。值得注意的是,并不是所有的段都应当被保留,事实上每一段有两种选择:要么按照之前我们所求的最优删除策略来,要么全删光。做一遍单调栈即可。

时间复杂度 \(\Theta(n^2)\)

const int MAXN = 6000;
int n; char s[MAXN + 5];
string solve(int l, int r) {
	if (s[l] == 'a') {
		static int posa[MAXN + 5], posb[MAXN + 5];
		int cura = 0, curb = 0, cur = 0, cnt = 0;
		for (int i = l; i <= r; i++) {
			if (s[i] == 'a') posa[++cura] = i;
			if (s[i] == 'b') posb[++curb] = i;
		}
		for (int i = 1; i <= cura; i++) if (cur < posa[i]) cur = posb[i], ++cnt;
		string res; for (int i = 1; i <= cnt; i++) res.pb('a'), res.pb('b');
		return res;
	} else {
		string res, preb; static int pre[MAXN + 5]; memset(pre, 0, sizeof(pre));
		for (int i = l; i <= r; i++) pre[i] = (pre[i - 1] + ((s[i] == 'b') ? 1 : -1));
		int mx = 0;
		for (int i = l; i <= r; i++) chkmax(mx, pre[i]);
		for (int i = 1; i <= mx; i++) preb.pb('b');
		for (int i = l; i <= r; i++) if (pre[i] == mx) {
			string ss;
			for (int j = i + 1; j <= r; j++) ss.pb(s[j]);
			chkmax(res, ss);
		}
		return preb + res;
	}
}
int main() {
	scanf("%d%s", &n, s + 1); int pre = 0, sum = 0;
	static string stk[MAXN + 5]; int tp = 0;
	for (int i = 1; i <= n * 2; i++) {
		sum += (s[i] == 'a') ? 1 : -1;
		if (!sum) {
			string ss = solve(pre + 1, i); pre = i;
			while (tp && ss > stk[tp] + ss) --tp; stk[++tp] = ss;
		}
	}
	for (int i = 1; i <= tp; i++) cout << stk[i];
	cout << endl;
	return 0;
}
posted @ 2022-07-12 12:32  tzc_wk  阅读(52)  评论(0)    收藏  举报