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

官方题解

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

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

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

\(|s|\leq 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\) 个怪物伤害为 \(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\) 之后,我们基本上已经清楚了消灭所有怪兽的一种基本策略:

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

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

这东西是没法贪心选出来的,但是我们发现,这东西似乎可以线性枚举(就离谱):枚举和哪一个怪兽最后打,然后看看别的 \(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\)

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

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

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

  2. \(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 (贪心)

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

不会,告辞。

posted @ 2021-02-09 15:13  cyhforlight  阅读(138)  评论(1编辑  收藏  举报