CSP-S 模拟赛34

Rock-N-Roll / 2024-10-05 / 原文

CSP-S 模拟赛34

T1

考虑对原序列将 \(k\) 的左右分成两个序列。simple 的想法是分别从 \(1\) 开始跑前缀和,每一次总跑到下一个小于它的点,然后依次类推。发现这样做碰到序列最小值之后难以继续。

然而我们发现这样跑点的过程从前往后和从后往前是等价的。这样考虑的原因是发现这样的选数问题不具有方向性。于是时间复杂度 \(O(n)\)

代码:

#include <bits/stdc++.h>
#define N 100005
#define int long long
using namespace std;
int T;
int n, k;
int a[N];
int a1[N], a2[N];
int ct1, ct2;
int sm1[N], sm2[N];
int nx1[N], nx2[N];
void sve() {
	memset(sm1, 0, sizeof sm1);
	memset(sm2, 0, sizeof sm2);
	memset(nx1, 0, sizeof nx1);
	memset(nx2, 0, sizeof nx2);
	cin >> n >> k;
	for (int i = 1; i <= n; i++)
		scanf("%lld", &a[i]);
	ct1 = ct2 = 1;
	a1[1] = a2[1] = 0;
	for (int i = k; i > 1; i--)
		a1[++ct1] = a[i];
	for (int i = k + 1; i <= n; i++)
		a2[++ct2] = a[i];
	for (int i = 1; i <= ct1; i++)
		sm1[i] = sm1[i - 1] + a1[i];
	for (int i = 1; i <= ct2; i++)
		sm2[i] = sm2[i - 1] + a2[i];
	int mn = sm1[1], nw = 1;
	for (int i = 2; i <= ct1; i++)
		if (sm1[i] <= mn) {
			nx1[nw] = i;
			nw = i;
			mn = sm1[i];
		}
	mn = sm1[ct1], nw = ct1;
	for (int i = ct1 - 1; i >= 1; i--)
		if (sm1[i] < mn) {
			nx1[nw] = i;
			nw = i;
			mn = sm1[i];
		}
	mn = sm2[1], nw = 1;
	for (int i = 2; i <= ct2; i++)
		if (sm2[i] <= mn) {
			nx2[nw] = i;
			nw = i;
			mn = sm2[i];
		}
	mn = sm2[ct2], nw = ct2;
	for (int i = ct2 - 1; i >= 1; i--)
		if (sm2[i] < mn) {
			nx2[nw] = i;
			nw = i;
			mn = sm2[i];
		}
	if (sm1[ct1] + sm2[ct2] > 0)
		return puts("No"), void();
	int p1 = 1, p2 = 1;
	while (nx1[p1] || nx2[p2]) {
		if (!nx1[p1]) {
			for (int j = p2 + 1; j <= nx2[p2]; j++)
				if (sm1[p1] + sm2[j] > 0) {
					puts("No");
					return;
				}
			p2 = nx2[p2];
			continue;
		}
		if (!nx2[p2]) {
			for (int j = p1 + 1; j <= nx1[p1]; j++)
				if (sm1[j] + sm2[p2] > 0) {
					puts("No");
					return;
				}
			p1 = nx2[p1];
			continue;
		}
		int fg = 0;
		for (int i = p1 + 1; i <= nx1[p1]; i++)
			if (sm1[i] + sm2[p2] > 0) {
				fg = 1;
				break;
			}
		if (!fg) {
			p1 = nx1[p1];
			continue;
		}
		for (int j = p2 + 1; j <= nx2[p2]; j++)
			if (sm1[p1] + sm2[j] > 0) {
				puts("No");
			return;
		}
		p2 = nx2[p2];
	}
	p1 = ct1, p2 = ct2;
	while (nx1[p1] || nx2[p2]) {
		if (!nx1[p1]) {
			for (int j = p2 - 1; j >= nx2[p2]; j--)
				if (sm1[p1] + sm2[j] > 0) {
					puts("No");
					return;
				}
			p2 = nx2[p2];
			continue;			
		}
		if (!nx2[p2]) {
			for (int j = p1 - 1; j >= nx1[p1]; j--)
				if (sm1[j] + sm2[p2] > 0) {
					puts("No");
					return;
				}
			p1 = nx2[p1];
			continue;			
		}
		int fg = 0;
		for (int i = p1 - 1; i >= nx1[p1]; i--)
			if (sm1[i] + sm2[p2] > 0) {
				fg = 1;
				break;
			}
		if (!fg) {
			p1 = nx1[p1];
			continue;
		}
		for (int j = p2 - 1; j >= nx2[p2]; j--)
			if (sm1[p1] + sm2[j] > 0) {
				puts("No");
			return;
		}
		p2 = nx2[p2];
	}
	puts("Yes");
}

signed main() {
	freopen("game.in", "r", stdin);
	freopen("game.out", "w", stdout);
	cin >> T;
	for (int i = 1; i <= T; i++)
		sve();
	return 0;
}

T2

显然考虑 \(O(n^2)\) 的 dp。

朴素的 dp 定义是 \(dp_{i,j}\) 表示长度为 \(i\) 的序列,\(j\) 次消除的方案数。然而发现这样转移的复杂度难以接受,需要分别枚举左右区间的消除次数。

考虑某一个位置 \(x\) 的消除次数由什么决定。对于只有某一边有 \(>a_x\) 的,则这个位置的消除次数一定是 \(j-1\)。对于两边都有 \(>a_x\) 的,两边的消除次数有一个是 \(j-1\)。那么就考虑前缀和优化这个 dp,则定义 \(dp_{i,j,0/1}\) 表示长度为 \(i\) 的序列,至多 \(j\) 次消除,有一边 / 两边 \(>a_x\) 的方案数,那么 \(j\) 便由 \(j,j-1\) 转移而来。

时间复杂度大抵是 \(O(n^2\log^2n)\)