CF教育场 124 题解

A题 Playoff (数学)

现在有 \(2^n\) 名选手,编号从 \(1\)\(2^n\),现在他们两两组队,以 \((1,2),(3,4),\cdots,(2^n-1,2^n)\) 的形式进行组队,然后进行单败赛制淘汰赛(可以参照原题中的图)。

两名编号为 \(i,j\) 的选手相遇的时候:

  1. \((i+j)\bmod 2=1\) 时,编号小者胜。
  2. \((i+j)\bmod 2=0\) 时,编号大者胜。

给定 \(n\),问最后胜者的编号是多少?

\(n\leq 30\)

我们不难想出一个 \(O(2^n)\) 的暴力:

int f(int l, int r) {
    if (l == r) return l;
    int mid = (l + r) >> 1;
    int x = f(l, mid), y = f(mid + 1, r);
    if ((x + y) % 2 == 1) return min(x, y);
    return max(x, y);
}
void solve()
{
    int n;
    cin >> n;
    cout << f(1, 1 << n) << endl;
}

可惜有点错估复杂度了(\(O(2^n)\) 的取值上限并不是 30(它只是 int 的极限),25可能已经是极限了),所以上手先 TLE 了一发,很烦。

好在我们至少可以打表,惊讶的发现 \(ans=2^{n}-1\)。为啥捏?

虽然知道结论八九不离十,但我们还是先证明一下:在第一轮游戏中,每一组中必然是编号小的那个奇数获胜(因为一开始每一组中都是奇偶配对,且奇数更小)。那么在第二轮及之后,所有对抗都是两个奇数之间的,奇奇相加为偶,那么则是编号大者获胜,而 \([1,2^n]\) 中最大的奇数恰好是 \(2^n-1\)

#include<bits/stdc++.h>
using namespace std;
int main()
{
    int T;
    cin >> T;
    while (T--) {
        int n;
        cin >> n;
        cout << (1 << n) - 1 << endl;
    }
    return 0;
}

B题 Prove Him Wrong (数学)

对于一个序列 \(\{a_n\}\),我们可以进行操作:

  1. 选定两个下标 \(i,j(i\not= j)\)
  2. 使得 \(a_i=a_j=|a_i-a_j|\)

小明觉得,对于任意长度,且 \(1\leq a_i\leq 10^9\) 的数组,在进行一次操作之后,数值所有元素之和一定会变小。小红觉得小明的想法并不是很对,所以希望我们能构造出一个数列来反驳他。

给定若干组数据,每次给定长度 \(n\),问能否构造出一个长度为 \(n\)\(1\leq a_i\leq 10^9\),且能够证伪小明观点的数组?

\(2\leq n\leq 1000\)

这个序列是不考虑顺序的,所以我们可以先排个序,然后去掉绝对值之后来看性质。

我们考虑两个数 \(x,y(x<y)\),那么在操作前的元素和为 \(x+y\),操作后的元素和为 \(2|x-y|=2y-2x\)

为了证伪小明观点,必须使 \(x+y\leq 2y-2x\),即 \(3x\leq y\)

那很显然,我们可以构造一个首项为 1,公比为 3 的等比数列。不过由于范围限制,数列的最大值得不超过 \(10^9\)。因为这个等比数列已经是所有合法数列里面增长最慢的了,所以我们直接求出满足 \(3^{n-1}\leq 10^9\) 的最大的 \(n\)。计算发现(Python 大法好),\(n\) 的最大值是 19。

#include <bits/stdc++.h>
using namespace std;
int n, a[20];
int main()
{
    a[1] = 1;
    for (int i = 2; i <= 19; ++i)
        a[i] = a[i - 1] * 3;
    //
    int T;
    cin >> T;
    while (T--) {
        cin >> n;
        if (n > 19) {
            puts("NO");
            continue;
        }
        puts("YES");
        for (int i = 1; i <= n; ++i)
            printf("%d ", a[i]);
        puts("");
    }
    return 0;
}

C题 Fault-tolerant Network (思维,暴力枚举)

给定两排电脑,每排 \(n\) 台,第一排电脑的权值从左到右分别为 \(a_i\),第二排则为 \(b_i\)。每一排内部,第 \(i\) 台电脑和第 \(i+1\) 台电脑用电缆相连(\(1\leq i < n\))。显然,两排电脑各自组成了一个局域网。

