[APIO2016]划艇

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

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

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

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

[dp_{i, j} = sumlimits_{k = 0} ^ {j - 1} dp_{i - 1, k} (a_i le j le b_i) ]

前缀和优化一下就可以做到 (O(nmax(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} = sumlimits_{k = 0} ^ {j - 1} sumlimits_{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{inom{L}{n}}{inom{L}{n - 1}} = dfrac{L - n + 1}{n})

因此有转移:

[dp_{i, j, k} = dp_{i - 1, j, k - 1} imes 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;
}

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

GO!
原文地址:https://www.cnblogs.com/Go7338395/p/13843687.html