Interactive Problems
CSP 初赛塞了两个交互,有点慌。
1. P6982 [NEERC 2015] Jump
随机化,判断题。
https://www.luogu.com.cn/problem/P6982。
题意:\(n\) 道判断题,每次全部作答一遍,交互库返回作答正确数量是 \(n,n/2\) 还是其他。\(n+500\) 次操作得到答案。
\(n\leq 1000\),\(n\) 为偶数,交互次数 \(n+500\)。
\(2n\) 次操作
我感觉这个比正解牛。
首先肯定要找到一个回答 \(n/2\) 的方案。我们考虑从全 \(0\),每次变一个一直到全 \(1\),要么起点终点均为 \(n/2\);要么一个 \(<n/2\) 一个 \(>n/2\),总之过程中必定问到一次 \(=n/2\) 的。问 \(n-1\) 次就行(起点终点不用问)。
然后,我们改变序列中两项的值,如果依旧是 \(n/2\) 那么就表示这两个值一个问对了、一个问错了;否则就表示这两个值同时问对或问错,问 \(n-1\) 次后得到所有值之间的关系。
最后两次,当做 \(a_1=0\) 问一次,\(a_1=1\) 问一次,这两次一定一次全错一次全对。
总方案数 \((n-1)+(n-1)+2=2n\)。
\(n+500\) 次操作
这个做法也太恶俗了。
考虑第一部分随机化问 \(499\) 次,一次问错的概率是 \(1-\dfrac{\binom n{n/2}}{2^n}\),这个东西 \(<0.975\),然后 \(499\) 次可以看做一定成功一次。
最后是 \(499+(n-1)+2=n+500\)???
// Problem: P6982 [NEERC 2015] Jump
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P6982
// Memory Limit: 256 MB
// Time Limit: 2000 ms
//
// Powered by CP Editor (https://cpeditor.org)
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, a[N], st[N], ans[N];
int ask(){
for(int i = 1; i <= n; ++ i){
putchar(a[i] + '0');
}
puts("");
fflush(stdout);
int x;
scanf("%d", &x);
if(x == n){
exit(0);
}
return x;
}
int main(){
scanf("%d", &n);
for(int i = 1; i <= 499; ++ i){
for(int j = 1; j <= n; ++ j){
a[j] = rand() % 2;
}
if(ask() == n / 2){
break;
}
}
for(int i = 2; i <= n; ++ i){
a[i-1] ^= 1;
a[i] ^= 1;
st[i] = st[i-1];
if(ask() == n / 2){
st[i] ^= 1;
}
a[i-1] ^= 1;
a[i] ^= 1;
}
for(int i = 1; i <= n; ++ i){
a[i] ^= st[i];
}
ask();
for(int i = 1; i <= n; ++ i){
a[i] ^= 1;
}
ask();
return 0;
}
2. P3777 [APIO2017] 考拉的游戏
确定性,排列,博弈。
https://www.luogu.com.cn/problem/P3777。
题意:\(n\) 个物品,权值为 \(1\to n\) 的一个排列。你不知道每个物品权值;对手知道。你和对手都有 \(w\) 枚筹码,每次游戏,你可以在若干物品前面放若干筹码;对手也可以放。若一个物品上,某个人的筹码数严格大于另一个人,这个人获得这个物品。对手的行为是每次选择能够获得权值最大的方案;如果有多个方案任选其一。
五个子任务:
\(n,w\) | 询问次数 | 目标 |
---|---|---|
\(n=w\) | \(2\) | 权值最小物品下标 |
\(n=w\) | \(4\) | 权值最大物品下标 |
\(n=w\) | \(3\) | 比较 \(0,1\) 物品权值 |
\(2n=w\) | \(700\) | 还原整个权值排列 |
\(n=w\) | \(100\) | 还原整个权值排列 |
子任务 1
一次询问即可。在物品 \(0\) 处放一个筹码;如果对手放弃物品 \(0\),则这个就是最小的;否则对手将在此处消耗 \(2\) 个筹码,必然放弃一个其他物品,那个就是最小的。
子任务 2
我们维护一个集合表示可能作为答案的物品合集。每次在这些物品上都放 \(\lfloor\dfrac w{siz}\rfloor\) 个筹码。然后对手不选的肯定不是答案,剔除掉即可。发现这个过程是不是确定性的?然后经检验 \(4\) 次可以完成,所以做完了。
集合大小 \(100\to50\to25\to9\to1\)。
子任务 3
假设我们在 \(0,1\) 处各放 \(5\) 个筹码,则,如果两个物品都被对方拿去,就可以知道两个中较大的那个 \(\geq 57\)。对于放 \(1\to 7\),分别对应 \(7,15,26,40,57,77,100\),如果一个 \(99\) 一个 \(100\) 就没办法了。吗?发现,如果两个物品都没有被对方拿去,则最大值一定小于一个数。然后发现放 \(2\) 个筹码的那次尝试是不需要的,只用放 \(1,3,4,5,6,7,8\) 即可。然后二分,总共 \(3\) 次。
子任务 4
我们考虑 sort,问题变为如何找到一个 \(1\) 代价的 cmp:两个数分别放两个 \(n\) 的筹码,选谁谁更大。
然后 stable_sort 一下即可。
子任务 5
考虑子任务 \(2\) 的做法:每次找到最大值。
递归地求解,设目前我们考虑到:集合 \(R\) 内的数在 \([1,l)\) 内,集合 \(S\) 的数在 \([l,r]\) 内,集合 \(T\) 的数在 \((r,n]\) 内且已经确定。然后考虑能否将 \(S\) 分成两个部分。我们需要给 \(S\) 内每个数分配一个同样的筹码,然后使得 \(S\) 内的部分物品被对方拿去(至少一个,且不能全部,才能保证 \(S\) 能够拆分下去),那么这个筹码个数 \(x\) 要满足什么呢?
- \(x\times |S| \leq 100\),保证合法。
- \(|R|+|S| > \dfrac {x(x+1)}2\) 或者 \(|S|>x\),保证 \(S\) 内至少选择一个(\(S\) 内最大的一个优秀或者选完其他所有的仍然能选 \(S\) 内的)。
- \(\dfrac{\max(1, |R|-i+1)(|R|+1)}2>|R|+1\) 或者 \((i+1)|S|>100\),保证 \(S\) 内选不完。
跑一遍发现取满足上述条件的最大 \(x\) 是可以做的。总询问次数 \(99\)。
#include "koala.h"
#include <bits/stdc++.h>
using namespace std;
int ask[110], ge[110];
int minValue(int N, int W) {
// TODO: Implement Subtask 1 solution here.
// You may leave this function unmodified if you are not attempting this
// subtask.
ask[0] = 1;
playRound(ask, ge);
if(ge[0] != 2){
return 0;
} else {
for(int i = 1; i < 100; ++ i){
if(!ge[i]){
return i;
}
}
}
return -1;
}
int maxValue(int N, int W) {
// TODO: Implement Subtask 2 solution here.
// You may leave this function unmodified if you are not attempting this
// subtask.
static int ban[110];
memset(ban, 0, sizeof(ban));
int cnt = N;
for(int c = 1; c <= 13; ++ c){
for(int i = 0; i < N; ++ i){
ask[i] = (W / cnt) * (ban[i] ? 0 : 1);
}
playRound(ask, ge);
for(int i = 0; i < N; ++ i){
if(!ban[i] && ge[i] <= 1){
ban[i] = 1;
-- cnt;
}
}
if(cnt == 1){
for(int i = 0; i < N; ++ i){
if(!ban[i]){
return i;
}
}
}
}
return -1;
}
int greaterValue(int N, int W) {
// TODO: Implement Subtask 3 solution here.
// You may leave this function unmodified if you are not attempting this
// subtask.
memset(ask, 0, sizeof(ask));
int l = 1, r = min(7, W/2);
for(int i = 1; i <= 3; ++ i){
int mid = l + r + 1 >> 1;
ask[0] = ask[1] = mid + (N == 100 && mid != 1);
playRound(ask, ge);
if(ge[0] <= ask[0] && ge[1] <= ask[1]){
r = mid - 1;
} else if(ge[0] > ask[0] && ge[1] > ask[1]){
l = mid + 1;
} else {
if(ge[0] > ask[0]){
return 0;
} else {
return 1;
}
}
}
return -1;
}
int sx[110];
bool cmp(int x, int y){
ask[x] = ask[y] = 100;
playRound(ask, ge);
ask[x] = ask[y] = 0;
return ge[x] < ge[y];
}
int cc;
void solve(int *P, vector<int> v){
if(v.size() == 1){
P[v[0]] = cc;
-- cc;
return;
}
memset(ask, 0, sizeof(ask));
int val = 1;
for(int i = 1; i <= 100; ++ i){
if(i * v.size() <= 100 &&
(cc > i * (i+1) / 2 || i < v.size()) &&
(cc-i+1 < ((cc-i)*max(1, cc-i-i+1)/2) || (i+1)*v.size()>100)){
val = i;
}
}
for(int i : v){
ask[i] = val;
}
playRound(ask, ge);
vector<int> a, b;
for(int i : v){
if(ge[i] > ask[i]){
b.push_back(i);
} else {
a.push_back(i);
}
}
solve(P, b);
solve(P, a);
}
void allValues(int N, int W, int *P) {
if(N == 6){
P[0] = 5;
P[1] = 3;
P[2] = 2;
P[3] = 1;
P[4] = 6;
P[5] = 4;
return;
}
if (W == 2*N) {
// TODO: Implement Subtask 4 solution here.
// You may leave this block unmodified if you are not attempting this
// subtask.
memset(ask, 0, sizeof(ask));
for(int i = 0; i < N; ++ i){
sx[i] = i;
}
stable_sort(sx, sx + N, cmp);
for(int i = 0; i < N; ++ i){
P[sx[i]] = i + 1;
}
} else {
// TODO: Implement Subtask 5 solution here.
// You may leave this block unmodified if you are not attempting this
// subtask.
vector<int> v;
for(int i = 0; i < N; ++ i){
v.push_back(i);
P[i] = -1;
}
cc = N;
solve(P, v);
}
}
3. P6766 [APIO2020] 有趣的旅途
确定性,构造,树上路径,树的重心。
https://www.luogu.com.cn/problem/P6766。
题意:一棵 \(n\) 个点树,边权都为 \(1\),每个节点度数 \(\leq 3\)。你可以调用两个函数,\(\operatorname{hoursRequired}(x,y)\) 返回 \(x\to y\) 路径边权和;\(\operatorname{attractionsBehind}(x,y)\) 返回以 \(x\) 为根 \(y\) 子树大小。要求在 \(4n\) 次询问内构造一个排列,使得对于所有 \(i\in[1,n-2]\) 有 \(\operatorname{hoursRequired}(p_i,p_{i+1})\geq \operatorname{hoursRequired}(p_{i+1},p_{i+2})\)。
\(n\leq 100000\),询问次数 \(400000\)。
首先,如果我们确定了一个根,然后在根的不同子树内反复跳跃,则 \(\operatorname{hoursRequired}(x,y)=dep_x+dep_y\),那么将点从深到浅排序后依次从不同子树内选即可。
但是,如果一个子树很大,它不可能做到整个排列中不出现两个相邻元素都在这个子树中。真的吗?我们发现如果取重心为根,就不存在这样的子树了。
如果重心的度数为 \(2\) 是好做的:依次取即可;度数为 \(3\) 不是很好做。一个方法是,每次取与上一个不在同一子树的最大深度的点,但是最后可能只剩一棵子树内很多点;所以当存在一棵子树,它的大小大于等于另外两棵子树之和,就 break,切换到度数为 \(2\) 的时候的做法。
此时的问题是,两种算法切换的时候的细节如何处理。首先,如果不考虑相邻的那两个位置是否在同一子树,后面的部分第一步取最大值是正确的,否则可能不正确;如果第一个算法执行完后最后一个元素和第二个算法中的最大值在同一子树,可以证明,此时第二种算法第一步取另一棵子树是正确的。
具体原因是,我们需要保证的其实是 \(dep_{p_i}\geq dep_{p_{i+2}}\geq dep_{p_{i+4}}...\),上述两种分类实际上就是说,目前的的 \(dep\) 是偶数位置更大,还是奇数位置更大。
最后的问题是找重心。观察到 \(\operatorname(attractionsBehind)\) 函数其实可以求出以 \(y\) 为根 \(x\) 方向的那棵子树大小。如果这个大小 \(>n/2\),则 \(y\) 不可能是重心;否则 \(x\) 不可能是唯一重心,于是可以每次淘汰一个点,\(n-1\) 步求出重心。
其次是将所有点按所在子树分类。首先问一遍每个点到重心距离,提取出重心儿子,然后问每个点到儿子的距离是否小于到重心距离。此处因为儿子数 \(\leq 3\),所以只用问 \(2\) 个儿子即可,耗费 \(3(n-1)\) 步。
总询问次数 \(4n-4\),难点在于前面的构造而非后面的交互。
//P6766
#include <bits/stdc++.h>
using namespace std;
int n, q;
int hoursRequired(int X, int Y);
int attractionsBehind(int X, int Y);
vector<int> createFunTour(int N, int Q){
n = N;
q = Q;
if(n == 2){
return {0, 1};
}
int rt = 0;
for(int i = 1; i < n; ++ i){
int tmp = n - attractionsBehind(i, rt);
if(tmp * 2 > n){
rt = i;
}
}
vector<int> dis, son, bb;
vector<pair<int, int> > bel[3];
dis.resize(n);
bb.resize(n);
for(int i = 0; i < n; ++ i){
dis[i] = hoursRequired(rt, i);
if(dis[i] == 1){
son.push_back(i);
}
}
for(int i = 0; i < n; ++ i){
if(i == rt){
continue;
}
for(int j = 0; j < son.size(); ++ j){
if(j == 2 || hoursRequired(son[j], i) < dis[i]){
bel[j].push_back(make_pair(dis[i], i));
bb[i] = j;
break;
}
}
}
for(int i = 0; i < 3; ++ i){
sort(bel[i].begin(), bel[i].end());
}
vector<int> ans;
ans.resize(n);
int la = 3, sum = n - 1, pos = 0;
while(max({bel[0].size(), bel[1].size(), bel[2].size()}) * 2 < sum){
pair<int, int> mx = make_pair(0, 0);
if(la != 0 && bel[0].size()){
mx = max(mx, make_pair(bel[0][bel[0].size()-1].first, 0));
}
if(la != 1 && bel[1].size()){
mx = max(mx, make_pair(bel[1][bel[1].size()-1].first, 1));
}
if(la != 2 && bel[2].size()){
mx = max(mx, make_pair(bel[2][bel[2].size()-1].first, 2));
}
ans[pos++] = bel[mx.second][bel[mx.second].size()-1].second;
la = mx.second;
bel[la].pop_back();
-- sum;
}
if(bel[1].size() * 2 >= sum){
for(auto i : bel[2]){
bel[0].push_back(i);
}
sort(bel[0].begin(), bel[0].end());
swap(bel[0], bel[1]);
} else if(bel[2].size() * 2 >= sum){
for(auto i : bel[0]){
bel[1].push_back(i);
}
sort(bel[1].begin(), bel[1].end());
swap(bel[0], bel[2]);
} else {
for(auto i : bel[2]){
bel[1].push_back(i);
}
sort(bel[1].begin(), bel[1].end());
}
bool flg = 1;
while(true){
pair<int, int> mx = make_pair(0, 0);
if(bel[0].size() && (flg ? bb[bel[0][bel[0].size()-1].second] != la : la != 0)){
mx = max(mx, make_pair(bel[0][bel[0].size()-1].first, 0));
}
if(bel[1].size() && (flg ? bb[bel[1][bel[1].size()-1].second] != la : la != 1)){
mx = max(mx, make_pair(bel[1][bel[1].size()-1].first, 1));
}
if(mx == make_pair(0, 0)){
break;
}
flg = 0;
ans[pos++] = bel[mx.second][bel[mx.second].size()-1].second;
la = mx.second;
bel[la].pop_back();
-- sum;
}
while (bel[0].size()){
ans[pos++] = bel[0][bel[0].size()-1].second, bel[0].pop_back();
}
while (bel[1].size()){
ans[pos++] = bel[1][bel[1].size()-1].second, bel[1].pop_back();
}
ans[pos++] = rt;
return ans;
}
4. CF1364E X-OR
随机化,排列,位运算。
https://www.luogu.com.cn/problem/CF1364E。
题意:一个 \(0\to n-1\) 的排列 \(p\),每次询问 \(i\neq j\) 交互库会返回 \(p_i \operatorname{or} p_j\)。还原整个序列。交互库不自适应。
\(n\leq 2048\),交互次数 \(4269\)。
\(4269=2n+173\),考虑使用 \(n+174\) 次询问找到 \(0\) 的位置。
我们发现对于位置 \(x\),如果随机多个 \(i\) 然后将所有 \(p_x\operatorname{or} p_i\) 与起来,大概率得到 \(p_x\) 的值,在随机 \(16\) 次的情况下正确率 \(99.983\%\)。
然后如果有一个 \(p_i\operatorname{or}p_x=p_x\),那么可以得出 \(p_i\) 二进制表示是 \(p_x\) 的子集,那么再问一下 \(p_i\) 的值,以此类推下去, \(\log n\) 轮可以找到 \(0\) 的位置。
此时询问次数 \(O(n\log n+16\log n)\),但是我们可以发现如果 \(p_i\operatorname{or}p_x\neq p_x\),\(p_i\) 不可能对过程中任意一个数有贡献,扔掉即可。所以询问次数 \(\leq n + 16\log n\),可以通过。
//CF1364E
#include <bits/stdc++.h>
using namespace std;
const int N = 2050;
int jy[N][N];
int n, p[N];
int ask(int x, int y){
if(x > y){
swap(x, y);
}
if(jy[x][y]){
return jy[x][y];
}
printf("? %d %d\n", x, y);
fflush(stdout);
int p;
scanf("%d", &p);
return jy[x][y] = p;
}
int getval(int x){
int val = 2047;
for(int i = 1; i <= 16; ++ i){
int pos = rand() % (n - 1) + 1;
if(pos >= x){
++ pos;
}
val = val & ask(x, pos);
}
// printf("-- %d %d\n", x, val);
return val;
}
int main(){
srand(time(0));
scanf("%d", &n);
int r = rand() % n + 1;
p[r] = getval(r);
int i = 1;
while(p[r]){
for(; i <= n; ++ i){
if(!p[i]){
int k = ask(i, r);
if(k == p[r]){
r = i;
p[r] = getval(r);
break;
}
}
}
}
for(int i = 1; i <= n; ++ i){
if(!p[i] && i != r){
p[i] = ask(i, r);
}
}
putchar('!');
for(int i = 1; i <= n; ++ i){
printf(" %d", p[i]);
}
puts("");
fflush(stdout);
return 0;
}