CF1863G

发布时间 2023-09-17 09:36:59作者: weakpyt

简洁的题面,深邃的思想。

首先,一个经典的套路是:

对于序列中涉及到对于 \(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"; 
}