连续段 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;
}