潜龙未见静水流,沉默深藏待时秋。一朝破空声势振,惊世骇俗展雄猷。

P5473 [NOI2019] I 君的探险

题目描述

这是一道交互题。

一张 \(n\) 个点, \(m\) 条边的无向图,点编号为 \(0\sim n-1\) ,每个点有一个光源,初始为熄灭状态。

初始你不知道边的具体信息,你的目标是复原整张图。

你需要实现以下函数:

  • void explore(int n,int m);

你可以调用以下函数:

  • void modify(int x);

    你需要保证 \(0\le x\lt n\) ,这个函数会改变 \(x\)\(x\) 的所有邻点的光源状态,无返回值,最多调用 \(L_m\) 次。

  • void query(int x);

    你需要保证 \(0\le x\lt n\) ,这个函数会返回 \(x\) 号点的光源状态, \(0\) 表示熄灭, \(1\) 表示点亮,最多调用 \(L_q\) 次。

  • void report(int x,int y);

    你需要保证 \(0\le x\neq y\lt n\) ,这个函数表明你确定有一条连接 \(x\)\(y\) 的无向边。每条边只能被记录一次,程序正确运行时该函数恰好调用 \(m\) 次。

  • void check(int x);

    你需要保证 \(0\le x\lt n\) ,这个函数会判断与 \(x\) 相连的所有边是否都被 report 操作记录, \(0\) 表示否, \(1\) 表示是,最多调用 \(L_c\) 次。

数据范围

测试点编号 \(N=\) \(M=\) \(L_m=\) \(L_q=\) \(L_c=\) 特殊性质
\(1\) \(3\) \(2\) \(100\) \(100\) \(100\)
\(2\) \(100\) \(10\times N\) \(200\) \(10^4\) \(2\times M\)
\(3\) \(200\) \(10\times N\) \(200\) \(4\times 10^4\) \(2\times M\)
\(4\) \(300\) \(10\times N\) \(299\) \(9\times 10^4\) \(2\times M\)
\(5\) \(500\) \(10\times N\) \(499\) \(1.5\times 10^5\) \(2\times M\)
\(6\) \(59998\) \(\frac{N}{2}\) \(17\times N\) \(17\times N\) \(0\) \(A\)
\(7\) \(99998\) \(\frac{N}{2}\) \(18\times N\) \(18\times N\) \(0\) \(A\)
\(8\) \(199998\) \(\frac{N}{2}\) \(19\times N\) \(19\times N\) \(0\) \(A\)
\(9\) \(199998\) \(\frac{N}{2}\) \(19\times N\) \(19\times N\) \(0\) \(A\)
\(10\) \(99997\) \(N-1\) \(18\times N\) \(18\times N\) \(0\) \(B\)
\(11\) \(199997\) \(N-1\) \(19\times N\) \(19\times N\) \(0\) \(B\)
\(12\) \(99996\) \(N-1\) \(10^7\) \(10^7\) \(2\times M\) \(C\)
\(13\) \(199996\) \(N-1\) \(10^7\) \(10^7\) \(2\times M\) \(C\)
\(14\) \(199996\) \(N-1\) \(10^7\) \(10^7\) \(2\times M\) \(C\)
\(15\) \(99995\) \(N-1\) \(10^7\) \(10^7\) \(2\times M\) \(D\)
\(16\) \(99995\) \(N-1\) \(10^7\) \(10^7\) \(2\times M\) \(D\)
\(17\) \(199995\) \(N-1\) \(10^7\) \(10^7\) \(2\times M\) \(D\)
\(18\) \(1004\) \(2\times 10^3\) \(10^7\) \(5\times 10^4\) \(2\times M\)
\(19\) \(1004\) \(3\times 10^3\) \(10^7\) \(5\times 10^4\) \(2\times M\)
\(20\) \(1004\) \(3\times 10^3\) \(10^7\) \(5\times 10^4\) \(2\times M\)
\(21\) \(5\times 10^4\) \(2\times N\) \(10^7\) \(10^7\) \(2\times M\)
\(22\) \(10^5\) \(2\times N\) \(10^7\) \(10^7\) \(2\times M\)
\(23\) \(1.5\times 10^5\) \(2\times 10^5\) \(10^7\) \(10^7\) \(2\times M\)
\(24\) \(2\times 10^5\) \(2.5\times 10^5\) \(10^7\) \(10^7\) \(2\times M\)
\(25\) \(2\times 10^5\) \(3\times 10^5\) \(10^7\) \(10^7\) \(2\times M\)

