[题解] CF GYM 106554 E. 交互题 / 二进制编号 / 构造 / 不变量
[题解] CF GYM 106554 E. 交互题 / 二进制编号 / 构造 / 不变量
TAG: 交互题,二进制编号,构造,不变量,CF GYM,南昌邀请赛
题目链接: 点击此处打开题目
题目大意
有一个未知的长度为 \( n \) 的 01 序列 \( a_1,a_2,\ldots,a_n \),保证至少存在一个 1。每次选择位置集合 \( I \);若集合内有 \( x \) 个 1,整个序列共有 \( S \) 个 1,交互器返回:
最多询问 \( 17 \) 次,需要求出 \( S \)。由于 \( n\le 105<2 \),询问次数提示我们从位置编号的二进制表示入手。
核心思路
1. 突破口:构造出恰好含有一个 1 的集合
设一次询问集合中有 \( x \) 个 1,返回值为:
最关键的两个性质是:
- 当 \( q>0 \) 时,说明询问集合内、外都至少存在一个
1; - 当询问集合中恰好有一个
1时,
因此答案就是 \( q+1 \)。
问题转化为:在不超过 \( 17 \) 次询问中,构造出一个恰好含有一个 1 的集合,并保留它对应的询问结果。
直接二分位置并不可行。因为返回 0 时可能有两种含义:
无法直接判断当前询问集合中是没有 1,还是包含了所有 1。
2. 用位置的二进制编号区分所有下标
令位置 \( i \) 的二进制编号为 \( i-1 \),设:
从高位到低位处理这些编号。不同位置的完整二进制编号互不相同,这是最终将候选集合缩小到唯一 1 的基础。
3. 第一阶段:寻找第一次正回答
对于当前二进制位 \( b \),询问所有满足以下条件的位置:
如果回答为 0,说明所有值为 1 的位置在这一位上的取值完全相同:要么全为 0,要么全为 1。具体是哪一种并不重要,继续处理下一位即可。
如果回答大于 0,说明当前集合内外都存在 1。将当前集合记为 cur,并保存回答 val。从此得到重要不变量:
cur中至少有一个1,并且cur外至少有一个1。
4. 第二阶段:在候选集合中继续按位缩小
将 cur 按当前位分成两个集合:
c0:这一位为0;c1:这一位为1。
询问 c1。
如果回答大于 0,说明 c1 内外都存在 1,于是令:
cur = c1
val = 本次回答
如果回答为 0,旧 cur 外已经存在一个 1,所以不可能是“所有 1 都在 c1”这种情况,只能说明 c1 中没有 1。于是令:
cur = c0
此时从 cur 中删去的全部是 0,cur 中 1 的数量没有变化,因此 val 也不需要修改。
如果 c0 或 c1 为空,说明当前候选集合中的所有位置在这一位上本来就相同,不需要询问,直接保留非空的一侧。
5. 为什么最后恰好剩下一个 1
第一次正回答之前处理过的高位,其询问结果都是 0,所以全体 1 在这些位上的取值相同。
第一次正回答及之后处理的每一位,我们都只保留某一个确定的二进制取值,所以 cur 中的 1 在这些位上的取值也相同。
因此,最终 cur 中所有值为 1 的位置具有完全相同的二进制编号。每个位置的编号互不相同,所以 cur 中至多有一个 1;算法又始终保证 cur 中至少存在一个 1,故最终恰好有一个 1。
设最终保存的回答为 val,则:
所以:
6. 全部询问均为 0 的情况
如果所有二进制位的询问结果都为 0,说明所有值为 1 的位置在每个二进制位上的取值都相同。
这些位置的完整编号也就相同。由于位置编号唯一,序列中只能存在一个 1,答案为 1。
正确性说明
算法维护如下不变量:进入第二阶段后,cur 内至少有一个 1,并且 cur 外至少有一个 1。
- 第一次得到正回答时,询问值为 \( x(S-x)>0 \),集合内外都有
1,不变量成立; - 若询问
c1得到正回答,则c1内外都有1,选择c1后不变量继续成立; - 若询问
c1得到0,旧cur外已经有1,因此c1不可能包含全部1,只能是c1中没有1。选择c0后,旧cur中的全部1都被保留,不变量继续成立。
处理完所有位后,cur 中的 1 具有相同的完整二进制编号,因此只能有一个。最终询问值为 \( S-1 \),输出 val+1 正确。
复杂度
设 \( m=\lceil\log_2 n\rceil \)。
- 询问次数:\( m\le 17 \)。每个二进制位至多询问一次。
- 时间复杂度:\( O(n\log n) \)。每一位最多扫描当前候选位置或全部位置一次。
- 空间复杂度:\( O(n) \)。主要存储询问集合和候选集合。
代码前说明
关键变量
m:能够区分所有位置所需的二进制位数;found:是否已经获得第一次正回答;cur:当前保证含有1,且集合外也含有1的候选集合;val:与当前cur中1的数量对应的最近一次有效回答;c0,c1:按照当前二进制位将cur划分出的两个集合。
核心公式与转移
若询问集合内有 \( x \) 个 1,回答为:
得到正回答后,始终维护 cur 内外都有 1。询问 c1 时:
- 若结果大于
0,令cur=c1并更新val; - 若结果等于
0,由不变量可知c1中没有1,令cur=c0,val不变。
最终 cur 中恰好有一个 1,因此:
易错点
- 询问格式必须在同一行输出
? k i1 i2 ... ik; - 由于代码中将
endl定义为'\n',它不会自动刷新缓冲区,每次询问和最终回答后必须显式调用cout.flush(); - 第一阶段与第二阶段应由
found区分,不能把第二阶段写进res==0的分支; - 最终应当在
found==false时输出1,否则输出val+1; - 不要询问空集合。当
c0或c1为空时直接保留非空集合; cur.swap(c0)和cur.swap(c1)只是交换vector的内部数据,通常为常数复杂度,比逐元素复制更合适。
完整代码
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl '\n'
int n;
int ask(vector<int> &v){
cout<<"? "<<v.size();
for(int x:v) cout<<" "<<x;
cout<<endl;
cout.flush();
int res;
cin>>res;
if(res==-1) exit(0);
return res;
}
void answer(int s){
cout<<"! "<<s<<endl;
cout.flush();
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n;
//找到满足 2^m>=n 的最小 m
int m=0;
while((1ll<<m)<n) m++;
bool found=false;//是否已经找到一次正回答
int val=0;
vector<int> cur;//当前保证含有 1,且集合外也含有 1 的候选集合
for(int b=m-1;b>=0;b--){
if(!found){//还没有正回答时,询问所有第 b 位为 1 的位置
vector<int> q;
for(int i=1;i<=n;i++){
if(((i-1)>>b)&1) q.push_back(i);
}
if(q.empty()) continue;
int res=ask(q);
if(res>0){//q 内外都至少存在一个 1
found=true;
cur=q;
val=res;
}
}else{
//将当前候选集合按照第 b 位划分
vector<int> c0,c1;
for(int x:cur){
if(((x-1)>>b)&1) c1.push_back(x);
else c0.push_back(x);
}
//这一位在当前候选集合中全部为 0
if(c1.empty()){
cur.swap(c0);
continue;
}
//这一位在当前候选集合中全部为 1
if(c0.empty()){
cur.swap(c1);
continue;
}
int res=ask(c1);
if(res>0){
cur.swap(c1);
val=res;
}
/*
旧 cur 外已经存在一个 1,
所以 res=0 只能说明 c1 内没有 1,
所有原本在 cur 内的 1 都位于 c0。
*/
else cur.swap(c0);
}
}
if(!found) answer(1);
else answer(val+1);
return 0;
}

浙公网安备 33010602011771号