Codeforces Round #712 (Div. 2)

本文介绍了Codeforces比赛中涉及的算法问题,包括如何构造非回文串、判断01串是否能通过有限次翻转变为目标串,以及如何构造合法的括号串。通过解析题目的核心思想和解决方案,展示了如何利用字符串处理、前缀和以及博弈论策略来解决这些问题。
摘要由CSDN通过智能技术生成

在 codeforces 的第一场比赛就基本全是思维题,对于我这种算法和数据结构全都忘光的人来说十分友好XD,但是俺只做出来了两题。(第四题明明想到国际象棋棋盘了!)

A. Déjà Vu

大意:给定一个字符串,询问是否能通过“在任意位置插入一个字符 a”的方式构造一个非回文串,若能则输出任意一个解。
思路:思考后可以发现“一个字符串在任意位置插入 'a' 后都是回文串”是一个相当强的条件。假设字符串为 s,长度为 l,那么“s为回文串”等价于:

                                                                            \forall i\in [0,l-1], s[i]=s[l - i - 1]

插入的位置一共有 l + 1 种选择,因此,对于满足这一条件的字符串,我们有:

                                                                            \forall i\in [0,l], s[l - i] = a

即这个字符串为全 'a' 串。

当这个字符串不为全 'a' 串时,一定能构造出非回文串。我们对上述的第一个命题取否定,可以得到:

                                                                            \exists i \in [0,l-1],s[i]\neq s[l-i-1]

假设我们插入 'a',我们希望此次插入能够破坏回文串这一性质(可能本来也不是回文串),因此我们让插入的 'a' 对称的位置为非 'a',即我们需要找到原字符串某一个不为 'a' 的位置,并把 'a' 插入到对称位置上。

另一种解法:假设原字符串为 s,我们可以断言:当解存在时,s + 'a' 和 'a' + s 必有一个是解。

对于每一组数据,由于要检查是否为全 'a' 以及遍历寻找第一个非 'a' 字符,因此时间复杂度为 O(n)

证明:当 s 不全为 'a' 时,假设 s + 'a' 和 'a' + s 都不是解,那么这两个字符串都是回文串。因此 s[0] 和 s[l - 1] 为 'a',因此 s[l - 2] 和 s[1] 为 'a',由此可以推出 s 为全 'a' 串。

代码实现:

#include<bits/stdc++.h>
using namespace std;

char str[300005];

bool is_all_a(int length) {
	for (int i = 0; i < length; ++i) {
		if (str[i] != 'a') return false;
	}
	return true;
}

int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		scanf("%s", str);
		int length = strlen(str);
		if (is_all_a(length)) {
			printf("NO\n");
			continue;
		}
		for (int i = 0; i < length; ++i) {
			if (str[i] != 'a') {
				printf("YES\n");
				for (int j = 0; j < length - i; ++j) printf("%c", str[j]);
				printf("a");
				for (int j = length - i; j < length; ++j) printf("%c", str[j]);
				printf("\n");
				break;
			}
		}
	}
	return 0;
}

B. Flip the Bits

大意:对于一个 01 串 a,定义一次修改操作为“选择 a 的一个具有相等个数的 0 和 1 的前缀并反转”。给定两个 01 串 a 和 b,询问是否能通过有限次修改将 a 变换为 b。

思路:问题的核心在于:执行修改操作并不会改变 0 和 1 的个数是否相等。这是因为如果 0 和 1 的个数不相等,那么便没有办法进行修改操作;如果 0 和 1 的个数相等,那么修改后 0 和 1 的个数交换,它们仍然相等。因此,我们是否能对一个前缀进行修改是固定的。继而我们能够知道:如果我们能够在有限步之内将 a 变换为 b,那么这些步骤的顺序可以任意交换。

基于这个性质,我们可以对问题进行转化:设 a 和 b 的长度为 n,若有 a[n-1]=b[n-1],那么问题等价于“是否通能过有限次修改将 a[: n - 1] 变换为 b[: n - 1]”;若有a[n-1]\neq b[n-1],那么我们还要思考是否能对 a 的长度为 n 的前缀执行转化操作。换言之,我们对 a 从后往前的每一位依次进行考虑。在代码实现上,为了保证时间复杂度,我们使用前缀和来维护前缀中 0 和 1 的个数。

另一种做法:我们考虑每一个能够进行修改的前缀,假设这些前缀有 k 个,它们的长度升序存储在数组 prefix 中。所以 01 串 a 被分成了 k + 1 个闭区间:

                                                                            a[0]-a[prefix[0]]

                                                                            a[prefix[0] + 1]-a[prefix[1]]

                                                                            ............

                                                                            a[prefix[k-1] + 1]-a[prefix[k]]

                                                                            a[prefix[k] + 1]-a[l - 1]

对于每一个区间,a 在其中的值必须全部与 b 相等或全部不等,不然无论我们后续进行什么样的修改,都无法使得 a 与 b 在这个区间中相等。

