在 codeforces 的第一场比赛就基本全是思维题,对于我这种算法和数据结构全都忘光的人来说十分友好XD,但是俺只做出来了两题。(第四题明明想到国际象棋棋盘了!)
A. Déjà Vu
大意:给定一个字符串,询问是否能通过“在任意位置插入一个字符 a”的方式构造一个非回文串,若能则输出任意一个解。
思路:思考后可以发现“一个字符串在任意位置插入 'a' 后都是回文串”是一个相当强的条件。假设字符串为 s,长度为 l,那么“s为回文串”等价于:
插入的位置一共有 l + 1 种选择,因此,对于满足这一条件的字符串,我们有:
即这个字符串为全 'a' 串。
当这个字符串不为全 'a' 串时,一定能构造出非回文串。我们对上述的第一个命题取否定,可以得到:
假设我们插入 'a',我们希望此次插入能够破坏回文串这一性质(可能本来也不是回文串),因此我们让插入的 'a' 对称的位置为非 'a',即我们需要找到原字符串某一个不为 'a' 的位置,并把 'a' 插入到对称位置上。
另一种解法:假设原字符串为 s,我们可以断言:当解存在时,s + 'a' 和 'a' + s 必有一个是解。
对于每一组数据,由于要检查是否为全 'a' 以及遍历寻找第一个非 'a' 字符,因此时间复杂度为 。
证明:当 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 的前缀执行转化操作。换言之,我们对 a 从后往前的每一位依次进行考虑。在代码实现上,为了保证时间复杂度,我们使用前缀和来维护前缀中 0 和 1 的个数。
另一种做法:我们考虑每一个能够进行修改的前缀,假设这些前缀有 k 个,它们的长度升序存储在数组 prefix 中。所以 01 串 a 被分成了 k + 1 个闭区间:
............
对于每一个区间,a 在其中的值必须全部与 b 相等或全部不等,不然无论我们后续进行什么样的修改,都无法使得 a 与 b 在这个区间中相等。
前缀和和扫描一遍的时间复杂度都为,因此解法的时间复杂度为。
第一个解法的代码实现:
#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(由 '(' 和 ')' 组成),它们满足。
(其实就是 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。从左到右的遍历过程,可以看作是从零点经过一条路径回到零点的过程。我们有四种移动方式:
“括号串的合法性”意味着我们不会移动到第二、第三、第四象限去。因此对于对于这种要求的最大保障是:我们尽量执行第一种移动操作。为了不越界我们尽量反复横跳而不是一直往一个方向走。
我们可以将 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;
}