NOI2011 Noi嘉年华(神级dp)

题目

分析:

拿到题,注意到数据范围很小,又是求最大值,可以往dp那方面想一下。

难点在于,如何使AB会场的分配尽量均匀,且活动多。考虑固定A选的活动数,去求B选的最多活动数。为了使时间不交叉,将时间压成一维。

预处理每一个区间中对应的活动数sum[ i ][ j ]。

定义:dp[ i ][ j ]表示1~i 的时间中,A会场选j个,B会场最多选的个数。

转移:为了保证不交叉,模拟区间转移的方法:枚举断点k,大区间由小区间转移过来。

1.dp[ k ][ j ]+sum[ k ][ i ];

A会场只在1~k的时间里选j个,后面的全部留给B,前面的也留一些给B。

2.dp[ k ][ j-sum[ k ][ i ] ];

A会场从前面选 j-sum[ k ][ i ] 个,再从后面选 sum[ k ][ i ] 。

这样就可以不重不漏地考虑完所有情况了。

题目中还要求强制性选择,就加一个ansss[ l ][ r ]数组,表示强制选择l~r区间的最大值,转移见代码。

#include<bits/stdc++.h>
using namespace std;
#define ri register int
#define N 405
int n,l[N],r[N],tmp[N],u[N],v[N],ls[N],pre[N][N],aft[N][N],sum[N][N],anss[N];
int main()
{
    freopen("show.in","r",stdin);
    freopen("show.out","w",stdout);
    int cnt=0,t;
    scanf("%d",&n);
    for(ri i=1;i<=n;++i){
        scanf("%d%d",&l[i],&r[i]);
        r[i]+=l[i];
        tmp[++cnt]=l[i],tmp[++cnt]=r[i];
    } 
    sort(tmp+1,tmp+1+cnt);
    ls[1]=1;//!!!保证从1开始 因为后面的时间点都是从1开始枚举的 
    for(ri i=2;i<=cnt;++i)
    if(tmp[i]!=tmp[i-1]) ls[i]=ls[i-1]+1;//离散化 
    else ls[i]=ls[i-1];
    
    for(ri i=1;i<=n;++i)
     for(ri j=1;j<=cnt;++j){
        if(l[i]==tmp[j]) u[i]=ls[j];
        if(r[i]==tmp[j]) v[i]=ls[j];
    }
    t=ls[cnt];
    for(ri i=1;i<=t;++i)
     for(ri j=i;j<=t;++j)
      for(ri k=1;k<=n;++k){
          if(u[k]>=i && v[k]<=j) sum[i][j]++;
    }
    int inf=1000;
    for(ri i=1;i<=t;++i)
     for(ri j=1;j<=n;++j) 
      pre[i][j]=aft[i][j]=-inf;
    
    for(ri i=1;i<=t;++i)
     for(ri j=0;j<=sum[1][i];++j){
         if(j==0) { pre[i][j]=sum[1][i]; continue; }
         //先初始化 令前i的时间选0个给A会场 = 把前i时间的所有都给B会场 
         for(ri k=1;k<=i;++k){
              pre[i][j]=max(pre[i][j],pre[k][j]+sum[k][i]);//从前k的时间选j个给A会场 后k~i的时间都给B会场 
              if(j>=sum[k][i]) pre[i][j]=max(pre[i][j],pre[k][j-sum[k][i]]);
        //从前k的时间选j-sum[k][i]个给A会场 后面再选sum[k][i]个给A会场 就凑齐了j个给A会场 
        }
    }
    //反着再求一次aft 表示从i~t A会场选j个,B会场选的个数 
    for(ri i=t;i>=1;--i)
     for(ri j=0;j<=sum[i][t];++j){
         if(j==0) { aft[i][j]=sum[i][t]; continue; }
         for(ri k=t;k>=i;--k){//从后往前枚举断点 类区间dp的更新 
              aft[i][j]=max(aft[i][j],aft[k][j]+sum[i][k]);
              if(j>=sum[i][k]) aft[i][j]=max(aft[i][j],aft[k][j-sum[i][k]]);
        }
    }
      
    int ans=0;
    for(ri i=1;i<=n;++i) ans=max(ans,min(i,pre[t][i]));
    printf("%d
",ans);
    //ans[l][r]表示强制选l到r这段区间的最大数 
    for(ri k=1;k<=n;++k)
     for(ri i=u[k];i>=1;--i)//将左端点向左延伸 
      for(ri j=v[k];j<=t;++j){//右端点向右延伸 
          for(ri l=0;l<=sum[1][i];++l){//让A在左边选l个 
           for(ri r=0;r<=sum[j][t];++r){//让A在右边选r个 
                   //中间的全部分给A 
                anss[k]=max(anss[k],min( sum[i][j]+l+r , pre[i][l]+aft[j][r] ));
                if(aft[j][r]<r) break;//r在增加时 aft[j][r]是单调递减的 
                //这个剪枝的意思是:右边选的太少了 B会场相应的也会很少 答案就不会再被更新了 
            }
            if(pre[i][l]<l) break;//同上 
        }
        if(sum[i][j]>sum[1][i]+sum[j][t]) break;//A选得太多了 会导致A、B不平衡 答案也无法被更新 相当于一个剪枝 
        if(anss[k]>sum[1][i]+sum[j][t]) break;//左右全部都分给B都更新不了答案 再枚举也没有意义了 
    }
       
    for(ri i=1;i<=n;++i) printf("%d
",anss[i]);
}
/*
10

15 6

13 12

20 17

1 18

5 14

18 9

15 25

21 16

16 9

19 14



5 
8 2 
1 5 
5 3 
3 2 
5 3 
*/
View Code
原文地址:https://www.cnblogs.com/mowanying/p/11514836.html