「APIO2016」划艇 (dp+组合数+区间离散化)(考试)

题干:

  在首尔城中,汉江横贯东西。在汉江的北岸,从西向东星星点点地分布着 N 个划艇学校,编号依次为 1 到 N。每个学校都拥有若干艘划艇。同一所学校的所有划艇颜色相同,不同的学校的划艇颜色互不相同。颜色相同的划艇被认为是一样的。每个学校可以选择派出一些划艇参加节日的庆典,也可以选择不派出任何划艇参加。如果编号为 i的学校选择派出划艇参加庆典,那么,派出的划艇数量可以在ai到bi之间选择。值得注意的是,编号为i 的学校如果选择派出划艇参加庆典,那么它派出的划艇数量必须大于任意一所编号小于它的学校派出的划艇数量。输入所有学校的 ai,bi

  第一行包括一个整数 N,表示学校的数量。接下来 N 行,每行包括两个正整数,用来描述一所学校。其中第 i 行包括的两个正整数分别表示 ai,bi(1 ≤ ai ≤ bi ≤ 1e9 )。
  子任务 1(9 分):1≤N≤500 且对于所有的 1≤i≤N,保证 ai=bii​​。
  子任务 2(22 分):1≤N≤500 且 ∑ni=1 (bi−ai) ≤ 106​​
  子任务 3(27 分):1≤N≤100。
  子任务 4(42 分):1≤N≤500。

题解:

9%:

  9%的数据是ai==bi的,所以我们将其看作一个点,用O(n2)的时间跑一个线性dp——dp[i]表示现在最大高度为i时满足的方案数(与拦截导弹有点像)。唉。考试没想到。。。

31%:

  ni=1(bi−ai)≤106​​的范围其实可以用动态开点的权值线段树来维护,需要离散化。但1 ≤ ai ≤ bi ≤ 1e9,普通数组一定承受不住(数组最多只能开到1e8,且时间复杂度为O(106×2),想想都酸爽),会多开出许多用不到的空间,所以利用权值线段树将一个最大的平均数2000(106/500)优化为log22000。同时 O(106×500) 还可以用前缀和来优化(省去再枚举的平均区间长度2000,就变成O(106))。

  为什么突然要用前缀和了?那是因为在dp转移中有所效果(31%算法与100%算法的dp式很相似),在100%的算法中会有所解释。

Code:

 1 #include<cstdio>
 2 #define ll long long
 3 #define $ 555
 4 const int mod=1000000007;
 5 using namespace std;
 6 int a[$],b[$],n,boss;
 7 int a[20000000],ls[20000000],rs[20000000],cnt;
 8 inline int max(int x,int y){    return x>y?x:y;    }
 9 inline int min(int x,int y){    return x<y?x:y;    }
10 inline int mo(int x){
11     if(x>=mod) x-=mod;
12     if(x<0) x+=mod;
13     return x;
14 }
15 void insert(int &x,int y,int l,int r,int val){
16     if(!x)  x=++cnt;
17     if(l==r){    a[x]=mo(a[x]+y); return;    }
18     int mid=(l+r)>>1;
19     if(val<=mid) insert(ls[x],y,l,mid,val);
20     else         insert(rs[x],y,mid+1,r,val);
21     a[x]=mo(a[ls[x]]+a[rs[x]]);
22 }
23 int get(int x,int y,int l,int r){
24     if(!x) return 0;
25     int mid=l+r>>1,ans=0;
26     if(r<=y) return a[x];
27     ans=(ans+get(ls[x],y,l,mid))%mod;
28     if(y>mid) ans=(ans+get(rs[x],y,mid+1,r))%mod;
29     return ans;
30 }
31 signed main(){
32     scanf("%d",&n);
33     for(int i=1;i<=n;i++) scanf("%d%d",&a[i],&b[i]),boss=max(boss,b[i]);
34     int ans,root=0;
35     insert(root,1,0,boss,0);
36     for(int i=1;i<=n;i++){
37         for(int j=b[i];j>=a[i];j--)
38             insert(root,get(root,j-1,0,boss),0,boss,j);
39     }
40     printf("%d",mo(get(root,boss,0,boss)-1));
41 }
View Code

 

100%:

  首先看到数据范围这么大,离散化没的说,在输入中最多不过2n个不同的数,103 的数量级可以承受。

  我们可以比较容易地得到dp[i][j]的定义:最后一所派出游艇的学校是 i ,第i所学校派出了 j 个划艇时的方案数。但是现在已经离散化, j 相应地从表示一个单点,变为一个区间。假如在出现的那2n个数去重离散化后,第j小的数是a[j],第j+1小的数是a[j+1],那么我们的dp[i][j]就相应地表示:最后一所派出游艇的学校是i,第i所学校派出划艇的数量在[a[j],a[j+1])(这里强调了区间的左开右闭,这里的左开右闭主要是为了转移时的方便,更容易判断与合成区间)。既然区间变为左开右闭,相应的输入也要随之改变,让离散化后的区间恰好能拼出原区间

  接下来考虑一下转移方程。

1.上一个派出游艇的学校派出的游艇数量 和 本学校派出的 不在同一个区间时:

dp[i][j]+=i-1L=1j-1R=1 dp[L][R]

  我们可以发现用来更新dp[i][j]的那一坨是一个矩阵,可以用单步容斥O(1)求得,在31%的算法中所提到的前缀和就是指此(二维前缀和),即:

dp[i][j]+=sum[i-1][j-1]

sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+dp[i][j]