现在,我们尝试将两台不在同一排的电脑相连,每连一条电缆的代价为 \(|a_i-b_j|\)。一台电脑可以和若干个其他电脑相连。

我们的目标是:

  1. 所有电脑相连成一个局域网
  2. 这个局域网具有容错性(倘若坏了一台电脑,且仅坏了这么一台,那么剩下来的电脑还能够连成一个局域网)

\(n\leq 2*10^5,1\leq a_i,b_i\leq 10^9\)

我们考虑将 \(a_i,b_j\) 相连,那么当 \(a_i\) 损坏的时候,\([a_1,a_{i-1}]\)\([a_{i+1},a_n]\) 就和下面断开了连接,\(b_j\) 损坏同理。

对于每个点,我们考虑一手:如果他要连入整个局域网(即和对面的电脑连接),那么:

  1. 通过左边的电脑作为中转点
  2. 通过右边的电脑作为中转点
  3. 自己直连下面的电脑

我们从中可以得到一个不太显然的结论:\(a_1,b_1,a_n,b_n\) 这四台电脑必须得直连对面排的某台电脑(如果他们自己不直连,那么他就必须依赖于自己某一边的某台电脑和对面相连,而一旦那台电脑坏了,那就 G 了)

而一旦这四台电脑已经搞定,那么突然发现,剩下来的电脑也不用考虑了:他们要么和所在排最左边电脑相连(\(a_1/b_1\)),要么和最右边电脑(\(a_n/b_n\))相连,或者自己恰好和对面直连。无论哪台电脑坏了,都能走另一条路到达对面。

现在问题就变成了,选取若干条边,使得 \(a_1,b_1,a_n,b_n\) 四台电脑均与对面相连的情况下,边的权值最小。

我们发现,在这种指导方案下,对于 \(a_1,1<i<n\)\(a_1\)\(b_i\) 相连是没有去别的,那么我们可以采取类似缩点的方法, 抽象意义上将其视为一个点,而对应边权则是和 \(a_1/b_1/a_n/b_n\) 相连的最小值。

现在问题就变成了,给定一个 6 个点的图(每排 3 个),我们要选取若干条边,使得四个顶点均和对面相连。

讲道理,下面就是各位 dalao 展示自己怎么暴力的绝佳场所了,我自己给出一下我考场上写的暴力:

我们列一个表格,展示可选边的性质,并采取状态压缩来简化复杂程度和常数:

1 5 2
3 6 4
编号 所连点对 边权 联通状态
0 (1,3) \(\vert a_1-b_1\vert\) 10(1010)
1 (1,6) \(\min\limits_{1<i<n}\vert a_1-b_i\vert\) 8(1000)
2 (1,4) \(\vert a_1-b_n\vert\) 9(1001)
3 (3,5) \(\min\limits_{1<i<n}\vert b_1-a_i\vert\) 2(0010)
4 (2,3) \(\vert a_n-b_1\vert\) 6(0110)
5 (4,5) \(\min\limits_{1<i<n}\vert b_n-b_i\vert\) 1(0001)
6 (2,4) \(\vert a_n-b_n\vert\) 5(0101)
7 (2,6) \(\min\limits_{1<i<n}\vert a_n-b_i\vert\) 4(0100)

接下来,我们就是从中选取若干条边,在联通状态的或和为 15(1111) 的情况下,使得边权最小化,复杂度 \(O(8*2^8)\)

综上,总复杂度 \(O(n)\),本质上就是思维题过后尽能力去暴力罢了。

#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 200010;
int n, a[N], b[N];
int val[10], vis[10];
LL solve() {
    //
    cin >> n;
    for (int i = 1; i <= n; ++i)
        cin >> a[i];
    for (int i = 1; i <= n; ++i)
        cin >> b[i];
    //
        val[0] = abs(a[1] - b[1]), val[2] = abs(a[1] - b[n]),
        val[4] = abs(a[n] - b[1]), val[6] = abs(a[n] - b[n]);
    val[1] = 2e9, val[3] = 2e9, val[5] = 2e9, val[7] = 2e9;
    for (int i = 2; i < n; ++i) {
        val[1] = min(val[1], abs(a[1] - b[i]));
        val[3] = min(val[3], abs(b[1] - a[i]));
        val[5] = min(val[5], abs(b[n] - a[i]));
        val[7] = min(val[7], abs(a[n] - b[i]));
    }
    //
    vis[0] = 10, vis[1] = 8, vis[2] = 9, vis[3] = 2;
    vis[4] =  6, vis[5] = 1, vis[6] = 5, vis[7] = 4;
    LL ans = 1e18;
    for (int x = 0; x < (1 << 8); ++x) {
        LL res = 0, V = 0;
        for (int k = 0; k < 8; ++k)
            if ((x >> k) & 1) res += val[k], V |= vis[k];
        if (V == 15) ans = min(ans, res);
    }
    return ans;
}
int main()
{
    int T;
    cin >> T;
    while (T--) cout << solve() << endl;
    return 0;
}

