连续段 DP

连续段 DP:
在一些数排列的问题中,往往会遇到感觉是 DP 但是状态都列不来的情况,而连续段 DP 就是一个解决排列计数的利器。
具体思路是依次插入每个元素(通常是排序后从小到大/从大到小)。考虑当前元素插入到哪个位置,这样的话状态就需要记下当前插到了哪个数以及当前连续段个数。
转移时考虑:当前元素新开一个连通块,接在连通块的首/尾,连接两个连通块。

洛谷 P5999:
考虑把数字从小到大插入到序列中,从而得到一个排列。
定义 \(f_{i,j}\) 表示枚举到了第 \(i\) 个数,已经有了 \(j\) 个连续段的方案数。
那么每次加入数字有两种情况:
把两个段合并。有 \(j-1\) 个位置可以合并。\(f_{i,j}+=f_{i-1,j+1}\times j\)
重新创建一个段,但是注意 \(s\) 在最左边,\(t\) 在最右边。\(f_{i,j}+=f_{i-1,j-1}\times (j-[j>s]-[j>t])\)
\(i\) 等于 \(s\)\(t\) 时也要特判,考虑他们在边界单独开一个段,要么和边界的段合并,所以是 \(f_{i,j}=f_{i-1,j-1}+f_{i-1,j}\)

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 2005, mod = 1e9 + 7;
int n, s, t, f[N][N];

int main() {
	scanf("%d%d%d", &n, &s, &t);
	f[1][1] = 1;
	for (int i = 2; i <= n; ++i)
		for (int j = 1; j <= i; ++j) {
			if (i != s && i != t)
				f[i][j] = 1ll * (j - (i > s) - (i > t)) * f[i - 1][j - 1] % mod + 1ll * j * f[i - 1][j + 1] % mod, f[i][j] %= mod;
			else
				f[i][j] = (f[i - 1][j] + f[i - 1][j - 1]) % mod;
		}
	printf("%d", f[n][1]);
	return 0;
}

洛谷 P7967:
先把磁铁按照 \(r\) 从小到大排序。
\(f_{i,j,k}\) 表示插入了 \(i\) 个磁铁,\(j\) 个连通块,占用了 \(k\) 个空位的方案数。
这里的连通块是指:这些磁铁之间任意删除一个空都会导致他们吸引。
三种转移:
当前元素新开一个连通块:\(f_{i,j,k}+=f_{i-1,j-1,k-1}\times j\)
接在连通块的首/尾:\(f_{i,j,k}+=f_{i-1,j,k-r_i}\times 2\times j\)
连接两个连通块:\(f_{i,j,k}+=f_{i-1,j+1,k-2r_i+1}\times j\)
最终统计答案就是插板。

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 55, M = 10005, mod = 1e9 + 7;
int n, m;
int a[N];
int f[N][N][M];
int fac[M], inv[M];

int qpow(int x, int y) {
	int res = 1;
	while (y) {
		if (y & 1) res = 1ll * res * x % mod;
		x = 1ll * x * x % mod;
		y >>= 1;
	}
	return res;
}

void init(int n) {
	fac[0] = 1;
	for (int i = 1; i <= n; ++i) fac[i] = 1ll * fac[i - 1] * i % mod;
	inv[n] = qpow(fac[n], mod - 2);
	for (int i = n - 1; ~i; --i) inv[i] = 1ll * inv[i + 1] * (i + 1) % mod;
}

int C(int n, int m) {
	if (n < 0 || m < 0 || n < m) return 0;
	return 1ll * fac[n] * inv[n - m] % mod * inv[m] % mod;
}

void add(int &a, int b) {
	a += b;
	if (a >= mod) a -= mod;
}