保证图在交互之前已经完全确定,不会根据你的程序动态构造。

特殊性质:

A:每个点的度数恰好为 \(1\)

B:\(n\) 个点的树,保证 \(\forall 1\le i\lt n\)\(i\) 的祖先编号 \(\lt i\)

C:长为 \(n\) 的链。

D:\(n\) 个点的树。

时间限制 \(\texttt{2s}\) ,空间限制 \(\texttt{500MB}\)

分析

最朴素的暴力。

点亮第 \(i\) 个点, \(\forall j\gt i\)\((i,j)\) 有边当且仅当第 \(j\) 个点被点亮。

实现的时候精细一点,记录 lst[j] 上一次查询第 \(j\) 个点的结果,最后一个点无需操作,期望得分 \(20pts\)

namespace task1
{
    int lst[maxn];
    void work()
    {
        for(int i=0;i<n-1;i++)
        {
            modify(i);
            for(int j=i+1;j<n;j++) if(query(j)^lst[j]) lst[j]^=1,report(i,j);
        }
    }
}

对特殊性质 \(A\) ,记 \(f_i\)\(i\) 的邻点,目标求出所有 \(f_i\)

看到 \(L_q\) 范围容易想到二分,点亮 \(0\sim mid\)\(f_i\le mid\) 当且仅当 \(i\) 的状态为 \([i\gt mid]\)

同时求所有 \(f_i\) ,整体二分就好了,期望得分 \(36pts\)

namespace task1
{
    int lst[maxn];
    void work()
    {
        for(int i=0;i<n-1;i++)
        {
            modify(i);
            for(int j=i+1;j<n;j++) if(query(j)^lst[j]) lst[j]^=1,report(i,j);
        }
    }
}

对特殊性质 \(B\) ,记 \(f_i\)\(i\) 的祖先,目标求出所有 \(f_i\)

二分区间 \([0,i-1]\)\(f_i\le mid\) 当且仅当 \(i\) 的状态为 \(1\)

整体二分的时候实现稍微精细一点即可,期望得分 \(44pts\)

namespace task3
{
    int f[maxn];
    void solve(int l,int r,vector<int> v)
    {
        if(l==r)
        {
            for(auto p:v) f[p]=l;
            return ;
        }
        int mid=(l+r)>>1;
        vector<int> v1,v2;
        for(int i=l;i<=mid;i++) modify(i);
        for(auto p:v) (p<=mid||query(p)?v1:v2).push_back(p);
        solve(mid+1,r,v2);
        for(int i=l;i<=mid;i++) modify(i);
        solve(l,mid,v1);
    }
    void work()
    {
        vector<int> v(n-1);
        iota(v.begin(),v.end(),1);
        solve(0,n-1,v);
        for(int i=1;i<n;i++) report(i,f[i]);
    }
}

整体二分似乎走到尽头了,但是这并不是特殊性质 \(A,B\) 的唯一做法。

考虑拆位。点亮所有第 \(j\) 位为 \(1\) 的点,我们可以算出 \(i\) 的邻点异或和第 \(j\) 位是 \(0\) 还是 \(1\)

这样我们可以算出 \(i\) 的邻点异或和,记为 \(f_i\)

于是特殊性质 \(A\) 已经做完了,特殊性质 \(B\) 只需按照编号倒序递推一遍。

拆位的做法可以扩展到特殊性质 \(C\)

求出 \(f_i\) 后,先用 \(1\)modify 操作和 \(n\)query 操作求出 \(0\) 号点的邻点,再往两边递推扩展,期望得分 \(56pts\)

