2022牛客寒假算法基础集训营1

A 九小时九个人九扇门

题意:
给定n个正整数,分别计算出数字根为1—9的组合数。

涉及算法:数字根,线性dp

数字根:
将一正整数的各个位数相加(即横向相加)后,若加完后的值大于等于10的话,则继续将各位数进行横向相加直到其值小于十为止所得到的数,即为数字根。

性质一:
两个正整数相加,原来两个数的数字根之和等于和的数字根。
abcd % 9
= a * 1000 % 9 + b * 100 % 9 + c * 10 % 9 + d * 1 % 9
= a % 9 + b % 9 + c % 9 + d % 9
= (a + b + c + d) % 9
性质二:
设一个正整数为 x,则其数字根为 (x - 1) % 9 + 1。

数字根的性质二数字根求解的核心代码:

int f (int x) {
	return (x - 1) % 9 + 1;
}

数字根的性质一,可以采取线性dp

dp[i][j] 表示:从第1个数字到第i个数字时,所有的组合中,数字根为j的数量
显然,满足以下递推关系

dp[i][j] = dp[i - 1][j];
dp[i][f(j + a[i])] += dp[i - 1][j];

核心代码如下:

for (int i = 1; i <= n; i++) {
		int tt = f(a[i]);
		dp[i][tt] = (dp[i][tt] + 1) % mod;
		for (int j = 1; j <= 9; j++) {
			dp[i][j] = (dp[i][j] + dp[i - 1][j]) % mod;
			dp[i][f(tt + j)] = (dp[i][f(tt + j)] + dp[i - 1][j]) % mod;
		}
	}

B 炸鸡块君与FIFA2

题意:
给定一个只含有’W’,‘D’,'L’的字符串和若干次询问,每次询问格式为(l,r,s),表示:若你初始有s分,按从左到右的顺序经历了[l,r]这一子串的游戏结果后,最终分数是多少。

每局游戏可能有胜利(用W表示)、失败(用L表示)、平局(用D表示)三种结果,胜利将使得排位分加一、失败使排位分减一、平局使排位分不变。
特别地,若你当前的排位分是33的整倍数(包括0倍),则若下一局游戏失败,你的排位分将不变(而不是减一)。

涉及算法:线段树

对于每一段区间,维护三个变量:区间左端点,区间右端点,区间变化值
其中,对于区间变化值令区间初始值为x,根据x % 3的结果,分别维护三个变化值

线段树:

struct SegmentTree {
	int l, r;
	// x % 3 == 0 / 1 / 2
	// 开始值 x % 3 == ? 经过 [l, r] 的 变化值,而非 变化后的x
	ll sum[3];
}tr[sz << 2];

主体函数:

cin >> n >> q;
	scanf("%s", a + 1);
	build(1, 1, n);
	while (q--) {
		int l, r;
		ll s;
		cin >> l >> r >> s;
		cout << s + ask(1, l, r, s) << '\n';
	}

build函数:

void build(int u, int l, int r) {
	tr[u].l = l, tr[u].r = r;
	if (l == r) {
		// win
		if (a[l] == 'W') {
			tr[u].sum[0] = 1;
			tr[u].sum[1] = 1;
			tr[u].sum[2] = 1;
		}
		// lose
		if (a[l] == 'L') {
			tr[u].sum[0] = 0;
			tr[u].sum[1] = -1;
			tr[u].sum[2] = -1;
		}
		// draw
		if (a[l] == 'D') {
			tr[u].sum[0] = 0;
			tr[u].sum[1] = 0;
			tr[u].sum[2] = 0;
		}
		return;
	}
	int mid = l + r >> 1;
	build(u << 1, l, mid);
	build(u << 1 | 1, mid + 1, r);
	pushup(u);
}

pushup函数:

void pushup(int u) {
	// +333333330/1/2,使得区间初始值永远大于0,且不改变其 % 3 的结果
	tr[u].sum[0] = tr[u << 1].sum[0] + tr[u << 1 | 1].sum[(3333333330 + tr[u << 1].sum[0]) % 3];
	tr[u].sum[1] = tr[u << 1].sum[1] + tr[u << 1 | 1].sum[(3333333331 + tr[u << 1].sum[1]) % 3];
	tr[u].sum[2] = tr[u << 1].sum[2] + tr[u << 1 | 1].sum[(3333333332 + tr[u << 1].sum[2]) % 3];
}

ask函数:

// 返回的是变化值
ll ask(int u, int l, int r, ll x) { // 返回 变化值 而非 变化之后的x
	if (tr[u].l == l && tr[u].r == r) return tr[u].sum[x % 3];
	int mid = tr[u].l + tr[u].r >> 1;
	if (r <= mid) return ask(u << 1, l, r, x);
	else if (l > mid) return ask(u << 1 | 1, l, r, x);
	else {
		ll tt = ask(u << 1, l, mid, x); // left 变化值
		ll ttt = ask(u << 1 | 1, mid + 1, r, x + tt + 3333333333); // right 变化值 // + 3...防止负数
		return tt + ttt;
	}
}