int main() {
	scanf("%d%d", &n, &m);
	init(m);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	sort(a + 1, a + n + 1);
	f[0][0][0] = 1;
	for (int i = 1; i <= n; ++i)
		for (int j = 1; j <= i; ++j)
			for (int k = 1; k <= m; ++k) {
				add(f[i][j][k], 1ll * f[i - 1][j - 1][k - 1] * j % mod);
				if (k >= a[i]) add(f[i][j][k], 1ll * f[i - 1][j][k - a[i]] * j * 2 % mod);
				if (k >= a[i] * 2 - 1) add(f[i][j][k], 1ll * f[i - 1][j + 1][k - (a[i] * 2 - 1)] * j % mod);
			}
	int ans = 0;
	for (int i = 1; i <= m; ++i) add(ans, 1ll * f[n][1][i] * C(m - i + n, n) % mod);
	printf("%d", ans);
	return 0;
}

LOJ 2743:
这题涉及到另外一个很重要的套路。
首先是要将 \(a\) 从小到大排序。
引出一个处理绝对值的技巧:考虑每个绝对值能表示成 \(\sum_{i=l}^{r}a_{i+1}-a_{i}\)
我们对于每个 \(a_{i+1}-a_{i}\) 统计其贡献次数即可。
他的贡献次数就是前 \(i\) 个数构成连续段的端点个数,因为端点旁边以后一定会插入比 \(i\) 大的数,就会产生贡献,有费用提前计算的感觉。
然后就可以用连续段 DP 来解决这个问题了,因为边界(端点是 \(1/n\))并不会产生贡献所以要把它记录进状态。
\(f_{i,j,k,l}\) 表示插入了 \(i\) 个数,有 \(j\) 个连续段,贡献和是 \(k\)\(l\) 个边界已经固定。每次增量法考虑一个新数的插入,新的贡献和为 \(k^{′}=k+(a_{i+1}-a_i)\times (2\times j-l)\)

  • 作为一个新的连续段插入到不为边界的空隙处:\(f_{i+1,j+1,k^{′},d}+=f_{i,j,k,l}\times (j+1-l)\)
  • 合并两个连续段:\(f_{i+1,j-1,k^{′},l}+=f_{i,j,k,l}\times (j-1)\)
  • 添加到某个连续段的非边界端点处:\(f_{i+1,j,k^{′},l}+=f_{i,j,k,l}\times (2\times j-l)\)
  • 作为一个新的连续段插入到边界:\(f_{i+1,j+1,k^{′},l+1}+=f_{i,j,k,l}\times (2-l)\)
  • 添加到某个连续段作为边界:\(f_{i+1,j,k^{′},l+1}+=f_{i,j,k,l}\times (2-l)\)

最后答案是 \(\sum f_{n,1,i,2}\)

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 105, M = 1005, mod = 1e9 + 7;
int n, m;
int a[N];
int ans;
int f[N][N][M][3];

void add(int &a, int b) {
	a += b;
	if (a >= mod) a -= mod;
}

int main() {
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);
	if (n == 1) return printf("%d", 1), 0;
	sort(a + 1, a + n + 1);
	f[0][0][0][0] = 1;
	for (int i = 0; i < n; ++i)
		for (int j = 0; j <= i; ++j)
			for (int k = 0; k <= m; ++k)
				for (int l = 0; l <= 2; ++l) {
					int K = k + (2 * j - l) * (a[i + 1] - a[i]), t = f[i][j][k][l];
					if (K > m || !t) continue;
					add(f[i + 1][j + 1][K][l], 1ll * t * (j + 1 - l) % mod);
					if (j) add(f[i + 1][j - 1][K][l], 1ll * t * (j - 1) % mod), add(f[i + 1][j][K][l], 1ll * t * (2 * j - l) % mod);
					if (l < 2) add(f[i + 1][j + 1][K][l + 1], 1ll * t * (2 - l) % mod);
					if (l < 2 && j) add(f[i + 1][j][K][l + 1], 1ll * t * (2 - l) % mod);
				}
	int ans = 0;
	for (int i = 0; i <= m; ++i) add(ans, f[n][1][i][2]);
	printf("%d", ans);
	return 0;
}

