同色不相邻的方案数求解
引例:
有\(n\)种颜色的小球,
每种颜色的小球有\(a_i\)个,即一共有\(\sum_{i=1}^n a_i\)个小球。
现在要求把这些小球排同色不相邻的方案数求解成一行,要求同种颜色的小球不相邻。
求方案数,答案对\(10^9+7\)取模。 提交网址:web
前言
下面的算法都只考虑 同色小球之间无区别 的方案数。
即可重排列的情况。
如果同色小球之间有区别,为不重排列,那么对应乘上 \(\prod (a_i!)\) 即可。
方法1:动态规划
解法
从前往后枚举每种颜色的小球。
设\(f_{i,j}\)表示考虑完前\(i\)种小球,存在\(j\)个违法相邻的方案数。
转移首先考虑将\(a_i\)个小球分为\(k\)段,一共\(\binom{a_i-1}{k-1}\)种方法。
这一共造成了\(a_i - k\)个新的不合法情况。
枚举把其中\(l\)块插入原先的不合法位置之间,那么一共消除了\(l\)个不合法相邻。
这一共有\(\binom{j}{l}\)种选择,剩下的块有\(\binom{sum + 1 - j}{k - l}\)种选择。
所以转移方程式:
\[f_{i,j-l+(a_i-k)} = \sum f_{i,j} \binom{a_i-1}{k-1} \binom{j}{l} \binom{sum + 1 - j}{k - l} \]
其中\(sum = \sum_{e=1}^{i-1} a_e\) ;
满复杂度:\(O(n(\sum a)a_{max}^2)\),但是非常不满,实测跑 \(n,\sum a\leq 1000\)并不虚。
这种方法好写好理解,但是适用性较窄(不方便添加整段权值)。
实现代码
#include<bits/stdc++.h>
#define ll long long
#define maxn 2005
#define mod 1000000007
using namespace std;
ll n , q, x , sum , cur;
ll a[maxn] , fac[maxn] , c[maxn][maxn] , f[3][maxn];
void Pre(){
fac[0] = 1;
for(int i = 1; i <= n; i ++)fac[i] = 1ll*i*fac[i-1] % mod;
c[0][0] = 1;
for(int i = 1; i <= n; i ++){
for(int j = 1; j < i; j ++)
c[i][j] = ( c[i-1][j] + c[i-1][j-1] ) % mod;
c[i][0] = c[i][i] = 1;
}
return;
}
int main(){
cin >> n >> q;
for(int i = 1; i <= n; i ++) a[i] = gi();
Pre();
sum = 0; cur = 0;
f[cur][0] = 1;
for(int i = 1; i <= n; i ++){
cur ^= 1;
for(int t = 0; t <= a[i]+sum+1; t ++)
f[cur][t] = 0;
for(int k = 1; k <= a[i] && k <= sum+1; k ++)
for(int j = 0; j <= sum; j ++)
for(int l = 0; l <= k && l <= j; l ++){
f[cur][j-l+a[i]-k] +=
1ll*f[cur^1][j]*c[a[i]-1][k-1]%mod*(1ll*c[j][l]*c[sum+1-j][k-l]%mod)%mod;
f[cur][j-l+a[i]-l] %= mod;
}
sum += a[i];
}
cout<<f[cur][0]; return 0;
}
方法2:容斥原理
解法
全部不相邻的方案数 = 全部不相临的排列数 - 1个相邻的排列数 + 2个相邻的排列数 ......
直接枚举不好算,单独考虑一种小球的贡献。
由于可重排列:\(P = \frac{(\sum a)!}{ \prod (a!)}\),所以先只乘\(\frac{1}{a!}\),最后乘上\((\sum a)!\)即可。
我们可以列出有\(a_i\)个小球的容斥多项式:
\[f(a_i) = \sum_{i = 1} ^ {a_i} (-1)^{a_i-i} \frac{1}{i!}E(a_i , i)\]
其中\(E(i,j)\)表示\(i\)个小球因为违法相邻而实际只有\(j\)段的方案数。
显然就是要保留\(i-1\)个间隔中的\(j-1\)个,所以\(E(i,j) = \binom{i-1}{j-1}\)。
把每种小球的容斥多项式卷积卷起来就可以得到总的容斥式。
对于最后的总容斥式的每一项,乘上对应的阶乘(可重排列的分子),再加起来即可。
总时间复杂度:\(O(n(\sum a)a_{max})\)。
这种做法较难理解、容易写错,但是复杂度低,适用性好(添加权值直接在计算系数时乘上即可)。
实现代码
//http://acm.hdu.edu.cn/showproblem.php?pid=4532
#include<bits/stdc++.h>
#define RG register
#define IL inline
#define N 505
using namespace std;
const int mod = 1000000007 ;
int dp[10*N],C[N+5][N+5],E[N+5][N+5],fact[N+5],a[N+5],n,sum,ans;
int inv[N+5],inv_fact[N+5],coef[N+5];
IL void Pre(){
C[0][0] = 1;
for(RG int i = 1,j; i <= N; i ++)
for(j = C[i][0] = 1; j <= i; j ++)
C[i][j] = (C[i-1][j-1] + C[i-1][j]) % mod ;
for(RG int i = 1,e; i <= N; i ++)
for(RG int j = i,e = 1; j >= 1; j --)
E[i][j] = (mod + C[i-1][j-1] * e) % mod, e = -e ;
fact[0] = inv[0] = 1 ;
inv[1] = inv_fact[0] = 1 ;
for(RG int i = 1; i <= N; i ++){
fact[i] = 1ll * i * fact[i-1] % mod;
if(i ^ 1)inv[i] = 1ll * (mod - mod/i) * inv[mod % i] % mod;
inv_fact[i] = 1ll * inv_fact[i-1] * inv[i] % mod ;
}return ;
}
IL void upt(RG int &x,RG int y){x += y; if(x>=mod) x-= mod ; }
int main(){
Pre() ;
scanf("%d",&n) ;
sum = 0; dp[0] = 1 ;
for(RG int i = 1; i <= N; i ++) dp[i] = 0;
for(RG int sq = 1; sq <= n; sq ++){
scanf("%d",&a[sq]) ; // 有一种小球的个数为a个.
for(RG int k = 1; k <= a[sq]; k ++)
coef[k] = 1ll * E[a[sq]][k] * inv_fact[k] % mod ;
//coef是 当前这个小球的容斥多项式 的每一项的系数
for(RG int j = sum; j >= 0; j --){
RG int tmp = dp[j] ;
dp[j] = 0;
for(RG int k = 0; k <= a[sq]; k ++)
upt(dp[j + k] , 1ll * coef[k] * tmp % mod) ;
}
sum += a[sq]; //dp记录的是最后的总容斥式的每一项系数.
}
ans = 0;
for(RG int k = 0; k <= sum; k ++) upt(ans , 1ll * dp[k] * fact[k] % mod) ;
printf("%d\n" , ans) ; return 0;
}