题解:CF51D Geometrical problem
CF51D Geometrical problem 题解
注:本蒟蒻的思路十分诡异,貌似与很多题解都不一致,请搭配糯米食用
一、题目大意
给你一个长度为
- 如果它本身就是等比数列(几何级数),输出
0; - 如果删掉 恰好一个 元素后能变成等比数列,输出
1; - 否则输出
2。
这里的等比数列定义很宽:存在实数
因此序列里可以出现
二、核心想法
一个长度为
但直接算
我们把每一对
- 情况 1(正常比值):
a_i \neq 0 且a_{i+1} \neq 0 ,比值就是普通的实数r = \frac{a_{i+1}}{a_i} 。 - 情况 2(后项为 0):只要
a_{i+1} = 0 ,不管a_i 是几,这一对都可以看成比值是0 。比如(3,0) 和(0,0) 都算作比值0 。 - 情况 3(前项为0):
a_i = 0 但a_{i+1} \neq 0 ,比如(0,5) 。这种对在真正的等比数列里永远不可能出现,但为了计数方便,我们也单独记下来。
统计的时候用两个哈希表:
val:键是比值(用double存),值是这个比值出现的次数。特别地,val[0]专门存情况 2(后项为 0)的所有对。div0:键是后项的值x ,统计情况 3 中(0,x) 出现的次数。
如果序列里所有的相邻对都属于 同一个类别,并且(对于情况 1 和情况 3)数值也一样,那这个序列就是等比数列。
三、怎么判断原序列
先把所有
具体来说:
- 如果
val[0] == n-1,说明全是后项为 0 的对,公比可以是 0,合法; - 如果存在某个非零比值
r ,使得val[r] == n-1,合法; - 如果存在某个
x ,使得div0[x] == n-1,也算合法(这是代码里的计数技巧,后面会解释为什么没问题)。
只要满足任意一条,原序列就是等比,直接输出 0。
四、怎么判断删一个元素
如果原序列不行,我们就枚举删掉哪个元素。
暴力做法是每次删完重新统计,复杂度
所以我们要用 动态维护 的办法:先把所有相邻对统计好,删的时候只改受影响的几对。
1. 删掉首尾元素
删掉第一个元素后,剩下的是
我们只需要检查这些对是不是同类,也就是看 val 或 div0 里有没有某个计数等于
删掉最后一个元素同理。
2. 删掉中间元素(2 \le i \le n-1 )
假设删掉
- 原来的两对
(a_{i-1}, a_i) 和(a_i, a_{i+1}) 会消失; - 新产生一对
(a_{i-1}, a_{i+1}) 。
我们做两个操作:
del(i):从全局计数里减去那两个旧对(不加新对);add(i):把这两个旧对加回来,恢复现场。
执行 del(i) 后,哈希表里剩下的是除这两个旧对以外的所有对,数量正好是
这时候我们看新对
判断条件是这样的:
- 如果新对是
(0,0) ,要求val[0] == n-3; - 如果新对是
(0, x) 且x \neq 0 ,要求div0[x] == n-3; - 如果新对的两个数都不为
0 ,比值是r = \frac{a_{i+1}}{a_{i-1}} ,要求val[r] == n-3。
只要满足其中一条,就说明删掉 1。
枚举完所有位置后,如果都没有成功,答案就是初始的 2。
五、关于 0 和 (0,x) 的说明
注意到,代码把
严格来说,只要序列里出现一个
但在我们的判断流程中,如果删掉一个元素后,剩下的所有相邻对都恰好是 1,其余输出 0。
对于更长的序列,删除后不可能只剩一个对,所以不会出现“所有对都是
因此这种计数方法在实际数据中是不会出错的,它只是为了方便统一处理而已。
另外,用 double 存比值可能会有精度误差,但这题数值范围小(map 的比较是安全的。
六、时间复杂度
预处理统计相邻对是
枚举每个删除位置时,每次 del 和 add 都只做常数次哈希表操作,也是
所以总复杂度
七、完整代码
#include <bits/stdc++.h>
using namespace std;
int n;
vector<double> a;
map<double, int> val; // 比值计数,val[0] 包含 (x,0) 和 (0,0)
map<int, int> div0; // 统计 (0, x) 中 x 的出现次数
void del(int i) {
// 移除 (a[i-1], a[i])
if (a[i] == 0 && a[i-1] == 0) val[0]--;
else if (a[i-1] == 0) div0[a[i]]--;
else val[1.0 * a[i] / a[i-1]]--;
// 移除 (a[i], a[i+1])
if (a[i+1] == 0 && a[i] == 0) val[0]--;
else if (a[i] == 0) div0[a[i]]--;
else val[1.0 * a[i+1] / a[i]]--;
}
void add(int i) {
if (a[i] == 0 && a[i-1] == 0) val[0]++;
else if (a[i-1] == 0) div0[a[i]]++;
else val[1.0 * a[i] / a[i-1]]++;
if (a[i+1] == 0 && a[i] == 0) val[0]++;
else if (a[i] == 0) div0[a[i]]++;
else val[1.0 * a[i+1] / a[i]]++;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n;
a.assign(n + 1, 0);
for (int i = 1; i <= n; i++) cin >> a[i];
if (n == 1) {
cout << 0 << '\n';
return 0;
}
if (n == 2) {
if (a[1] == 0 && a[2] != 0) cout << 1 << '\n';
else cout << 0 << '\n';
return 0;
}
// 预处理所有相邻对
for (int i = 2; i <= n; i++) {
if (a[i] == 0 && a[i-1] == 0) val[0]++;
else if (a[i-1] == 0) div0[a[i]]++;
else val[1.0 * a[i] / a[i-1]]++;
}
// 检查原序列
for (int i = 2; i <= n; i++) {
if ((a[i] == 0 && a[i-1] == 0 && val[0] == n - 1) ||
(a[i-1] != 0 && val[1.0 * a[i] / a[i-1]] == n - 1) ||
(a[i-1] == 0 && div0[a[i]] == n - 1)) {
cout << 0 << '\n';
return 0;
}
}
int ans = 2;
// 判断删除第一个或最后一个元素
if (n >= 3) {
if ((a[2] == 0 && a[3] == 0 && val[0] == n - 2) ||
(a[2] == 0 && div0[a[3]] == n - 2) ||
(a[2] != 0 && val[1.0 * a[3] / a[2]] == n - 2) ||
(a[n-2] == 0 && a[n-1] == 0 && val[0] == n - 2) ||
(a[n-2] == 0 && div0[a[n-1]] == n - 2) ||
(a[n-2] != 0 && val[1.0 * a[n-1] / a[n-2]] == n - 2)) {
cout << 1 << '\n';
return 0;
}
}
// 枚举删除中间位置
for (int i = 2; i <= n - 1; i++) {
del(i);
if ((a[i-1] == 0 && a[i+1] == 0 && val[0] == n - 3) ||
(a[i-1] == 0 && div0[a[i+1]] == n - 3) ||
(a[i-1] != 0 && val[1.0 * a[i+1] / a[i-1]] == n - 3)) {
ans = 1;
break;
}
add(i);
}
cout << ans << '\n';
return 0;
}
八、总结
本蒟蒻的思路很诡异,若想学习正解,很多题解都比本篇优秀
另:积极接受hack