Loading

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\)

总结

认为这题非常有价值。

首先要增加对不确定性算法(哈希、随机化)的敏感度。

其次就是随机算法或根号分治一类的算法,学习了调参思想。

posted @ 2026-04-18 08:40  Trent900  阅读(20)  评论(0)    收藏  举报