1 条题解
-
0
感谢来自上一篇题解的大佬 rui_er 的指教!
弦的平行
观察题目给出的两个圆形示例。
在这两个圆形中,所有共 个琴钉连接的 条弦线都互相平行。规律
试试在图中找找规律,可以发现:在这个顺时针均匀编号的圆中,两条弦线的左右端点的编号(设其为第 条弦 ,第 条弦 ),如果满足 ,且 为奇数,那么这两条线平行。
注:如果 是偶数,就会出现自测样例 的情况。
简洁地说,调整后必然会存在恰好 条弦,它们各自的两个端点相邻,这时它们各自的端点编号之和就都是奇数,那么其它的也必须是奇数(因为模 (偶数)同余)。表示方法
为了方便表示,规定 。
规律应用
此时我们可以设第 条弦有一个“弦值” 。
我们的终极目标就是通过移动弦的端点使所有 相等(这样它们就都平行了)。此处用到贪心策略,步骤如下。
- 求出所有 。
- 统计这些 的出现次数。
- 求出出现次数最多的 ,规定这个 为“最好值”,用 记录。 如果出现多个 出现次数相同且都是最多,那么任意选择 个 作为 值。
- 把其它所有 的弦都进行挪动操作,使其符合 。这些弦都要挪动恰好一次,所以要最大化 ,这样一来挪动的弦的数量就会尽可能小,即 尽可能小。
细节:特殊判断
根据上文内容,所有 最后都要被调整为奇数,所以 也必须是奇数。
那么,对于类似自测样例 的情况,所有 初始都是偶数,就需要特别规定 为某个奇数(例如 )。求 的值
循环检查,从 至 ,如果 ,就说明这条弦需要被挪动, 增加 。最后求出 ,输出。
求 的 分代码
注:题目的 SPJ 疑似与题面描述不符,这份代码可能被误判为 分(这份部分分代码没有错误,因为它参与到了之后的 AC 代码中)。
#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。
规定已知 时, 能使其满足 。
反推可以得到 。规定 表示 已经找到了其对应的 , 则表示没有找到。
以下两种顺序被验证是错误的,但这是一种思维的过程。
如果各位大佬觉得浪费时间也可以直接跳到结论。一、先看 再看
先来考虑一种最简朴的挪动顺序:从 到 遍历,如果当前的 ,不用继续考虑。
反之,需要考虑挪动这条弦。先考虑 ,如果 ,就将 和 连在一起,并直接考虑下一条弦。
这种考虑顺序被证明是错误的,可以参考第 个自测样例。
6 3 9 7 5 10 2 0 6 1 11 8 4假设这组样例的 ,那么这种顺序会导致考虑编号为 的弦(
1 11)时出现 的情况,即都已经被连过了,无法继续考虑。以下是这种考虑顺序对应的代码(为缩短文章长度,仅展示部分代码,不含求 部分)。
for(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'; } }如果将这份代码加入求 的部分,测试自测样例 ,会得到如下结果。
6 0 9 10 1 5 6 2 10 11 3 6 1 5 4 5是的,面对编号为 的弦出现的前文提到的矛盾情况,我们的考虑顺序无法处理,所以没有输出。
二、dfs 确定连接顺序
开头同理。
在接下来的处理中,首先进入一层
dfs(i),如果 ,可以将它们更新为 ,即连接,并进入下一层 dfsdfs(i+1)。
如果 ,输出答案,退出。如果遇到刚才提到的“都被连接”的问题,再返回(改进部分)。
显然这个写法
是正确的,但必然超时。以下给出 dfs 部分的代码,如果补全可以得到 分,其余测试点会超时。
void 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,但是可以得到 分并通过第 自测点,所以我们不妨来观察第 自测点的输出,并分析其考虑顺序。
6 3 6 1 4 1 2 2 2 3 0 3 4 5 4 5 1 5 6看这个输出,我们可以发现规律: 行中,第 行的第三个数等于第 行的第二个数。
注意:输出的各行顺序是可以调整的,所以我们发现的规律不一定是正确思路。
但这个规律也确实是一个可以尝试并且能够 AC 的代码(经尝试确认)。AC 思路结论
详细解释这个顺序。
定义“集体”:如果若干条弦只需要相互交换端点位置,就可以让这些弦的 ,那么就认为这些弦时一个集体。
显然,总共 条 的弦,可以被划分为至少 个集体。解释定义
我将使用自测样例解释“集体”。
自测样例 中,所有 根需要被挪动的弦在同一个集体。
自测样例 中,如果第 根弦不动,那第 根弦是同一个集体,第 根弦是同一个集体。此时出现了 个集体。大致思路
从任意一根弦出发,如果这根弦的 ,就通过挪动,将这根弦以及它的集体全部符合 ,然后再去考虑其他集体的其他弦(如果只有 个集体,就完成了题目全部要求;如果有多个集体,就要考虑各个集体)。
具体步骤
- 首先任意考虑一根弦(设其端点为 ),调整 这个端点,让这根弦的端点变成 ,成功。
- 如果 位置原来有一根弦(设其端点为 ),调整 这个端点,让这根弦的端点变成 ,成功。
- 重复第 步,让端点为 的弦变成 ,让所有弦都成功。
- 反之,对应第 步,如果 挪到的 位置原来没有弦,就说明不需要再调整 位置之前连接的弦了,这个循环模拟就暂时中断了,也就是说明这个集体都已经符合 。所以退出当前循环,去寻找下一个集体,即回到第 步。
处理集体考虑完毕的情况
观察自测样例 。
5 0 1 3 2 4 5 6 7 9 8可以使用 记录第 个位置连接的弦的编号。
表示这个位置不连接任何弦。按照思路 。不妨设 。
$${a_2}' = 6, a_3 = 7\\ \therefore pos({a_2}') = pos(a_2)\\$$
如果按照刚才提到的顺序,模拟其过程。最后得到: 说明 现在不连接任何弦,无需继续考虑。
也说明这个集体已经全部符合 ,退出循环,去考虑下一个集体。对于本测试点,所有集合都考虑完毕。现在,我们只需要将刚才这个循环模拟、判断集体是否考虑完毕的过程编写成代码就可以了。
代码
#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; }
- 1
信息
- ID
- 10186
- 时间
- 2000ms
- 内存
- 1024MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者