连续段 DP

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

洛谷 P5999:
考虑把数字从小到大插入到序列中,从而得到一个排列。
定义 fi,j 表示枚举到了第 i 个数,已经有了 j 个连续段的方案数。
那么每次加入数字有两种情况:
把两个段合并。有 j1 个位置可以合并。fi,j+=fi1,j+1×j
重新创建一个段,但是注意 s 在最左边,t 在最右边。fi,j+=fi1,j1×(j[j>s][j>t])
i 等于 st 时也要特判,考虑他们在边界单独开一个段,要么和边界的段合并,所以是 fi,j=fi1,j1+fi1,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 从小到大排序。
fi,j,k 表示插入了 i 个磁铁,j 个连通块,占用了 k 个空位的方案数。
这里的连通块是指:这些磁铁之间任意删除一个空都会导致他们吸引。
三种转移:
当前元素新开一个连通块:fi,j,k+=fi1,j1,k1×j
接在连通块的首/尾:fi,j,k+=fi1,j,kri×2×j
连接两个连通块:fi,j,k+=fi1,j+1,k2ri+1×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 从小到大排序。
引出一个处理绝对值的技巧:考虑每个绝对值能表示成 ri=lai+1ai
我们对于每个 ai+1ai 统计其贡献次数即可。
他的贡献次数就是前 i 个数构成连续段的端点个数,因为端点旁边以后一定会插入比 i 大的数,就会产生贡献,有费用提前计算的感觉。
然后就可以用连续段 DP 来解决这个问题了,因为边界(端点是 1/n)并不会产生贡献所以要把它记录进状态。
fi,j,k,l 表示插入了 i 个数,有 j 个连续段,贡献和是 kl 个边界已经固定。每次增量法考虑一个新数的插入,新的贡献和为 k=k+(ai+1ai)×(2×jl)

  • 作为一个新的连续段插入到不为边界的空隙处:fi+1,j+1,k,d+=fi,j,k,l×(j+1l)
  • 合并两个连续段:fi+1,j1,k,l+=fi,j,k,l×(j1)
  • 添加到某个连续段的非边界端点处:fi+1,j,k,l+=fi,j,k,l×(2×jl)
  • 作为一个新的连续段插入到边界:fi+1,j+1,k,l+1+=fi,j,k,l×(2l)
  • 添加到某个连续段作为边界:fi+1,j,k,l+1+=fi,j,k,l×(2l)

最后答案是 fn,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:
fi,j 表示 i 个元素,形成了 j 个连续段的方案数。

  • 作为一个新的连续段,fi+1,j+1+=fi,j×(j+1)
  • 插入到原有连续段的首/尾,fi+1,j+=fi,j×2×j,fi+2,j+=fi,j×2×j
  • 合并两个连续段,fi+2,j1+=fi,j×2×(j1),fi+3,j1+=fi,j×(j1)

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:
aiai+xi,bibixi,cici+xi,didixi
w(i,j)={di+aj(i<j)ci+bj(i>j)
这样会发现 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 @   Kobe303  阅读(299)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示