C Baby’s first attempt on CPU

题意:
给定n个语句,并给出语句之间的“先写后读关系”,求出,最少添加多少个“空语句”,使得程序正常运行。
先写后读关系:若第i个语句与第j个语句之间存在先写后读关系,那么在程序运行了第i个语句后,至少要等三个语句才能运行第j个语句。
空语句:为了程序正常运行,在某些必要的情况下,会在语句之间添加空语句,程序会运行该语句。

涉及算法:差分约束
2022牛客寒假算法基础集训营1_第1张图片
以两个语句i和j来举例(i < j):
由于程序按照输入语句的先后顺序来运行,那么两个语句必然不能同时运行
两个语句的关系:
1.有先写后读关系,那么此时,两个语句之间的距离一定 >= 4
2.无先写后读关系,那么此时,两个语句之间的距离一定 >= 1

建边:

for (int i = 1; i <= n; i++) {
		for (int j = 1; j <= 3; j++) {
			int tt;
			cin >> tt;
			if (i - j >= 1) {
				// 从n到1跑最短路的方向建边
				if (tt) add(i, i - j, -4);
				else add(i, i - j, -1);
			}
		}
	}

跑最短路(由于n最大为100,bf即可):

void bf() {
	// 从n到1的最短路
	for (int i = 1; i <= n; i++) 
		dis[i] = inf;

	dis[n] = 0;
	while (n--) {
		for (int i = 1; i <= cnt; i++) {
			int u = es[i].u;
			int v = es[i].v;
			int w = es[i].cost;
			if (dis[u] != inf && dis[v] > dis[u] + w) {
				dis[v] = dis[u] + w;
			}
		}
	}
}

输出:

cout << (-dis[1]) - (n - 2) - 1;

D 牛牛做数论

题意:
给一个数n,回答两个问题:

1、令x ∈ [2,n],使得H(x)取到 H(x)在[2,n]的最大值。若存在多个这样的x ,输出最大的一个。
2、令x ∈ [2,n],使得H(x)取到 H(x)在[2,n]的最大值。若存在多个这样的x ,输出最大的一个。

涉及算法:欧拉函数

ϕ(x):对于x,[1, x - 1] 中与其互质的数的个数。
H(x) = x * (1 - 1 / pi) ,pi为x的质因子(按种类)。

对于问题1:
由于 () = ∗ (1 − 1 / ),之中取遍的所有种类的质因子(注意是种类而不是个数,如素因子2出现两次在公式里也只乘上一次),则 () = (1 − 1 / ),显然乘的项数越多、每一项越小()就越小,而按照2, 3, 5, 7, 11 … …的顺序取就可以达到这一目标。

对于问题2:
对于素数有 = − 1,因此() = −1 / ,直觉可以感到这是挺大一
(毕竟()最大不超过1),所以可以猜出取最大的素数即可。

核心代码如下:

void solve() {
	// 欧拉筛
	ola();
	// 预处理
	ll res = 1;
	for (int i = 1; i <= cnt; i++) {
		res *= prime[i];
		if (res >= 1000000000) break;
		use[++sum] = res;
	}
	
	cin >> n;
	if (n == 1) {
		cout << -1 << '\n';
		return;
	}
	
	// x ∈ [2,n],使得 H(x) 取得 H(x) 在 [2,n] 的最小值中的最小值x

	/*由于  =  ∗ ς(1 − 1 / ),
	之中取遍的所有种类的质因子(注意是种类而不是个数,
	如素因子2出现两次在公式里也只乘上一次),
	则  = ς(1 − 1 / ),显然乘的项数越多、每一项越小()就越小,
	而按照2, 3, 5, 7, 11 … …的顺序取就可以达到这一目标。*/

	for (int i = 9; i >= 1; i--) {
		if (use[i] <= n) {
			cout << use[i] << ' ';
			break;
		}
	}
	// x ∈ [2,n],使得 H(x) 取得 H(x) 在 [2,n] 的最大值中的最大值x

	/*对于素数有  =  − 1,因此() = −1 / ,
	直觉可以感到这是挺大一数(毕竟()最大不超过1),
	所以可以猜出取最大的素数即可。*/

	for (int i = n; i >= 2; i--) {
		if (isPrime(i)) {
			cout << i << '\n';
			break;
		}
	}
}

F 中位数切分

题意:给定一个长为n的数字a和一个整数m,需要将这个数组划分为若干段,且每一段的中位数都大于等于m,求最多可以划分成多少段,若无论如何划分都不能满足条件,输出-1。

涉及算法:离散化,前缀和

对于每个大于等于m的数,将其离散化为+1
对于每个小于m的数,将其离散化为-1

首先,对于任意一个符合条件的片段,其前缀和一定大于等于1

其次,要求整个数组划分为若干段

