CF1840G In Search of Truth 解题报告
很深刻交互题。
Easy version
给了 \(2000\) 次操作,根据前面 catch the mole 的经验,这个东西很有可能是 \(2 \sqrt{n}\)。显然我们想重复经过同一个点。直接考虑从出发点开始连续 \(1000\) 次 + 1,然后如果没有碰到起始点,那接下来就 \(1000\) 次 + 1000,这样一定会遇到前面走过的点,并且遇到时,肯定只走了一圈。问题解决。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 7, B = 1e3;
int x, dis[N], nd;
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> x;
memset(dis, -1, sizeof dis);
dis[x] = 0;
for(int i = 1; i <= B; i ++){
cout << "+ 1" << endl; nd ++;
int res; cin >> res;
if(dis[res] == -1) dis[res] = nd;
else{
cout << "! " << nd - dis[res] << endl;
return 0;
}
}
for(int i = 1; i <= B; i ++){
cout << "+ " << B << endl; nd += B;
int res; cin >> res;
if(dis[res] == -1) dis[res] = nd;
else{
cout << "! " << nd - dis[res] << endl;
return 0;
}
}
return 0;
}
Hard version
这次把 \(2000\) 次减少到了 \(1000\) 次。
整体感知下简单版的思路,其实就是一个圈分成了很多 \(\sqrt{n}\) 的块。那么此时有两种做法来减小询问次数:尽量加大 \(k\) 或者缩小 \(n\) 的范围。
考虑前者,如果加大 \(k\),最终结果肯定没有 \(2 \sqrt{n}\) 更优(数学原理),我已经思考过了基于这个思想的随机算法,发现并不合理,成功概率较小。所以直接考虑缩小 \(n\) 的范围。根据交互题算法优化的基本思想,注意到题目给的很多信息我们还没用上。
我们目前知道的东西其实是十分有限的:环的总长不大于 \(10^6\)。显然题目还给了一个所有环的编号是一个排列的条件。询问一个东西的结果 \(w\) 似乎带来的有效信息就只有“答案不小于 \(w\)”。设 \(res = \max{w}\)。 接下来不妨假设 \(res \le n \le res + d\) ,且这样的询问我们进行 \(m\) 次。
考虑求出 \(s = n - res\)。那么整个环可以被划分为长度分别为 \(res\) 和 \(s\) 的两段。根据我们的假设,\(s \le d\)。那么问题就被转化成了在一条链上执行刚刚的 Easy version 算法,且数据不大于 \(d\)。直接在链的一端放置 \(\sqrt{d}\) 个连续标记,从另一端开始大跳就行,使用 \(2 \sqrt{d}\) 次询问。
直接调参,对于一个固定的 \(m\),显然 \(1000 - m \ge 2 \sqrt{d}\)。估算一下,\(d \le (\frac{1000 - m}{2}) ^ 2\)。失败的条件是 \(res < n - d\)。也就是抽了 \(m\) 遍全是比 \(n - d\) 小的。失败概率 \(P = (\frac{n - d - 1}{n}) ^ m\)。直接证明好麻烦,不妨写个暴力来找参数(令 \(n = 10^6\))。然后发现 \(m = 400\),即 \(d = 90000\) 时失败概率很小。至此,问题解决。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e6 + 7, M = 400, L = 300;
mt19937_64 gen(time(0));
int Misaka(){
return gen() % 1000000000 + 1;
}
void term(int len){
cout << "! " << len << endl;
exit(0);
}
int x, dis[N], cur, lb;
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
cin >> x;
for(int i = 1; i <= M; i ++){
cout << "+ "<< Misaka() << endl;
cin >> x, lb = max(lb, x);
}
memset(dis, -1, sizeof dis);
dis[x] = cur = 0;
for(int i = 1; i < L; i ++){
cout << "+ 1" << endl; cur ++;
cin >> x;
if(dis[x] != -1) term(cur - dis[x]);
dis[x] = cur;
}
cout << "+ " << lb << endl; cur += lb;
cin >> x;
if(dis[x] != -1) term(cur - dis[x]);
dis[x] = cur;
for(int i = 1; i <= L; i ++){
cout << "+ " << L << endl; cur += L;
cin >> x;
if(dis[x] != -1) term(cur - dis[x]);
dis[x] = cur;
}
return 0;
}
双倍经验:/gym/104090/problem/I,可以自己尝试调一下参数。
调参具体过程
#include <bits/stdc++.h>
#define int long long
using namespace std;
signed main(){
ios::sync_with_stdio(0), cin.tie(0);
double mn = 1e18, ans = -1;
for(double m = 1; m <= 5000; m ++){
double res = pow((1e9 - pow((10000 - m) / 2, 2)) / 1e9, m);
if(mn > res) mn = res, ans = m;
}
cout << ans << " " << mn << "\n";
return 0;
}
运行这串代码,发现 \(m = 3321\) 时错误概率最小,约为 \(6 \cdot 10^{-17}\)。
于是我取了 \(m = 3000\),\(\sqrt{d} = 3400\)。
总结
认为这题非常有价值。
首先要增加对不确定性算法(哈希、随机化)的敏感度。
其次就是随机算法或根号分治一类的算法,学习了调参思想。

浙公网安备 33010602011771号