Codeforces Round #648 (Div. 2) G.Secure Password(交互题+数论Sperner定理)

题目

一个神奇的交互题,

系统首先会生成一个长为n(n<=1e3)的数组A,Ai在[0,2^63)范围内,

主人公会产生一个长为n的数组P,代表它的密码,Pi是A数组中除了Ai以外的其它所有值的二进制or的值

你可以最多询问13次,每次询问输入c(c<=n)及c个整数,系统回答你这c个下标对应的Ai的二进制之或

询问之后,你需要输出n个整数,代表P1-Pn

思路来源

【学习笔记】Sperner定理及其证明 - suncongbo - 博客园 Sperner定理的证明

G_哔哩哔哩_bilibili heyuhhh的B站讲解

题解

交互题小贴士:

交互题要清空缓存区,可以在printf输出完之后,加一fflush(stdout);

也可以直接使用cout<<endl来输出,endl不仅是换行,还可以直接清空缓存区。

仔细看题目给的fflush(stdout)的那段,否则可能会Idleness limit exceeded。

Sperner定理,是一个神奇的定理,看tls在群里发了个图片竟然是和Dilworth定理放一起的……

这个定理是说,n元集合Sn,从中选出若干个子集,满足没有任何两个子集之间存在包含关系,问最多能选出多少个?

答案是C_{n}^{\left \lfloor \frac{n}{2} \right \rfloor},证明是一种非常神奇的构造,可以参考思路来源

这个定理暂时没有什么用。

考虑先用二进制对n个数进行编码。

如果允许询问2logn次,

对于i来说,如果i的二进制在第j位为0,我们可以对Pi的答案或上那些在第j位为1的值,

如果i的二进制在第j位位1,可以或上那些在第j位为0的值,

这样由于其他的数,至少在某一位,0/1的取值和i不同,就或上了所有的值

然后考虑怎么优化成13次,

如果保留二进制的映射方式不变,不妨只保留一半询问,

即i在二进制的第j位为1时,或上那些第j位为0的值,询问时把第j位对应的询问加入第j位本身为0的二进制值

那询问的时候,对于0001来说,1000的答案可以在个位统计到,

但形如0011的答案统计不到,

因为0011并不存在一个位j,使得对于0001来说在第j位是1,而对于0011来说在第j位是0

本质是0001是0011的子集,所以新的映射方式应该满足任意两个数都不存在一个是另一个的子集

于是,0到2^13的二进制中,恰有6个二进制位的编码方式满足条件,共1716种,取前n个用于映射即可

由于任意两个不同数i和k来说,都没有包含和被包含关系,

所以对于i来说,必存在一位j,i在第j位是1,而k在第j位是0,在第j位的询问中就会包含k的或的答案

那询问1000组的下限为什么是13次呢,因为根据上述证明的Sperner定理,

长为12的集合的最优选取方式是C_{12}^{6}小于1000,故长为13的集合方能满足

代码

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define pb push_back
const int Q=13,up=1<<13,N=1e3+10;
typedef long long ll;
int n,h[N],cnt;//映射下标
vector<int>ask[Q];
ll res[N],ans[Q];
ll query(vector<int>&q){
    printf("? %d",(int)q.size());
    for(auto x:q){
        printf(" %d",x);
    }
    printf("\n");
    fflush(stdout);
    ll v;
    scanf("%lld",&v);
    return v;
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<up;++i){
        if(__builtin_popcount(i)!=6)continue;
        h[++cnt]=i;
        for(int j=0;j<Q;++j){
            if(!(i>>j&1)){//第cnt个映射的这一位为0 就得问第cnt个 求第cnt位没有的方案
                ask[j].pb(cnt);
            }
        }
        if(cnt==n)break;
    }
    for(int j=0;j<Q;++j){
        if(!ask[j].size())continue;
        ans[j]=query(ask[j]);
    }
    //由于任意两个 一个都不是另一个的子集
    //故 对于任意数j来说 必存在i的数中的一位1<<k j里没有1<<k 就覆盖到了j
    for(int i=1;i<=n;++i){
        for(int j=0;j<Q;++j){
            if(h[i]>>j&1){//如果映射在这一位有 就应或上别的在这一位没有值的答案
                res[i]|=ans[j];
            }
        }
    }
    printf("!");
    for(int i=1;i<=n;++i){
        printf(" %lld",res[i]);
    }
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值