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

思路來源

https://www.cnblogs.com/suncongbo/p/10321099.html Sperner定理的證明

https://www.bilibili.com/video/BV1az4y1976v?p=7 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;
}

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章