D题 Nearest Excluded Points (基于离散化的 BFS)

给定 \(n\) 个点,第 \(i\) 个点的坐标为 \((x_i,y_i)\)

对于每个点,给出坐标系上距离他最近(曼哈顿距离),且在给定的点中没有出现的点。

\(1\leq n,x_i,y_i \leq 2*10^5\),给出的点的坐标数值范围在 \([-10^6,10^6]\)

纯纯牛马题,我怎么就不能跟出题人一样逆天呢?

如果一个点,他上下左右有一个点是空着的,那么这个点距离最近的空点就是这个点,最近距离为 1。

那么,剩下来的点都是那些上下左右有点的,我们怎么求他们距离最近的空点呢?注意到曼哈顿距离这个词有点学术,如果我们将其描述为 方格地图上两点之间的路径长度,那可能就有点思路了。。。

我们采取一个 BFS,开个队列,首先先将那些最短距离为 1 的点扔进去,然后依次 BFS,每次扩展新的点扔进去,前驱和后驱共享同一个那个最近的空点。能够证明,这种策略是最优的(因为如果一个外面的空点和这个点相连,那么中间任意一条路径都必然经过他的上下左右四个点,再加上点距离的可加和传递性,所以我们可以通过 BFS 来求解)

有点遗憾的是,这题的地图大小是 \((2*10^5,2*10^5)\),我们显然不能开一个这么大的二维数组。好在点的数量规模仅有 \(n=2*10^5\),所以我们可以开一个 map 或者 set 来存储所有的点,然后查询某个点是否存在的话就可以直接尝试查找即可。

总复杂度是 \(O(n\log n)\),常数极大(还好 CF 默认吸氧,而且时限放到了 4s)。如果进一步优化,我们可以手写 hash 来代替 pair,或者用 unordered 系列容器代替普通的 map/set。

#include<bits/stdc++.h>
using namespace std;
#define PII pair<int, int>
const int N = 200010;
const int dx[4] = {0, 0, -1, 1}, dy[4] = {-1, 1, 0, 0};
int n;
PII node[N];
set<PII> se;
bool exist(int x, int y) {
    return se.find(make_pair(x, y)) != se.end();
}
map<PII, PII> ans;
map<PII, int> dis;
queue<PII> q;
int main()
{
    //read & build
    cin >> n;
    for (int i = 1; i <= n; ++i) {
        int x, y;
        cin >> x >> y;
        node[i] = make_pair(x, y);
        se.insert(node[i]);
    }
    //solve
    for (int i = 1; i <= n; ++i) {
        int x = node[i].first, y = node[i].second;
        for (int k = 0; k < 4; ++k) {
            int tx = x + dx[k], ty = y + dy[k];
            if (!exist(tx, ty)) {
                dis[node[i]] = 1, ans[node[i]] = make_pair(tx, ty);
                q.push(node[i]);
                break;
            }
        }
    }
    while (!q.empty()) {
        PII Now = q.front(); q.pop();
        int x = Now.first, y = Now.second;
        for (int k = 0; k < 4; ++k) {
            int tx = x + dx[k], ty = y + dy[k];
            PII To = make_pair(tx, ty);
            if (exist(tx, ty) && dis[To] == 0) {
                dis[To] = dis[Now] + 1, ans[To] = ans[Now];
                q.push(To);
            }
        }
    }
    //output
    for (int i = 1; i <= n; ++i)
        printf("%d %d\n", ans[node[i]].first, ans[node[i]].second);
    return 0;
}
posted @ 2022-03-11 10:40  cyhforlight  阅读(42)  评论(0编辑  收藏  举报