那么,假设数组a可以划分为x(x >= 1)段,那么从头遍历数组a,其前缀和一定大于等于1,若其前缀和小于1,那么无论如何都不可能满足条件

并且,要求划分的段数的最大值

因此,整个数组的前缀和,即为答案

核心代码如下:

cin >> n >> m;
	ll ans = 0;
	for (int i = 1; i <= n; i++) {
		int tt;
		cin >> tt;
		if (tt < m) ans--;
		else ans++;
	}
	if (ans > 0) cout << ans << '\n';
	else cout << -1 << '\n';

K 冒险公社

题意:给定一个长为n的只有’R’,‘G’,'B’组成的字符串,当玩家在第i个点时,若i,i - 1,i - 2,三个点中:G的数量多于R,该点显示G;G的数量等于R,该点显示B;G的数量小于R,该点显示R。
求出这个字符串最多有多少个点为G(非显示为G)。

涉及算法:线性dp,模拟

dp[i][j][k][l]:
i为第i个点的显示字母,j为第i个点的真实字母,k为第i-1个点的真实字母,l为第i-2个点的真实字母,若(i,j,k,l)四元组符合题意,那么计算从1到i所有搭配中,真实字母为G的最大值。

状态转移:
考虑到,对于第i个点,其真实字母,只能由第i-1个点的所有合法条件进行转移。
因此,可以通过dp[j][k][l][x]是否有值来判断该四元组(i,j,k,l)是否合法,并进行状态转移,保留每种状态的最大值。
当转移到第i个时,对其所有合法状态,求取最大值,即为答案。

核心代码如下:

void init() {
	cin >> n;
	scanf("%s", a + 1);

	memset(dp, -1, sizeof dp);

	// 0 -> R, 1 -> G, 2 -> B
	// index, i , i - 1, i - 2

	if (a[3] == 'R') {
		dp[3][0][0][0] = 0;
		// //
		dp[3][0][0][1] = 1;
		dp[3][0][1][0] = 1;
		dp[3][1][0][0] = 1;
		// //
		dp[3][0][0][2] = 0;
		dp[3][0][2][0] = 0;
		dp[3][2][0][0] = 0;
		// //
		dp[3][0][2][2] = 0;
		dp[3][2][0][2] = 0;
		dp[3][2][2][0] = 0;
		// //
		ans = 1;
	}

	if (a[3] == 'G') {
		dp[3][1][1][1] = 3;
		// //
		dp[3][1][1][0] = 2;
		dp[3][1][0][1] = 2;
		dp[3][0][1][1] = 2;
		// //
		dp[3][1][1][2] = 2;
		dp[3][1][2][1] = 2;
		dp[3][2][1][1] = 2;
		// //
		dp[3][1][2][2] = 1;
		dp[3][2][1][2] = 1;
		dp[3][2][2][1] = 1;
		// //
		ans = 3;
	}

	if (a[3] == 'B') {
		dp[3][2][2][2] = 0;
		// //
		dp[3][2][0][1] = 1;
		dp[3][2][1][0] = 1;
		dp[3][0][2][1] = 1;
		dp[3][1][2][0] = 1;
		dp[3][0][1][2] = 1;
		dp[3][1][0][2] = 1;
		// //
		ans = 1;
	}
}

void solve() {
	init();

	// 0 -> R, 1 -> G, 2 -> B
	for (int i = 4; i <= n; i++) {
		int res = -1;
		for (int j = 0; j <= 2; j++) {
			for (int k = 0; k <= 2; k++) {
				for (int l = 0; l <= 2; l++) {
					for (int x = 0; x <= 2; x++) {
						if (dp[i - 1][k][l][x] != -1) {
							int cnt[3] = { 0 };
							cnt[j]++,cnt[k]++,cnt[l]++;

							if (a[i] == 'R' && cnt[0] > cnt[1]) {
								dp[i][j][k][l] = max(dp[i][j][k][l], j == 1 ? dp[i - 1][k][l][x] + 1 : dp[i - 1][k][l][x]);
								res = max(res, dp[i][j][k][l]);
							}

							if (a[i] == 'G' && cnt[0] < cnt[1]) {
								dp[i][j][k][l] = max(dp[i][j][k][l], j == 1 ? dp[i - 1][k][l][x] + 1 : dp[i - 1][k][l][x]);
								res = max(res, dp[i][j][k][l]);
							}

							if (a[i] == 'B' && cnt[0] == cnt[1]) {
								dp[i][j][k][l] = max(dp[i][j][k][l], j == 1 ? dp[i - 1][k][l][x] + 1 : dp[i - 1][k][l][x]);
								res = max(res, dp[i][j][k][l]);
							}
						}
					}
				}
			}
		}
		if (res == -1) {
			cout << -1;
			return;
		}
		ans = res;
	}

	cout << ans;
}

你可能感兴趣的:(牛客寒假算法基础训练营,算法,acm竞赛,动态规划,数学,图论)