2.上一个派出游艇的学校派出的游艇数量 和 本学校派出的 在同一个区间时:

  为了方便,我们枚举出现在同一区间的编号最小的学校,设之为k那么在k到i之间的所有学校,要么也出现在这个区间,要么干脆没有派出划艇。我们求出k~i这一段的方案数,乘以k以前的方案数 sum[k-1][j-1] 即可(乘法原理)。那么对于k~i这一段里的学校,如果它派出的划艇数量不能在j对应的这个区间内,那么它一定不能派出游艇(在第k和第i个学校都派出了j区间内游艇的前提下,否则前面都不派出时,它就可以派出任意数量的游艇),因为它不可能派出比现在最大值还大的游艇数。所以我们只需要考虑k~i之间能派出j个游艇的学校,设这样的学校数量为x(含i和k),那么就有如下子问题:

 

  x 个相同的区间j,从每个区间中可以选数也可以不选,要求所有选出的数严格递增,且第一个和最后一个区间必需要选数,求方案数?

 

  假如我们一共选择了p个数(p>=2因为i和k必选),区间j中一共有len个不同的数。分两步:从len个数中选出p个从小到大排序,再从i~k中能派出划艇的 x 个学校中选出p-2个要派出划艇的学校。(将选出的p个数依次赋予p个选中的学校,即那p-2个加上i和k,每个学校被赋予的数字就是它派出的数量)。

 

  对于每一种合理的分数字的方案,它都是一种派划艇的方案。

 

  那么选择p个学校时的方案数是:

C(len,p)*C(x,p-2) = C(len,len-p)*C(x,p-2)

 

  全部方案就是:

xp=2 C(len,len-p)*C(x,p-2)

 

  其实p也可以从0开始枚举,因为那时C(x,p-2)=0,不会对答案产生影响为了方便,就当作从0开始吧。我们可以发现两个组合数中的len-p与p-2的和是定值,我们模拟一下:

 

  你有l个物品,我有x个,你要从你的里选出len-p个,我要从我的里选出p-2个把咱们的物品放在一起,一共x+len个,从里面随便选出(len-p+p-2=len-2)个不就好了么?

 

  所以那个和式,其实就是                    C(x+len,len-2)=C(x+len,x+2)

 

  所以对dp[i][j]的贡献就是i-1k=1C(x+len,x+2)*sum[k-1][j-1](k可以派出j区间内的划艇数)如果我们能O(1)计算那个组合数,那么就可以O(n3)解决了。我们可以发现当枚举k时,如果k不能派出j之间的划艇数,就跳过;否则,x++。(x其实每次只增加了1)。

 

  C(n,m)= n! / m! / (n-m)!

  C(n+1,m+1)=(n+1)! /(m+1)! /(n-m)! = n! / m! / (n-m)! *(n+1)/(m+1)

 

  所以我们只需要把逆元搞出来,组合数就可以O(1)递推了。初状态是C(len-2,0),不计入答案,随着++x,c×=(x+L-2)*inv[x]。

  其实也可以不必这么麻烦,组合数貌似是可以杨辉三角的。

 

  注意一下:在枚举j的时候,因为左开右闭的性质,是从a[i]~b[i]-1而非a[i]~b[i]。

       在取模运算中,注意模数一定要是int型的,否则时间难以承受。

       在离散化中,STL中的 map 用起来比较方便。

Code:

 

 1 #include<cstdio>
 2 #include<cstring>
 3 #include<algorithm>
 4 #include<map>
 5 #define $ 555
 6 #define ll long long
 7 using namespace std;
 8 const int mod=1000000007;
 9 map< ll,ll > q;
10 ll aa[$*2],a[$],b[$],n,tot,sor[$*2],dp[$][$*2],sum[$][$*2],len[$*2],inv[$];
11 inline void renew(int i,int j){
12     sum[i][j]=(sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+dp[i][j]+mod)%mod;
13 }
14 signed main(){
15     inv[0]=inv[1]=1;
16     for(register int i=2;i<=500;++i) inv[i]=(mod-1ll*mod/i*inv[mod%i]%mod)%mod;
17     scanf("%lld",&n);
18     for(register int i=1;i<=n;++i){
19         scanf("%lld%lld",&a[i],&b[i]),b[i]++;
20         sor[i*2-1]=a[i],sor[i*2]=b[i];
21     }
22     sort(sor+1,sor+1+2*n);
23     for(register int i=1;i<=n*2;++i)
24         if(sor[i]!=sor[i-1]) aa[++tot]=sor[i],q[sor[i]]=tot;
25     for(register int i=1;i<=n;++i) a[i]=q[a[i]],b[i]=q[b[i]];//离散化
26     dp[0][0]=sum[0][0]=1;
27     for(register int i=1;i<=n;++i)   sum[i][0]=1;
28     for(register int i=1;i<=tot;++i) sum[0][i]=1;
29     sor[tot+1]=sor[tot]+1;
30     for(register int i=1;i<=tot;++i) len[i]=aa[i+1]-aa[i];
31     for(register int i=1;i<=n;++i){
32         for(register int j=1;j<a[i];++j) renew(i,j);
33         for(register int j=a[i];j<b[i];++j){
34             dp[i][j]=1ll*sum[i-1][j-1]*len[j]%mod;
35             register ll add=len[j]-1,x=1,l=len[j];
36             for(register int k=i-1;k>=1;--k)
37                 if(a[k]<=j&&j<b[k]){
38                     ++x;
39                     add=add*(x+l-2)%mod*inv[x]%mod;
40                     dp[i][j]=(dp[i][j]+1ll*sum[k-1][j-1]*add)%mod;
41                 }
42             renew(i,j);
43         }
44         for(register int j=b[i];j<=tot;++j)  renew(i,j);
45     }
46     printf("%lld\n",sum[n][tot]-1);
47 }
View Code

 

 

 

posted @ 2019-07-10 16:31  OI_zzyy  阅读(246)  评论(1编辑  收藏  举报