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();
}
本文来自博客园,作者:peiwenjun,转载请注明原文链接:https://www.cnblogs.com/peiwenjun/p/18678916