[题解] 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,交互器返回:

\[x(S-x) \]

最多询问 \( 17 \) 次,需要求出 \( S \)。由于 \( n\le 105<2 \),询问次数提示我们从位置编号的二进制表示入手。

核心思路

1. 突破口:构造出恰好含有一个 1 的集合

设一次询问集合中有 \( x \) 个 1,返回值为:

\[q=x(S-x) \]

最关键的两个性质是:

  1. 当 \( q>0 \) 时,说明询问集合内、外都至少存在一个 1
  2. 当询问集合中恰好有一个 1 时,

\[q=1\cdot(S-1)=S-1 \]

因此答案就是 \( q+1 \)。

问题转化为:在不超过 \( 17 \) 次询问中,构造出一个恰好含有一个 1 的集合,并保留它对应的询问结果。

直接二分位置并不可行。因为返回 0 时可能有两种含义:

\[x=0\qquad\text{或}\qquad x=S \]

无法直接判断当前询问集合中是没有 1,还是包含了所有 1

2. 用位置的二进制编号区分所有下标

令位置 \( i \) 的二进制编号为 \( i-1 \),设:

\[m=\lceil\log_2 n\rceil\le 17 \]

从高位到低位处理这些编号。不同位置的完整二进制编号互不相同,这是最终将候选集合缩小到唯一 1 的基础。

3. 第一阶段:寻找第一次正回答

对于当前二进制位 \( b \),询问所有满足以下条件的位置:

\[(i-1)\text{ 的第 }b\text{ 位为 }1 \]

如果回答为 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 中删去的全部是 0cur1 的数量没有变化,因此 val 也不需要修改。

如果 c0c1 为空,说明当前候选集合中的所有位置在这一位上本来就相同,不需要询问,直接保留非空的一侧。

5. 为什么最后恰好剩下一个 1

第一次正回答之前处理过的高位,其询问结果都是 0,所以全体 1 在这些位上的取值相同。

第一次正回答及之后处理的每一位,我们都只保留某一个确定的二进制取值,所以 cur 中的 1 在这些位上的取值也相同。

因此,最终 cur 中所有值为 1 的位置具有完全相同的二进制编号。每个位置的编号互不相同,所以 cur 中至多有一个 1;算法又始终保证 cur 中至少存在一个 1,故最终恰好有一个 1

设最终保存的回答为 val,则:

\[val=1\cdot(S-1) \]

所以:

\[S=val+1 \]

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:与当前 cur1 的数量对应的最近一次有效回答;
  • c0,c1:按照当前二进制位将 cur 划分出的两个集合。

核心公式与转移

若询问集合内有 \( x \) 个 1,回答为:

\[q=x(S-x) \]

得到正回答后,始终维护 cur 内外都有 1。询问 c1 时:

  • 若结果大于 0,令 cur=c1 并更新 val
  • 若结果等于 0,由不变量可知 c1 中没有 1,令 cur=c0val 不变。

最终 cur 中恰好有一个 1,因此:

\[S=val+1 \]

易错点

  1. 询问格式必须在同一行输出 ? k i1 i2 ... ik
  2. 由于代码中将 endl 定义为 '\n',它不会自动刷新缓冲区,每次询问和最终回答后必须显式调用 cout.flush()
  3. 第一阶段与第二阶段应由 found 区分,不能把第二阶段写进 res==0 的分支;
  4. 最终应当在 found==false 时输出 1,否则输出 val+1
  5. 不要询问空集合。当 c0c1 为空时直接保留非空集合;
  6. 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;
}
posted @ 2026-07-18 16:36  艾拉别哭  阅读(10)  评论(0)    收藏  举报