P12992
简化题意
\(n\) 个点的无向图,初始没有边。重复以下操作:
从所有 \(\deg_u=0\lor\deg_v=0\) 的点对 \((u,v)\) 中等概率选一个,加边 \((u,v)\)。
无法进行操作时停止。给定 \(m\),求恰好 \(m\) 次操作后停止的概率,答案对给定质数 \(P\) 取模。这里的 \(n,m\) 对应原题的 \(M,M-K\)。
\(n\le 5\)
暴搜即可。
点击查看代码
//by lovely Autumn_Rain
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
int n,m,P;
vector<int>deg;
ll ans=0;
ll qpow(ll a,ll b){
ll res=1;
while(b){
if(b&1)res=res*a%P;
a=a*a%P;b>>=1;
}
return res;
}
#define pii pair<int,int>
#define fi first
#define se second
#define pb push_back
#define mp make_pair
void dfs(int cnt,vector<int>°,ll res){
bool f=0;
For(i,1,n)if(deg[i]==0){f=1;break;}
if(!f){
if(cnt==m)ans=(ans+res)%P;
return;
}
vector<pii>G;
ll num=0;
For(u,1,n-1){
For(v,u+1,n){
if(!deg[u]||!deg[v]){
G.pb(mp(u,v));
num++;
}
}
}
if(num==0)return;
ll inv=qpow(num,P-2)%P;
for(pii &x:G){
int u=x.fi,v=x.se;
deg[u]++;deg[v]++;
dfs(cnt+1,deg,res*inv%P);
deg[u]--;deg[v]--;
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>P;
deg.resize(n+1,0);
dfs(0,deg,1);
cout<<ans%P;
return 0;
}
\(n\le 4\times 10^3\)
考虑 \(O(n^2)\) 动态规划。\(f_{i,j}\) 表示操作 \(i\) 次,现在还有 \(j\) 个孤立点的概率。
点击查看代码
//by lovely Autumn_Rain
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
int n,m;
ll P;
const int N=4e3+10;
ll f[N][N];
ll qpow(ll a,ll b){
ll res=1;
while(b){
if(b&1)res=res*a%P;
a=a*a%P;
b>>=1;
}
return res;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>P;
f[0][n]=1;
For(i,0,m-1){
for(int j=n;j>=1;j--){
if(f[i][j]==0)continue;
ll t=(n*(n-1)/2-(n-j)*(n-j-1)/2+P)%P;
ll inv=qpow(t,P-2)%P;
if(j>=2)f[i+1][j-2]=(f[i+1][j-2]+j*(j-1)/2*inv%P*f[i][j]%P+P)%P;
if(j>=1)f[i+1][j-1]=(f[i+1][j-1]+j*(n-j)*inv%P*f[i][j]%P+P)%P;
}
}
cout<<(f[m][0]+P)%P;
return 0;
}
\(m=n-1\)
打表或者推式子,答案为 \(\frac{2^{n-2}}{Catalan_{n-1}}\)。
点击查看代码
//by lovely Autumn_Rain
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
int n,m;
ll P;
const int N=4e3+10;
ll f[N][N];
ll qpow(ll a,ll b){
ll res=1;
while(b){
if(b&1)res=res*a%P;
a=a*a%P;
b>>=1;
}
return res;
}
ll cal(int x){
ll inv=qpow(x+1,P-2)%P;
ll res=1;
For(i,x+1,2*x){
res=res*i%P;
int ar=i-x;
ll t=qpow(ar,P-2)%P;
res=res*t%P;
}
return res*inv%P;
}
void solve(){
ll res=cal(n-1);
ll inv=qpow(res,P-2)%P;
cout<<qpow(2,n-2)*inv%P;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m>>P;
if(m==n-1){solve();return 0;}
f[0][n]=1;
For(i,0,m-1){
for(int j=n;j>=1;j--){
if(f[i][j]==0)continue;
ll t=(n*(n-1)/2-(n-j)*(n-j-1)/2+P)%P;
ll inv=qpow(t,P-2)%P;
if(j>=2)f[i+1][j-2]=(f[i+1][j-2]+j*(j-1)/2*inv%P*f[i][j]%P+P)%P;
if(j>=1)f[i+1][j-1]=(f[i+1][j-1]+j*(n-j)*inv%P*f[i][j]%P+P)%P;
}
}
cout<<(f[m][0]+P)%P;
return 0;
}
\(O(n)\) 正解
两种操作情况。
- 一个孤立点和一个非孤立点操作。
- 两个孤立点连边。
假设做了 \(c_0\) 次操作一,\(c_1\) 次操作二,那么有:
\(\because c_0+c_1=m\)。
\(\ \ \ \ c_0+2\times c_1=n\)。
\(\therefore c_1=n-m\)。
不妨假设 \(k=n-m\),那么 \(k\) 就是操作二的数目。
总共有 \(\binom{n}{2}\) 条边,这些边任意顺序操作共 \(\binom{n}{2}!\) 种情况。我们考虑在所有情况里筛选符合题意的排列。每次操作到不合法的情况就操作跳过(可以发现这并不影响概率)。
此时可以进行操作二,要求两个点先前没有连过边,也就是不存在先前就已经操作完的相邻边。
我们希望刚好选出 \(k\) 条这样的操作二边。
这个问题太难了,考虑至少选出 \(k\) 条操作二边怎么做,也就是选 \(x\geq k\) 条出来。如果解决了这个问题,就可以容斥一下 \(k\) 的贡献,最终得出刚好 \(=k\) 条操作二边的答案。具体的容斥系数是 \((-1)^{x-k}\binom{x}{k}\)。
此时的排列方案数相当于一个 DAG 的拓扑序个数,要求这 \(x\geq k\) 条边比相邻的边要更早选择。我们人为钦定一下这 \(x\) 条边的顺序(最后再乘上排列数 \(\binom{n}{2x}(2x-1)!!\) 就可以了)。
现在相当于,有 \(\binom{n}{2}\) 个点,已经连下来一条链 \((0,1),(2,3)\cdots(2x-2,2x-1)\)。对于剩下的边,要求它们通过某种链接方法,保证自己不会成为符合操作二要求的边,且满足可以不破坏钦定的 \(x\) 条操作二的边。
因此,如果该边和至少某个特殊边相邻,那么它在树上的父亲就是它相邻的特殊边中编号最靠前的一个。如果它不和特殊边相邻,顺序不重要,可以直接连到 \((2x-2,2x-1)\)。这样连出来会是一颗树。答案就是这颗树的拓扑序个数。
为什么是一棵树?
选出来的 \(x\) 条边钦定顺序编号后:\((0,1),(2,3)\cdots(2x-2,2x-1)\)。
对于边 \((i,j)\),有如下情况:
-
\(i>2x-1 \ \land \ j>2x-1\)。
此时边 \((i,j)\) 没有限制。
-
\(\exists \ i \lor j\) 是钦定边的某端点。
选编号最靠前的那个端点作为另一个点的父亲。
此时发现,每个点父亲是唯一的。那么这个 DAG 就是一棵树。
\(n\) 个点的树的拓扑序个数:
我们发现按照上面那种连接方法是可以算出每个子树的 \(sz\) 的。非钦定边都是叶子结点,可以不用考虑。与 \((2x-2,2x-1)\) 相邻的边有 \(2n-4\) 条。与 \((2x-4,2x-3)\) 相邻且不与 \((2x-2,2x-1)\) 相邻的边有 \(2n-6\) 条……
因此钦定点的 \(sz_i=1+\sum_{j\le i}(2n-4j)\)。
拓扑序个数:
因为求的是概率,所以去掉了全情况 \(\binom{n}{2}!\),答案是:
因为一开始钦定 \(x\) 条边一个顺序。总共有 \(x!\) 种顺序,每种顺序的答案都是这个,所以最终答案里分母的 \(x!\) 抵掉了。
算上容斥和顺序数,最终结果:
时间复杂度 \(O(n)\)。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define For(i,l,r) for(int i=l;i<=r;i++)
int n,m,k,p;
const int N=5e6+10;
ll fac[N],ifc[N];
ll f[N],g[N],s[N];
ll qpow(ll a,ll b){
ll res=1;
while(b){
if(b&1)res=res*a%p;
a=a*a%p;
b>>=1;
}
return res;
}
ll C(int n,int m){
if(n<m||m<0)return 0;
return (fac[n]*ifc[m]%p*ifc[n-m]%p)%p;
}
ll ans;
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>k>>p;k=n-k;
if(k*2>n){cout<<0<<'\n';return 0;}
m=n>>1;
For(i,1,m){
s[i]=s[i-1]+2*(n-2*i)+1;
s[i]%=p;
}
fac[0]=f[0]=1;
For(i,1,m){
fac[i]=i*fac[i-1]%p;
f[i]=s[i]*f[i-1]%p;
}
ifc[m]=qpow(fac[m],p-2);
g[m]=qpow(f[m],p-2);
for(int i=m;i>=1;i--){
ifc[i-1]=i*ifc[i]%p;
g[i-1]=s[i]*g[i]%p;
}
For(i,1,m){
ll t=1ll*(n-2*i+1)*(n-2*i+2)/2;
f[i]=(t%p*f[i-1])%p;
}
For(i,k,m){
if((i-k)&1)ans=(ans+(2*p-C(i,k))*(g[i]%p*f[i]%p)%p)%p;
else ans=(ans+C(i,k)*(g[i]%p*f[i]%p)%p)%p;
}
cout<<ans%p;
return 0;
}

浙公网安备 33010602011771号