CF1516E Baby Ehab Plays with Permutations(牛逼计数)

组合数学与DP求解排列问题
本文介绍了一种利用组合数学与动态规划方法解决特定排列生成问题的算法。问题要求计算长度为n的有序排列,在经过k次元素交换后能产生的不同排列数量,并要求答案对1e9+7取模。文章首先给出了朴素解法,然后提出了一种更高效的进阶解法,通过多项式卷积技巧降低复杂度。

题意

给定 n,Kn,Kn,K,每次可以交换两个数,问长度为 nnn 的有序排列经过 kkk 次交换后能生成多少排列,答案对 1e9+71e9+71e9+7 取模。
n≤109,K≤200n\le 10^9, K\le 200n109,K200

分析

如果你有看题解,会发现我只是翻译了一遍题解=.=
但是这也没有办法,因为我实在太菜了+。+

普通做法

dpn,kdp_{n,k}dpn,k 表示长度为 nnn 的排列必须经过 kkk 次交换才能生成的排列数(也就是小于 kkk 次生成不了这个排列),那么 ansk=dpn,k+dpn,k−2+dpn,k−4...+dpn,k%2ans_k=dp_{n,k}+dp_{n,k-2}+dp_{n,k-4}...+dp_{n,k\%2}ansk=dpn,k+dpn,k2+dpn,k4...+dpn,k%2
由于 kkk 次操作后,最多 2k2k2k 个位置发生改变。我们可以枚举发生改变的位置数 n′n'n。现在问题就变成了长度为 n′n'n 的有序排列,交换 kkk 次后的排列数,其中 n′≤2kn'\le 2kn2k
但是这样统计会发生重复,于是我们钦定生成的排列必须是一个错位排列,也就是 kkk 次交换后,对于任意 i∈[1,n′]i\in[1,n']i[1,n]i≠pii\neq p_ii=pi。这样子我们对每个 n′n'n,都会产生独特的贡献。
接下来我们考虑怎么求一个大小为 nnn 的排列必须经过 kkk 次交换后才能生成的错位排列数。
fn,kf_{n,k}fn,k 表示大小为 nnn 的排列必须经过 kkk 次交换后能生成的错位排列数,gn,kg_{n,k}gn,k 表示大小为 nnn 的排列必须经过 kkk 次交换后才能生成的排列数,根据容斥原理,我们可以得到:fn,k=∑i=0n(−1)iC(n,i)gn−i,kf_{n,k}=\sum\limits_{i=0}^{n}(-1)^iC(n,i)g_{n-i,k}fn,k=i=0n(1)iC(n,i)gni,k
接下来我们考虑计算 gn,kg_{n,k}gn,k,我们倒着思考,看看有多少排列必须经过 kkk 次交换后会变成有序排列。我们考虑递推,如果第 nnn 个位置已经是 nnn,那么有 gn−1,kg_{n-1,k}gn1,k 个这种排列,否则需要先把 nnn 放到第 nnn 个位置,剩下的再进行排列,有 (n−1)gn−1,k−1(n-1)g_{n-1,k-1}(n1)gn1,k1个这种排列。因此,gn,k=gn−1,k+(n−1)gn−1,k−1g_{n,k}=g_{n-1,k}+(n-1)g_{n-1,k-1}gn,k=gn1,k+(n1)gn1,k1
那么必须经过 kkk 次交换生成的排列数 dpn,kdp_{n,k}dpn,k 就为 ∑i=0min(n,2k)C(n,i)fi,k\sum\limits_{i=0}^{min(n,2k)}C(n,i)f_{i,k}i=0min(n,2k)C(n,i)fi,k
这样子好像就做完了呀=.=
代码如下。复杂度是 O(K3)O(K^3)O(K3) 的。

#include <bits/stdc++.h>
#define all(x) x.begin(), x.end()
#define pii pair<int, int>
#define fi first
#define se second
using namespace std;
typedef long long LL;
const int N = 405, maxn = 400, mod = 1e9 + 7;
int c[N][N], dp[N][N], ans[2], inv[N];
int C(int n, int m){
	int s = 1;
	for(int i = n; i >= n - m + 1; i--) s = (LL)s * i % mod;
	for(int i = 1; i <= m; i++) s = (LL)s * inv[i] % mod;
	return s;
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	int n, k;
	cin >> n >> k;
	inv[1] = 1;
	for(int i = 2; i <= maxn; i++) inv[i] = (LL)(mod - mod / i) * inv[mod % i] % mod;
	for(int i = 0; i <= maxn; i++){
		c[i][0] = dp[i][0] = 1;
		for(int j = 1; j <= i; j++){
			c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
			dp[i][j] = (dp[i - 1][j] + (LL)(i - 1) * dp[i - 1][j - 1] % mod) % mod;
		}
	}
	ans[0] = 1;
	for(int i = 1; i <= k; i++){
		for(int j = 0; j <= min(k * 2, n); j++){
			int s1 = C(n, j), s2 = 0;
			for(int t = 0, cof; t <= j; t++){
				if(t % 2) cof = mod - 1;
				else cof = 1;
				s2 = (s2 + (LL)cof * c[j][t] % mod * dp[j - t][i] % mod) % mod;
			}
			ans[i % 2] = (ans[i % 2] + (LL)s1 * s2 % mod) % mod;
		}
		cout << ans[i % 2] << ' ';
	}
	return 0;
}

进阶做法

如果 kkk 更大的话,我们应该怎么做这题呢?
我们得重新考虑 dpn,kdp_{n,k}dpn,k 的求法。根据上面的推理,我们也可以得到 dpn,k=dpn−1,k+(n−1)dpn−1,k−1dp_{n,k}=dp_{n-1,k}+(n-1)dp_{n-1,k-1}dpn,k=dpn1,k+(n1)dpn1,k1。我们从另一个意义上来看 dpn,kdp_{n,k}dpn,k,会发现是从 [0,n−1][0,n-1][0,n1] 中选出 kkk 个数乘起来,再求和。形式化的,dpn,k=∑s⊆{0,1,2,...,n−1},∣s∣=k∏x∈sxdp_{n,k}=\sum\limits_{s\subseteq\{0,1,2,...,n-1\},|s|=k}\prod\limits_{x\in s}xdpn,k=s{0,1,2,...,n1},s=kxsx
我们考虑倍增求 dpn,k,k∈[0,K]dp_{n,k},k\in[0,K]dpn,k,k[0,K],假设目前得到了 dpndp_{n}dpn 的生成函数,我们要求 dp2ndp_{2n}dp2n 的生成函数。令 dpn,k′=∑s⊆{n,n+1,n+2,...,2n−1},∣s∣=k∏x∈sx=∑s⊆{0,1,2,...,n−1},∣s∣=k∏x∈s(x+n)dp'_{n,k}=\sum\limits_{s\subseteq\{n,n+1,n+2,...,2n-1\},|s|=k}\prod\limits_{x\in s}x=\sum\limits_{s\subseteq\{0,1,2,...,n-1\},|s|=k}\prod\limits_{x\in s}(x+n)dpn,k=s{n,n+1,n+2,...,2n1},s=kxsx=s{0,1,2,...,n1},s=kxs(x+n)。我们将 dpndp_ndpndpn′dp'_ndpn 做一次卷积,就得到了 dp2ndp_{2n}dp2n
现在关键就是求 dpn′dp'_{n}dpn 了。而 dpn,k′=∑s⊆{0,1,2,...,n−1},∣s∣=k∏x∈s(x+n)=∑(x1+n)(x2+n)...(xk+n)dp'_{n,k}=\sum\limits_{s\subseteq\{0,1,2,...,n-1\},|s|=k}\prod\limits_{x\in s}(x+n)=\sum(x_1+n)(x_2+n)...(x_k+n)dpn,k=s{0,1,2,...,n1},s=kxs(x+n)=(x1+n)(x2+n)...(xk+n),假设 kkk 项里面选了 jjjxxx,那么会有 k−jk-jkjnnn,我们枚举 jjj,那么 jjjxxx 的贡献就是 nk−j×dpn,jn^{k-j}\times dp_{n,j}nkj×dpn,j,而总共有 nnn 个数,剩下的 xxx 选法就是 C(n−j,k−j)C(n-j,k-j)C(nj,kj)。因此,dpn,k′=∑j=0kC(n−j,k−j)nk−jdpn,kdp'_{n,k}=\sum\limits_{j=0}^{k}C(n-j,k-j)n^{k-j}dp_{n,k}dpn,k=j=0kC(nj,kj)nkjdpn,k
来到这里就做完了!
这样子的复杂度可以是 O(K3logn)O(K^3logn)O(K3logn),也可以做到 O(K2logn)O(K^2logn)O(K2logn)
其实容易注意到 dp′dp'dp 的求法也是卷积的形式,如果模数是 nttnttntt 模数或者使用任意模数 nttnttntt ,我们这题可以在 O(KlogKlogn)O(KlogKlogn)O(KlogKlogn) 复杂度内解决。
代码如下。

#include <bits/stdc++.h>
#define all(x) x.begin(), x.end()
#define pii pair<int, int>
#define fi first
#define se second
using namespace std;
typedef long long LL;

void debug_out(){
    cerr << endl;
}
template<typename Head, typename... Tail>
void debug_out(Head H, Tail... T){
    cerr << " " << to_string(H);
    debug_out(T...);
}
#ifdef local
#define debug(...) cerr<<"["<<#__VA_ARGS__<<"]:",debug_out(__VA_ARGS__)
#else
#define debug(...) 55
#endif

typedef vector<int> poly;
const int N = 205, mod = 1e9 + 7;
int n, k, c[N][N], inv[N], ans[2];
int C(int n, int m){
	int s = 1;
	for(int i = n; i >= n - m + 1; i--) s = (LL)s * i % mod;
	for(int i = 1; i <= m; i++) s = (LL)s * inv[i] % mod;
	return s;
}
poly solve(int n){
	if(n == 1) return {1};
	int m = n / 2;
	poly po(k + 1);
	po[0] = 1;
	for(int i = 1; i <= k; i++) po[i] = (LL)po[i - 1] * m % mod;
	poly f = solve(m), g, h;
	f.resize(k + 1);
	g.resize(k + 1);
	for(int i = 0; i <= min(m, k); i++){
		for(int j = 0; j <= i; j++){
			g[i] = (g[i] + (LL)C(m - j, i - j) * po[i - j] % mod * f[j] % mod) % mod;
		}
	}
	if(n & 1){
		for(int i = k; i >= 1; i--) g[i] = (g[i] + (LL)(n - 1) * g[i - 1] % mod) % mod;
	}
	h.resize(k + 1);
	for(int i = 0; i <= min(n, k); i++){
		for(int j = 0; j <= i; j++){
			h[i] = (h[i] + (LL)f[j] * g[i - j] % mod) % mod;
		}
	}
	return h;
}
int main(){
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> n >> k;
	inv[1] = 1;
	for(int i = 2; i <= k; i++) inv[i] = (LL)(mod - mod / i) * inv[mod % i] % mod;
	for(int i = 0; i <= k; i++){
		c[i][0] = 1;
		for(int j = 1; j <= i; j++) c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
	}
	poly f = solve(n);
	ans[0] = 1;
	for(int i = 1; i <= k; i++){
		ans[i % 2] += f[i];
		debug(i, f[i]);
		if(ans[i % 2] >= mod) ans[i % 2] -= mod;
		cout << ans[i % 2] << ' ';
	}
	return 0;
}

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值