Codeforces Round #681 (Div. 2, based on VK Cup 2019-2020

A

题意

要你构造 (n) 个数,每个数的范围是 ([1,4n]),使得其中任意两个数 (a,b) 不满足 (gcd(a,b)=1) 且不满足 (gcd(a,b)=a) 且不满足 (gcd(a,b)=b)

题解

一道一眼构造题。我们发现如果只取偶数的话就不可能出现 ((a,b)=1) 的情况,而又他的范围那么大,所以如果取 (2n+2,2n+4,...,4n)(n) 个数一定不会出现后两种情况。这样就构造完了。

#include <bits/stdc++.h>
using namespace std;
int t;
int n;
int main() {
	scanf("%d", &t);
	while (t--) {
		scanf("%d", &n);
		for (int i = 4 * n; i > 4 * n - 2 * n; i -= 2) {
			printf("%d ", i);
		} printf("
");
	}
	return 0;
}

B

题意

给你两个数 (a,b) 和一个长度为 (n) 的01字符串。对于一个1的连通块你需要花费 (a) 去炸掉它并且你可以花费 (b) 去把一个0变成1,求你炸掉所有1的最小花费是多少。

题解

从左往右推,把两个1的连通块之间的0的个数记为 (x),如果 (x imes b<a) 的话就把这一段填成1,答案加上 (x imes b),否则则需一段新的1连通块,答案加上 (a)。注意一开始必须有一个1的连通块,即如果存在1的话答案至少为 (a)

#include <bits/stdc++.h>
using namespace std;
int t;
int a, b;
char s[100010];
int n;
long long ans;
int main() {
	scanf("%d", &t);
	while (t--) {
		ans = 0;
		scanf("%d%d", &a, &b);
		scanf("%s", s + 1);
		n = strlen(s + 1);
		bool have = false;
		int now = 0;
		for (int i = 1; i <= n; i++) {
			if (s[i] != s[i - 1]) {
				if (s[i] == '1') ans += have ? min(now * b, a) : a, have = true;
				now = 1;
			} else now++;
		}
		printf("%lld
", ans);
	}
	return 0;
}

C

题意

给你两个数列:({a},{b}),要你求出 (min_{Psubseteq {1,2,...,n}}{max{max_{i in P}a_i,sum_{i otin P}b_i}})

题解

此题不需要二分答案,直接排序加枚举即可。我们发现在枚举 (P) 的时候如果对于一个 (i otin P)(a_i le max_{i in P}a_i) 那这个 (i) 也包含进来一定更优。所以我们可以先对 (a) 排序,枚举前 (i) 个人是送外卖的,然后维护一个后缀和就可以了。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll inf = 0x3f3f3f3f3f3f3f3f;
struct node{
	int a, b;
	friend bool operator < (node x, node y) {
		return x.a < y.a;
	}
}Dish[200010];
int t;
int n;
ll sum, ans;
int main() {
	scanf("%d", &t);
	while (t--) {
		ans = inf;
		sum = 0;
		scanf("%d", &n);
		for (int i = 1; i <= n; i++) scanf("%d", &Dish[i].a);
		for (int i = 1; i <= n; i++) scanf("%d", &Dish[i].b);
		sort(Dish + 1, Dish + 1 + n);
		for (int i = n; i >= 1; i--) {
			ans = min(ans, max((ll)Dish[i].a, sum));
			sum += Dish[i].b;
		}
		ans = min(ans, sum);
		printf("%lld
", ans);
	}
	return 0;
}

D

考虑一个位置至少有几次操作是从最后到它这个位置的?如果 (a_{i-1}<a_i) 那么显然是 (a_{i}-a_{i-1}),那么 (a[i...n]) 这一段数至少减 (a_{i}-a_{i-1}),如果把所有这些要减的加在一起,比某个 (a_x) 大了那么就是NO,否则就是YES

为什么不用从后往前做一遍?看一下这张图就懂了。

这一类题因为是区间同时减去某数所以有一段的差分数组不变,多往差分上去想。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int inf = 0x3f3f3f3f;
int q;
int n;
int a[100010];
int now;
int main() {
	scanf("%d", &q); 
	while (q--) {
		scanf("%d", &n);
		for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
		now = 0;
		bool ans = true;
		for (int i = 1; i < n; i++) {
			if (now > a[i]) {
				ans = false;
				break;
			}
			if (a[i] < a[i + 1]) now += a[i + 1] - a[i]; 
		}
		if (now > a[n]) ans = false;
		if (ans) puts("YES");
		else puts("NO");
	}
	return 0;
}

E

挺水的一题,可惜赛时不知道F更水,然后花了挺多时间搞这题害的最后没时间写F了/kk/kk/kk。

看到这道题让你实现的就是跳到字典序比当前大一的排列上,而字典序最大为 (2 imes 10^{10}),这对于排列的个数来说其实是很小的。保守点说,排列变化的肯定是最后 (20) 个数。那么对于前 (n-20) 个数维护一个前缀和,后 (20) 个数暴力统计就可以做第一问。对于第二个操作,根据排名求排列有个逆康托展开的算法,具体就是挨个确定每个位置是什么数,如果暴力做的话复杂度上限是 (20^3),然而到不了所以这题可以过,如果要追求效率的话可以做到 (20 imes log^2{20}) 或者 (20 imes log{20}).

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, q;
ll now = 1;
ll a[200010], cnt[200010];
bool vis[200010];
ll ans;
int main() {
	scanf("%d%d", &n, &q);
	for (int i = 1; i <= n; i++) a[i] = i, cnt[i] = a[i] + cnt[i - 1];
	while (q--) {
		int opt, l, r;
		scanf("%d", &opt);
		if (opt == 1) {
			scanf("%d%d", &l, &r);
			ans = 0;
			if (n > 20) {
				if (l < n - 20 && r >= n - 20) {
					ans = cnt[n - 21] - cnt[l - 1];
					l = n - 20;
					for (int i = l; i <= r; i++) {
						ans += a[i];
					}
				} else if (r < n - 20) {
					ans = cnt[r] - cnt[l - 1];
				} else {
					for (int i = l; i <= r; i++) {
						ans += a[i];
					}
				}
			} else {
				for (int i = l; i <= r; i++) {
					ans += a[i];
				}
			}
			printf("%lld
", ans);
		} else {
			scanf("%d", &l);
			now += l;
			ll tmp = now;
			ll cur = 1;
			int pos;
			for (pos = n; pos; pos--) {
				if (cur >= now) {
					break;
				}
				cur *= (n - pos + 1);
			}
			for (int i = pos + 1; i <= n; i++) vis[i] = false;
			for (int i = pos + 1; i <= n; i++) {
				cur /= (n - i + 1);
				for (int j = pos + 1; j <= n; j++) {
					if (vis[j]) continue;
					ll num = 0;
					for (int k = pos + 1; k < i; k++) {
						if (a[k] < j) {
							num++;
						}
					}
					if (cur * (j - pos - num) >= now) {
						now -= cur * (j - pos - num - 1);
						a[i] = j;
						vis[j] = true;
						break;
					}
				}
			}
			now = tmp;
		}
	}
	return 0;
}

F

首先我们发现要想某个数加入 (b) 就得把它旁边两个中的一个删掉,然后这是两种情况,所以每次操作最多两种情况,答案肯定是 (2) 的倍数。考虑什么情况不是 (2)?记录每个数在 (a) 中出现的位置 (p),如果 (a_{p_{b_i}+1}) 是在 (b)(i) 的后面出现的数,那么一定不能删。如果 (p_{b_i}=n) 那么后面的数也没得删,前面同理。于是我们从后往前推,对于每个 (b_i) 打上标记,这样就可以 (O(1)) 判断了。

为什么只有一开始的位置才影响答案?因为如果想要把旁边的删掉一定要把你这个位置的删掉,而你这个位置在之前有不能删,所以旁边的一定还存在。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll mod = 998244353;
const int inf = 0x3f3f3f3f;
int T;
int n, m;
int a[200010], b[200010], pos[200010];
bool vis[200010];
ll ans;
int main() {
	scanf("%d", &T); 
	while (T--) {
		scanf("%d%d", &n, &m);
		for (int i = 1; i <= n; i++) scanf("%d", &a[i]), pos[a[i]] = i, vis[i] = false;
		for (int i = 1; i <= m; i++) scanf("%d", &b[i]);
		ans = 1;
		for (int i = m; i >= 1; i--) {
			ll cur = 2;
			if (pos[b[i]] == 1 || vis[a[pos[b[i]] - 1]]) cur--;
			if (pos[b[i]] == n || vis[a[pos[b[i]] + 1]]) cur--;
			ans = (ans * cur) % mod;
			vis[b[i]] = true;
		}
		printf("%lld
", ans);
	}
	return 0;
}

总结一下这场比赛,前3题很水随便过,但是B花的时间还是多了点,以后要做到前两题15min以内做完(除非是公认的毒瘤场)。然后这个D的套路一开始竟然没有直接想到,这种区间加减的一般都要考虑差分,我还是太菜了。E应该算是马上就想到了只有最后几个会变,但是逆康托展开打了好久,还是知识点不熟悉,并且犯了低级错误wa了两发,打代码时还是不能太急。最后这个F应该比E水但是考试时没看榜导致还是先去做了E,最后没时间打F了。。。然而还是比较套路的,这种一个一个策略的问题其实可以考虑从后往前倒推。大概就这么多吧,我还是太菜了。

原文地址:https://www.cnblogs.com/zcr-blog/p/13938120.html