题解:P9150 邮箱题
all_for_god · · 题解
P9150 邮箱题
这个题卡了我一整个下午。主要是感觉题解都写的比较形式化,不是很直观,对于我这种抽象思维比较差的人来说简直就是天书。(当然是我太菜了) 所以个人花了比较多的图来辅助理解。
思路
初步观察
首先我们观察到钥匙这个性质本身是相对强的。如果我们拿到了某个点的钥匙,其下一步能走的点是确定的。但是这个题烦就烦在除了钥匙的限制以外还有一个原图的限制。 比如下面的情况:(注意这个是原图)
假设每个点
由于钥匙的限制是一一对应的,因此如果我们从某一个点开始一直令
然后我们考虑在
因此,如果我们已经知道了一串后缀的点最多可以走多少点,那我们在统计前面的点的答案的时候这些后缀的点能走到的最多走多少点一定不会变。因此我们考虑从后向前统计答案。
我们继续延续前面维护强联通分量的思路。可以发现一个强联通分量在
其中方块代表这是一个强联通分量,而区间表示这个区间内的所有点最远都只能到达这个区间的右端点。可以发现一个区间一定完整包含若干个强联通分量。注意,
同时我们发现,区间右端点与某个点的距离以及这个点所在强联通分量的大小分别就是我们要求的两个问的答案。因此我们去考虑从右往左动态维护区间与强联通分量的信息。
区间合并
我们先考虑插入一个点
我们可以发现,
由于每一个区间在
假设
(我们先不管强联通分量是如何合并的)
我们假设这个区间的最靠右的点是
由于单独一个点本身也算是整个区间都是强联通分量,因此第一个情况可以合并进第二种情况里。
强联通分量合并
现在考虑一个区间内的前两个强联通分量可不可以合并。
我们维护每一个区间中有类似返祖边的最远的强联通分量,记为
然后就做完了。
统计答案
最后统计答案的时候就是直接用并查集查找区间和强联通分量的右端点即可。显然当前点是当前所在区间和强联通分量的左端点。
code
细节奇多。注意实现的时候维护区间的并查集和维护强联通分量的并查集要分清楚。
同时,由于 vector 挂在了被指向的点上,也就是我们记录的实际上是反边,这个在理解代码时要注意。
#include<bits/stdc++.h>
using namespace std;
const int N=3e6+7;
int n,m,k[N],id[N],vis[N],cnt,stk[N],loc[N],pre[N],ans1[N],ans2[N];//意义基本与题解一样
vector <int> q[N];
struct node{
int fa[N];
int find(int x){return fa[x]==x?x:fa[x]=find(fa[x]);}
void merge(int x,int y){x=find(x),y=find(y);fa[x]=y;}
};node huan;node lian;//huan 和 lian 分别维护强联通分量和区间的右端点
void calc(int beg){
while(!vis[beg]){stk[++cnt]=beg,id[beg]=cnt,vis[beg]=1;beg=k[beg];}//标记环上的所有点
for(int i=2*cnt;i>=1;i--){
int u=stk[(i-1)%cnt+1];huan.fa[i]=lian.fa[i]=i;pre[i]=loc[i]=0;//另类的初始化别忘了
for(int v:q[u]){
if(id[v]){
v=id[v];v=v>i?v-cnt:v;v=v+cnt<i?v+cnt:v;
pre[i]=max(pre[i],v);v=v<i?v+cnt:v;
if(v<=cnt*2) loc[lian.find(v)]=max(loc[lian.find(v)],huan.find(v));//找最右端的有返祖边的强联通分量
}
}
while(1){
while(1){
int p1=huan.find(i),p2=lian.find(i);
if(p1<loc[p2])huan.merge(p1,p1+1);else break; //合并强联通分量
}
int p1=huan.find(i),p2=lian.find(i);loc[p2]=0;
if(p1!=p2||pre[p2+1]<i||p2==2*cnt)break; //注意这里如果链的右端点已经是区间右端点了也可以跳了
lian.merge(p2,p2+1); //合并区间
}
ans1[u]=min(cnt,lian.find(i)-i+1);ans2[u]=min(cnt,huan.find(i)-i+1);//统计答案。由于复制了两次,显然最后一次更新的答案最优(可以统计的区间更长)
}
}
void init1(){
for(int i=1;i<=cnt;i++)id[stk[i]]=0,stk[i]=0; //注意每次操作后清空序列
cnt=0;
}
void solve(){
cin>>n>>m;for(int i=1;i<=n;i++) cin>>k[i];
for(int i=1,u,v;i<=m;i++) cin>>u>>v,q[v].push_back(u); //记录的是反边!!!
for(int i=1;i<=n;i++) if(!vis[i]) calc(i),init1();
for(int i=1;i<=n;i++) cout<<ans1[i]<<' '<<ans2[i]<<'\n',q[i].clear(),vis[i]=0;//多测不清空,原地见祖宗
}
signed main(){
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int T;cin>>T;while(T--) solve();
}