AtCoder Regular Contest 194 (Div. 2)

ARC194A Operations on a Stack

发现一定是删掉若干段偶数长度的不选,直接 DP 即可做到线性。

void slv() {
	int n = Read<int>();
	
	vector<int> A(n);
	for (int i = 0; i < n; i ++) {
		Read(A[i]);
	}
	
	vector<array<ll, 3>> f(n);
	f[0][2] = A[0], f[0][1] = 0, f[0][0] = -inf;
	for (int i = 1; i < n; i ++) {
		f[i][2] = A[i] + max(f[i - 1][0], f[i - 1][2]);
		f[i][1] = max(f[i - 1][0], f[i - 1][2]);
		f[i][0] = f[i - 1][1];
	}
	
	Write(max(f[n - 1][0], f[n - 1][2]), '\n');
	return;
}

ARC194B Minimum Cost Sort

考虑下界。

对于每个数 \(i\),如果存在逆序对 \((j, i)\),那么就至少需要和 \(j\) 交换一次.,那么下界就是一个等差数列求和。

\(c_i\) 表示 \(i\) 位置的逆序对个数,不难得到下界为:

\[\sum_{1 \le i \le N} \frac{c_i(2i - c_i - 1)}{2} \]

用一种你喜欢的方式求出逆序对后计算即可。

void slv() {
	int n = Read<int>();
	ll ans = 0; Fenwick<int> tr(n + 1);
	for (int i = 1; i <= n; i ++) {
		int x = Read<int>();
		int s = tr.Query(n - x + 1);
		ans += 1LL * ((i - s) + (i - 1)) * ((i - 1) - (i - s) + 1) / 2;
		tr.Update(n - x + 1, 1);
	}
	Write(ans, '\n');
	return;
}

ARC194C Cost to Flip

一定是先把一些 \(1\) 变成 \(0\),然后再把 \(0\) 变成 \(1\)

其中第一步操作的一定是所有的 \(A_i = 1, B_i = 0\) 位置和一部分 \(A_i = B_i = 1\) 的位置,第二步操作的一定是所有的 \(A_i = 0, B_i = 1\) 的位置和在第一步中操作过的 \(A_i = B_i = 1\) 的位置。

通过进一步的观察,发现对于 \(A_i = B_i = 1\) 的位置,我们只会改按 \(C_i\) 从大到小排序后的一段前缀。

然后就枚举这段前缀随便统计一下答案啥的,反正细节很多,烦。

void slv() {
	int n = Read<int>();
	
	vector<int> A(n), B(n), C(n);
	for (int i = 0; i < n; i ++) {
		Read(A[i]);
	}
	for (int i = 0; i < n; i ++) {
		Read(B[i]);
	}
	for (int i = 0; i < n; i ++) {
		Read(C[i]);
	}
	
	vector<int> pA, pB, pC;
	for (int i = 0; i < n; i ++) {
		if (A[i] == 1 && B[i] == 0) {
			pA.emplace_back(i);
		} else if (A[i] == 0 && B[i] == 1) {
			pB.emplace_back(i);
		} else if (A[i] == 1 && B[i] == 1) {
			pC.emplace_back(i);
		}
	}
	
	sort(pA.begin(), pA.end(), [&](int i, int j) { return C[i] > C[j]; });
	sort(pB.begin(), pB.end(), [&](int i, int j) { return C[i] < C[j]; });
	sort(pC.begin(), pC.end(), [&](int i, int j) { return C[i] > C[j]; });
	
	const int nA = pA.size(), nB = pB.size(), nC = pC.size();
	vector<ll> f(1 + nC);
	{
		ll s = 0;
		for (int i = 0; i < n; i ++)
			if (A[i] == 1) s += C[i];
		for (int i = 0; i < nA; i ++) {
			s -= C[pA[i]], f[0] += s;
		}
		s = 0;
		for (int i = 0; i < n; i ++)
			if (A[i] == 1) s += C[i];
		for (int i = 1, j = 0; i <= nC; i ++) {
			while (j < nA && C[pA[j]] > C[pC[i - 1]]) s -= C[pA[j ++]];
			s -= C[pC[i - 1]];
			f[i] = f[i - 1] - 1LL * C[pC[i - 1]] * (nA - j) + s;
		}
	};
	
	vector<ll> g(1 + nC);
	{
		ll s = 0;
		for (int i = 0; i < n; i ++)
			if (B[i] == 1 && A[i] == 1) s += C[i];
		for (int i = 0; i < nB; i ++) {
			s += C[pB[i]], g[0] += s;
		}
		s = 0;
		for (int i = 0; i < n; i ++)
			if (B[i] == 1) s += C[i];
		for (int i = 1, j = nB - 1; i <= nC; i ++) {
			while (j >= 0 && C[pB[j]] > C[pC[i - 1]]) s -= C[pB[j --]];
			g[i] = g[i - 1] + s - 1LL * C[pC[i - 1]] * (j + 1);
			s -= C[pC[i - 1]];
		}
	};
	
	ll ans = 1E18;
	for (int i = 0; i <= nC; i ++)
		cmin(ans, f[i] + g[i]);
	Write(ans, '\n');
	
	return;
}

