简洁的题面,深邃的思想。
首先,一个经典的套路是:
对于序列中涉及到对于 \(a_{a_i}\) 和 \(a_i\) 进行操作的问题,一般可以考虑建立 \((i,a_i)\) 的内向基环树或者 \((a_i,i)\) 的外向基环树转化为图论问题。
我们建立 \((i,a_i)\) 的内向基环树,\(swap(a_i,a_{a_i})\implies a'_i=a_{a_i},a'_{a_i}=a_i\)。这等价于将 \(a_i\) 改为自环,然后令 \(i\) 的父亲变为 \(a_i\) 的父亲。
让我们想想,这样操作是很复杂的。
如何简化操作,并且支持快速地查找到某个点的实际父亲?
先不考虑环,一棵树的情况怎么办?
可以对被删除的边打标记,这样在删除了若干边之后,点 \(x\) 的实际父亲是往上找的第一条未被标记的边的指向点。
而为了对应自环的限制,则点 \(x\) 最多只能有一个儿子到它的边被标记。
所以最终每个点有两种情况,第一种是有一个儿子到它的边被标记,第二种是没有。
方案数为 \(in_{x}+1\)。
那么这棵树的总方案是 \(\prod_{i=1}^n(in_i+1)\)
转为基环树?
可以发现,对于一个环而言,若有且仅有边 \((cir_i,cir_{i+1})\) 没有被标记,也等价于 \(a'_{cir_i}=cir_{i}\)。全部都被自环。
并且环不可以被全部标记。
容斥原理,钦定某条边不被标记且其他边都被标记,则方案数是该边端点的入度。
有 \(-1+\sum_{i=1}^{num}in_{cir_{i}}\) 个方案重复了,且有一个方案不合法。
所以容斥后,环上的贡献是 \(\prod_{i=1}^{num}in_{cir_i}-sum_{i=1}^{num}in_{cir_i}+1-1=\prod_{i=1}^{num}in_{cir_i}-sum_{i=1}^{num}in_{cir_i}\)
然后在把其他不在环上的点 \(i\) 的 \(in_i+1\) 乘上去就行了。
对于每一个基环树的方案全部乘起来就好了。
#define int long long
const int p=1e9+7;
#define N 2050500
vector<int>g;
int dcc,vis[N],c[N],in[N],head[N],flag,tag,ver[N],nxt[N],tot,n,m,a[N],num,cir[N];
void ad(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void add(int u,int v){
ad(u,v);ad(v,u);
}
void pai(int u){
c[u]=dcc;g.push_back(u);
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!c[v])pai(v);
}
}
int get(int u){
num=0;g.clear();++dcc;pai(u);
for(auto x:g)vis[x]=0;
while(!vis[u])vis[u]=1,u=a[u];
int v=a[u];cir[++num]=u;
while(v!=u)cir[++num]=v,v=a[v];
for(auto x:g)vis[x]=0;
for(int i=1;i<=num;i++)vis[cir[i]]=1;
int s=0,t=1;
for(int i=1;i<=num;i++)t=t*(in[cir[i]]+1)%p,s+=in[cir[i]];
t=(t-s+p)%p;
for(auto x:g){
if(!vis[x])t=t*(in[x]+1)%p;
}
return t;
}
signed main(){
ios::sync_with_stdio(false);
int ans=1,n;cin>>n;
for(int i=1;i<=n;i++){
cin>>a[i];in[a[i]]++;add(i,a[i]);
}
for(int i=1;i<=n;i++)if(!c[i])ans=ans*get(i)%p;
ans=(ans+p)%p;
cout<<ans<<"\n";
}
标签:标记,int,++,CF1863G,cir,num,基环树
From: https://www.cnblogs.com/oierpyt/p/17707841.html