void init()
{
    for(int j=0;j<=__lg(n);j++)
    {
        for(int i=0;i<n;i++) if(i>>j&1) modify(i);
        for(int i=0;i<n;i++) f[i]|=query(i)<<j;
        for(int i=0;i<n;i++) if(i>>j&1) modify(i);
    }
    for(int i=0;i<n;i++) f[i]^=i;
}
namespace task2
{
    void work()
    {
        init();
        for(int i=0;i<n;i++) if(i<f[i]) report(i,f[i]);
    }
}
namespace task3
{
    void work()
    {
        init();
        for(int i=n-1;i>=1;i--) report(i,f[i]),f[f[i]]^=i;
    }
}
namespace task4
{
    void work()
    {
        init(),modify(0);
        for(int i=1;i<n;i++)
        {
            if(!query(i)) continue;
            report(0,i);
            for(int j=i;f[j];j=f[j]) report(j,f[j]),f[f[j]]^=j;
        }
    }
}

特殊性质 \(B\) 拆位做法的本质是,邻点异或和 \(f_i\) 很有用,然后剥叶子。

对于特殊性质 \(D\) ,它与特殊性质 \(B\) 的区别是我们不知道树的拓扑序。

用队列维护可能是叶子的节点集合,每次取一个点 \(u\) ,判断 \((u,f_u)\) 是否有边,如果有边则修改 \(f_{f_u}\) 的值并将 \(f_u\) 加入队列。

判断 \((u,v)\) 是否有边可以用 modify,check,modify 的方法。

注意 \((u,f_u)\) 有边不能保证 \(u\) 是叶子,比如 \(u=5\) 但它与 \(1,2,3,4\) 都有边,此时 \(f_u=4\)

正确的做法是先特判掉所有与 \(0\) 相连的边,删掉 \((u,f_u)\) 的边后,要么 \(u\) 被删掉,要么 \(u\) 的其他邻点异或和为 \(0\) ,无论哪种情况 \(u\) 都不是叶子,因此不会加入队列。

这样每条边恰好只会将一个点加入队列,队列总共有 \(n+m\) 个点。

记得给边去重,以防出现 \(f_u=v\) 时找到边 \((u,v)\) ,但是访问到 \(v\) 时机缘巧合 \(f_v=u\) 的情况。

时间复杂度 \(\mathcal O((n+m)\log n)\) ,除了求 \(f_i\) 以外还需用 \(2(n+m)\)modify\(n+m\)query 次操作,期望得分 \(68pts\)

namespace task5
{
    queue<int> q;
    set<pii> s;
    bool check(int u,int v)
    {
        if(u==v||max(u,v)>=n) return 0;
        static bool b;
        modify(u),b=query(v),modify(u);
        if(!b||s.count(mp(u,v))) return 0;
        report(u,v),s.insert(mp(u,v)),s.insert(mp(v,u));
        return 1;
    }
    void work()
    {
        init();
        for(int i=1;i<n;i++) q.push(i),check(0,i);
        while(!q.empty())
        {
            int u=q.front();q.pop();
            if(f[u]&&check(u,f[u])) f[f[u]]^=u,q.push(f[u]);
        }
    }
}

拆位剥叶子的做法只对树有效,如果有环的话一条边都剥不下来。

而且到目前为止 \(2M\)check 函数还没用上。

回到特殊性质 \(A,B\) 中整体二分的做法,二分判断的本质是查询一个前缀是否有奇数个点与 \(i\) 相邻。如果一个点向前缀连了奇数条边,那么一定可以通过二分找到一条边。

然后有一个结论:

对于一张点数为 \(n\) ,无孤立点的图,随机化后期望至少 \(\frac n3\) 个点向前连了奇数条边。

证明:

对于一个度数为 \(k\) 的点,我们计算它产生贡献的概率。

有贡献当且仅当它在这 \(k+1\) 个点中排在偶数位置上,概率为 \(\frac{\lfloor\frac{k+1}2\rfloor}{k+1}\) ,当 \(k=2\) 时取最小值 \(\frac 13\)

