[APIO 2016] 赛艇 Boat:动态规划、离散化、组合数学和常数优化

水能载舟,亦可赛艇。

这是今年APIO第一题。考场上,我的期望得分从58降为31,再降为9,实际得分为0。

背景

从区间[0, L]中取n个数,要求所有非零数严格递增,问方案数。

如果不能取0,答案是C(L, n)。因为一旦组合确定了,对应关系也就确定了:第1次对应最小数、第2次对应次小数……

加上0,情况发生了怎样的变化?把一种情形的确定分为两步:
1. 0还是非0?
2. 如果非0,取多少?

这两步的地位应是等同的,所以,考虑这样一个序列:

0 0 0 … 0 1 2 3 … L

前面是n个0。

从中取n个数,方案数为C(L+n, n)。取法和原问题的取法一一对应:
1. 取第i个0——第i次取0。
2. 取某个非零数k——没取0的第k次取k。

故C(L+n, n)是原问题的解。

分析

题意

给出n个闭区间,从每个区间取一个整数,或者取0,要求非零数严格递增,问方案数。

想法和解法

问的是方案数,考虑DP。不难想到一种直接的定义和转移:定义f(i, j)为前i所学校中,第i所学校参赛,且派出j艘划艇的方案数。把所有的f(i>0, j>0)加起来即得答案。

f(0,1)=1f(i,j)=k=1j1p=0i1f(p,k),jIif(i,j)=0,jIi

定义f(0, 1)=1是为了方便表示“只取j一个非零数”。

这种方法的劣势在于状态数太多。题目只保证1<=Ai<=Bi<=10^9。

可不可以离散化呢?题目给的是闭区间,代之以左闭右开区间。重新定义:f(i, j)为前i所学校中,第i所学校参赛,且派出的划艇数落到区间j内的方案数。第i所学校之前的学校便分为了两类:在区间j内的和不在区间j内的。

在区间j内的,联系背景问题,可以算出来。不同的是,要求第i所学校必须参赛。因此,0的数目减1,方案数为C(L+m-1, m),m是能在区间j内挑数的学校。具体地,我们枚举上一个有学校的区间k,前p所学校不在区间j中,则m是p+1~i号学校中能选区间j的学校的数量。

f(0,1)=1f(i,j)=k=1j1p=0i1CmL+m1f(p,k),jIif(i,j)=0,jIi

前缀和处理一下,

g(i,j)=k=1j1f(i,k)

递推式(2)简化为

f(i,j)=p=0i1CmL+m1g(p,j1),jIi

实现

空间优化与计算顺序

观察到f(i, j)能仅由g(*, j-1)推出,我们采用滚动数组:省去第二维,外层顺序枚举j,中层逆序枚举j。内层由于涉及到m的计算,也逆序枚举。

组合数的计算

Ckn=Ck1n1nk

代入n=L+m-1,k=m,得


CmL+m1=Cm1L+m2mL+m1

到这里会发现点小问题:结果要取模,又得做除法。学到一种用O(n)时间预处理1~n模素数M的乘法逆元的方法:


111(modM)x1[Mx](M mod x)1(modM)

常数优化

听闻此题卡常数。写出正确代码后,用比赛时的数据测了测,子任务2的第1组数据用时1.3s左右,TLE。事后在uoj上测得的也是相似的结果。把ll重定义成int,发现答案虽然错了,但用时仅0.5s,又把久闻大名的骆可强《论程序底层优化的一些方法与技巧》一文浏览了一下,想到可能是取模运算的问题。于是,在后面给出的代码中,我开了个C数组,处理在当前的区间j中,m所对应的组合数。

Code

#include <cstdio>
#include <algorithm>
typedef long long ll;
const int MAXN = 500;
const ll MOD = 1000000007LL;
using namespace std;
int top, a[MAXN+1], b[MAXN+1], l[MAXN+1], r[MAXN+1], h[2*MAXN+1], g[MAXN+1];
ll inv[MAXN+1], C[MAXN+1];

int main()
{
    int N;
    scanf("%d", &N);

    inv[1] = 1;
    for (int i = 2; i <= N; ++i)
        inv[i] = (MOD-MOD/i)*inv[MOD%i]%MOD;

    for (int i = 1; i <= N; ++i) {
        scanf("%d %d", &a[i], &b[i]);
        h[2*i-1] = a[i];
        h[2*i] = b[i]+1;
    }
    sort(h+1, h+2*N+1);
    top = unique(h+1, h+2*N+1)-h-1;

    for (int i = 1; i <= N; ++i) { // [l, r)
        l[i] = lower_bound(h+1, h+1+top, a[i])-h;
        r[i] = lower_bound(h+1, h+1+top, b[i]+1)-h;
    }

    g[0] = 1;
    C[0] = 1;
    for (int j = 1; j < top; ++j) {
        int L = h[j+1]-h[j];

        for (int i = 1; i <= N; ++i)
            C[i] = C[i-1]*(L+i-1) %MOD * inv[i] %MOD;

        for (int i = N; i >= 1; --i) {
            if (l[i]<=j && j+1<=r[i]) {
                ll f = 0, m = 1, c = L; // m: p+1~i号学校有多少能选区间j
                for (int p = i-1; p >= 0; --p) {
                    f = (f + c*g[p]) % MOD;
                    if (l[p]<=j && j+1<=r[p])
                        c = C[++m];
                }
                g[i] = (g[i]+f) % MOD;
            }
        }
    }

    int ans = 0;
    for (int i = 1; i <= N; ++i)
        ans = (ans+g[i]) % MOD;
    printf("%d", ans);
    return 0;
}

来源与链接

  1. 参考了uoj上离我的WA最近的AC代码:http://uoj.ac/submission/80830 感谢excited同学。
  2. 思路参考了APIO考完后出题人的讲解,和ClarisimmortalCOEle_Ele等的文章。
  3. 求逆元的方法汇总》by slongle_amazing。
  4. 《论程序底层优化的一些方法与技巧》by 骆可强。

参加了《NOI 导刊》组织的培训。Day 0。

    原文作者:动态规划
    原文地址: https://blog.csdn.net/ruoruo_cheng/article/details/51873577
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