交互杂题选做
交互杂题选做
递归子问题
UOJ52. 【UR #4】元旦激光炮
有三个有序序列 \(a_{0 \sim n_a - 1}, b_{0 \sim n_b - 1}, c_{0 \sim n_c - 1}\) 。
每次可以询问任意序列的任意下标上的元素,访问到非法下标返回 \(+\infty\) 。
求三个序列归并后的第 \(k\) 大数。
\(n_a, n_b, n_c \leq 10^5\) ,询问次数上限 \(100\) 次
令 \(l = \lfloor \frac{k}{3} \rfloor\) ,询问 \(a_l, b_l, c_l\) ,不妨假设 \(a_l\) 最小。
考虑 \(\leq a_l\) 的数字数量最坏只有 \(3(l - 1)\) 个,于是 \(a_l\) 的排名至多是 \(3l - 2 < k\) 。
于是可以扔掉 \(a\) 的前 \(l\) 个元素,令 \(k \gets k - l\) ,继续递归处理。最后到 \(k < 3\) 时暴力 \(6\) 次询问解决。
对于 \(k \leq 3 \times 10^5\) ,至多调用 \(96\) 次,需要注意一些边界。
#include <bits/stdc++.h>
#include "kth.h"
using namespace std;
int query_kth(int na, int nb, int nc, int k) {
int pa = 0, pb = 0, pc = 0;
while (k >= 3) {
int a = get_a(pa + k / 3 - 1), b = get_b(pb + k / 3 - 1), c = get_c(pc + k / 3 - 1);
if (a <= b && a <= c)
pa += k / 3;
else if (b <= a && b <= c)
pb += k / 3;
else
pc += k / 3;
k -= k / 3;
}
vector<int> vec;
for (int i = 0; i < 2; ++i)
vec.emplace_back(get_a(pa + i)), vec.emplace_back(get_b(pb + i)), vec.emplace_back(get_c(pc + i));
sort(vec.begin(), vec.end());
return vec[k - 1];
}
UOJ891. 【UNR #8】难度查找
有一个 \(n \times n\) 的矩阵,满足 \(\forall 1 \leq x \leq n, 1 \leq y_1 < y_2 \leq n\) ,有 \(a_{x, y_1} \leq a_{x, y_2}\) 且 \(a_{y_1, x} \leq a_{y_2, x}\) 。
每次询问可以得到一个格子的值,试用 \(\leq 10^6\) 次询问求出矩阵第 \(k\) 大值。
\(n \leq 2 \times 10^5\)
维护 \(p_i\) 表示第 \(i\) 列的前 \(p_i\) 个元素排名 \(\leq k\) ,不难发现 \(p_i\) 是不升的。
考虑先拿出所有偶数行,求出这些行中的前 \(\lceil \frac{k}{2} \rceil\) 小元素,这个过程可以递归子问题处理。此时可以令 \(p_{2i - 1} = p_{2i}\) ,就求出了前至少 \(2 \times \lceil \frac{k}{2} \rceil\) 、至多 \(2 \times \lceil \frac{k}{2} \rceil + m\) 大元素。此时还能得到这些行的 \(p\) ,下面为了与当前列的 \(p\) 区分,将其记为 \(q\) 。
接下来需要去掉多余的元素。不难发现 \(q\) 放到网格图上形如阶梯,因此可以用双指针求出每一列初始的 \(p\) ,覆盖整个阶梯。
接下来考虑用大根堆维护候补答案集合,每次取出堆顶尝试令 \(p\) 减小 \(1\) 。
记 \(T(n, m)\) 表示 \(n\) 行 \(m\) 列的矩阵所需的询问次数,不妨钦定 \(n \geq m\) ,则 \(T(n, m) = T(\frac{n}{2}, m) + 2m\) ,解得 \(T(n, n) \leq 6n\) ,无法通过。
考虑优化,发现初始时 \(p\) 的变化点数量 \(\leq \lfloor \frac{n}{2} \rfloor\) ,而每次扩展后只可能添加前后两个变化点,并且一段 \(p\) 相等的区间一定是最靠后的先被扩展,因此大根堆内只需要维护最靠后的变化点。
这样单次只需要询问至多 \(\lfloor \frac{n}{2} \rfloor + m\) 次,此时 \(T(n, m) = T(\frac{n}{2}, m) + \frac{n}{2} + m\) ,解得 \(T(n, n) \leq 5n\) 。
#include<bits/stdc++.h>
#include "matrix.h"
typedef long long ll;
using namespace std;
const int N = 2e5 + 7;
map<pair<int, int>, ll> mp;
int p[N], q[N];
inline ll getval(int x, int y) {
if (mp.find(make_pair(x, y)) == mp.end())
mp[make_pair(x, y)] = query(x, y);
return mp[make_pair(x, y)];
}
void solve(int n, int m, ll k, int u, int v, bool op) {
if (m == 1) {
p[1] = k;
return;
}
solve(m, n / 2, (k + 1) / 2, v, u + 1, op ^ 1);
memcpy(q + 1, p + 1, sizeof(int) * (n / 2));
ll cnt = 0;
for (int i = m, j = 1; i; --i) {
while (j <= n / 2 && q[j] >= i)
++j;
cnt += (p[i] = min(j * 2 - 1, n));
}
auto calc = [&](int x, int y) {
return op ? getval(y << v, x << u) : getval(x << u, y << v);
};
priority_queue<pair<ll, int> > q;
for (int i = 1; i <= m; ++i)
if (i == m || p[i] != p[i + 1])
q.emplace(calc(p[i], i), i);
for (; cnt > k; --cnt) {
int x = q.top().second;
q.pop();
if (x > 1 && p[x] == p[x - 1])
q.emplace(calc(p[x - 1], x - 1), x - 1);
--p[x];
if (p[x] && (x == m || p[x] != p[x + 1]))
q.emplace(calc(p[x], x), x);
}
}
ll solve(int n, ll k) {
solve(n, n, k, 0, 0, false);
ll ans = 0;
for (int i = 1; i <= n; ++i)
if (p[i])
ans = max(ans, getval(p[i], i));
return ans;
}
CF1705F Mark and the Online Exam
有一个 01 串 \(s\) ,需要用 \(\leq 675\) 次询问确定这个 01 串。
每次询问可以得知询问的 01 串与 \(s\) 对应位置匹配的位置数量。
\(n \leq 1000\)
询问一次 \(S\) 可以得到 \((\sum_{i \in S} [s_i = 1]) + (\sum_{i \notin S} [s_i = 0])\) 。
考虑先问一次全 \(0\) ,则之后就可以用一次询问得出 \(S\) 的子集和。具体地,解如下方程即可:
设 \(f_n\) 表示 \(2^n\) 次子集询问能够确定的最长序列长度,\(q_{n, i}\) 表示 \(f_n\) 第 \(i\) 次的询问。
首先有 \(f_0 = 1\) ,为了方便,不妨钦定最后多一次 \(q_{n, 2^n} = U\) 的操作,可以用如下方案得出 \(f_{n + 1} = 2 f_n + 2^n - 1 = n 2^{n - 1} + 1\) 的构造:
-
将长度为 \(2 f_n + 2^n - 1\) 的序列分别分为长度为 \(f_n, f_n, 2^n - 1\) 的三段。
-
询问第二块中 \(1\) 的数量,记作 \(c\) 。
-
对所有 \(i \in [1, 2^n - 1]\) ,询问:
\[\begin{aligned} a &= \mathrm{query}(q_{n, i} \cup \{ f_n + k \mid k \in q_{n, i} \} \cup \{ 2 f_n + i \}) \\ &= \mathrm{query}(q_{n, i}) + \mathrm{query}(\{ f_n + k \mid k \in q_{n, i} \}) + \mathrm{query}(\{ 2 f_n + i \}) \end{aligned} \]以及:
\[\begin{aligned} b &= \mathrm{query}(q_{n, i} \cup \{ f_n + k \mid k \notin q_{n, i} \}) \\ &= \mathrm{query}(q_{n, i}) + \mathrm{query}(\{ f_n + k \mid k \notin q_{n, i} \}) \\ &= \mathrm{query}(q_{n, i}) + (c - \mathrm{query}(\{ f_n + k \mid k \in q_{n, i} \})) \end{aligned} \]由此可以得到:
\[\begin{cases} \mathrm{query}(q_{n, i}) = \lfloor \frac{a + b - c}{2} \rfloor \\ \mathrm{query}(\{ f_n + k \mid k \in q_{n, i} \}) = \lfloor \frac{a - b + c}{2} \rfloor \\ \mathrm{query}(\{ 2 f_n + i \}) = (a + b - c) \bmod 2 \end{cases} \] -
询问整个序列 \(1\) 的数量。
于是便可以在 \(O(\frac{n}{\log n})\) 次询问内还原原序列。
#include <bits/stdc++.h>
using namespace std;
int n, m;
inline int ask(string str) {
cout << str << endl;
int res;
cin >> res;
if (res == n)
exit(0);
return res;
}
inline int query(string str) {
str.resize(n);
int cnt = count(str.begin(), str.end(), 'T'), res = ask(str);
return (cnt - (m - res)) / 2;
}
vector<string> solve1(int h) {
if (!h)
return {"T"};
vector<string> res = solve1(h - 1), ans;
int n = res[0].length(), m = res.size() - 1;
for (int i = 0; i < m; ++i) {
ans.emplace_back(res[i] + res[i] + string(i, 'F') + "T" + string(m - i - 1, 'F'));
string tmp = res[i];
for (char &c : tmp)
c = (c == 'T' ? 'F' : 'T');
ans.emplace_back(res[i] + tmp + string(m, 'F'));
}
ans.emplace_back(string(n, 'F') + string(n, 'T') + string(m, 'F'));
ans.emplace_back(string(n * 2 + m, 'T'));
return ans;
}
string solve2(int h, vector<int> &q) {
if (!h)
return q[0] ? "T" : "F";
vector<int> L, R;
int c = q[q.size() - 2];
string ans, bac;
for (int i = 0; i + 2 < q.size(); i += 2) {
int a = q[i], b = q[i + 1];
L.emplace_back((a + b - c) / 2), R.emplace_back((a - b + c) / 2);
bac += ((a + b - c) & 1 ? "T" : "F");
}
L.emplace_back(q.back() - c - count(bac.begin(), bac.end(), 'T')), R.emplace_back(c);
return solve2(h - 1, L) + solve2(h - 1, R) + bac;
}
signed main() {
cin >> n;
m = ask(string(n, 'F'));
if (n == 1)
return ask(string(n, 'T')), 0;
int h = 0;
while (n > (h << (h - 1)) + 1)
++h;
vector<string> qry = solve1(h);
vector<int> vec;
for (string it : qry)
vec.emplace_back(query(it));
query(solve2(h, vec));
return 0;
}
寻宝
有一个 \((2n + 1) \times (2n + 1)\) 的网格,有一个格子中有宝藏。A、B 二人初始在 \((n + 1, n + 1)\) 处,每个人每次只能往上下左右之一的方向移动一格,且不能走出场地。
A 先进行了一次寻宝。A 在经过一个格子时会留下足迹,其指向他下一步走到的格子。B 不知道 A 的行动和宝藏的位置,他只知道:
- 宝藏不在初始位置 \((n + 1, n + 1)\) 。
- A 第一步向上走。
- A 没有重复经过一个格子。
- A 成功找到了宝藏,且找到宝藏后没有继续移动。
每次操作可以将 B 向某个方向移动一格,并得知该格子上 A 留下的足迹(或没有,即 A 没有经过该格子),试用 \(\leq 30000\) 次操作找到宝藏。
\(n \leq 2000\)
考虑二分宝藏的坐标,每轮将可行矩形中较长边减半,这样操作次数就是 \(O(n)\) 的。
假设已知宝藏在 \(([x_l, x_r], [y_l, y_r])\) 内,不妨设当前需要将 \(x\) 坐标减半,记 \(m = \lfloor \frac{x_l + x_r}{2} \rfloor\) 。
考虑把整个网格分为三部分:\(([x_l, m], [y_l, y_r])\) 、\(([m + 1, x_r], [y_l, y_r])\) 、其他部分。在两部分的交界处,统计一部分走到另一部分的足迹数量。于是 A 的路径可以看做是三个点的图上的一个欧拉路,只要知道图上每条边和起点就能知道欧拉路的终点。
精细实现可做到移动次数为 \(12n + O(\log n)\)。
#include <bits/stdc++.h>
#include "treasure.h"
using namespace std;
const int N = 4e3 + 7;
char a[N][N];
int n, X, Y;
inline void moveto(int x, int y) {
while (X > x)
if ((a[--X][Y] = walk('^')) == 'G')
return;
while (X < x)
if ((a[++X][Y] = walk('v')) == 'G')
return;
while (Y > y)
if ((a[X][--Y] = walk('<')) == 'G')
return;
while (Y < y)
if ((a[X][++Y] = walk('>')) == 'G')
return;
}
void solve(int xl, int xr, int yl, int yr) {
int xmid = (xl + xr) >> 1, ymid = (yl + yr) >> 1;
moveto(xmid, ymid);
if (a[X][Y] == 'G')
return;
if (xr - xl + 1 > yr - yl + 1) {
for (int i = ymid; i > yl; --i)
if ((a[X][--Y] = walk('<')) == 'G')
return;
if ((a[++X][Y] = walk('v')) == 'G')
return;
for (int i = yl; i < yr; ++i)
if ((a[X][++Y] = walk('>')) == 'G')
return;
if ((a[--X][Y] = walk('^')) == 'G')
return;
for (int i = yr; i > ymid + 1; --i)
if ((a[X][--Y] = walk('<')) == 'G')
return;
int cnt[3] = {0, 0, 0};
for (int i = yl; i <= yr; ++i) {
cnt[0] += (a[xl - 1][i] == 'v') - (a[xl][i] == '^');
cnt[2] += (a[xr][i] == 'v') - (a[xr + 1][i] == '^');
}
for (int i = xl; i <= xmid; ++i) {
cnt[0] += (a[i][yl - 1] == '>') - (a[i][yl] == '<');
cnt[0] += (a[i][yr + 1] == '<') - (a[i][yr] == '>');
}
for (int i = xmid + 1; i <= xr; ++i) {
cnt[2] += (a[i][yl] == '<') - (a[i][yl - 1] == '>');
cnt[2] += (a[i][yr] == '>') - (a[i][yr + 1] == '<');
}
for (int i = yl; i <= yr; ++i)
cnt[1] += (a[xmid][i] == 'v') - (a[xmid + 1][i] == '^');
if (cnt[1] < cnt[0] || (cnt[2] >= cnt[1] && n + 1 <= xmid))
solve(xl, xmid, yl, yr);
else
solve(xmid + 1, xr, yl, yr);
} else {
for (int i = xmid; i > xl; --i)
if ((a[--X][Y] = walk('^')) == 'G')
return;
if ((a[X][++Y] = walk('>')) == 'G')
return;
for (int i = xl; i < xr; ++i)
if ((a[++X][Y] = walk('v')) == 'G')
return;
if ((a[X][--Y] = walk('<')) == 'G')
return;
for (int i = xr; i > xmid + 1; --i)
if ((a[--X][Y] = walk('^')) == 'G')
return;
int cnt[3] = {0, 0, 0};
for (int i = xl; i <= xr; ++i) {
cnt[0] += (a[i][yl - 1] == '>') - (a[i][yl] == '<');
cnt[2] += (a[i][yr] == '>') - (a[i][yr + 1] == '<');
}
for (int i = yl; i <= ymid; ++i) {
cnt[0] += (a[xl - 1][i] == 'v') - (a[xl][i] == '^');
cnt[0] += (a[xr + 1][i] == '^') - (a[xr][i] == 'v');
}
for (int i = ymid + 1; i <= yr; ++i) {
cnt[2] += (a[xl][i] == '^') - (a[xl - 1][i] == 'v');
cnt[2] += (a[xr][i] == 'v') - (a[xr + 1][i] == '^');
}
for (int i = xl; i <= xr; ++i)
cnt[1] += (a[i][ymid] == '>') - (a[i][ymid + 1] == '<');
if (cnt[1] < cnt[0] || (cnt[2] >= cnt[1] && n + 1 <= ymid))
solve(xl, xr, yl, ymid);
else
solve(xl, xr, ymid + 1, yr);
}
}
void find_treasure(int _n) {
n = _n, X = Y = n + 1, solve(1, n * 2 + 1, 1, n * 2 + 1);
}
QOJ9432. Permutation
有一个长度为 \(n\) 的排列,你需要通过询问确定该排列。
每次询问可以给出一个长度为 \(n\) 的整数序列,每个元素 \(\in [1, n]\) ,交互库会返回该序列与排列对应位置匹配的位置数量。
\(n \leq 1000\) ,询问次数上限 \(6666\) 次
考虑递归解决如下问题:已知区间 \([l, r]\) 内值的集合 \(S\) ,求每个位置对应的值。
记 \(mid = \lfloor \frac{l + r}{2} \rfloor\) ,考虑确定每个数在左半边还是右半边。
每次从 \(S\) 中任取两个数 \(x, y\) ,询问 \(a_{l \sim mid} = x, a_{mid + 1, \sim r} = y\) 。若返回值为 \(0\) 或 \(2\) ,则可以同时确定 \(x, y\) 两个数,否则 \(x, y\) 两个数在同一边,可以将它们合并。
一次询问期望减少 \(\frac{3}{2}\) 个未知数,因此期望使用 \(\frac{2}{3} (r - l + 1)\) 次询问就可以确定每个数在左半边还是右半边,期望总询问次数为 \(\frac{2}{3} n (\log n - 1) = 6000\) 次。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
int ans[N];
mt19937 myrand(time(0));
int n;
inline int query(vector<int> vec) {
cout << "0 ";
for (int it : vec)
cout << it << ' ';
cout << endl;
int res;
cin >> res;
return res;
}
void solve(int l, int r, vector<vector<int> > id) {
if (l == r) {
if (!id.empty())
ans[l] = id[0][0];
return;
}
int mid = (l + r) >> 1;
shuffle(id.begin(), id.end(), myrand);
vector<vector<int> > idl, idr;
while (id.size() > 1) {
auto x = id.back(), y = id[id.size() - 2];
id.pop_back(), id.pop_back();
vector<int> qry(n);
fill(qry.begin(), qry.begin() + mid + 1, x[0]), fill(qry.begin() + mid + 1, qry.end(), y[0]);
int res = query(qry);
if (!res) {
for (int it : y)
idl.emplace_back(vector<int>{it});
for (int it : x)
idr.emplace_back(vector<int>{it});
} else if (res == 2) {
for (int it : x)
idl.emplace_back(vector<int>{it});
for (int it : y)
idr.emplace_back(vector<int>{it});
} else {
for (int it : y)
x.emplace_back(it);
id.emplace_back(x);
}
}
if (!id.empty()) {
if (idl.size() == mid - l + 1) {
for (int it : id[0])
idr.emplace_back(vector<int>{it});
} else {
for (int it : id[0])
idl.emplace_back(vector<int>{it});
}
}
solve(l, mid, idl), solve(mid + 1, r, idr);
}
signed main() {
scanf("%d", &n);
vector<vector<int> > id;
for (int i = 1; i <= n; ++i)
id.emplace_back((vector<int>){i});
solve(0, n - 1, id);
cout << "1 ";
for (int i = 0; i < n; ++i)
cout << ans[i] << ' ';
return 0;
}
DP 构造策略
P10786 [NOI2024] 百万富翁
评测端有序列 \(w_{0 \sim N - 1}\) ,保证 \(w\) 互不相同。
一次询问可以给出两个长度均为 \(k\) 的序列 \(a, b\) ,交互库会返回一个长度为 \(k\) 的序列 \(c\) ,其中 \(c_i\) 表示 \(a_i, b_i\) 中 \(w\) 的较大者。
求 \(w\) 最大者的下标。
记 \(T\) 为询问次数,\(S\) 为询问序列总长度。
- Task 1:\(N = 1000\) ,\(T = 1\) ,\(S = 499500\) 。
- Task 2:\(N = 10^6\) ,\(T = 8\) ,\(S = 1099944\) 。
对于 Task 1,直接一次做 \(\frac{N (N - 1)}{2}\) 次比较即可。
对于 Task 2,发现限制条件卡的很紧。设 \(f_{n, t}\) 表示 \(N = n, T = t\) 时 \(S\) 的最小值,则:
其中 \(\mathrm{cost}(n, k)\) 表示用一次询问将候选集合大小为 \(n\) 降为候选集合大小为 \(k\) 的最小比较次数。
接下来考虑构造一个 \(\mathrm{cost}(n, k)\) 比较优秀的策略,一种方案是将 \(n\) 均分为 \(k\) 个团,团内两两比较。
但是直接做 DP 是 \(O(TN^2)\) 的,考虑整除分块,对于 \(\lfloor \frac{n}{k} \rfloor\) 相同的一段 \([l, r]\) ,考虑只用 \(l, r\) 两端点值更新,可以做到 \(O(T N \sqrt{N})\) 求出结果,该方案求得 \(f_{8, 10^6} = 1099944\) 。
由于 \(N, T\) 固定,因此只要本题跑好方案存一下就行了。
#include <bits/stdc++.h>
using namespace std;
vector<int> ask(vector<int> a, vector<int> b);
inline vector<int> solve(vector<int> id, int k) {
int n = id.size(), p = 0;
vector<int> a, b;
for (int i = 1; i <= n % k; ++i) {
for (int j = 0; j < n / k + 1; ++j)
for (int l = j + 1; l < n / k + 1; ++l)
a.emplace_back(id[p + j]), b.emplace_back(id[p + l]);
p += n / k + 1;
}
for (int i = 1; i <= k - n % k; ++i) {
for (int j = 0; j < n / k; ++j)
for (int l = j + 1; l < n / k; ++l)
a.emplace_back(id[p + j]), b.emplace_back(id[p + l]);
p += n / k;
}
vector<int> c = ask(a, b), ans;
int p1 = 0, p2 = 0;
for (int i = 1; i <= n % k; ++i) {
for (int j = p1; j < p1 + n / k + 1; ++j)
if (count(c.begin() + p2, c.begin() + p2 + (n / k + 1) * (n / k) / 2, id[j]) == n / k) {
ans.emplace_back(id[j]);
break;
}
p1 += n / k + 1, p2 += (n / k + 1) * (n / k) / 2;
}
for (int i = 1; i <= k - n % k; ++i) {
for (int j = p1; j < p1 + n / k; ++j)
if (count(c.begin() + p2, c.begin() + p2 + (n / k) * (n / k - 1) / 2, id[j]) == n / k - 1) {
ans.emplace_back(id[j]);
break;
}
p1 += n / k, p2 += (n / k) * (n / k - 1) / 2;
}
return ans;
}
int richest(int n, int T, int S) {
if (n == 1e3) {
vector<int> a, b;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
a.emplace_back(i), b.emplace_back(j);
vector<int> c = ask(a, b);
for (int i = 0; i < n; ++i)
if (count(c.begin(), c.end(), i) == n - 1)
return i;
}
vector<int> id(n);
iota(id.begin(), id.end(), 0);
return solve(solve(solve(solve(solve(solve(solve(solve(id, 500000), 250000), 125000), 62500), 20833), 3472), 183), 1)[0];
}
P11083 [ROI 2019] 黑洞 (Day 1)
交互库有一个整数 \(x\) ,保证 \(1 \leq x \leq n\) 。
给出 \(n\) ,每次询问可以给出一个 \(y\) ,交互库会返回 \([x \geq y]\) ,但是在所有询问中至多有一次返回值是错误的。
试用 \(\leq q\) 次操作求出 \(x\) ,其中 \(q\) 为最坏情况下的操作次数。
\(n \leq 30000\)
考虑维护 \(a_i\) 表示 \(i\) 被判断为不合法的次数,若 \(a_i \geq 2\) 则 \(i\) 一定不合法,因此可以去掉,只要对 01 序列判断即可。
可以发现可能的 01 序列一定形如一段 \(1\) 拼上一段 \(0\) 再拼上一段 \(1\) (有的段可能退化为空),因此可以 DP 决策。
设 \(f_{a, b, c}\) 表示三段的长度,每次就枚举当前询问的位置,取最优的决策即可,但是这样 DP 状态数就达到了 \(O(n^3)\) 级别,无法接受。
注意到 \(f_{a, b, c}\) 值域很小,考虑把值域记录到状态中,设 \(g_{i, a, c}\) 表示 \(\max \{ b \mid f_{a, b, c} \leq i \}\) ,可以做到 \(O(n^2 \log n)\) 预处理,足以处理 \(n \leq 4000\) 的情况。
本地打表可以发现 \(17\) 次操作最多能处理 \(n = 6444\) ,考虑对于更大的数据范围打表,只要打 \(n = 6444, 12286, 23464, 30000\) 的决策点即可。
#include <bits/stdc++.h>
using namespace std;
const int L[] = {6444, 6444, 6444, 4738, 6444, 6444, 4738, 12286, 12286, 6143, 6143, 4438, 12286, 6143, 4773, 9047, 7513, 6685, 4088, 12286, 12286, 9047, 7513, 6685, 4096, 6143, 4773, 6143, 6143, 4438, 23464, 23464, 11732, 6170, 6170, 4465, 11732, 6170, 4770, 8495, 6962, 6137, 4096, 23464, 11732, 9105, 7536, 6710, 4096, 5562, 4196, 17294, 5562, 4502, 14359, 12792, 4858, 4016, 8152, 4096, 4056, 23464, 23464, 17295, 14360, 12793, 8192, 4096, 4096, 4821, 5563, 4502, 11732, 5563, 4197, 9104, 7535, 6709, 4088, 11732, 11732, 8496, 6962, 6137, 4096, 6169, 4770, 6169, 6169, 4465, 30000, 30000, 23155, 11747, 6169, 6169, 4464, 11747, 6169, 4770, 8510, 6977, 6152, 4093, 23155, 11747, 9102, 7535, 6709, 4096, 5578, 4212, 16986, 5578, 4499, 14053, 12487, 4534, 8191, 4096, 4095, 30000, 23155, 17325, 14373, 12807, 8192, 4096, 4096, 4834, 5578, 4518, 11408, 5578, 4195, 8782, 7213, 6390, 4092, 18253, 11408, 8528, 6975, 6153, 4096, 5830, 4433, 12675, 5830, 4502, 9725, 8173, 7346, 4087, 30000, 30000, 28385, 25181, 16384, 8192, 4096, 4096, 8192, 4096, 4096, 9235, 7647, 4096, 5230, 4792, 6845, 5230, 4819, 6845, 6845, 6726, 5822, 4096};
const int A[] = {0, 3222, 3222, 3222, 0, 1706, 805, 0, 6143, 2904, 2904, 2904, 6143, 1370, 826, 6143, 6143, 6143, 4088, 0, 3239, 1534, 828, 444, 4096, 3239, 3239, 0, 1705, 804, 0, 11732, 5562, 2933, 2933, 2933, 5562, 1400, 825, 5562, 5562, 5562, 4096, 11732, 2627, 1569, 826, 444, 4096, 2627, 2627, 11732, 1060, 842, 11732, 11732, 3798, 3798, 8152, 4096, 4056, 0, 6169, 2935, 1567, 841, 8192, 4096, 4096, 841, 2935, 2935, 6169, 1366, 825, 6169, 6169, 6169, 4088, 0, 3236, 1534, 825, 442, 4096, 3236, 3236, 0, 1704, 804, 0, 6845, 11408, 5578, 2932, 2932, 2932, 5578, 1399, 825, 5578, 5578, 5578, 4093, 11408, 2645, 1567, 826, 444, 4096, 2645, 2645, 11408, 1079, 841, 11408, 11408, 3455, 8191, 4096, 4095, 6845, 5830, 2952, 1566, 841, 8192, 4096, 4096, 841, 2952, 2952, 5830, 1383, 825, 5830, 5830, 5830, 4092, 6845, 2880, 1553, 822, 443, 4096, 2880, 2880, 6845, 1328, 827, 6845, 6845, 6845, 4087, 0, 1615, 3204, 1588, 16384, 8192, 4096, 4096, 8192, 4096, 4096, 1588, 401, 4096, 3204, 3204, 1615, 1615, 1615, 0, 119, 904, 438, 4096};
const int C[] = {0, 0, 1706, 805, 3222, 3222, 3222, 0, 0, 0, 1705, 804, 3239, 3239, 3239, 1534, 828, 444, 0, 6143, 6143, 6143, 6143, 6143, 0, 1370, 826, 2904, 2904, 2904, 0, 0, 0, 0, 1705, 804, 3237, 3237, 3237, 1533, 825, 442, 0, 6170, 6170, 6170, 6170, 6170, 0, 1366, 825, 2935, 2935, 2935, 1567, 842, 842, 212, 0, 0, 0, 11732, 11732, 11732, 11732, 11732, 0, 0, 0, 3760, 1061, 842, 2628, 2628, 2628, 1569, 826, 444, 0, 5563, 5563, 5563, 5563, 5563, 0, 1399, 825, 2933, 2933, 2933, 0, 0, 0, 0, 0, 1705, 804, 3237, 3237, 3237, 1533, 825, 442, 0, 6169, 6169, 6169, 6169, 6169, 0, 1366, 825, 2933, 2933, 2933, 1566, 841, 841, 0, 0, 0, 11747, 11747, 11747, 11747, 11747, 0, 0, 0, 3774, 1060, 842, 2626, 2626, 2626, 1569, 823, 443, 0, 5578, 5578, 5578, 5578, 5578, 0, 1397, 824, 2950, 2950, 2950, 1552, 827, 447, 0, 23155, 23155, 23155, 23155, 0, 0, 0, 0, 0, 0, 0, 7209, 7209, 0, 438, 879, 2026, 2026, 1589, 5230, 5230, 5230, 5230, 0};
const int X[] = {3222, 4738, 3933, 3498, 1706, 2511, 1240, 6143, 9047, 4438, 3634, 3200, 7513, 2196, 1259, 6685, 6241, 3990, 2040, 3239, 4773, 2362, 1272, 2687, 2048, 3947, 3514, 1705, 2509, 1238, 11732, 17294, 8495, 4465, 3661, 3227, 6962, 2225, 1258, 6137, 5695, 3963, 2048, 14359, 4196, 2395, 1270, 2710, 2048, 3371, 2940, 12792, 1902, 1272, 11950, 7934, 3804, 2042, 4056, 2048, 2008, 6169, 9104, 4502, 2408, 4821, 4096, 2048, 2048, 1053, 3660, 3229, 7535, 2191, 1255, 6709, 6265, 3992, 2040, 3236, 4770, 2359, 1267, 2173, 2048, 3945, 3512, 1704, 2508, 1238, 6845, 18253, 16986, 8510, 4464, 3660, 3226, 6977, 2224, 1258, 6152, 5710, 3961, 2045, 14053, 4212, 2393, 1270, 2709, 2048, 3387, 2956, 12487, 1920, 1271, 11646, 7953, 3483, 4095, 2048, 2047, 12675, 8782, 4518, 2407, 4834, 4096, 2048, 2048, 1053, 3676, 3245, 7213, 2208, 1255, 6390, 5947, 3975, 2044, 9725, 4433, 2375, 1265, 2189, 2048, 3609, 3177, 8173, 2155, 1259, 7346, 6899, 4033, 2039, 1615, 4819, 4792, 9235, 8192, 4096, 2048, 2048, 4096, 2048, 2048, 1989, 3588, 2048, 3913, 3483, 3230, 2423, 2422, 119, 1023, 1342, 1880, 2048};
const int inf = 1e4;
const int N = 4e3 + 7, B = 21;
int len[B], f[B][N][N];
int pren, n;
inline bool query(int x) {
if (x > pren)
return false;
cout << "? " << x << endl;
string str;
cin >> str;
return str == "Yes";
}
inline void prework() {
for (int i = 0; i < B; ++i) {
len[i] = min(min(1 << i, n), 4000);
for (int j = 0; j <= len[i]; ++j)
for (int k = 0; k <= len[i] - j; ++k)
f[i][j][k] = -inf;
}
f[0][0][0] = 1, f[0][1][0] = f[0][0][1] = 0;
for (int i = 0; i < B; ++i) {
for (int j = len[i]; ~j; --j)
for (int k = len[i] - j - 1; ~k; --k) {
f[i][j][k] = max(f[i][j][k], f[i][j + 1][k]);
if (k + 1 <= len[i])
f[i][j][k] = max(f[i][j][k], f[i][j][k + 1]);
}
if (i + 1 < B) {
for (int j = 0; j <= len[i]; ++j)
for (int k = 0; k <= len[i] - j; ++k) {
if (f[i][j][k] == -inf)
continue;
if (f[i][k][j] != -inf)
f[i + 1][min(f[i][k][j], len[i + 1] - j - k)][j + k] =
max(f[i + 1][min(f[i][k][j], len[i + 1] - j - k)][j + k], f[i][j][k]);
if ((1 << i) >= k) {
f[i + 1][j][k] = max(f[i + 1][j][k], f[i][j][k] + (1 << i) - k);
f[i + 1][min(j + (1 << i) - k, len[i + 1] - k)][k] = max(
f[i + 1][min(j + (1 << i) - k, len[i + 1] - k)][k], f[i][j][k]);
}
}
}
}
}
inline int search(int x, int y, int z) {
for (int i = 0; i < B; ++i)
if (x - z <= len[i] && f[i][y][x - y - z] >= z)
return i;
return -1;
}
inline int calc(int len, int a, int c) {
pair<int, int> ans = make_pair(inf, 0);
for (int i = 1; i < len; ++i) {
int x, y;
if (i <= a)
x = search(len - i, a - i, c), y = search(len - c - (a - i), len - c - (a - i), 0);
else if (i <= len - c)
x = search(len - a, i - a, c), y = search(len - c, a, len - c - i);
else
x = search(len - a - (i - (len - c)), len - a - (i - (len - c)), 0), y = search(i, a, c - (len - i));
ans = min(ans, make_pair(max(x, y) + 1, i));
}
return ans.second;
}
inline void solve(int n) {
vector<pair<int, int> > vec;
for (int i = 1; i <= n; ++i)
vec.emplace_back(i, 0);
while (vec.size() > 1) {
int a = 0, b = 0, c = 0;
bool flag = false;
for (auto it : vec) {
if (it.second) {
if (flag)
++c;
else
++a;
} else
++b, flag = true;
}
int x;
if (vec.size() <= 4000)
x = calc(vec.size(), a, c);
else {
for (int i = 0; i < N; ++i)
if (L[i] == vec.size() && A[i] == a && C[i] == c) {
x = X[i];
break;
}
}
if (query(vec[x].first)) {
for (int i = 0; i < x; ++i)
++vec[i].second;
} else {
for (int i = x; i < vec.size(); ++i)
++vec[i].second;
}
vector<pair<int, int> > now;
for (auto it : vec)
if (it.second <= 1)
now.emplace_back(it);
vec = now;
}
cout << "! " << vec[0].first << endl;
}
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
cin >> n;
pren = n, prework();
if (n > 4000) {
if (n <= 6444)
n = 6444;
else if (n <= 12286)
n = 12286;
else if (n <= 23464)
n = 23464;
else
n = 30000;
}
for (;;) {
solve(n);
string str;
cin >> str;
if (str == "Done")
break;
}
return 0;
}
随机化相关
P10204 [湖北省选模拟 2024] 白草净华 / buer
有一个长度为 \(n\) 的序列 \(a_{0 \sim n - 1}\) ,满足:
- \(\forall 0 \leq i < n, a_i \neq a_{(i + 1) \bmod n}\) 。
- 有且仅有一个 \(i(0 \leq i < n)\) 满足 \(a_i > \max(a_{(i + 1) \bmod n}, a_{(i - 1 + n) \bmod n})\) 。
初始时 \(n\) 和 \(a_{0 \sim n - 1}\) 均未知。每次可以询问一个 \(k\) ,交互库将返回 \(a_{(d + k) \bmod n}\) 的值,其中 \(d\) 是上一次返回的元素下标,初始时 \(d = 0\) 。
试用 \(\leq 333\) 次询问找到序列最大值。
\(2 \leq n \leq 10^6\)
先考虑求出 \(n\) ,随机询问 \(m\) 次,然后枚举序列长度 \(n\) 判定合法性,求出 \(n\) 之后就可以直接 \(O(\log n)\) 倍增求解最大值了。
考虑 \(m\) 的期望,由于询问随机,因此可以感性认为相邻两个点大小关系也是随机的,从而 \(m = O(\log n)\) 。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int inf = 0x3f3f3f3f;
vector<pair<ll, int> > qry;
mt19937 myrand(time(0));
int ask(int k);
int cheat();
inline bool check(int n) {
vector<pair<int, int> > vec;
for (auto it : qry)
vec.emplace_back(it.first % n, it.second);
sort(vec.begin(), vec.end()), vec.erase(unique(vec.begin(), vec.end()), vec.end());
int m = vec.size();
if (m == 1)
return true;
for (int i = 0; i + 1 < m; ++i)
if (vec[i].first == vec[i + 1].first && vec[i].second != vec[i + 1].second)
return false;
int mxp = 0, mnp = 0;
for (int i = 1; i < m; ++i) {
if (vec[i].second > vec[mxp].second)
mxp = i;
if (vec[i].second < vec[mnp].second)
mnp = i;
}
for (int i = (mnp + 1) % m; i != mxp; i = (i + 1) % m)
if (vec[i].second <= vec[(i - 1 + m) % m].second)
return false;
for (int i = (mxp + 1) % m, lst = inf; i != mnp; i = (i + 1) % m)
if (vec[i].second >= vec[(i - 1 + m) % m].second)
return false;
return true;
}
int buer(int T) {
vector<int> vec(999999);
iota(vec.begin(), vec.end(), 2);
ll lst = 0;
while (vec.size() > 1) {
int k = myrand() % 1000000000 + 1;
qry.emplace_back(lst += k, ask(k));
vector<int> now;
for (int it : vec)
if (check(it))
now.emplace_back(it);
vec = now;
}
int n = vec[0], mxp = (lst + 1) % n, p = ask(1), q = ask(1);
if (p < q) {
int val = q, lst = 0;
for (int i = __lg(n); ~i; --i) {
p = ask(lst + (1 << i)), q = ask(1);
if (val < p && p < q)
mxp = (mxp + (1 << i) + 1) % n, val = q, lst = 0;
else
lst = n - (1 << i) - 1;
}
return max(val, ask(lst + 1));
} else {
int val = q, lst = 0;
for (int i = __lg(n); ~i; --i) {
p = ask(lst + n - (1 << i)), q = ask(n - 1);
if (val < p && p < q)
mxp = (mxp + n * 2 - (1 << i) - 1) % n, val = q, lst = 0;
else
lst = (1 << i) + 1;
}
return max(val, ask(lst + n - 1));
}
}
CF1840G2 In Search of Truth (Hard Version)
有一个 \(n\) 个点的环,初始在某个未知的位置。
每次询问可以给出一个 \(x(x \leq 1000)\) 和方向,交互库会顺时针/逆时针走 \(x\) 步后所在点的标号(此时初始位置也移动到相应位置)。
试用 \(\leq 10^3\) 次询问求出 \(n\) 。
\(n \leq 10^6\)
考虑询问一组 \(a_{1 \sim m}\) ,若走到了起点,则说明 \(n \mid \sum_{i = 1}^m a_i\) 。考虑构造一组 \(a_{1 \sim m}\) ,满足任意 \(n \in [1, 10^6]\) ,均存在 \(1 \leq l \leq r \leq m\) 满足 \(\sum_{i = l}^r a_i = n\) ,那么就做完了。
考虑 BSGS,构造 \(a_{1 \sim B} = 1, a_{B + 1 \sim 2 B} = B\) 即可,其中 \(B = \sqrt{n} = 1000\) ,但是需要询问 \(2000\) 次,无法接受。
注意到随机跳到的格子的标号一定 \(\geq n\) ,考虑随机跳 \(1000 - 2d\) 次,若干次记询问到的最大值为 \(n_0\) ,不妨钦定 \(n_0 \leq n < n_0 + d(d + 1)\) ,则之后可以用 \(2d\) 次操作求得 \(n\) 。具体地,先询问 \(d\) 次 \(0\) ,再询问一次 \(n_0 - d\) ,再询问若干次 \(d\) 即可。
但是这样显然不对,在 \(d\) 不够大时 \(n\) 可能 \(\geq n_0 + d(d + 1)\) 。那么不妨分析一下错误率,即:
取 \(d = 340\) 时可以做到极高的正确率。
#include <bits/stdc++.h>
using namespace std;
const int B = 340;
mt19937 myrand(time(0));
int res;
signed main() {
cin >> res;
int n0 = 0;
for (int i = 1; i <= 1000 - B * 2 - 1; ++i) {
cout << "+ " << myrand() % 100000000 << endl;
cin >> res;
n0 = max(n0, res);
}
map<int, int> mp;
for (int i = 1; i <= B; ++i) {
cout << "+ 1" << endl;
cin >> res;
if (mp.find(res) != mp.end()) {
cout << "! " << i - mp[res] << endl;
return 0;
}
mp[res] = i;
}
if (n0 - B >= 0)
cout << "+ " << n0 - B << endl;
else
cout << "- " << B - n0 << endl;
cin >> res;
for (int i = 1; i <= B; ++i) {
cout << "+ " << B << endl;
cin >> res;
if (mp.find(res) != mp.end()) {
cout << "! " << n0 + i * B - mp[res] << endl;
return 0;
}
}
return 0;
}
QOJ8416. Dzielniki [B]
交互库有一个正整数 \(x \in [1, n]\) ,每次询问可以给出一个 \(y \in [0, C]\) ,交互库会返回 \(x + y\) 的因数个数。
共 \(10\) 组数据,试用总共 \(\leq 720\) 次询问求出 \(x\) ,交互库不自适应。
\(n = 10^{14}\) ,\(C = 10^{17}\)
首先,若 \(2^k \mid n\) 且 \(2^{k + 1} \nmid n\) ,则 \((k + 1) \mid d(n)\) 。
考虑求出 \(x\) 在模 \(2^{47}\) 意义下的值,每次尝试在得知 \(x \bmod 2^k\) 的情况下推出 \(x \bmod 2^{k + 1}\) 的值,初始时 \(x \bmod 2^0 = 0\) ,每次:
- 若询问 \(y = 2^k - (x \bmod 2^k)\) 返回值为 \((k + 1)\) 的倍数,则认为 \(x \bmod 2^{k + 1} = x \bmod 2^k\) 。
- 若询问 \(y = 2^{k + 1} - (x \bmod 2^k)\) 返回值为 \((k + 1)\) 的倍数,则认为 \(x \bmod 2^{k + 1} = (x \bmod 2^k) + 2^k\) 。
- 否则说明当前的值不为 \(x \bmod 2^k\) ,这是因为前面递归时有概率是假的。
对询问记忆化即可通过。
#include <bits/stdc++.h>
#include "dzilib.h"
typedef long long ll;
using namespace std;
map<ll, ll> mp;
ll n;
inline ll query(ll x) {
return mp.find(x) == mp.end() ? mp[x] = Ask(x) : mp[x];
}
bool dfs(ll x, int k) {
if ((1ll << k) > n) {
Answer(x);
return true;
}
if (!(query((1ll << k) - x) % (k + 1)) && dfs(x, k + 1))
return true;
else if (!(query((1ll << (k + 1)) - x) % (k + 1)) && dfs(x + (1ll << k), k + 1))
return true;
else
return false;
}
signed main() {
n = GetN();
int T = GetT();
while (T--)
mp.clear(), dfs(0, 0);
return 0;
}
极端返回值
P6982 [NEERC2015] Jump
有一个长度为 \(n\) 的 01 串 \(S\) ,其中 \(n\) 是偶数。
每次可以询问一个长度为 \(n\) 的 01 串:
- 若完全匹配,则返回 \(n\) 。
- 若与 \(S\) 恰有 \(\frac{n}{2}\) 个字符匹配,则返回 \(\frac{n}{2}\) 。
- 否则返回 \(0\) 。
给定 \(n\) ,求 \(S\) (只要令交互库返回 \(n\) 即可)。
\(n \leq 1000\) ,询问上限 \(n + 500\) 次
发现这个询问得到的信息很少,一个显然的想法是在一个有信息的串上调整。考虑先用若干次询问找到一个匹配了 \(\frac{n}{2}\) 位的串 \(T\) ,单次询问成功的概率为 \(\frac{\binom{n}{2}}{2^n}\) 。随机 \(499\) 次,成功的概率 \(> 0.999\) 。下面考虑通过 \(T\) 求出 \(S\) 。
考虑固定一个位置,枚举剩下的 \(n - 1\) 位。每次询问将 \(T\) 的这两个位置翻转,若返回 \(\frac{n}{2}\) ,则说明 \(T\) 的这两个位置正确性不同,否则说明正确性相同。注意询问后不改变 \(T\) 。
最后将与固定位置正确性不同的位置翻转再询问一次,若返回 \(0\) 则将与固定位置正确性相同的位置翻转询问即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
int res[N], ans[N];
mt19937 myrand(time(0));
int n;
inline int query(int *a) {
for (int i = 1; i <= n; ++i)
cout << a[i];
cout << endl;
int res;
cin >> res;
if (res == n)
exit(0);
return res;
}
signed main() {
cin >> n;
for (;;) {
for (int i = 1; i <= n; ++i)
res[i] = myrand() & 1;
if (query(res))
break;
}
ans[1] = (res[1] ^= 1);
for (int i = 2; i <= n; ++i) {
res[i] ^= 1;
ans[i] = res[i] ^ (query(res) == n / 2);
res[i] ^= 1;
}
query(ans);
for (int i = 1; i <= n; ++i)
ans[i] ^= 1;
query(ans);
return 0;
}
CF1299E So Mean
给定 \(n\) ,交互库有一个 \(1 \sim n\) 的排列 \(p\) ,保证 \(2 \mid n\) 。
每次询问可以给出一个整数 \(k\) 和 \(k\) 个互不相同的 \(1 \sim n\) 的整数 \(a_{1 \sim k}\) ,交互库将返回 \([k = \sum_{i = 1}^k p_{a_i}]\) 的真假。
试用 \(\leq 18n\) 次询问得到排列 \(p\) 。由于排列 \(q_i = n - p_i + 1\) 答案始终相同,因此钦定 \(p_1 \leq \frac{n}{2}\) 。
\(n \leq 800\)
首先可以想到取 \(k = n - 1\) ,遍历 \(n\) 种位置集合,此时恰好有两个询问返回 \(1\) ,那么这两个位置一定为 \(1\) 和 \(n\) ,任意钦定即可。
类似的可以确定 \(2\) 和 \(n - 1\) ,但是注意此时不能随机钦定,可以通过和 \(1\) 询问确定奇偶性从而确定 \(2\) 和 \(n - 1\) 的具体位置。
该做法需要 \(O(n^2)\) 次询问,无法通过。
注意到若询问返回 \(1\) ,且已知其中 \(k - 1\) 个数时,可以直接得到一个未知数 \(\bmod k\) 的值。
考虑 CRT,选取若干两两互质的数满足 \(\mathrm{lcm} \geq 800\) ,不难发现选取 \(S = \{ 3, 5, 7, 8 \}\) 即可。接下来问题转化为确定一些数,满足对于任意 \(m \in S, r \in [0, m - 1]\) 均存在一个已知数集满足 \(\bmod m = r\) 。
考虑前面 \(O(n^2)\) 的想法,不难发现只要确定 \(1 \sim 5\) 和 \(n - 4 \sim n\) 即可,该部分询问次数为 \(5n\) 。
下面考虑确定剩下的 \(n - 10\) 个位置。对于每个 \(m \in S\) ,只要确定了 \(\bmod m\) 的值,即可得出原值。从小到大枚举 \(\bmod m\) 的余数判定,注意需要从 \(1\) 开始枚举,这样枚举 \(1 \sim m - 1\) 均不合法时余数即为 \(0\) ,可以少一次询问。
总共约 \(17.5 n\) 次询问,可以通过。其中求 \(\bmod 8\) 的余数时可以依次查询模 \(2, 4, 8\) 的余数,这样一个数只要询问 \(3\) 次,但是没有必要。
#include <bits/stdc++.h>
using namespace std;
const int N = 8e2 + 7;
int p[N], id[N];
int n;
inline bool query(vector<int> vec) {
cout << "? " << vec.size() << ' ';
for (int it : vec)
cout << it << ' ';
cout << endl;
int res;
cin >> res;
return res;
}
inline void output() {
if (p[1] > n / 2) {
for (int i = 1; i <= n; ++i)
p[i] = n - p[i] + 1;
}
cout << "! ";
for (int i = 1; i <= n; ++i)
cout << p[i] << ' ';
cout << endl;
}
signed main() {
cin >> n;
for (int i = 1; i <= n; ++i) {
vector<int> vec(n);
iota(vec.begin(), vec.end(), 1), vec.erase(find(vec.begin(), vec.end(), i));
if (query(vec)) {
if (id[1])
id[p[i] = n] = i;
else
id[p[i] = 1] = i;
}
}
int lim = min(n / 2, 5);
for (int i = 2; i <= lim; ++i) {
vector<int> a;
for (int j = 1; j <= n; ++j) {
if (p[j])
continue;
vector<int> vec;
for (int k = 1; k <= n; ++k)
if (k != j && !p[k])
vec.emplace_back(k);
if (query(vec))
a.emplace_back(j);
}
if (query({id[1], a[0]}) == (i & 1))
id[p[a[0]] = i] = a[0], id[p[a[1]] = n - i + 1] = a[1];
else
id[p[a[0]] = n - i + 1] = a[0], id[p[a[1]] = i] = a[1];
}
if (lim == n / 2)
return output(), 0;
vector<vector<vector<int> > > vec(10);
for (int m : {3, 5, 7, 8}) {
vec[m].resize(m);
for (int s = 0; s < (1 << 10); ++s) {
if (__builtin_popcount(s) != m - 1)
continue;
vector<int> now;
int sum = 0;
for (int i = 0; i < 10; ++i)
if (s >> i & 1) {
int x = i < 5 ? i + 1 : n - i + 5;
now.emplace_back(id[x]), sum += x;
}
vec[m][sum % m] = now;
}
}
for (int i = 1; i <= n; ++i) {
if (p[i])
continue;
int lcm = 1;
for (int m : {3, 5, 7, 8}) {
int r = 0;
for (int j = 1; j <= m - 1; ++j) {
vector<int> qry = vec[m][m - j];
qry.emplace_back(i);
if (query(qry)) {
r = j;
break;
}
}
vector<int> qry = vec[m][m - 1 - r];
qry.emplace_back(i);
while (p[i] % m != r)
p[i] += lcm;
lcm *= m;
}
}
output();
return 0;
}
树上问题
CF1592D Hemose in ICPC ?
交互库有一棵 \(n\) 个点的带边权树,给定树的形态(边权未给)。定义 \(\mathrm{dist}(x, y)\) 为 \(x\) 到 \(y\) 路径上边权的 \(\gcd\) 。
每次可以询问一个点集 \(S\) 内最大的 \(\mathrm{dist}(x, y) (x, y \in S, x \neq y)\) 。
试用 \(\leq 12\) 次询问求出 \(\mathrm{dist}\) 最大的点对。
\(n \leq 10^3\)
首先不难发现 \(\mathrm{dist}\) 最大的点对一定是一条边的两端。
考虑欧拉序,其满足相邻点对一定有边,那么直接在欧拉序上对半分治就只要查询 \(\log(2n - 1) + 1\) 次。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
int id[N << 1];
int n, cnt;
inline int query(int l, int r) {
set<int> st;
for (int i = l; i <= r; ++i)
st.insert(id[i]);
cout << "? " << st.size() << ' ';
for (int it : st)
cout << it << ' ';
cout << endl;
int res;
cin >> res;
return res;
}
void dfs(int u, int f) {
id[++cnt] = u;
for (int v : G.e[u])
if (v != f)
dfs(v, u), id[++cnt] = u;
}
signed main() {
cin >> n;
for (int i = 1, u, v; i < n; ++i) {
scanf("%d%d", &u, &v);
G.insert(u, v), G.insert(v, u);
}
dfs(1, -1);
int l = 1, r = cnt, ans = query(1, cnt);
while (r - l + 1 > 2) {
int mid = (l + r) >> 1;
if (query(l, mid) == ans)
r = mid;
else
l = mid;
}
cout << "! " << id[l] << ' ' << id[r] << endl;
return 0;
}
CF2032D Genokraken
有一棵 \(n\) 个点的树,编号为 \(0 \sim n - 1\) ,满足:
- 除 \(0\) 外每个点最多一个儿子。
- \(1\) 一定有儿子。
- \(\forall i < j, fa_i \leq fa_j\) 。
每次可以询问 \(x \to y\) 路径上是否经过 \(0\) 。
给定 \(n\) ,求 \(1 \sim n - 1\) 的父亲。
\(n \leq 10^4\) ,询问次数上限 \(2n - 6\) 次。
可以发现树的形态一定是 \(0\) 上挂若干条链。
考虑先询问 \((1, 2 \sim t)\) 直到 \((1, t)\) 的答案为 \(0\) ,此时 \(t\) 一定是 \(1\) 的儿子,且 \(1 \sim t - 1\) 都是 \(0\) 的儿子。
接下来考虑询问 \(t + 1 \sim n - 1\) 的父亲,由于父亲编号单调,双指针即可。
第一部分需要询问 \(t - 2 \leq n - 3\) 次,第二部分最坏询问 \(n - 3\) 次(指针从 \(2\) 移动到 \(n - 2\) ),最坏情况下总询问次数为 \(2n - 6\) 次。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 7;
int fa[N];
int n;
inline int query(int x, int y) {
cout << "? " << x << " "<< y << endl;
int res;
cin >> res;
return res;
}
signed main() {
int T;
cin >> T;
while (T--) {
cin >> n;
int t = 0;
fa[1] = 0;
for (int i = 2; i < n; ++i) {
int res = query(1, i);
if (!res) {
t = i;
break;
}
fa[i] = 0;
}
fa[t] = 1;
int p = 2;
for (int i = t + 1; i < n; ++i) {
while (query(p, i))
++p;
fa[i] = p++;
}
cout << "! ";
for (int i = 1; i < n; ++i)
cout << fa[i] << ' ';
cout << endl;
}
return 0;
}
LOJ6669. Nauuo and Binary Tree
交互库有一个 \(n\) 个点的二叉树,\(1\) 为根。每次可以询问两点的树上距离,试用 \(\leq 30000\) 次询问还原整棵树(返回 \(2 \sim n\) 每个点的父亲编号)。
\(n \leq 3000\)
发现询问次数上限为 \(O(n \log n)\) 级别,考虑对每个点用 \(O(\log n)\) 次询问确定父亲。
考虑先用 \(n - 1\) 次询问将所有点按深度分层,然后从上到下依次确定。
首先有一个显然的暴力,每次确定一个点时从根开始走,每次用一次询问就可以确定走的方向,不难发现这会被链卡成 \(O(n^2)\) 。
考虑利用轻重链剖分优化结构,如果每次只走轻边,则询问的复杂度就优化为 \(O(n \log n)\) 。每次先询问当前点与重链底部的距离,若距离为 \(1\) 则直接接上去即可,否则可以得到 LCA 的深度,走到 LCA 的另一棵子树即可。
不难发现这样只会走轻边,每次加入一个点之后暴力向上更新轻重链剖分即可,时间复杂度 \(O(n^2)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 3e3 + 7;
vector<int> vec[N];
int fa[N], lc[N], rc[N], dep[N], siz[N], son[N], bt[N];
int n;
inline int dist(int x, int y) {
cout << "? " << x << ' ' << y << endl;
int res;
cin >> res;
return res;
}
void dfs(int u) {
siz[u] = 1, son[u] = 0, bt[u] = u;
for (int v : {lc[u], rc[u]})
if (v) {
dfs(v), siz[u] += siz[v];
if (siz[v] > siz[son[u]])
son[u] = v, bt[u] = bt[v];
}
}
signed main() {
cin >> n;
for (int i = 2; i <= n; ++i)
vec[dep[i] = dist(1, i)].emplace_back(i);
bt[1] = 1;
for (int i = 1; i < n; ++i)
for (int x : vec[i]) {
for(int u = 1, j = 1; j <= n; ++j) {
int d = dist(bt[u], x);
if (d == 1) {
fa[lc[bt[u]] = x] = bt[u];
break;
}
d = (dep[bt[u]] + dep[x] - d) / 2;
while (dep[u] < d)
u = son[u];
int &v = (lc[u] == son[u] ? rc[u] : lc[u]);
if (!v) {
fa[v = x] = u;
break;
}
u = v;
}
dfs(1);
}
cout << "! ";
for (int i = 2; i <= n; ++i)
cout << fa[i] << ' ';
return 0;
}
QOJ9678. 网友小 Z 的树
交互库有一个 \(n\) 个点的树,给定 \(n\) ,询问有两种:
- 询问一:给出互不相同的 \(x, y, z\) ,询问 \(f(x, y, z) = \mathrm{dist}(x, y) + \mathrm{dist}(x, z) + \mathrm{dist}(y, z)\) ,询问上限 \(3 \times 10^5\) 次。
- 询问二:给出 \(x, y, z\) ,询问 \(x\) 是否在 \(y\) 到 \(z\) 的路径上,询问上限 \(2\) 次。
求该树其中一条直径的两端。
\(n \leq 10^5\)
先特判 \(n = 3\) 的情况,考虑用 \(3(n - 2)\) 次询问一求出 \(f(x, 1, 2)\) 最大的 \(x\) 、\(f(y, x, 2)\) 最大的 \(y\) 、\(f(z, x, y)\) 最大的 \(z\) ,容易证明 \(x, y, z\) 必有两个直径的端点(显然 \(x, y\) 必为直径一端,这个可以画图理解,然后若 \((x, y)\) 不是直径则 \(z\) 就是另一端了),同时记录 \(d_i = f(i, x, y)\) 。
考虑 \(d_i\) 取到最小值的点,不难发现其一定在 \(x\) 到 \(y\) 的路径上,任取一个点记为 \(t\) 。
先特判掉 \(d_z = \min d\) 的情况,直接返回 \((x, y)\) 即可。
若 \(x, y\) 不相邻,则 \(t\) 在 \(x\) 到 \(y\) 的路径上,并且 \(t\) 要么在 \(x\) 到 \(z\) 的路径上,要么在 \(y\) 到 \(z\) 的路径上,也可以同时成立。先用一次询问二求出 \(t\) 在哪一边,不妨设 \(t\) 在 \(x\) 到 \(z\) 的路径上。由于 \(\mathrm{dist}(x, y) = \frac{d_t}{2}\) ,考虑用一次询问一得出 \(\mathrm{dist}(x, z) = \frac{f(x, t, z)}{2}\) ,则 \(\mathrm{dist}(y, z) = d_z - \mathrm{dist}(x, y) - \mathrm{dist}(x, z)\) ,直接比较即可。
否则 \(x, y\) 相邻,容易发现此时图必然是一条链。先询问 \(y\) 是否在 \(x\) 到 \(z\) 的路径上,若是则返回 \((x, z)\) ,否则去掉 \(\min d\) 显然在 \(x\) 向 \(z\) 走一步的点取到,记这个点为 \(t\) ,后面处理方法就相同了。
一共使用 \(3n - 5\) 次询问一,\(2\) 次询问二。
#include <bits/stdc++.h>
#include "diameter.h"
using namespace std;
pair<int, int> find_diameter(int testid, int n) {
if (n == 1)
return make_pair(1, 1);
else if (n == 2)
return make_pair(1, 2);
else if (n == 3)
return in(1, 2, 3) ? make_pair(2, 3) : (in(2, 1, 3) ? make_pair(1, 3) : make_pair(1, 2));
vector<int> d(n + 1);
auto search = [&](int x, int y) {
int z = -1;
for (int i = 1; i <= n; ++i) {
if (i != x && i != y) {
d[i] = query(i, x, y);
if (z == -1 || d[i] > d[z])
z = i;
}
}
return z;
};
int x = search(1, 2), y = search(x, 2), z = search(x, y), t = -1;
for (int i = 1; i <= n; ++i)
if (i != x && i != y && (t == -1 || d[i] < d[t]))
t = i;
if (d[z] == d[t])
return make_pair(x, y);
else if (in(y, x, z))
return make_pair(x, z);
if (in(t, y, z))
swap(x, y);
int dxy = d[t] / 2, dxz = query(x, t, z) / 2, dyz = d[z] - dxy - dxz;
if (dyz >= max(dxy, dxz))
return make_pair(y, z);
else if (dxy >= max(dxz, dyz))
return make_pair(x, y);
else
return make_pair(x, z);
}
QOJ11116. Yearning for Yonder / 对远方的向往
评测端有一棵 \(n\) 个点的树,保证树的形态为随机 Prufer 序列生成,边权为 \([1, 10^4]\) 中的随机整数。
给出 \(n\) ,每次可以询问两点的树上距离,试用 \(\leq 7n\) 次询问还原这棵树。
\(n \leq 10^5\)
考虑任选一个点 \(u\) ,用 \(n\) 次操作求出 \(u\) 到所有点的距离。再任选一个距离 \(u\) 的最远点 \(v\) ,用 \(n\) 次操作求出 \(v\) 到所有点的距离。
若 \(w\) 满足 \(\mathrm{dist}(u, v) = \mathrm{dist}(u, w) + \mathrm{dist}(w, v)\) ,则说明 \(w\) 在 \(u, v\) 路径上,否则可以算出其所属的子树。对每个子树递归查询即可,递归时根据之前的查询结果可以得到根节点到其他所有点的距离,可以省下一半常数。
由于随机树的树高是 \(O(\sqrt{n})\) 的,分析得到期望查询次数为 \(O(n \log \log n)\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 7;
vector<tuple<int, int, int> > edg;
vector<int> vec[N];
int disx[N], disy[N];
int n;
inline int query(int x, int y) {
cout << "? " << x << ' ' << y << endl;
int res;
cin >> res;
return res;
}
void solve(vector<int> id) {
if (id.size() == 1)
return;
int x = id[0], y = id.back();
disy[x] = disx[y], disy[y] = 0, vec[x].clear();
vector<pair<int, int> > chain = {make_pair(0, x)};
for (int i = 1; i < id.size(); ++i) {
if (id[i] == y)
continue;
disy[id[i]] = query(y, id[i]);
if (disx[id[i]] + disy[id[i]] == disx[y])
chain.emplace_back(disx[id[i]], id[i]), vec[id[i]].clear();
}
sort(chain.begin(), chain.end());
for (int i = 1; i < chain.size(); ++i)
edg.emplace_back(chain[i - 1].second, chain[i].second, disx[chain[i].second] - disx[chain[i - 1].second]);
edg.emplace_back(chain.back().second, y, disx[y] - disx[chain.back().second]);
for (int it : id) {
if (it == y)
continue;
int d = disx[it] - (disx[it] + disy[it] - disx[y]) / 2;
vec[lower_bound(chain.begin(), chain.end(), make_pair(d, 0))->second].emplace_back(it);
}
for (auto it : chain) {
int x = it.second, d = disx[x];
if (vec[x][0] != x)
swap(vec[x][0], *find(vec[x].begin(), vec[x].end(), x));
for (int &it : vec[x])
disx[it] -= d;
solve(vec[x]);
}
}
signed main() {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
int T;
cin >> T;
while (T--) {
cin >> n;
edg.clear(), disx[1] = 0;
for (int i = 2; i <= n; ++i)
disx[i] = query(1, i);
vector<int> id(n);
iota(id.begin(), id.end(), 1), sort(id.begin(), id.end(), [](const int &a, const int &b) {
return disx[a] < disx[b];
});
solve(id), cout << "! ";
for (auto it : edg)
cout << get<0>(it) << ' ' << get<1>(it) << ' ' << get<2>(it) << ' ';
cout << endl;
}
return 0;
}
QOJ5015. 树
有一棵 \(n\) 个点的树,你需要通过询问确定这棵树的形态。
每次询问可以给出一个点 \(x\) 和点集 \(S\) ,交互库会返回该点到集合内的点的树上距离之和。
限制:询问次数 \(\leq 8.5 \times 10^3\) ,\(\sum |S| \leq 3 \times 10^5\)
\(n \leq 10^3\)
考虑随机选一个点作为根,然后就可以依次询问确定每个点的深度,接下来考虑确定相邻层之间的连边。
不妨假设现在是要确定 \(A, B\) 两层之间的连边,其中 \(A\) 的深度为 \(B\) 的深度减一。
若对于 \(i \in B, j \in A\) ,\(i\) 是 \(j\) 的儿子,则 \(\mathrm{ask}(i, S) = \mathrm{ask}(j, S) + |S|\) ,其中 \(S \subseteq A\) 。
考虑随机选取 \(A\) 的非空子集 \(S\) ,按 \(\mathrm{ask}(x, S)\) 分类,递归处理,注意 \(x \in A\) 的时候无需询问,可以直接处理,因为树的部分形态已经确定。
操作次数最坏情况为 \(9226\) ,但是由于随机化的原因很难卡满。
#include <bits/stdc++.h>
#include "tree.h"
using namespace std;
const int N = 1e3 + 7, LOGN = 11;
struct Graph {
vector<int> e[N];
inline void insert(int u, int v) {
e[u].emplace_back(v);
}
} G;
vector<int> vec[N];
int fa[N][LOGN], dep[N];
mt19937 myrand(time(0));
inline int LCA(int x, int y) {
if (dep[x] < dep[y])
swap(x, y);
for (int h = dep[x] - dep[y]; h; h &= h - 1)
x = fa[x][__builtin_ctz(h)];
if (x == y)
return x;
for (int i = LOGN - 1; ~i; --i)
if (fa[x][i] != fa[y][i])
x = fa[x][i], y = fa[y][i];
return fa[x][0];
}
void solve(vector<int> a, vector<int> b) {
if (b.empty())
return;
if (a.size() == 1) {
for (int it : b) {
answer(a[0], it), fa[it][0] = a[0];
for (int i = 1; i < LOGN; ++i)
fa[it][i] = fa[fa[it][i - 1]][i - 1];
}
return;
}
shuffle(a.begin(), a.end(), myrand);
vector<int> c(a.begin(), a.begin() + a.size() / 2);
map<int, vector<int> > A, B;
for (int it : a) {
int res = 0;
for (int x : c)
res += dep[it] + dep[x] - dep[LCA(it, x)] * 2;
A[res].emplace_back(it);
}
for (int it : b)
B[ask(it, c)].emplace_back(it);
for (auto it : B)
solve(A[it.first - c.size()], it.second);
}
void solver(int n, int A, int B) {
int rt = myrand() % n + 1;
vec[0].emplace_back(rt);
for (int i = 1; i <= n; ++i)
if (i != rt)
vec[dep[i] = ask(rt, {i})].emplace_back(i);
for (int i = 1; !vec[i].empty(); ++i)
solve(vec[i - 1], vec[i]);
}
CF1061F Lost Root
交互库有一棵 \(n\) 个点的满 \(k\) 叉树,每次询问给出 \(a, b, c\) ,交互库会返回 \(b\) 是否在 \(a, c\) 路径上。试用 \(\leq 60n\) 次询问找到根。
\(n \leq 1500\)
考虑先询问出一条直径,然后在直径上寻找根。
先考虑 \(k = 2\) 的情况,随机两个点能成为直径的概率为 \(\frac{1}{4} \times \frac{1}{4} \times 2 = \frac{1}{8}\) ,前两个 \(\frac{1}{4}\) 是根左右子树儿子的数量,后者是因为 \((a, b) = (b, a)\) 。
考虑如何判定两点是否为直径端点,用 \(n\) 次求出距离即可,最后找根时一个个判断距离即可。
#include <bits/stdc++.h>
using namespace std;
const int N = 1.5e3 + 7;
bool flag[N];
mt19937 myrand(time(0));
int n, k;
inline bool query(int a, int b, int c) {
cout << "? " << a << ' ' << b << ' ' << c << endl;
string str;
cin >> str;
return str == "Yes";
}
inline int dist(int a, int b) {
int res = 1;
for (int i = 1; i <= n; ++i)
if (i != a && i != b)
res += query(a, i, b);
return res;
}
signed main() {
cin >> n >> k;
int h = log(n) / log(k), r;
for (;;) {
int x = myrand() % n + 1, y = myrand() % n + 1;
if (x == y)
continue;
for (int i = 1; i <= n; ++i)
flag[i] = (i != x && i != y && query(x, i, y));
if (count(flag + 1, flag + n + 1, true) == h * 2 - 1) {
r = x;
break;
}
}
for (int i = 1; i <= n; ++i) {
if (!flag[i])
continue;
int d = 0;
for (int j = 1; j <= n; ++j)
if (j != r && j != i && query(r, j, i))
++d;
if (d == h - 1)
return cout << "! " << i, 0;
}
return 0;
}
其他
CF1479A Searching Local Minimum
有一个 \(1 \sim n\) 的排列 \(a_{1 \sim n}\) ,每次可以询问 \(a_i\) 的值( \(i \in [1, n]\) ),规定 \(a_0 = a_{n + 1} = +\infty\) 。
给定 \(n\) ,试用 \(\leq 100\) 次询问找到一个 \(i\) 满足 \(a_i < a_{i - 1} \and a_i < a_{i + 1}\) 。
\(n \leq 10^5\)
先考虑单谷序列的情况,显然可以二分 \(mid\) ,每次询问 \(a_{mid - 1}, a_{mid}, a_{mid + 1}\) 的值然后缩小区间即可得到谷的位置。
考虑一般情况,直觉上不难发现如果直接二分最后一定会收敛到一个谷底,于是可以用 \(3 \log n\) 次询问解决问题。
#include <bits/stdc++.h>
using namespace std;
int n;
inline int query(int x) {
if (!x || x == n + 1)
return n + 1;
cout << "? " << x << endl;
int res;
cin >> res;
return res;
}
signed main() {
cin >> n;
int l = 1, r = n, ans = -1;
while (l <= r) {
int mid = (l + r) >> 1, a = query(mid - 1), b = query(mid), c = query(mid + 1);
if (b < a && b < c) {
ans = mid;
break;
} else if (a > b && b > c)
l = mid + 1;
else
r = mid - 1;
}
cout << "! " << ans << endl;
return 0;
}
CF1407C Chocolate Bunny
有一个 \(1 \sim n\) 的排列 \(a\) ,每次可以询问 \(a_x \bmod a_y\) 。
给定 \(n\) ,求 \(a\) 。
\(n \leq 10^4\) ,询问次数上限 \(2n\) 次
考虑对称操作,不难发现 \(\max(a_x \bmod a_y, a_y \bmod a_x) = \min(a_x, a_y)\) ,这样每次可以用 \(2\) 次询问确定一个位置,那么就可以用 \(2n - 2\) 次询问确定所有位置,最后剩下的位置上就是 \(n\) 。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e4 + 7;
int ans[N];
int n;
inline int query(int x, int y) {
cout << "? " << x << ' ' << y << endl;
int res;
cin >> res;
return res;
}
signed main() {
cin >> n;
int p = 1;
for (int i = 2; i <= n; ++i) {
int res1 = query(i, p), res2 = query(p, i);
if (res1 > res2)
ans[i] = res1;
else
ans[p] = res2, p = i;
}
ans[p] = n;
cout << "! ";
for (int i = 1; i <= n; ++i)
cout << ans[i] << ' ';
cout << endl;
return 0;
}
P8079 [WC2022] 猜词
交互库会在词库( \(n = 8869\) 个词,长度为 \(5\) ,在交互开始前给出)中等概率随机选一个词,并给出其的首字母。
每次可以从词库里面选一个词告诉交互库,如果猜错了,交互库返回:
- 哪些字母的位置是正确的
- 哪些字母在待猜单词中出现了但位置是错误的。
需要用 \(\leq 5\) 次询问猜出这个词,用 \(1 \sim 5\) 次猜测猜对的得分分别为 \(150, 120, 100, 90, 85\) 。
一共玩 \(T = 1000\) 次,总得分为每次得分的平均值。
先考虑暴力,每次从候选词库中随机输出,然后排除掉非法的单词。
考虑优化,尝试对每个单词设计一个估价函数,每次选一个估价函数最大的单次输出。
猜测一个词后,可以将剩下的单次分为若干类,记第 \(i\) 类的占比为 \(p_i\) ,则该词划分后的信息熵为 \(E(s) = - \sum p_i \log_2 p_y\) ,其中 \(-\log_2 p_i\) 表示增加的信息量,\(p_i\) 表示增加这么多信息量的概率。
但是这样还是不够优秀,当 \(E\) 相近时,不妨优先询问可能合法的单词,以减少询问次数。
综上,定义估价函数:
取 \(\epsilon = 0.01\) 可以获得比较好的效果。
但是直接这么做会超时,原因是计算一个单词的价值是 \(O(n^2)\) 的,考虑对每个首字母预处理价值最高的单词,这样就可以减少运行时间。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
const int N = 8869, S = 26;
const string fir[S] = {"slier", "lares", "lares", "tores", "tarns", "arles", "lares", "lares",
"snare", "ousel", "ranis", "nares", "tares", "aides", "tries", "lares", "raise",
"aides", "plate", "nares", "snare", "riles", "nares", "cones", "kanes", "aeons"};
set<string> dictionary[S], st;
string str[N], lst;
void init(int num_scramble, const char *scramble) {
for (int i = 0; i < N; ++i)
str[i] = string(scramble + i * 5, scramble + (i + 1) * 5), dictionary[str[i][0] - 'a'].emplace(str[i]);
}
string check(string a, string b) {
string chk = "-----";
for (int i = 0; i < 5; ++i)
if (a[i] == b[i])
chk[i] = 'g';
for (int i = 0; i < 5; ++i)
if (chk[i] != 'g')
for (int j = 0; j < 5; ++j)
if (chk[j] != 'g' && a[i] == b[j]) {
chk[i] = 's';
break;
}
return chk;
}
const char *guess(int num_testcase, int remaining_guesses, char initial_letter, bool *gold, bool *silver) {
if (remaining_guesses == 5)
st = dictionary[initial_letter - 'a'];
else {
string chk = "-----";
for (int i = 0; i < 5; ++i) {
if (gold[i])
chk[i] = 'g';
else if (silver[i])
chk[i] = 's';
}
set<string> now;
for (string it : st)
if (check(lst, it) == chk)
now.emplace(it);
st = now;
}
pair<double, string> res = make_pair(-1e9, "");
if (st.size() > 1 && remaining_guesses < 5) {
for (int i = 0; i < N; ++i) {
map<string, int> mp;
for (string it : st)
++mp[check(str[i], it)];
double value = (st.find(str[i]) == st.end() ? 0 : 0.01);
for (auto it : mp) {
double p = (double)it.second / st.size();
value -= p * log2(p);
}
res = max(res, make_pair(value, str[i]));
}
} else if (remaining_guesses == 5)
res.second = fir[initial_letter - 'a'];
else
res.second = *st.begin();
return (lst = res.second).c_str();
}
P12541 [APIO2025] Hack!
交互库有一个整数 \(n\) ,试用若干次询问求出 \(n\) 。
每次询问可以给出一个 \(\subseteq [1, 10^{18}] \cap \mathbb{Z}\) 的集合,交互库会返回 \(\bmod n\) 相等的数字对数。
\(1 \leq n \leq 10^9\) ,约束为询问总集合大小 \(\leq 1.1 \times 10^5\)
首先可以发现,若 \(\{ 1, n + 1 \}\) 返回 \(1\) ,则对于 \(n \mid m\) ,\(\{ 1, m + 1 \}\) 也返回 \(1\) 。因此可以尝试找到一个 \(n\) 的倍数,然后枚举因数判定。
考虑二分求解,问题转化为判定 \([l, r]\) 区间内是否存在 \(n\) 的倍数。
考虑 BSGS,构造集合 \(S = \{ 1, 2, \cdots, B, l + B, l + 2B, \cdots, l + kB, r + 1 \}\) ,其中 \(B = \lfloor \sqrt{r - l + 1} \rfloor, k = \lfloor \frac{r - l}{B} \rfloor\) ,则只要查询该集合即可判定。
对于 \(a, b \in S\) ,记 \(d = |a - b|\) ,不难发现 \(d\) 的取值覆盖了 \([l, r] \cup [1, B]\) 内的所有整数。而若 \([1, B]\) 存在 \(n\) 的倍数,显然 \([l, r]\) 中也存在 \(n\) 的倍数。
这样一次二分就需要 \(2 \sqrt{r - l + 1} + 1\) 的代价,\(n \leq 10^9\) 时算得代价 \(\leq 152695\) ,无法通过。
注意到 \([5 \times 10^8, 10^9]\) 中一定存在 \(n\) 的倍数,初始二分区间设为该区间即可做到 \(\leq 107973\) 的代价。
下面考虑枚举因数判定,由于 \(n \leq 10^9\) 时 \(d(n) \leq 1344\) ,因此直接枚举因数无法通过。考虑每次试着除去一个质因子判定合法性,这样就只需要 \(\leq 2 \log n\) 的代价,可以通过。
#include <bits/stdc++.h>
typedef long long ll;
using namespace std;
ll collisions(vector<ll> x);
inline bool check(int l, int r) {
int b = sqrt(r - l + 1), k = (r - l) / b;
vector<ll> vec = {r + 1};
for (int i = 1; i <= b; ++i)
vec.emplace_back(i);
for (int i = 1; i <= k; ++i)
vec.emplace_back(l + i * b);
return collisions(vec);
}
int hack() {
int l = 5e8, r = 1e9;
while (l < r) {
int mid = (l + r) >> 1;
if (check(l, mid))
r = mid;
else
l = mid + 1;
}
vector<pair<int, int> > factor;
for (int i = 2; i * i <= l; ++i)
if (!(l % i)) {
int cnt = 0;
while (!(l % i))
++cnt, l /= i;
factor.emplace_back(i, cnt);
}
if (l)
factor.emplace_back(l, 1);
for (auto &it : factor)
for (; it.second && collisions({1, r / it.first + 1}); r /= it.first, --it.second);
return r;
}
最棒题目人人赞叹你
二维平面上有 \(n\) 条直线 \(l_{1 \sim n}\),\(l_i\) 的解析式为 \(y = k_i x + b_i\) ,保证 \(k_i\) 两两不同、均不为 \(0\)、有正有负。
对于函数 \(f(x) = \max_{i = 1}^n (k_i x + b)\) ,求使其取到最小值的自变量 \(x_0\) ,可以证明 \(x_0\) 存在且唯一。
一开始只知道 \(n\) 的大小,只能通过以下三种查询获取直线的信息:
- 查询一(
int sign(int i)
):给出 \(i\) ,查询 \(k_i\) 的符号。- 查询二(
pair<int, int> intersection(int i, int j)
):给出 \(i, j\),返回 \(l_i, l_j\) 的交点的 \(x\) 坐标的编号(而非实际值),以及 \(k_i, k_j\) 的大小关系。- 查询三(
int cmp(int x, int y)
):给出 \(x, y\) ,查询编号为 \(x\) 的值和编号为 \(y\) 的值的大小关系。\(n \leq 5 \times 10^5\) ,查询一使用上限 \(20\) 次,查询二使用上限 \(3n\) 次
先用 \(n - 1\) 次比较求出斜率最小的直线 \(l_x\) ,然后将剩下 \(n - 1\) 条直线按与其交点的 \(x\) 坐标升序排序。求出这些交点的凸包,注意对于交点相同的直线保留斜率更大者,然后在凸包上二分求出最低点即可。
一共需要 \(\leq \log n\) 次操作一, \(\leq 3n\) 次操作二,时间复杂度 \(O(n \log n)\) 。
#include <bits/stdc++.h>
#include "line.h"
typedef long long ll;
using namespace std;
const int N = 5e5 + 7;
int sta[N];
int solve(int n) {
unordered_map<ll, pair<int, int> > mp;
auto query = [&](int x, int y) {
ll id = 1ll * x * n + y;
if (mp.find(id) == mp.end())
return mp[id] = intersection(x, y);
else
return mp[id];
};
int mnp = 1;
for (int i = 2; i <= n; ++i)
if (query(i, mnp).second == -1)
mnp = i;
vector<pair<int, int> > vec;
for (int i = 1; i <= n; ++i)
if (i != mnp)
vec.emplace_back(query(i, mnp).first, i);
sort(vec.begin(), vec.end(), [](const pair<int, int> &a, const pair<int, int> &b) {
return cmp(a.first, b.first) == -1;
});
int top = 0;
sta[++top] = mnp;
for (auto it : vec) {
int x = it.second;
if (top >= 2 && query(sta[top], x).second == 1)
continue;
while (top >= 2 && cmp(query(sta[top - 1], sta[top]).first, query(sta[top], x).first) == 1)
--top;
sta[++top] = x;
}
int l = 1, r = top, ans = 0;
while (l <= r) {
int mid = (l + r) >> 1;
if (sign(sta[mid]) == -1)
ans = mid, l = mid + 1;
else
r = mid - 1;
}
return query(sta[ans], sta[ans + 1]).first;
}
P7109 滴水不漏
交互库有 \(n\) 个水杯,第 \(i\) 个水杯容积为 \(i\) ,初始有 \(a_i \in [0, i]\) 的水。给出 \(n\) ,试用 \(\leq 20000\) 次操作求出每个水杯的初始水量。
一次操作可以指定 \(1 \leq i, j \leq n\) :
- 若 \(i \neq j\) ,则会将第 \(i\) 个水杯里的水倒向第 \(j\) 个水杯,直至第 \(i\) 个水杯里的水倒完或第 \(j\) 个水杯已满,交互库会返回第 \(j\) 个水杯是否已满,注意倒水的影响将保留。
- 若 \(i = j\) ,则会返回第 \(i\) 个水杯是否已满。
\(n \leq 1000\) ,交互库非自适应
考虑增量法,在已知 \(1 \sim x - 1\) 水量的情况下求 \(x\) 的水量。
考虑不断将 \(x\) 的水往前到,将 \(1 \sim x - 1\) 倒成形如:
- \(1 \sim p - 1\) 都是满的。
- \(p\) 不满。
- \(p + 1 \sim x - 1\) 都是空的。
的形式,问题转化为求 \(p\) 的水量(特判 \(1 \sim x\) 都是满的情况)。
找到倒到 \(p\) 中恰好倒空且 \(p\) 恰好满的位置即可,不难发现这可以用二分查找解决。
操作次数约为 \(2n \log n\) ,但是二分常数很小,可以通过。
#include <bits/stdc++.h>
using namespace std;
const int N = 1e3 + 7;
int a[N];
int n;
inline bool query(int x, int y) {
cout << "? " << x << ' ' << y << endl;
int res;
cin >> res;
return res;
}
signed main() {
cin >> n;
int sum = a[1] = query(1, 1);
for (int i = 2, p = 1; i <= n; ++i) {
while (p < i && query(i, p))
++p;
if (p == i && query(i, i)) {
sum += (a[i] = i);
continue;
}
int l = 1, r = p - 1, pos = p;
while (l <= r) {
int mid = (l + r) >> 1;
if (query(mid, p))
pos = mid, r = mid - 1;
else
l = mid + 1;
query(p, mid);
}
sum += (a[i] = p * (p + 1) / 2 - sum - pos);
}
cout << "! ";
for (int i = 1; i <= n; ++i)
cout << a[i] << ' ';
return 0;
}