前缀和和扫描一遍的时间复杂度都为O(n),因此解法的时间复杂度为O(n)

第一个解法的代码实现:

#include<bits/stdc++.h>
using namespace std;

bool flag;
char stra[300005], strb[300005];
int length, cnt0[300005], cnt1[300005];

bool solve(int now) {
	//printf("-> %d %d %d %d\n", now, flag, stra[now], strb[now]);
	if (now == -1) return true;
	if ((stra[now] == strb[now]) ^ flag) return solve(now - 1);
	if (cnt0[now] != cnt1[now]) return false;
	flag ^= true;
	return solve(now - 1);
}

int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		flag = false;
		memset(cnt0, 0, sizeof(cnt0));
		memset(cnt1, 0, sizeof(cnt1));
		scanf("%d", &length);
		scanf("%s%s", stra, strb);
		cnt0[0] += stra[0] == '0';
		cnt1[0] += stra[0] == '1';
		for (int i = 1; i < length; ++i) {
			cnt0[i] += cnt0[i - 1] + (stra[i] == '0');
			cnt1[i] += cnt1[i - 1] + (stra[i] == '1');
		}
		if (solve(length - 1)) printf("YES\n");
		else printf("NO\n");
	}
	return 0;
}

C. Balance the Bits

大意:给定一个长度为 n 的 01 串 s ,构造两个长度为 n 的合法括号串 a 和 b(由 '(' 和 ')' 组成),它们满足\forall i \in [0, l - 1],s[i] = (a[i] == b[i])

(其实就是 s[i] 为 1 时 a[i] 和 b[i] 相等,s[i] 为 0 时 a[i] 和 b[i] 不等)

思路:首先我们明确判断括号串合法的方法,对于一个括号串中的所有左括号,总是存在一个右括号与它匹配。我们可以定义一个计数器 cnt = 0,从左到右依次遍历括号串,当遇到 '(' 时将计数器加一,遇到 ')' 时将计数器减一。这个括号串是合法的当且仅当计数器最后为 0,并且在这个过程中计数器始终非负。

显而易见:①. 我们的 n 必须是偶数,并且 a 中的左括号、a 中的右括号、b 中的左括号、b 中的右括号个数都为 n / 2。

②. s[0] 和 s[n - 1] 必须为 1,这是因为开头必须为 '(',结尾必须为 ')'。

③. 假设 s 中 1 和 0 的个数分别为 s1 和 s0,那么 s1 和 s0 必须为偶数。

证明:假如它们中有一个是奇数,那么 s1、s0 全部为奇数。

若 s[i] = 0,即 a[i] 和 b[i] 不一样,那么它们中有且仅有一个 '(' ,所有满足 s[i] = 0 的 i 对左括号总数的贡献为奇数。

若 s[i] = 1,即 a[i] 和 b[i] 一样,那么它们要么都是 '(',要么都是 ')',所有满足 s[i] = 1 的 i 对左括号总数的贡献为偶数。

因此,a 和 b 中左括号之和为偶数,因此 n 为偶数,矛盾。

当 s1 和 s0 都为偶数时,我们总可以这样构造出 a 和 b:

对于前一半的 s[i] = 1,令 a[i] = b[i] = '(',对于后一半的 s[i] = 1,令 a[i] = b[i] = ')';

对于剩下的 s[i] = 0,我们令 a[i] 和 b[i] 交替出现 '(' 和 ')'。

小编也觉得很神奇,但是事实就是这样,但是小编也没有办法。

玩笑归玩笑,但这题我没做出来,我能看出的是这个构造方法保证了括号串的合法性,换句话说,左括号越多越合法(显然),但前提是我们得有相等的右括号。

当 s[i] = 1,我们尽可能地多添加左括号,因此有如上的策略。

当 s[i] = 0,我们在添加左括号的同时也得添加一个右括号,因此采取了一种“尽量均衡”的策略。

同时我也看到一种对于这个问题的巧妙转化:

将状态的集合看作是一个二维坐标系。横坐标为 a 中的 cnt,纵坐标为 b 中的 cnt。从左到右的遍历过程,可以看作是从零点经过一条路径回到零点的过程。我们有四种移动方式:

                                                                            \\ a[i] = b[i] = (,(x,y)\rightarrow (x+1,y+1) \\ a[i] = b[i] = \ ),(x,y)\rightarrow (x-1,y-1) \\ a[i] = (, b[i] = \ ), (x,y)\rightarrow (x+1,y-1) \\ a[i] = ), b[i] = \ ( ,(x,y)\rightarrow (x-1,y+1)

“括号串的合法性”意味着我们不会移动到第二、第三、第四象限去。因此对于对于这种要求的最大保障是:我们尽量执行第一种移动操作。为了不越界我们尽量反复横跳而不是一直往一个方向走。

我们可以将 y = x 和 y = -x 看作新的 x 和 y 轴,构建新的平面直角坐标系。四种方法分别对应横纵坐标的加一减一,我们发现为了回到原点,横纵坐标应当被分别考虑,即 s[i] = 1 和 s[i] = 0 的情况互相独立。第一种移动操作和第二种移动操作的数量相等;第三种移动操作和第四种移动操作的数量相等。

代码实现:

#include<bits/stdc++.h>
using namespace std;

int length;
char is_diff[200005], stra[200005], strb[200005];

int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		int cnt1 = 0, cnt2 = 0;
		bool flag = true;
		scanf("%d", &length);
		scanf("%s", is_diff);
		if (is_diff[0] == '0' || is_diff[length - 1] == '0') {
			printf("NO\n");
			continue;
		}
		for (int i = 0; i < length; ++i) {
			cnt1 += (is_diff[i] == '1');
		}
		if (cnt1 & 1) {
			printf("NO\n");
			continue;
		}
		printf("YES\n");
		for (int i = 0; i < length; ++i) {
			if (is_diff[i] == '0') {
				if (flag) {
					stra[i] = '(';
					strb[i] = ')';
				}
				else {
					stra[i] = ')';
					strb[i] = '(';
				}
				flag ^= true;
			}
			else stra[i] = strb[i] = (++cnt2 <= (cnt1 / 2) ) ? '(' : ')';
		}
		for (int i = 0; i < length; ++i) {
			printf("%c", stra[i]);
		}
		printf("\n");
		for (int i = 0; i < length; ++i) {
			printf("%c", strb[i]);
		}
		printf("\n");
	}
	return 0;
}

D. 3-Coloring(交互题)

大意:给定三个颜色 1、2、3 和一个 n * n 的空白棋盘,每个格子都能填充一个颜色,你要和另一个人(测试数据)玩一个游戏并获得胜利:

每一轮,另一个人给出一个颜色,你可以用除了这个颜色之外的另两个颜色填充任意一个空白格子。

若有相邻的格子同色,你输;若所有格子填充完后你没有输,你赢。

已知你有必赢策略,试写出一个程序战胜任何对手。

思路:不管你能不能想到国际象棋棋盘,反正我是想到了。这是因为它实现了仅用黑白两色的不相邻填充。我们能不能只用两个颜色进行填充呢?显然不行,这是因为题目附加了“每次有一个颜色不能用”的限制。接下来我们具体的思考:假设 1 为白色,2 为黑色,我们按照国际象棋棋盘的样式涂色,会遇到什么问题呢?如果所有的黑格和白格都没被涂完,那么这时我们是不用担心颜色限制的,因为每一次我们至少能够涂一个黑格子或者白格子(一次没法限制两种颜色)。只有当一种颜色的格子涂完时,我们才会担心:我们想用的颜色是不是被禁止了。这个时候我们发现我们还有第三种颜色可以充数。同时,由于棋盘的黑白两色是交错排列的,我们永远不会遇到相邻同色的情况。

代码实现:

#include<bits/stdc++.h>
using namespace std;

int main() {
	int n, t, a, board[105][105];
	queue <pair <int, int> > black;
	queue <pair <int, int> > white;
	memset(board, 0, sizeof(board));
	scanf("%d", &n);
	t = n * n;
	for (int i = 1; i <= n; ++i) {
		for (int j = 1 + (!(i & 1)); j <= n; j += 2) {
			black.push(make_pair(i, j));
		}
	}
	for (int i = 1; i <= n; ++i) {
		for (int j = 1 + (i & 1); j <= n; j += 2) {
			white.push(make_pair(i, j));
		}
	}
	while (t--) {
		scanf("%d", &a);
		switch (a) {
			case 1 : {
				if (!white.empty()) {
					int x = white.front().first, y = white.front().second;
					white.pop();
					printf("%d %d %d\n", 2, x, y);
					fflush(stdout);
				}
				else {
					int x = black.front().first, y = black.front().second;
					black.pop();
					printf("%d %d %d\n", 3, x, y);
					fflush(stdout);
				}
				break;
			}
			case 2 : {
				if (!black.empty()) {
					int x = black.front().first, y = black.front().second;
					black.pop();
					printf("%d %d %d\n", 1, x, y);
					fflush(stdout);
				}
				else {
					int x = white.front().first, y = white.front().second;
					white.pop();
					printf("%d %d %d\n", 3, x, y);
					fflush(stdout);
				}
				break;
			}
			case 3 : {
				if (!black.empty()) {
					int x = black.front().first, y = black.front().second;
					black.pop();
					printf("%d %d %d\n", 1, x, y);
					fflush(stdout);
				}
				else {
					int x = white.front().first, y = white.front().second;
					white.pop();
					printf("%d %d %d\n", 2, x, y);
					fflush(stdout);
				}
				break;
			}
		}
	}
	return 0;
}

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值