CF1515E:
\(f_{i,j}\) 表示 \(i\) 个元素,形成了 \(j\) 个连续段的方案数。

  • 作为一个新的连续段,\(f_{i+1,j+1}+=f_{i,j}\times (j+1)\)
  • 插入到原有连续段的首/尾,\(f_{i+1,j}+=f_{i,j}\times 2\times j,f_{i+2,j}+=f_{i,j}\times 2\times j\)
  • 合并两个连续段,\(f_{i+2,j-1}+=f_{i,j}\times 2\times(j-1),f_{i+3,j-1}+=f_{i,j}\times (j-1)\)

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 405;
int n, mod;
int f[N][N];

void add(int &a, int b) {
	a += b;
	if (a >= mod) a -= mod;
}

int main() {
	scanf("%d%d", &n, &mod);
	f[1][1] = 1;
	for (int i = 1; i < n; ++i)
		for (int j = 1; j <= i; ++j) {
			add(f[i + 1][j + 1], 1ll * f[i][j] * (j + 1) % mod);
			add(f[i + 1][j], 1ll * f[i][j] * j * 2 % mod);
			add(f[i + 2][j], 1ll * f[i][j] * j * 2 % mod);
			if (j > 1) add(f[i + 2][j - 1], 1ll * f[i][j] * (j - 1) * 2 % mod), add(f[i + 3][j - 1], 1ll * f[i][j] * (j - 1) % mod);
		}
	printf("%d", f[n][1]);
	return 0;
}

CF704B:
\(a_i\to a_i+x_i,b_i\to b_i-x_i,c_i\to c_i+x_i,d_i\to d_i-x_i\)
\(w(i,j)=\left\{\begin{matrix} d_i + a_j(i\lt j) \\ c_i + b_j(i\gt j) \end{matrix}\right.\)
这样会发现 \(i,j\) 独立了。
剩下就没啥好说的了。

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 5005;
int n, s, t;
int x[N], a[N], b[N], c[N], d[N];
ll f[2][N];
int cur;

void chkmin(ll &a, ll b) { if (a > b) a = b; }

int main() {
	scanf("%d%d%d", &n, &s, &t);
	for (int i = 1; i <= n; ++i) scanf("%d", &x[i]);
	for (int i = 1; i <= n; ++i) scanf("%d", &a[i]), a[i] += x[i];
	for (int i = 1; i <= n; ++i) scanf("%d", &b[i]), b[i] -= x[i];
	for (int i = 1; i <= n; ++i) scanf("%d", &c[i]), c[i] += x[i];
	for (int i = 1; i <= n; ++i) scanf("%d", &d[i]), d[i] -= x[i];
	memset(f[0], 0x3f, sizeof f), f[0][0] = 0;
	for (int i = 0; i < n; ++i, cur ^= 1) {
		memset(f[cur ^ 1], 0x3f, sizeof f[cur ^ 1]);
		if (i + 1 == s) {
			for (int j = (i + 1 > t); j <= i; ++j) {
				if (j) chkmin(f[cur ^ 1][j], f[cur][j] + c[i + 1]);
				chkmin(f[cur ^ 1][j + 1], f[cur][j] + d[i + 1]);
			}
		}
		else if (i + 1 == t) {
			for (int j = (i + 1 > s); j <= i; ++j) {
				if (j) chkmin(f[cur ^ 1][j], f[cur][j] + a[i + 1]);
				chkmin(f[cur ^ 1][j + 1], f[cur][j] + b[i + 1]);
			}
		}
		else {
			for (int j = (i + 1 > s) + (i + 1 > t); j <= i; ++j) {
				if (j > (i + 1 > s)) chkmin(f[cur ^ 1][j], f[cur][j] + b[i + 1] + c[i + 1]);
				if (j > (i + 1 > t)) chkmin(f[cur ^ 1][j], f[cur][j] + a[i + 1] + d[i + 1]);
				if (j > 1) chkmin(f[cur ^ 1][j - 1], f[cur][j] + a[i + 1] + c[i + 1]);
				chkmin(f[cur ^ 1][j + 1], f[cur][j] + b[i + 1] + d[i + 1]);
			}
		}
	}
	printf("%lld", f[cur][1]);
	return 0;
}
posted @ 2022-10-02 07:36  Kobe303  阅读(294)  评论(0编辑  收藏  举报