[APIO2016]划艇

挺神奇的一个做法,貌似叫做值域分段?

首先考虑一个简单的暴力做法,令 \(dp_{i, j}\) 为考虑完前 \(i\) 个学校,当前派出的最大划艇数量为 \(j\) 的方案。

那么按照第 \(i\) 个学校派不派划艇转移就会有:

\[dp_{i, j} = dp_{i - 1, j} \]

\[dp_{i, j} = \sum\limits_{k = 0} ^ {j - 1} dp_{i - 1, k} (a_i \le j \le b_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\) 个学校的划艇的方案。

同样根据当前学校是否放置划艇转移即可:

\[dp_{i, j, k} = dp_{i - 1, j, k} \]

\[dp_{i, j, 1} = \sum\limits_{k = 0} ^ {j - 1} \sum\limits_{l = 1} ^ {\min(i - 1, d_{k + 1} - d_k)}dp_{i - 1, k, l} \]

对于这条转移,直接记录前缀和即可。

最后,考虑如何在当前区间最后再添加一个数。

可以发现,从一个长度为 \(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}\)

因此有转移:

\[dp_{i, j, k} = dp_{i - 1, j, k - 1} \times \frac{L - n + 1}{n}(k > 1) \]

最终实现的时候简单的区间要定义为左闭右开,这样就不会出现端点处选择记重的情况了,同时,需要滚动数组。

#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;
}

值得一提的是,某些能运用在其他领域的做法,可能在这个领域的某些特殊问题上也会适用,不要忽略这个方面的思考。

posted @ 2020-10-19 22:50  Achtoria  阅读(75)  评论(0编辑  收藏  举报