回到原题,每次随机化一个排列,通过 check 函数扔掉孤立点,二分判断时记得消除已经记录的边的影响,然后就能过了。

下面是完整代码。

#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pii pair<int,int>
using namespace std;
const int maxn=2e5+5;
int m,n,f[maxn];
void modify(int x);
int query(int x);
void report(int x, int y);
int check(int x);
namespace task1
{
    void work()
    {
        for(int i=0;i<n-1;i++)
        {
            modify(i);
            for(int j=i+1;j<n;j++) if(query(j)^f[j]) f[j]^=1,report(i,j);
        }
    }
}
void init()
{
    for(int j=0;j<=__lg(n);j++)
    {
        for(int i=0;i<n;i++) if(i>>j&1) modify(i);
        for(int i=0;i<n;i++) f[i]|=query(i)<<j;
        for(int i=0;i<n;i++) if(i>>j&1) modify(i);
    }
    for(int i=0;i<n;i++) f[i]^=i;
}
namespace task2
{
    void work()
    {
        init();
        for(int i=0;i<n;i++) if(i<f[i]) report(i,f[i]);
    }
}
namespace task3
{
    void work()
    {
        init();
        for(int i=n-1;i>=1;i--) report(i,f[i]),f[f[i]]^=i;
    }
}
namespace task4
{
    void work()
    {
        init(),modify(0);
        for(int i=1;i<n;i++)
        {
            if(!query(i)) continue;
            report(0,i);
            for(int j=i;f[j];j=f[j]) report(j,f[j]),f[f[j]]^=j;
        }
    }
}
namespace task5
{
    queue<int> q;
    set<pii> s;
    bool check(int u,int v)
    {
        if(u==v||max(u,v)>=n) return 0;
        static bool b;
        modify(u),b=query(v),modify(u);
        if(!b||s.count(mp(u,v))) return 0;
        report(u,v),s.insert(mp(u,v)),s.insert(mp(v,u));
        return 1;
    }
    void work()
    {
        init();
        for(int i=1;i<n;i++) q.push(i),check(0,i);
        while(!q.empty())
        {
            int u=q.front();q.pop();
            if(f[u]&&check(u,f[u])) f[f[u]]^=u,q.push(f[u]);
        }
    }
}
namespace task6
{
    int b[maxn],p[maxn],id[maxn];
    vector<int> g[maxn];
    void solve(int l,int r,vector<int> v)
    {
        if(l==r||v.empty())
        {///二分区间[0,id[x]],结果为id[x]表示没找到边
            for(auto x:v) if(x!=p[l]) report(x,p[l]),m--,g[x].push_back(p[l]),g[p[l]].push_back(x);
            return ;
        }
        int mid=(l+r)>>1;
        vector<int> v1,v2;
        for(int i=l;i<=mid;i++) modify(p[i]);
        auto ask=[&](int x)
        {
            int res=query(x);
            for(auto v:g[x]) res^=id[v]<=mid;
            return res;
        };
        for(auto x:v) (id[x]<=mid||ask(x)?v1:v2).push_back(x);
        solve(mid+1,r,v2);
        for(int i=l;i<=mid;i++) modify(p[i]);
        solve(l,mid,v1);
    }
    void work()
    {
        while(m)
        {
            int k=0;
            for(int i=0;i<n;i++) if(!b[i]) p[k++]=i;
            fill(id,id+n,n),random_shuffle(p,p+k);
            for(int i=0;i<k;i++) id[p[i]]=i;
            solve(0,k,vector<int>(p,p+k));
            for(int i=0;i<n;i++) if(!b[i]) b[i]=check(i);
        }
    }
}
void explore(int _n,int _m)
{
    n=_n,m=_m;
    if(n<=500) task1::work();
    else if(n%10==8) task2::work();
    else if(n%10==7) task3::work();
    else if(n%10==6) task4::work();
    else if(n%10==5) task5::work();
    else task6::work();
}

posted on 2025-01-18 21:48  peiwenjun  阅读(12)  评论(0编辑  收藏  举报

导航