[NOI2019] I君的探险
文章目录
- 题面
- 题解
- ex20pts(20pts),test 1-5 :
- ex16pts(36pts),test 6-9 : A A A,询问次数上界 n l o g n nlogn nlogn
- ex8pts(44pts),test 10-11 : B B B, M = N − 1 M=N-1 M=N−1,询问次数上界 n l o g n nlogn nlogn
- ex12pts(56pts),test 12-14: C C C, M = N − 1 M=N-1 M=N−1
- ex12pts(68pts),test 15-17: D D D, M = N − 1 M=N-1 M=N−1
- ex32pts(100pts),test 18-25: L c = 2 × M L_c=2\times M Lc=2×M,无其他特殊限制
题面
题解
ex20pts(20pts),test 1-5 :
注意到操作次数比较充足。
一种直接的想法时,对于某个
i
i
i,记录当前各点的状态,然后modify(i),比较各点状态是否与操作前相同,若不同则与
i
i
i有边。
然而这样无法通过test 4-5。(test 4的
M
M
M不足
N
N
N,test 5的
L
q
L_q
Lq不足
N
2
N^2
N2)
注意到枚举到最后一个点之前所有的边必然已经找到,所以没有必然枚举最后一个点。
同时,若我们从小到大枚举,对于一条边
x
→
y
(
x
<
y
)
x\rightarrow y(x<y)
x→y(x<y),再枚举
y
y
y之前这条边必然已在枚举
x
x
x时被计算过,所以不用再考虑连向编号比自己更小的点的边。即对于一个点
i
i
i,只考虑
i
→
i
+
1
i\rightarrow i+1
i→i+1~
n
−
1
n-1
n−1。
namespace task1{
int vis[N];
void sol(int n){
for(int i=0;i<n-2;i++){
modify(i);
vis[i]^=1;
for(int j=i+1;j<n;j++){
if(query(j)^vis[j]){
vis[j]^=1;
report(i,j);
}
}
}
}
}
ex16pts(36pts),test 6-9 : A A A,询问次数上界 n l o g n nlogn nlogn
数据保证
n
n
n个点构成若干个长度为
2
2
2的链,也就是说,对于一个
i
i
i,有且仅有一个点影响自己,自己影响且只影响一个点。
考虑对于一个编号,逐位考虑,若改位为
1
1
1则modify,
n
n
n个点都操作过一遍后进行query,
若该位为
1
1
1而状态为
0
0
0则与该点匹配的点的编号该位一定为
1
1
1,反之同理。
一共进行
l
o
g
n
logn
logn轮,每轮
n
n
n次query,然后就得到了每个点与之匹配的点的编号,report即可。
namespace task2{
int vis[N],pa[N];
void sol(int n){
for(int r=0;(1<<r)<n;r++){
for(int i=0;i<n;i++) if((i>>r)&1) modify(i),vis[i]^=1;
for(int i=0;i<n;i++) if(query(i)^vis[i]) vis[i]^=1,pa[i]|=(1<<r);
}
for(int i=0;i<n;i++) if(i<pa[i]) report(i,pa[i]);
}
}
ex8pts(44pts),test 10-11 : B B B, M = N − 1 M=N-1 M=N−1,询问次数上界 n l o g n nlogn nlogn
数据保证形成树且父亲编号比儿子小。
因为父亲唯一,可以尝试找到每个点的父亲。
因为父亲编号小于儿子编号,可以整体二分。
具体的,每层递归取一个
m
i
d
mid
mid,将
[
l
,
m
i
d
]
[l,mid]
[l,mid]的点都modify,对于一个
x
x
x,若
x
≤
m
i
d
x\leq mid
x≤mid直接分到左边;若query(
x
x
x)
=
1
=1
=1则其父亲必然在左边,
x
x
x分到左边;否则
x
x
x分到右边。
namespace task3{
int vis[N],q[N],lq[N],rq[N];
void solve(int l,int r,int ql,int qr){
if(ql>qr||l>r) return ;
if(l==r){
for(int i=ql;i<=qr;i++)
report(l-1,q[i]-1);
return ;
}
int mid=(l+r)>>1,lt=0,rt=0;
for(int i=l;i<=mid;i++) modify(i-1);
for(int i=ql;i<=qr;i++){
if((q[i]<=mid)||(query(q[i]-1))) lq[++lt]=q[i];
else rq[++rt]=q[i];
}
for(int i=l;i<=mid;i++) modify(i-1);
for(int i=1;i<=lt;i++) q[ql+i-1]=lq[i];
for(int i=1;i<=rt;i++) q[ql+lt+i-1]=rq[i];
solve(l,mid,ql,ql+lt-1);
solve(mid+1,r,ql+lt,qr);
}
void sol(int n){
report(0,1);
for(int i=3;i<=n;i++) q[i]=i;
solve(1,n,3,n);
}
}
ex12pts(56pts),test 12-14: C C C, M = N − 1 M=N-1 M=N−1
数据保证形成链。
依旧考虑二进制,按位处理,对于每一位,若该编号该位为
1
1
1则modify,比较该位是否为
1
1
1和操作后query的值就能得到与该编号相邻的两编号的异或和。
暴力找到
0
0
0号节点的两端的儿子,就可以通过异或直接递推得到一系列相邻的编号。向两边dfs即可。
namespace task4{
int pa[N];
void dfs(int x,int fa){
if(fa==pa[x]) return;
report(x,pa[x]^fa);
dfs(pa[x]^fa,x);
}
void sol(int n){
for(int r=0;(1<<r)<n;r++){
for(int i=0;i<n;i++) if((i>>r)&1) modify(i);
for(int i=0;i<n;i++) if(query(i)^((i>>r)&1)) pa[i]|=(1<<r);
for(int i=0;i<n;i++) if((i>>r)&1) modify(i);
}
int ls=-1,rs=-1;
modify(0);
for(int i=1;i<n;i++) if(query(i)) ls==-1?ls=i:rs=i;
report(0,ls);dfs(ls,0);
if(rs!=-1){
report(0,rs),dfs(rs,0);
}
}
}
ex12pts(68pts),test 15-17: D D D, M = N − 1 M=N-1 M=N−1
数据保证是树。
首先为了方便,这里实现时规定标号从
1
1
1开始。
套路的使用二进制的方法可以得到每个点相邻的点的编号的异或和,记作
p
a
[
x
]
pa[x]
pa[x]。
维护队列,一开始将
1
→
n
1\rightarrow n
1→n全部加入队列。
每次取出队列首部,通过modify+query+modify检查
x
x
x是否与
p
a
[
x
]
pa[x]
pa[x]有之前未找到的新边,若无则continue,否则,report,把
p
a
[
p
a
[
x
]
]
pa[pa[x]]
pa[pa[x]]异或上
x
x
x,然后让
p
a
[
x
]
pa[x]
pa[x]加入队列,再令
p
a
[
x
]
=
0
pa[x]=0
pa[x]=0,反复如此,直到队列为空。
这样做,对于一条链,因为没有编号为
0
0
0,所以非端点节点必然不能动弹,而端点节点必然可以把整个链消完。若链上存在分岔,若该交叉点可以向外拓展一条边,那么任意其他点再到这个点时不能够再向外拓展,也就是说这个交叉点成为了一个分界点,将原图拆成了若干条链;若该交叉点不能向外拓展,那么某端点节点一定能消去一侧的链直到到达端点成为一个新的子问题。归纳一下,整棵树一定会被消完。
由于初始
n
n
n个点,每断一条边才会做一次push操作处理新点,所以处理的点数不超过
N
+
M
≤
2
N
−
1
N+M\leq 2N-1
N+M≤2N−1,总操作数是
O
(
n
+
m
)
O(n+m)
O(n+m)级别的。
注意判重边。
namespace task5{
int pa[N];queue<int> Q;
map<pair<int,int>,bool> mp;
bool ins(int x,int y){
pair<int,int> tmp={min(x,y),max(x,y)};
if(mp[tmp]) return 0;
return mp[tmp]=1;
}
void sol(int n){
for(int r=0;(1<<r)<=n;r++){
for(int i=1;i<=n;i++) if((i>>r)&1) modify(i-1);
for(int i=1;i<=n;i++) if(((i>>r)&1)^query(i-1)) pa[i]|=(1<<r);
for(int i=1;i<=n;i++) if((i>>r)&1) modify(i-1);
}
for(int i=1;i<=n;i++) Q.push(i);
while(!Q.empty()){
int x=Q.front();Q.pop();
if(pa[x]>=1&&pa[x]<=n&&pa[x]!=x){
modify(pa[x]-1);
int tmp=query(x-1);
modify(pa[x]-1);
if(tmp&&ins(x,pa[x])){
report(x-1,pa[x]-1);
Q.push(pa[x]);
pa[pa[x]]^=x;
pa[x]=0;
}
}
}
}
}
ex32pts(100pts),test 18-25: L c = 2 × M L_c=2\times M Lc=2×M,无其他特殊限制
考虑拓展test 10-11整体二分的算法。
考虑由所有编号构成的一个足够好的排列,进行整体二分,通过操作我们可以得到某个点向决策集合内的点连边数的奇偶性,如果为奇必然有这么一条边。那么我们可以report并去掉这条边。循环执行这个分治过程。
但这个做法并不是一个确定性的策略,于是可以引入随机化。
有结论:假设图中不存在孤立点,那么随机一个排列后,期望下这个排列中至少有
N
/
3
N/3
N/3个点向前连了奇数条边。
证明:根据期望的线性性,考虑每个点的贡献,对于一个度数为
k
k
k的点,有
⌊
k
/
2
⌋
/
k
\lfloor{k/2}\rfloor /k
⌊k/2⌋/k的概率向前连了奇数条边,当
k
=
2
k=2
k=2时取到最小值
1
/
3
1/3
1/3。
于是我们只要每次随机一个排列做上述的分治过程即可。
具体的,我们每次整体二分前对编号序列进行random_shuffle,整体二分时仍将决策区间的一半划分为一个集合,每一层将集合内的点modify,然后检查询问集合内的点的query,检查决策集合内点对询问点的影响的奇偶性,若为奇则决策集合内必然有新的可以和询问点连边的点,若为偶则不能确定,直接将询问点按照编号分到对应的区间即可。到达最后一层时,注意忽略编号与区间编号相同点,剩余的report记录,将找到的边存到两端点的存边的桶里(邻接表即可),下一轮整体二分检查奇偶性时,对于一个点
i
i
i,标记之前
i
i
i已经找到的边,忽略这些边,只考虑未确定是否与
i
i
i连边的点对其奇偶性的影响。
反复做整体二分直到所有边都被找到。
注意每一轮整体二分要检查是否有点check=1,即所有与该点相连的边都已经找到,该点不再发挥作用,将该点标记,之后的整体二分要忽略被标记的点。
时间复杂度:
O
(
M
l
o
g
N
)
O(MlogN)
O(MlogN)
时间复杂度分析:每次执行分治算法时间消耗为
O
(
N
l
o
g
N
)
O(NlogN)
O(NlogN),期望下可以发现
O
(
n
)
O(n)
O(n)条边,我们可以认为平均花费了
O
(
l
o
g
n
)
O(logn)
O(logn)的时间发现一条边,所以总时间复杂度为
O
(
M
l
o
g
N
)
O(MlogN)
O(MlogN)。
存在更严谨的时间复杂度分析,但我不会也看不懂QWQ
namespace task6{
int cnt=0,id[N],q[N],lq[N],rq[N];
bool used[N],st[N];
vector<int> E[N];
int calc(int x){
int tmp=query(id[x]-1);
for(int it:E[id[x]]) tmp^=st[it];
return tmp;
}
void solve(int l,int r,int ql,int qr){
if(l>r||ql>qr) return ;
if(l==r){
for(int i=ql;i<=qr;i++) if(l!=q[i]){
report(id[l]-1,id[q[i]]-1);
E[id[l]].push_back(id[q[i]]);
E[id[q[i]]].push_back(id[l]);
cnt--;
}
return ;
}
int mid=(l+r)>>1,lt=0,rt=0;
for(int i=l;i<=mid;i++) if(!used[id[i]]) modify(id[i]-1),st[id[i]]=1;
for(int i=ql;i<=qr;i++){
if(q[i]<=mid||calc(q[i])) lq[++lt]=q[i];
else rq[++rt]=q[i];
}
for(int i=l;i<=mid;i++) if(!used[id[i]]) modify(id[i]-1),st[id[i]]=0;
for(int i=1;i<=lt;i++) q[ql+i-1]=lq[i];
for(int i=1;i<=rt;i++) q[ql+lt+i-1]=rq[i];
solve(l,mid,ql,ql+lt-1);
solve(mid+1,r,ql+lt,qr);
}
void sol(int n,int m){
srand(202021);
cnt=m;
for(int i=1;i<=n;i++) id[i]=i;
while(cnt){
random_shuffle(id+1,id+n+1);
int len=0;
for(int i=1;i<=n;i++) if(!used[id[i]]) q[++len]=i;
solve(1,n,1,len);
if(cnt) for(int i=1;i<=n;i++) if(!used[i]&&check(i-1)) used[i]=1;
}
}
}
代码全貌如下,实现了上述全部 6 6 6个部分分:(此为luogu上提交的版本)
#include<bits/stdc++.h>
//#include"explore.h"
#define pb push_back
#define fir first
#define sec second
#define mkp make_pair
using namespace std;
const int N=2e5+10;
void modify(int x);int query(int x);void report(int x,int y);int check(int x);
namespace task1{
int vis[N];
void sol(int n){
for(int i=0;i<n-2;i++){
modify(i);
vis[i]^=1;
for(int j=i+1;j<n;j++){
if(query(j)^vis[j]){
vis[j]^=1;
report(i,j);
}
}
}
}
}
namespace task2{
int vis[N],pa[N];
void sol(int n){
for(int r=0;(1<<r)<n;r++){
for(int i=0;i<n;i++) if((i>>r)&1) modify(i),vis[i]^=1;
for(int i=0;i<n;i++) if(query(i)^vis[i]) vis[i]^=1,pa[i]|=(1<<r);
}
for(int i=0;i<n;i++) if(i<pa[i]) report(i,pa[i]);
}
}
namespace task3{
int vis[N],q[N],lq[N],rq[N];
void solve(int l,int r,int ql,int qr){
if(ql>qr||l>r) return ;
if(l==r){
for(int i=ql;i<=qr;i++)
report(l-1,q[i]-1);
return ;
}
int mid=(l+r)>>1,lt=0,rt=0;
for(int i=l;i<=mid;i++) modify(i-1);
for(int i=ql;i<=qr;i++){
if((q[i]<=mid)||(query(q[i]-1))) lq[++lt]=q[i];
else rq[++rt]=q[i];
}
for(int i=l;i<=mid;i++) modify(i-1);
for(int i=1;i<=lt;i++) q[ql+i-1]=lq[i];
for(int i=1;i<=rt;i++) q[ql+lt+i-1]=rq[i];
solve(l,mid,ql,ql+lt-1);
solve(mid+1,r,ql+lt,qr);
}
void sol(int n){
report(0,1);
for(int i=3;i<=n;i++) q[i]=i;
solve(1,n,3,n);
}
}
namespace task4{
int pa[N];
void dfs(int x,int fa){
if(fa==pa[x]) return;
report(x,pa[x]^fa);
dfs(pa[x]^fa,x);
}
void sol(int n){
for(int r=0;(1<<r)<n;r++){
for(int i=0;i<n;i++) if((i>>r)&1) modify(i);
for(int i=0;i<n;i++) if(query(i)^((i>>r)&1)) pa[i]|=(1<<r);
for(int i=0;i<n;i++) if((i>>r)&1) modify(i);
}
int ls=-1,rs=-1;
modify(0);
for(int i=1;i<n;i++) if(query(i)) ls==-1?ls=i:rs=i;
report(0,ls);dfs(ls,0);
if(rs!=-1){
report(0,rs),dfs(rs,0);
}
}
}
namespace task5{
int pa[N];queue<int> Q;
map<pair<int,int>,bool> mp;
bool ins(int x,int y){
pair<int,int> tmp={min(x,y),max(x,y)};
if(mp[tmp]) return 0;
return mp[tmp]=1;
}
void sol(int n){
for(int r=0;(1<<r)<=n;r++){
for(int i=1;i<=n;i++) if((i>>r)&1) modify(i-1);
for(int i=1;i<=n;i++) if(((i>>r)&1)^query(i-1)) pa[i]|=(1<<r);
for(int i=1;i<=n;i++) if((i>>r)&1) modify(i-1);
}
for(int i=1;i<=n;i++) Q.push(i);
while(!Q.empty()){
int x=Q.front();Q.pop();
if(pa[x]>=1&&pa[x]<=n&&pa[x]!=x){
modify(pa[x]-1);
int tmp=query(x-1);
modify(pa[x]-1);
if(tmp&&ins(x,pa[x])){
report(x-1,pa[x]-1);
Q.push(pa[x]);
pa[pa[x]]^=x;
pa[x]=0;
}
}
}
}
}
namespace task6{
int cnt=0,id[N],q[N],lq[N],rq[N];
bool used[N],st[N];
vector<int> E[N];
int calc(int x){
int tmp=query(id[x]-1);
for(int it:E[id[x]]) tmp^=st[it];
return tmp;
}
void solve(int l,int r,int ql,int qr){
if(l>r||ql>qr) return ;
if(l==r){
for(int i=ql;i<=qr;i++) if(l!=q[i]){
report(id[l]-1,id[q[i]]-1);
E[id[l]].push_back(id[q[i]]);
E[id[q[i]]].push_back(id[l]);
cnt--;
}
return ;
}
int mid=(l+r)>>1,lt=0,rt=0;
for(int i=l;i<=mid;i++) if(!used[id[i]]) modify(id[i]-1),st[id[i]]=1;
for(int i=ql;i<=qr;i++){
if(q[i]<=mid||calc(q[i])) lq[++lt]=q[i];
else rq[++rt]=q[i];
}
for(int i=l;i<=mid;i++) if(!used[id[i]]) modify(id[i]-1),st[id[i]]=0;
for(int i=1;i<=lt;i++) q[ql+i-1]=lq[i];
for(int i=1;i<=rt;i++) q[ql+lt+i-1]=rq[i];
solve(l,mid,ql,ql+lt-1);
solve(mid+1,r,ql+lt,qr);
}
void sol(int n,int m){
srand(202021);
cnt=m;
for(int i=1;i<=n;i++) id[i]=i;
while(cnt){
random_shuffle(id+1,id+n+1);
int len=0;
for(int i=1;i<=n;i++) if(!used[id[i]]) q[++len]=i;
solve(1,n,1,len);
if(cnt) for(int i=1;i<=n;i++) if(!used[i]&&check(i-1)) used[i]=1;
}
}
}
void explore(int n,int m){
if(n<=500) task1::sol(n);
else if(n%10==8) task2::sol(n);
else if(n%10==7) task3::sol(n);
else if(n%10==6) task4::sol(n);
else if(n%10==5) task5::sol(n);
else task6::sol(n,m);
}
//int main(){
freopen(".in","r",stdin);
freopen(".out","w",stdout);
//
// return 0;
//}

浙公网安备 33010602011771号