ZR 集训 Day23 – 通信、交互、题答非传统题

ooliver 发布于 21 小时前 113 次阅读 OI


AI 摘要

Day23,通信交互题专场。发现这类题最妙的是:答案不是算出来的,而是“问”出来的。先问三对和就能还原数组,三分截断逼近k大,随机分治排序,根号分治找质因数——每一次提问都在缩小答案的范围。来,看非传统题怎么教你优雅地猜谜。

CF727C Guess the Array

先问前三个,就可以算出来前三个,后面就更简单了。

代码:

C++
#include<bits/stdc++.h>
using namespace std;

const int N=1e5+5;
int n;
int a[N],x,y,z;

signed main(){
    cin>>n;
    cout<<"? 1 2"<<endl;
    cin>>x;
    cout<<"? 1 3"<<endl;
    cin>>y;
    cout<<"? 2 3"<<endl;
    cin>>z;
    a[1]=(x+y-z)/2,a[2]=(x+z-y)/2,a[3]=(y+z-x)/2;
    for(int i=4;i<=n;i++){
        cout<<"? 1 "<<i<<endl;
        cin>>x;
        a[i]=x-a[1];
    }
    cout<<"! ";
    for(int i=1;i<=n;i++) cout<<a[i]<<" ";
    return 0;
}

UOJ52 元旦激光炮

考虑每次查询每个数组的第 $\frac{k}{3}$ 个数,取最小的,删掉其前缀,因为至多有 $k$ 个数小于它,所以正确性显然。之后重复即可。

代码:

C++
#include"kth.h"
#include<bits/stdc++.h>
using namespace std;

int query_kth(int n_a,int n_b,int n_c,int k){
    int id[3]={0,0,0},ans=0;
    while(k){
        int l=(k+2)/3;
        pair<int,int> g[3]={
            {get_a(id[0]+l-1),0},
            {get_b(id[1]+l-1),1},
            {get_c(id[2]+l-1),2}
        };
        sort(g,g+3);
        id[g[0].second]+=l,
        ans=max(g[0].first,ans),
        k-=l;
    }
    return ans;
}

CF1762D GCD Queries

发现一个性质:

$$
\begin{cases}
gcd(a,b)=gcd(a,c) \rightarrow a \not = 0 \\
gcd(a,b)<gcd(a,c) \rightarrow b \not = 0 \\
gcd(a,b)>gcd(a,c) \rightarrow c \not = 0 \\
\end{cases}
$$

维护一个可能为 $0$ 的二元组即可。

代码:

C++
#include<bits/stdc++.h>
using namespace std;

int main(){
    int T,n;
    cin>>T;
    while(T--){
        int a=1,b=2,c;
        cin>>n;
        for(c=3;c<=n;c++){
            int g1,g2;
            cout<<"? "<<a<<" "<<b<<endl;
            cin>>g1;
            cout<<"? "<<a<<" "<<c<<endl;
            cin>>g2;
            if(g1==g2) a=b,b=c;
            else if(g1<g2) b=c;
        }
        cout<<"! "<<a<<" "<<b<<endl;
        cin>>n;
        if(n==-1) break;
    }
    return 0;
}

CF1918E ace5 and Task Order

我们会发现,如果我们一直询问一个位置 $i$,最劣 $n$ 次询问后 $x$ 会变成 $a_i$。

又不难发现,当我们询问一次其他位置,再询问一次 $i$ 时,$x$ 最后还是会变回 $a_i$。

那我们就可以将 $a_i$ 与其他位置比较,进行分治排序。这里我们运用快排的思想,随机去取这个 $i$,期望复杂度为 $O(n\log n)$,每次分治需比较 $3n$ 次,期望询问次数为 $3n\log n$ 次,在规定范围内。

代码:

C++
#include<bits/stdc++.h>
using namespace std;

int T,n;
int ans[2005];

mt19937 rnd(time(0));

