专栏文章
题解:P13343 [EGOI 2025] 一个弦线问题
P13343题解参与者 1已保存评论 0
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 0 条
- 当前快照
- 1 份
- 快照标识符
- @mioqbbnm
- 此快照首次捕获于
- 2025/12/02 23:24 3 个月前
- 此快照最后确认于
- 2025/12/02 23:24 3 个月前
感谢来自上一篇题解的大佬 rui_er 的指教!
弦的平行
观察题目给出的两个圆形示例。
在这两个圆形中,所有共 个琴钉连接的 条弦线都互相平行。
在这两个圆形中,所有共 个琴钉连接的 条弦线都互相平行。
规律
试试在图中找找规律,可以发现:在这个顺时针均匀编号的圆中,两条弦线的左右端点的编号(设其为第 条弦 ,第 条弦 ),如果满足 ,且 为奇数,那么这两条线平行。
注:如果 是偶数,就会出现自测样例 的情况。
简洁地说,调整后必然会存在恰好 条弦,它们各自的两个端点相邻,这时它们各自的端点编号之和就都是奇数,那么其它的也必须是奇数(因为模 (偶数)同余)。
简洁地说,调整后必然会存在恰好 条弦,它们各自的两个端点相邻,这时它们各自的端点编号之和就都是奇数,那么其它的也必须是奇数(因为模 (偶数)同余)。
表示方法
为了方便表示,规定 。
规律应用
此时我们可以设第 条弦有一个“弦值” 。
我们的终极目标就是通过移动弦的端点使所有 相等(这样它们就都平行了)。
我们的终极目标就是通过移动弦的端点使所有 相等(这样它们就都平行了)。
此处用到贪心策略,步骤如下。
- 求出所有 。
- 统计这些 的出现次数。
- 求出出现次数最多的 ,规定这个 为“最好值”,用 记录。 如果出现多个 出现次数相同且都是最多,那么任意选择 个 作为 值。
- 把其它所有 的弦都进行挪动操作,使其符合 。这些弦都要挪动恰好一次,所以要最大化 ,这样一来挪动的弦的数量就会尽可能小,即 尽可能小。
细节:特殊判断
根据上文内容,所有 最后都要被调整为奇数,所以 也必须是奇数。
那么,对于类似自测样例 的情况,所有 初始都是偶数,就需要特别规定 为某个奇数(例如 )。
那么,对于类似自测样例 的情况,所有 初始都是偶数,就需要特别规定 为某个奇数(例如 )。
求 的值
循环检查,从 至 ,如果 ,就说明这条弦需要被挪动, 增加 。最后求出 ,输出。
求 的 分代码
注:题目的 SPJ 疑似与题面描述不符,这份代码可能被误判为 分(这份部分分代码没有错误,因为它参与到了之后的 AC 代码中)。
CPP#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int n, maxi;
struct Wire{
int x, y, sum = 0, id;
}a[N];
map <int, int> p;
int main(){
cin >> n;
for(int i=0; i<n; i++){
cin >> a[i].x >> a[i].y;
a[i].sum = (a[i].x + a[i].y) % (n*2);
if(a[i].sum % 2)
p[a[i].sum]++,
maxi = max(maxi, p[a[i].sum]);
a[i].id = i;
}
int best;
for(int i=0; i<n; i++)
if(p[a[i].sum] == maxi)
best = a[i].sum;
if(best % 2 == 0) best = 1; // 特判
int k = 0;
for(int i=0; i<n; i++)
if(a[i].sum != best) k++;
cout << k;
}
模拟:挪弦与挪弦顺序
这部分内容,要求我们必须要找到正确的挪弦顺序,否则不可能 AC。
规定已知 时, 能使其满足 。
反推可以得到 。
反推可以得到 。
规定 表示 已经找到了其对应的 , 则表示没有找到。
以下两种顺序被验证是错误的,但这是一种思维的过程。如果各位大佬觉得浪费时间也可以直接跳到结论。
一、先看 再看
先来考虑一种最简朴的挪动顺序:从 到 遍历,如果当前的 ,不用继续考虑。
反之,需要考虑挪动这条弦。
反之,需要考虑挪动这条弦。
先考虑 ,如果 ,就将 和 连在一起,并直接考虑下一条弦。
这种考虑顺序被证明是错误的,可以参考第 个自测样例。
CPP6
3 9
7 5
10 2
0 6
1 11
8 4
假设这组样例的 ,那么这种顺序会导致考虑编号为 的弦(
1 11)时出现 的情况,即都已经被连过了,无法继续考虑。以下是这种考虑顺序对应的代码(为缩短文章长度,仅展示部分代码,不含求 部分)。
CPPfor(int i=0; i<n; i++){
if(a[i].sum == best)
continue;
// x连接y 变成 x连接xx 或 y连接yy
int x = a[i].x, y = a[i].y;
int xx = (best+n*2 - x) % (n*2),
yy = (best+n*2 - y) % (n*2);
if(!ok[xx]){ // 优先考虑:如果x连接xx可以
ok[x] = ok[xx] = 1;
cout << i << ' ' << y << ' ' << xx << '\n';
}
else if(!ok[yy]){ // 否则如果y连接yy可以
ok[y] = ok[yy] = 1;
cout << i << ' ' << x << ' ' << yy << '\n';
}
}
如果将这份代码加入求 的部分,测试自测样例 ,会得到如下结果。
CPP6
0 9 10
1 5 6
2 10 11
3 6 1
5 4 5
是的,面对编号为 的弦出现的前文提到的矛盾情况,我们的考虑顺序无法处理,所以没有输出。
二、dfs 确定连接顺序
开头同理。
在接下来的处理中,首先进入一层
如果 ,输出答案,退出。
dfs(i),如果 ,可以将它们更新为 ,即连接,并进入下一层 dfs dfs(i+1)。如果 ,输出答案,退出。
如果遇到刚才提到的“都被连接”的问题,再返回(改进部分)。
显然这个写法是正确的,但必然超时。
以下给出 dfs 部分的代码,如果补全可以得到 分,其余测试点会超时。
CPPvoid dfs(int i){
if(i == n){
for(int i=1; i<=cnt; i++)
cout << ans[i].id << ' ' << ans[i].s1 << ' ' << ans[i].s2 << '\n';
exit(0);
}
if(a[i].sum == best)
dfs(i + 1);
int x = a[i].x,
y = a[i].y;
int tx = match(x),
ty = match(y);
if(!ok[x] && !ok[tx]){
ok[x] = ok[tx] = 1;
ans[++cnt].id = i,
ans[cnt].s1 = y, ans[cnt].s2 = tx;
// cout << ans[cnt].id << ' ' << ans[cnt].s1 << ' ' << ans[cnt].s2 << '\n';
dfs(i + 1);
ok[x] = ok[tx] = 0;
--cnt;
}
if(!ok[y] && !ok[ty]){
ok[y] = ok[ty] = 1;
ans[++cnt].id = i,
ans[cnt].s1 = x, ans[cnt].s2 = ty;
// cout << ans[cnt].id << ' ' << ans[cnt].s1 << ' ' << ans[cnt].s2 << '\n';
dfs(i + 1);
ok[y] = ok[ty] = 0;
--cnt;
}
return;
}
三、“集体考虑”(正确顺序)
在尝试了两种错误顺序之后,我们终于迎来了正确的部分。
我们的第 种考虑顺序虽然无法 AC,但是可以得到 分并通过第 自测点,所以我们不妨来观察第 自测点的输出,并分析其考虑顺序。
CPP6
3 6 1
4 1 2
2 2 3
0 3 4
5 4 5
1 5 6
看这个输出,我们可以发现规律: 行中,第 行的第三个数等于第 行的第二个数。
注意:输出的各行顺序是可以调整的,所以我们发现的规律不一定是正确思路。
但这个规律也确实是一个可以尝试并且能够 AC 的代码(经尝试确认)。
但这个规律也确实是一个可以尝试并且能够 AC 的代码(经尝试确认)。
AC 思路结论
详细解释这个顺序。
定义“集体”:如果若干条弦只需要相互交换端点位置,就可以让这些弦的 ,那么就认为这些弦时一个集体。
显然,总共 条 的弦,可以被划分为至少 个集体。
显然,总共 条 的弦,可以被划分为至少 个集体。
解释定义
我将使用自测样例解释“集体”。
自测样例 中,所有 根需要被挪动的弦在同一个集体。
自测样例 中,如果第 根弦不动,那第 根弦是同一个集体,第 根弦是同一个集体。此时出现了 个集体。
自测样例 中,如果第 根弦不动,那第 根弦是同一个集体,第 根弦是同一个集体。此时出现了 个集体。
大致思路
从任意一根弦出发,如果这根弦的 ,就通过挪动,将这根弦以及它的集体全部符合 ,然后再去考虑其他集体的其他弦(如果只有 个集体,就完成了题目全部要求;如果有多个集体,就要考虑各个集体)。
具体步骤
- 首先任意考虑一根弦(设其端点为 ),调整 这个端点,让这根弦的端点变成 ,成功。
- 如果 位置原来有一根弦(设其端点为 ),调整 这个端点,让这根弦的端点变成 ,成功。
- 重复第 步,让端点为 的弦变成 ,让所有弦都成功。
- 反之,对应第 步,如果 挪到的 位置原来没有弦,就说明不需要再调整 位置之前连接的弦了,这个循环模拟就暂时中断了,也就是说明这个集体都已经符合 。所以退出当前循环,去寻找下一个集体,即回到第 步。
处理集体考虑完毕的情况
观察自测样例 。
CPP5
0 1
3 2
4 5
6 7
9 8
可以使用 记录第 个位置连接的弦的编号。
表示这个位置不连接任何弦。
表示这个位置不连接任何弦。
按照思路 。不妨设 。
如果按照刚才提到的顺序,模拟其过程。
如果按照刚才提到的顺序,模拟其过程。
最后得到: 说明 现在不连接任何弦,无需继续考虑。
也说明这个集体已经全部符合 ,退出循环,去考虑下一个集体。对于本测试点,所有集合都考虑完毕。
也说明这个集体已经全部符合 ,退出循环,去考虑下一个集体。对于本测试点,所有集合都考虑完毕。
现在,我们只需要将刚才这个循环模拟、判断集体是否考虑完毕的过程编写成代码就可以了。
代码
CPP#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+5;
int n, maxi, k, best, a[N][2], d[N];
map <int, int> t, pos;
map <int, bool> ok;
int match(int x){ // 已知x, 求x'
return (best + n*2 - x) % (n*2);
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n;
for(int i=0; i<n; i++){
int x, y; cin >> x >> y;
a[i][0] = x, a[i][1] = y;
pos[x] = pos[y] = i; // 所在绳子的编号
d[i] = (x + y) % (n*2);
if(d[i] % 2)
t[d[i]]++,
maxi = max(maxi, t[d[i]]);
}
for(int i=0; i<n; i++){
if(t[d[i]] == maxi)
best = d[i];
}
if(best % 2 == 0) best = 1;
for(int i=0; i<n; i++)
if(d[i] != best) k++;
else ok[i] = 1;
cout << k << '\n';
// 这部分很复杂, 很重要
for(int i=0; i<n; i++){
if(!ok[i]){ // 如果第i根绳子及其集体没有被考虑.
int s = i, o = 0;
// 接下来一直考虑第s根绳子.
while(1){ // 集体考虑完毕时退出.
int u = a[s][o];
int v = match(u);
int s2 = pos[v]; // v原来在s2绳子上
// 第s1根绳子的非u端连到v上去.
cout << s << ' ' << a[s][o^1] << ' ' << v << '\n';
ok[s] = 1; // u所在的s绳子ok了.
pos[v] = s; // v点现在和u都在s上, u <-> v.
pos[a[s][o^1]] = -1; // u所在的s绳子的原端点o^1现在没有绳子.
// v点原来没有任何绳子, 当前集体考虑完毕, 结束.
if(s2 == -1) break;
s = s2; // v原来在s2绳子上, 现在去继续考虑s2.
if(a[s2][0] == v) o = 1;
if(a[s2][1] == v) o = 0; // else.
}
}
}
return 0;
}
相关推荐
评论
共 0 条评论,欢迎与作者交流。
正在加载评论...