ARC194D Reverse Brackets

建括号树,操作就是选一段连续的儿子区间,然后将 这段区间 和 其所有后代的儿子 reverse 一下。

同时还要对后代操作非常烦,但是可以通过对所有儿子操作一次变成 reverse 一段连续的儿子区间。

那么现在要做的就是每次可以 reverse 一段连续的儿子区间,问能生成多少棵本质不同的树。

考虑 DFS 计算答案,对于一个点,如果直接算儿子的方案数的乘积然后任意排列的话会算重。

进一步分析,发现不同构的子树是不互相影响的,同构的子树可以钦定必须是从左到右的顺序,这样就不会算重了。

暴力模拟上面的过程是 \(O(n^2)\) 的。

constexpr int N = 5e3 + 5;
char s[N];

void slv() {
	int n; Read(n), Read(s + 1);
	
	vector<int> sum(n + 1);
	for (int i = 1; i <= n; i ++) {
		sum[i] = (s[i] == '(' ? 1 : -1);
	}
	partial_sum(sum.begin(), sum.end(), sum.begin());
	
	int cur = 0;
	map<vector<int>, int> id;
	id[vector<int>()] = 0;
	auto ID = [&](const vector<int> &v) {
		if (!id.count(v)) {
			id[v] = ++ cur;
		}
		return id[v];
	};
	
	auto Solve = [&](auto self, int L, int R) -> pair<int, mint> {
		if (L > R) {
			return {0, 1};
		}
		
		vector<pair<int, mint>> son;
		for (int l = L, r; l <= R; l = r + 1) {
			r = l;
			while (sum[++ r] != sum[l - 1]);
			son.emplace_back(self(self, l + 1, r - 1));
		}
		
		sort(son.begin(), son.end());
		const int n = son.size();
		mint ans = 1;
		for (int l = 0, r; l < n; l = r + 1) {
			r = l, ans *= son[l].sec;
			while (r + 1 < n && son[r + 1].fir == son[l].fir) {
				ans *= son[++ r].sec;
			}
			ans *= comb.C(r + 1, r - l + 1);
		}
		
		vector<int> hsh(n);
		for (int i = 0; i < n; i ++)
			hsh[i] = son[i].fir;
		
		return {ID(hsh), ans};
	};
	
	Write((int)Solve(Solve, 1, n).sec, '\n');
	return;
}

ARC194E Swap 0^X and 1^Y

对于一段连续的 \(1\),将它分成若干段 \(= X\)\(A\)\(< X\)\(1\)\(0\) 同理分成 \(B\)\(0\)

不难发现,如果我们允许 \(A\)\(1\)\(B\)\(0\)\(A\)\(B\) 之间的交换,那么这个和原问题是没有区别的,因为不会发生初始不同的 \(0 / 1\) 的连续段之间的拼接。

发现此时 \(0\) 限制住了 \(1\)\(A\) 的移动,\(1\) 限制住了 \(0\)\(B\) 的移动,所以直接把相邻 \(0\) 之间 \(1\) 的个数、相邻 \(0\) 之间 \(A\) 的个数,相邻 \(1\) 之间 \(B\) 的个数这三个东西看成特征值,特征值相等的序列一定能互相转化。

求出 \(S\)\(T\) 的特征值之后进行判定即可,时间复杂度线性。

constexpr int N = 5E5 + 5;
char s[N], t[N];

void slv() {
	int N, X[2];
	Read(N, X[0], X[1]);
	
	vector<int> A(N), B(N);
	Read(s), Read(t);
	for (int i = 0; i < N; i ++) {
		A[i] = s[i] - '0';
		B[i] = t[i] - '0';
	}
	
	auto calc = [&](const vector<int> &a, int x, int y) {
		const int n = a.size();
		vector<int> ans; int cnt = 0;
		for (int i = 0; i < n; i ++) {
			if (a[i] == x) {
				++ cnt;
			} else if (a[i] == y) {
				ans.emplace_back(cnt);
				cnt = 0;
			}
		}
		ans.emplace_back(cnt);
		return ans;
	};
	auto trans = [&](const vector<int> &a) -> tuple<vi, vi, vi> {
		const int n = a.size();
		vector<int> b;
		for (int l = 0, r; l < n; l = r + 1) {
			r = l; int o = a[l];
			while (r + 1 < n && a[r + 1] == a[l]) {
				++ r;
			}
			int len = r - l + 1;
			while (len >= X[o]) {
				b.emplace_back(2 + o);
				len -= X[o];
			}
			while (len) {
				b.emplace_back(o);
				-- len;
			}
		}
		return {calc(b, 0, 1), calc(b, 0, 3), calc(b, 1, 2)};
	};
	
	Yes(trans(A) == trans(B));
	
	return;
}
posted @ 2025-03-10 12:12  definieren  阅读(185)  评论(0)    收藏  举报