[APIO2016]划艇
挺神奇的一个做法,貌似叫做值域分段?
首先考虑一个简单的暴力做法,令 \(dp_{i, j}\) 为考虑完前 \(i\) 个学校,当前派出的最大划艇数量为 \(j\) 的方案。
那么按照第 \(i\) 个学校派不派划艇转移就会有:
前缀和优化一下就可以做到 \(O(n\max(a_i, b_i))\) 了,但因为值域过大,这个做法显然是不行的。
那么一个直接的想法就是通过改写状态来解决这个问题,但是很难发现一个不依赖于值域的状态,那么就只能转化这个问题了。
既然值域过大,平常做其他的题目常用的办法就是使用离散化,何不在本题上也尝试一番呢?
离散化过后你会发现,值域会被至多划分成 \(2n - 1\) 个简单区间(不包含其他端点)。
并且每一个简单区间和原先的每所学校的划艇限制 \([a_i, b_i]\) 只能是包含或者是相离关系。
这意味着如果一所学校选择在某个区间内的一个权值放置划艇,可行的方案数实质上已经与这个学校无关了,只于放置的这个区间大小有关了。
那么就可以考虑一个基于区间放置的一个 \(dp\)。
具体地,对于学校 \(i\),考虑选择区间 \(j\) 中的一个权值放置划艇。
因为题目的要求,那么 \(i\) 之前的所有学校能放的区间一定不大于 \(j\),这样就形成了拓扑序,可以考虑 \(dp\) 了。
不难发现可以令 \(dp_{i, j, k}\) 表示当前考虑到第 \(i\) 所学校,当前第 \(j\) 个区间中放置了 \(k\) 个学校的划艇的方案。
同样根据当前学校是否放置划艇转移即可:
对于这条转移,直接记录前缀和即可。
最后,考虑如何在当前区间最后再添加一个数。
可以发现,从一个长度为 \(L\) 的区间中选出 \(n\) 个数形成递增序列的方案就相当于选出 \(n\) 个互不相同的数然后按顺序排好,即:\(\dbinom{L}{n}\)。
添加一个数的方案可以考虑先把之前的方案先除以 \(\dbinom{L}{n - 1}\) 再乘 \(\dbinom{L}{n}\) 即可。
添加一个数的方案即 \(\dfrac{\binom{L}{n}}{\binom{L}{n - 1}} = \dfrac{L - n + 1}{n}\)
因此有转移:
最终实现的时候简单的区间要定义为左闭右开,这样就不会出现端点处选择记重的情况了,同时,需要滚动数组。
#include <bits/stdc++.h>
using namespace std;
#define rep(i, l, r) for (int i = l; i <= r; ++i)
const int N = 500 + 5;
const int Mod = 1e9 + 7;
int n, tot, cur, a[N], b[N], inv[N], d[N * 2], S[2][N * 2], dp[2][N * 2][N];
int read() {
char c; int x = 0, f = 1;
c = getchar();
while (c > '9' || c < '0') { if(c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
return x * f;
}
int Inc(int a, int b) { return (a += b) >= Mod ? a - Mod : a;}
int Dec(int a, int b) { return (a -= b) < 0 ? a + Mod : a;}
int Mul(int a, int b) { return 1ll * a * b % Mod;}
int fpow(int a, int b) { int ans = 1; for (; b; a = Mul(a, a), b >>= 1) if(b & 1) ans = Mul(ans, a); return ans;}
int main() {
n = read();
rep(i, 1, n) inv[i] = fpow(i, Mod - 2);
rep(i, 1, n) a[i] = d[++tot] = read(), b[i] = d[++tot] = read() + 1;
sort(d + 1, d + tot + 1), tot = unique(d + 1, d + tot + 1) - d - 1;
cur = dp[0][0][0] = 1;
rep(i, 0, tot - 1) S[0][i] = 1;
rep(i, 1, n) {
rep(j, 0, tot - 1) rep(k, 0, i) dp[cur][j][k] = 0;
rep(j, 0, tot - 1) rep(k, 0, i) dp[cur][j][k] = Inc(dp[cur][j][k], dp[cur ^ 1][j][k]);
rep(j, 1, tot - 1) if(a[i] <= d[j] && b[i] >= d[j + 1]) {
dp[cur][j][1] = Inc(dp[cur][j][1], Mul(S[cur ^ 1][j - 1], d[j + 1] - d[j]));
int L = min(i, d[j + 1] - d[j]);
rep(k, 2, L) dp[cur][j][k] = Inc(dp[cur][j][k], Mul(dp[cur ^ 1][j][k - 1], Mul(d[j + 1] - d[j] - k + 1, inv[k])));
}
rep(j, 0, tot - 1) S[cur][j] = 0;
rep(j, 0, tot - 1) {
if(j) S[cur][j] = S[cur][j - 1];
rep(k, 0, i) S[cur][j] = Inc(S[cur][j], dp[cur][j][k]);
}
cur ^= 1;
}
printf("%d", Dec(S[cur ^ 1][tot - 1], 1));
return 0;
}
值得一提的是,某些能运用在其他领域的做法,可能在这个领域的某些特殊问题上也会适用,不要忽略这个方面的思考。