int ask(int k){
    char c;
    cout<<"? "<<k<<endl;
    cin>>c;
    if(c=='=') return 0;
    if(c=='>') return 1;
    return -1;
}

void solve(int l,int r,vector<int> v){
    if(!v.size()||l>r) return;
    if(l==r){
        ans[v[0]]=l;
        return;
    }
    vector<int> a,b;
    int id=v[rnd()%v.size()];
    while(1) if(ask(id)==0) break;
    for(int x:v){
        if(x==id) continue;
        if(ask(x)==-1) a.push_back(x);
        else b.push_back(x);
        ask(id);
    }
    ans[id]=l+a.size();
    solve(l,l+a.size()-1,a);
    solve(l+a.size()+1,r,b);
}

int main(){
    cin>>T;
    while(T--){
        cin>>n;
        vector<int> v(n);
        for(int i=0;i<n;i++) v[i]=i+1;
        solve(1,n,v);
        cout<<"! ";
        for(int i=1;i<=n;i++) cout<<ans[i]<<" ";
        cout<<endl;
    }
    return 0;
}

CF1406E Deleting Numbers

一个与众不同的写法,询问次数是一样的。

考虑根号分治,对于寻找答案小于 $\sqrt{n}$ 的质因数,我们直接暴力,也就是先问一次 B 把其倍数删掉,再问一次 A 看有没有剩下的,有的话就找到了一个质因数 $p$,之后直接问 A $p,p^2,p^3...$ 来找 $p$ 的指数。

进行这番操作后,我们分析当前的答案。

如果当前答案为 $1$,说明 $x$ 没有小于 $\sqrt{n}$ 的质因数,即 $x$ 要么是 $1$ 要么是一个大质数,且当前集合应该只剩下 $1$ 和大于 $\sqrt{n}$ 的质数。考虑二分,不管集合中的那个 $1$,对前半部分的质数暴力进行 B 操作,再进行 A 操作,查询 $1$,得到集合元素总数,判断 $x$ 是否在前半部分。如果在,就在对前半部分暴力查询一边;如果不在,就递归后半部分。

如果当前答案不为 $1$,说明如果 $x$ 有一个大于 $\sqrt{n}$ 的质因数,那我们对这个质因数进行 A 操作,得到的结果一定是 2,直接暴力扫一遍大于 $\sqrt{n}$ 的质数并查询即可。

代码:

C++
#include<bits/stdc++.h>
using namespace std;

#define int long long
const int N=1e5+5;
int n,idx=0,ans=1;
int vis[N],prm[N],num[N];

void euler(){
    for(int i=2;i<=n;i++){
        if(!vis[i]) prm[++idx]=i;
        for(int j=1;i*prm[j]<=n;j++){
            vis[i*prm[j]]=1;
            if(i%prm[j]==0) break;
        }
    }
}

int A(int x){
    int res;
    cout<<"A "<<x<<endl;
    cin>>res;
    return res;
}

int B(int x){
    int res;
    cout<<"B "<<x<<endl;
    cin>>res;
    return res;
}

void solve(int l,int r){
    if(l>r) return;
    int mid=(l+r)>>1;
    for(int i=l;i<=mid;i++) B(prm[i]);
    if(A(1)+mid==r+1) solve(mid+1,r);
    else{
        for(int i=l;i<=mid;i++) if(A(prm[i])){
            ans=prm[i];
            return;
        }
    }
}

signed main(){
    cin>>n;
    euler();
    for(int i=1;i<=min(idx,65ll);i++){
        int x,y;
        x=B(prm[i]);
        y=A(prm[i]);
        if(y){
            ans*=prm[i];
            for(int j=prm[i]*prm[i];j<=n;j*=prm[i]){
                if(A(j)) ans*=prm[i];
                else break;
            }
        }
    }
    if(ans!=1){
        for(int i=66;i<=idx;i++) if(A(prm[i])==2){
            ans*=prm[i];
            break;
        }
    }
    else solve(66,idx);
    cout<<"C "<<ans;
    return 0;
}