CF Round 700(Div2) 解题补题报告

官方题解

这把被吊锤就离谱,尤其是B题写的都失了智了

A题 Yet Another String Game(简单博弈)

给定一个字符串 s,两个人 AB 玩一个小游戏,每个人轮流出手,将某个位置的字符变换一下(不能和原来相同),每个位置仅能更改一次。A 的目标是使得最后的字符串的字典序尽量大, B 则相反。如果两人均采取最佳策略,问最后 s 会变成啥。

|s|50

看完题目,看下样例,答案就很显然了:

  1. 两个人都会尽可能选靠前的字符进行修改
  2. 对于 A,为了使字符串字典序尽量大,肯定是能改成 a 就改,改不了(原来就是 a)就改成 b
  3. 对于 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 个怪物伤害为 ai,血量为 bi

现在英雄每回合选一个怪物与之对战,一回合下来,两个人各自减少对应的血。如果谁血量小于等于 0,那么他就死了。

现在想要判断,能不能干掉全部怪兽(和最后一个怪兽同归于尽也行)

显然必须消灭全部怪兽,那就一个一个打就是了,看看能不能全部打掉

这样写了一发程序,然后荣耀的 WA

这时候群里面有大(han)佬(pi) 说,要排序,要排序!

我一听,确实,是这样的,打怪的顺序对于我们确实有影响,例如下面这组数据:

10 100 2 110 30 10 20

很显然,如果我们先和第一个怪物打,那就会和他同归于尽,所以应该先和第二个打,这样才能将怪兽全部击败

(一种贪心策略:对于某些怪兽,如果注定和他同归于尽或者大批掉血啥的,我们尽量往后拖,然后先打战斗力低的)

对于第 i 个怪兽,我们可以将 bi 转变为它可以挨英雄打的次数,也就是 bi=(bi+A1)/A,然后这个怪物战斗力就是 aibi,按照这玩意排序,然后顺着打就完了。

但我们有找到了一组反例:

10 102 3 1 100 20 10 10 50

这数据已经排好序了,但是显然打第三个怪物会 GG,但是我们应该先打第三个,然后和第二个同归于尽才是最佳策略。

懂了,aibi 相同时候,把 bi 小的排在后面

不好意思,又 WA 了,我们可以找到这样一组反例(对上面那个样例修改一下):

10 102 3 1 99 20 10 10 50

我就问,你现在还能怎么排?

我相信有不少大佬还能排,而且比赛的时候能过(随后被 HACK 的体无完肤),但是我们现在应该跳出这个思维了:按照特定思路贪心并且排序,真的是正解吗?

其实经过上面的一系列操作加上 WA 之后,我们基本上已经清楚了消灭所有怪兽的一种基本策略:

  1. 先消灭 n1 个怪兽,并且保证消灭完之后英雄还活着
  2. 消灭最后一个怪兽,要么全身而退(打完还活着),要么同归于尽

对于全身而退的方式,我们上面所有的方案都没问题,重点在于这个同归于尽:在血量不够的情况下,尽量最优化,和最后一个怪物换掉

这东西是没法贪心选出来的,但是我们发现,这东西似乎可以线性枚举(就离谱):枚举和哪一个怪兽最后打,然后看看别的 n1 个怪兽能不能全部消灭,然后和最后一个怪兽尝试互换一波

越说越迷糊了,直接上代码了(昨晚把我折磨傻了):

#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 (二分,交互题)

这是一个交互题。

给定一个 1n(1n105) 的排列 {an},但是我们只知道 n 的大小,不知道排列的具体情况。

我们每次可以向评测姬询问,问 ai 是多少 (1in),询问次数不得超过 100 次。

现在我们希望找到一个正整数 k,使得 ak<ak1ak<ak+1 (我们默认 a0=an+1=+

好,这题重新让我认识了二分(就离谱)

100 次找 105 规模的数据,不用怀疑,二分没跑了,主要是:这 B 玩意咋二分?

我们不妨设答案区间为 [l,r],也就是该区间必然存在 k,使得 ak1>ak<ak+1

我们令 mid=(l+r)/2,查询 amidamid+1 的值。

证明:当 amid<amid+1 时,区间 [l,mid] 必然有解:

我们可以反证,假设该区间无解:因为al1>al 作为先决条件必然成立(如果不懂就先接着看,待会就懂了),所以为了保证无解,{an} 在区间 [l,mid] 上必须单调递减,否则必然能够找出反例。尴尬的是,此时恰好出现 amid1>amid 的情况,说明 mid 是一个可行解。

反证矛盾,证明正向结论成立。

反之,如果 amid>amid+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 (贪心)

给定一个数列 {an},尝试将其分成两个数列 {bn}{cn} (这两个数列允许是空的)。

现在给定一个操作 seg:对于一个数列,在不排序的情况下将其去重(也就是只去重那些相邻的),然后剩下来的那个数列的长度,称其为该数列的 seg 值。

现在尝试找出一种分法,使得 seg(b)+seg(c) 最大,并且输出该最大值。

1n105,1ain

这题目似乎只能贪心来写就离谱。

我们不妨记 {bn} 的最后一个元素是 x{cn} 的最后一个元素是 yO(n) 线性扫描数组 {an},记扫描到的数字是 z

显然有这样的贪心策略(虽然这玩意证明就离谱):

  1. x=z 时就把 z 插到 {cn} 后面,反之亦然;如果x=y=z,那就随便插

    这个应该比较显然了吧,这种插法但凡修改一下,都不会使得答案更优

  2. xz,yz 时,这时候理论上插在谁后面都行,但是这里有个小问题:

    打个比方,例如数列 {1,3,5,3},此时 {bn}=1{cn}=3,这时扫到 z=5,显然,我们应该把这个数插到 {cn} 而非 {bn} 后面。

    这里引入一个 next 函数,例如 next(x) 表示 {an} 下一个值为 x 的下标。例如上述情况下, next(y)=4

    有个似乎比较显然的结论:当 xz,yz 时,虽然两种插法都行,但是最好当 next(x)<next(y) 时插在 {bn} 后面, next(y)<next(x) 时插在 {cn} 后面(赶紧堵住,防止到时候重复)。

这玩意只能说是靠直觉感受出来的,但是证明并不显然。实际上,我看了官网的题解证明,还以为点错了题,点进了 div2 D。所以说这玩意感受下就好,不要强求证明了(反正我肯定不会

现在考虑下如何维护这个 next 了,毕竟暴力的复杂度是 O(n2),铁超时。我们可以开一个 pos 数组维护一下,这样就可以 O(n) 从后向前扫一遍就出结果了(详情见代码)。

这里有个小细节:为了方便,我是一开始默认将前两个数给分别分到 {bn}{cn} 里面的,但是在第 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 (贪心)

这题和上一题的区别在于,这里是尝试使得表达式的值最小。

不会,告辞。


__EOF__

本文作者cyhforlight
本文链接https://www.cnblogs.com/cyhforlight/p/14393045.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   cyhforlight  阅读(143)  评论(1编辑  收藏  举报
编辑推荐:
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 25岁的心里话
点击右上角即可分享
微信分享提示