交互杂题选做

交互杂题选做

递归子问题

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\) 的子集和。具体地,解如下方程即可:

\[\begin{cases} (\sum_{i \in S} [s_i = 0]) + (\sum_{i \in S} [s_i = 1]) = |S| \\ (\sum_{i \in S} [s_i = 0]) - (\sum_{i \in S} [s_i = 1]) = (\sum_{i \in U} [s_i = 0]) - (\sum_{i \in S} [s_i = 1] + \sum_{i \notin S} [s_i = 0]) \end{cases} \]

\(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\) 的最小值,则:

\[f_{t, n} = \min_{k = 1}^n \{ f_{t - 1, k} + \mathrm{cost}(n, k) \} \]

其中 \(\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)\) 。那么不妨分析一下错误率,即:

\[\left( \frac{n - d(d + 1)}{n} \right)^{1000 - 2d} \]

\(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\) 相近时,不妨优先询问可能合法的单词,以减少询问次数。

综上,定义估价函数:

\[E(x) = [x是否可能为答案] \times \epsilon - \sum_{y \in 询问该单词后的所有结果} p(y) \log p(y) \]

\(\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;
}
posted @ 2025-05-23 21:47  wshcl  阅读(36)  评论(0)    收藏  举报