CF Round 700(Div2) 解题补题报告
这把被吊锤就离谱,尤其是B题写的都失了智了
A题 Yet Another String Game(简单博弈)
给定一个字符串 \(s\),两个人 \(A\) 和 \(B\) 玩一个小游戏,每个人轮流出手,将某个位置的字符变换一下(不能和原来相同),每个位置仅能更改一次。\(A\) 的目标是使得最后的字符串的字典序尽量大, \(B\) 则相反。如果两人均采取最佳策略,问最后 \(s\) 会变成啥。
\(|s|\leq 50\)
看完题目,看下样例,答案就很显然了:
- 两个人都会尽可能选靠前的字符进行修改
- 对于 \(A\),为了使字符串字典序尽量大,肯定是能改成 \(a\) 就改,改不了(原来就是 \(a\))就改成 \(b\)
- 对于 \(B\),思路也类似
没啥好说的,具体看代码
#include <bits/stdc++.h>
using namespace std;
char s[100];
void solve()
{
scanf("%s", s);
int n = strlen(s);
for (int i = 0; i < n; ++i)
if (i % 2) s[i] = (s[i] != 'z') ? 'z' : 'y';
else s[i] = (s[i] != 'a') ? 'a' : 'b';
puts(s);
}
int main()
{
int T;
scanf("%d", &T);
while (T--) solve();
return 0;
}
B题 The Great Hero (思维)
有个英雄,每次攻击伤害为 \(A\),自身血量为 \(B\)。现在有 \(n\) 个怪物,第 \(i\) 个怪物伤害为 \(a_i\),血量为 \(b_i\)。
现在英雄每回合选一个怪物与之对战,一回合下来,两个人各自减少对应的血。如果谁血量小于等于 \(0\),那么他就死了。
现在想要判断,能不能干掉全部怪兽(和最后一个怪兽同归于尽也行)
显然必须消灭全部怪兽,那就一个一个打就是了,看看能不能全部打掉
这样写了一发程序,然后荣耀的 \(WA\) 了
这时候群里面有大(han)佬(pi) 说,要排序,要排序!
我一听,确实,是这样的,打怪的顺序对于我们确实有影响,例如下面这组数据:
10 100 2
110 30
10 20
很显然,如果我们先和第一个怪物打,那就会和他同归于尽,所以应该先和第二个打,这样才能将怪兽全部击败
(一种贪心策略:对于某些怪兽,如果注定和他同归于尽或者大批掉血啥的,我们尽量往后拖,然后先打战斗力低的)
对于第 \(i\) 个怪兽,我们可以将 \(b_i\) 转变为它可以挨英雄打的次数,也就是 \(b_i=(b_i + A - 1) / A\),然后这个怪物战斗力就是 \(a_ib_i\),按照这玩意排序,然后顺着打就完了。
但我们有找到了一组反例:
10 102 3
1 100 20
10 10 50
这数据已经排好序了,但是显然打第三个怪物会 \(GG\),但是我们应该先打第三个,然后和第二个同归于尽才是最佳策略。
懂了,\(a_ib_i\) 相同时候,把 \(b_i\) 小的排在后面
不好意思,又 \(WA\) 了,我们可以找到这样一组反例(对上面那个样例修改一下):
10 102 3
1 99 20
10 10 50
我就问,你现在还能怎么排?
我相信有不少大佬还能排,而且比赛的时候能过(随后被 \(HACK\) 的体无完肤),但是我们现在应该跳出这个思维了:按照特定思路贪心并且排序,真的是正解吗?
其实经过上面的一系列操作加上 \(WA\) 之后,我们基本上已经清楚了消灭所有怪兽的一种基本策略:
- 先消灭 \(n-1\) 个怪兽,并且保证消灭完之后英雄还活着
- 消灭最后一个怪兽,要么全身而退(打完还活着),要么同归于尽
对于全身而退的方式,我们上面所有的方案都没问题,重点在于这个同归于尽:在血量不够的情况下,尽量最优化,和最后一个怪物换掉
这东西是没法贪心选出来的,但是我们发现,这东西似乎可以线性枚举(就离谱):枚举和哪一个怪兽最后打,然后看看别的 \(n-1\) 个怪兽能不能全部消灭,然后和最后一个怪兽尝试互换一波
越说越迷糊了,直接上代码了(昨晚把我折磨傻了):
#include <bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;
int n;
LL A, B, a[N], b[N];
bool solve()
{
scanf("%lld%lld%d", &A, &B, &n);
for (int i = 1; i <= n; ++i)
scanf("%lld", &a[i]);
for (int i = 1; i <= n; ++i)
scanf("%lld", &b[i]);
LL sum_attack = 0;
for (int i = 1; i <= n; ++i) {
b[i] = (b[i] + A - 1) / A;
sum_attack += a[i] * b[i];
}
for (int i = 1; i <= n; ++i) {
if (sum_attack - a[i] * b[i] >= B) continue;
B -= (sum_attack - a[i] * b[i]);
if ((B + a[i] - 1) / a[i] >= b[i]) return true;
B += (sum_attack - a[i] * b[i]);
}
return false;
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
puts(solve() ? "YES" : "NO");
return 0;
}
C题 Searching Local Minimum (二分,交互题)
这是一个交互题。
给定一个 \(1\) 到 \(n(1\leq n \leq 10^5)\) 的排列 \(\{a_n\}\),但是我们只知道 \(n\) 的大小,不知道排列的具体情况。
我们每次可以向评测姬询问,问 \(a_i\) 是多少 \((1\leq i \leq n)\),询问次数不得超过 \(100\) 次。
现在我们希望找到一个正整数 \(k\),使得 \(a_k<a_{k-1}\) 且 \(a_k<a_{k+1}\) (我们默认 \(a_0=a_{n+1}=+\infty\))
好,这题重新让我认识了二分(就离谱)
\(100\) 次找 \(10^5\) 规模的数据,不用怀疑,二分没跑了,主要是:这 B 玩意咋二分?
我们不妨设答案区间为 \([l,r]\),也就是该区间必然存在 \(k\),使得 \(a_{k-1}>a_k<a_{k+1}\)
我们令 \(mid=(l+r)/2\),查询 \(a_{mid}\) 和 \(a_{mid+1}\) 的值。
证明:当 \(a_{mid} < a_{mid+1}\) 时,区间 \([l,mid]\) 必然有解:
我们可以反证,假设该区间无解:因为\(a_{l-1}>a_l\) 作为先决条件必然成立(如果不懂就先接着看,待会就懂了),所以为了保证无解,\(\{a_n\}\) 在区间 \([l,mid]\) 上必须单调递减,否则必然能够找出反例。尴尬的是,此时恰好出现 \(a_{mid-1}>a_{mid}\) 的情况,说明 \(mid\) 是一个可行解。
反证矛盾,证明正向结论成立。
反之,如果 \(a_{mid}>a_{mid+1}\) 时,区间 \([l+1,r]\) 必然有解,证明同上。
如果严格遵守这套递归流程,那么可以保证边界条件成立。
好了,一套数学证明下来,我们便找出了二分策略,很显然,询问次数不会超过 \(100\) 次。
这题难度我给一分,因为我一份抵别人一伯分(逃
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, a[N];
void query(int x) {
if (x < 1 || x > n || a[x]) return;
printf("? %d\n", x);
fflush(stdout);
scanf("%d", &a[x]);
}
int main()
{
scanf("%d", &n);
a[0] = a[n + 1] = n + 1;
int l = 1, r = n + 1;
while (l < r) {
int mid = (l + r) >> 1;
query(mid);
query(mid + 1);
if (a[mid] < a[mid + 1]) r = mid;
else l = mid + 1;
}
printf("! %d\n", l);
fflush(stdout);
return 0;
}
D1题 Painting the Array I (贪心)
给定一个数列 \(\{a_n\}\),尝试将其分成两个数列 \(\{b_n\}\) 和 \(\{c_n\}\) (这两个数列允许是空的)。
现在给定一个操作 \(seg\):对于一个数列,在不排序的情况下将其去重(也就是只去重那些相邻的),然后剩下来的那个数列的长度,称其为该数列的 \(seg\) 值。
现在尝试找出一种分法,使得 \(seg(b)+seg(c)\) 最大,并且输出该最大值。
\(1\leq n \leq 10^5,1\leq a_i\leq n\)
这题目似乎只能贪心来写就离谱。
我们不妨记 \(\{b_n\}\) 的最后一个元素是 \(x\),\(\{c_n\}\) 的最后一个元素是 \(y\) ,\(O(n)\) 线性扫描数组 \(\{a_n\}\),记扫描到的数字是 \(z\) 。
显然有这样的贪心策略(虽然这玩意证明就离谱):
-
当 \(x=z\) 时就把 \(z\) 插到 \(\{c_n\}\) 后面,反之亦然;如果\(x=y=z\),那就随便插
这个应该比较显然了吧,这种插法但凡修改一下,都不会使得答案更优
-
当 \(x\not=z,y\not= z\) 时,这时候理论上插在谁后面都行,但是这里有个小问题:
打个比方,例如数列 \(\{1,3,5,3\}\),此时 \(\{b_n\} = {1}\) ,\(\{c_n\}=3\),这时扫到 \(z=5\),显然,我们应该把这个数插到 \(\{c_n\}\) 而非 \(\{b_n\}\) 后面。
这里引入一个 \(next\) 函数,例如 \(next(x)\) 表示 \(\{a_n\}\) 下一个值为 \(x\) 的下标。例如上述情况下, \(next(y)=4\)
有个似乎比较显然的结论:当 \(x\not=z,y\not=z\) 时,虽然两种插法都行,但是最好当 \(next(x) < next(y)\) 时插在 \(\{b_n\}\) 后面, \(next(y) < next(x)\) 时插在 \(\{c_n\}\) 后面(赶紧堵住,防止到时候重复)。
这玩意只能说是靠直觉感受出来的,但是证明并不显然。实际上,我看了官网的题解证明,还以为点错了题,点进了 div2 D。所以说这玩意感受下就好,不要强求证明了(反正我肯定不会
现在考虑下如何维护这个 \(next\) 了,毕竟暴力的复杂度是 \(O(n^2)\),铁超时。我们可以开一个 \(pos\) 数组维护一下,这样就可以 \(O(n)\) 从后向前扫一遍就出结果了(详情见代码)。
这里有个小细节:为了方便,我是一开始默认将前两个数给分别分到 \(\{b_n\}\) 和 \(\{c_n\}\) 里面的,但是在第 8 个点 \(WA\) 了,(因为前两个数在部分情况下应该放在一个数组里面,虽然我还没有找到一个足够简单的例子),所以我又稍微改了下,变成下面这段代码(不仅思维难度高,而且代码本身还有亿些小细节):
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
int n, a[N], Next[N], pos[N];
int main()
{
//input
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
//solve
for (int i = 0; i <= n; ++i)
pos[i] = n + 1;
for (int i = n; i >= 0; --i)
Next[i] = pos[a[i]], pos[a[i]] = i;
int cnt1 = 0, cnt2 = 0, x = 0, y = 0;
for (int i = 1; i <= n; ++i)
if (a[i] == a[x])
//注意这里,我一开始改的时候没注意,顺序反了,
//然后在第二个点 WA(qaq)
y = i, cnt2 += (a[i] != a[y]);
else if (a[i] == a[y])
x = i, ++cnt1;
else {
if (Next[x] < Next[y]) x = i, ++cnt1;
else y = i, ++cnt2;
}
//output
printf("%d", cnt1 + cnt2);
return 0;
}
D2题 Painting the Array II (贪心)
这题和上一题的区别在于,这里是尝试使得表达式的值最小。
不会,告辞。