CF教育场 124 题解
A题 Playoff (数学)
现在有 \(2^n\) 名选手,编号从 \(1\) 到 \(2^n\),现在他们两两组队,以 \((1,2),(3,4),\cdots,(2^n-1,2^n)\) 的形式进行组队,然后进行单败赛制淘汰赛(可以参照原题中的图)。
两名编号为 \(i,j\) 的选手相遇的时候:
- \((i+j)\bmod 2=1\) 时,编号小者胜。
- \((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\}\),我们可以进行操作:
- 选定两个下标 \(i,j(i\not= j)\)。
- 使得 \(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|\)。一台电脑可以和若干个其他电脑相连。
我们的目标是:
- 所有电脑相连成一个局域网
- 这个局域网具有容错性(倘若坏了一台电脑,且仅坏了这么一台,那么剩下来的电脑还能够连成一个局域网)
\(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\) 损坏同理。
对于每个点,我们考虑一手:如果他要连入整个局域网(即和对面的电脑连接),那么:
- 通过左边的电脑作为中转点
- 通过右边的电脑作为中转点
- 自己直连下面的电脑
我们从中可以得到一个不太显然的结论:\(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;
}