初探动规之线性DP

背包DP、区间DP是两类特殊的线性DP,应该没人不知道吧……

1.背包DP

基本的0/1背包、完全背包、多重背包、分组背包请自行复习!
例题1:AcWing 280.陪审团
这题是一道有多个"体积"维度的0/1背包题。一个是陪审团选的人数,一个是辩方总分D,还有一个是控方总分P。那么直接将这3个体积全部纳入状态表示之中显然是对的,但是空间不允许。所以我们要降维,把D-P的值作为一个体积维度来处理,就会方便很多,而且这样处理空间复杂度是最小的。那么接下来方程的导出就很简单了,在此不再赘述。至于具体方案求法,则会在代码中重点讲解。
代码如下:

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=205,M=805,base=400;//这里的base是第3维的偏移量,将负数偏移到正数的位置上
int n,m;
int p[N],d[N];
int f[N][21][M],ans[21];
int main(){
   
    int T=1;
    while(scanf("%d%d",&n,&m),n || m){
   
        for(int i=1;i<=n;i++) scanf("%d%d",&p[i],&d[i]);
        memset(f,-0x3f,sizeof f);
        f[0][0][base]=0;
        for(int i=1;i<=n;i++){
   
            for(int j=0;j<=m;j++){
   //由于已经加入了“阶段”维度,所以直接用正序循环
                for(int k=0;k<M;k++){
   
                    int t=k-(p[i]-d[i]);
                    f[i][j][k]=f[i-1][j][k];
                    if(t<0 || t>=M || j<1) continue;
                    f[i][j][k]=max(f[i][j][k],f[i-1][j-1][t]+p[i]+d[i]);
                }
            }
        }
        int k=0;
        while(f[n][m][base-k]<0 && f[n][m][base+k]<0) k++;
        if(f[n][m][base-k]>f[n][m][base+k]) k=base-k;
        else k=base+k;
        int i=n,j=m,cnt=0;
        while(j){
   
            if(f[i][j][k]==f[i-1][j][k]){
   //根据方程逆推,这种是没有选第i个人的情况
                i--;
            }
            else{
   //这种是选了第i个人的情况
                ans[++cnt]=i;
                k-=(p[i]-d[i]);
                i--;j--;
            }
        }
        int sp=0,sd=0;
        for(int i=1;i<=cnt;i++){
   
            sp+=p[ans[i]];
            sd+=d[ans[i]];
        }
        printf("Jury #%d\n",T++);
        printf("Best jury has value %d for prosecution and value %d for defence:\n",sp,sd);
        for(int i=cnt;i>=1;i--) printf(" %d",ans[i]);
        puts("\n");
    }
}

例题2:AcWing 281.硬币
这题看起来是一个多重背包的裸题,但显然数据恰好卡着不让你过,怎么办呢?那就用优先队列优化!但是我们还没学单调队列优化的DP,所以要换一种更加巧妙的思路。由于该题只考虑“可行性”而不考虑“最优性”,不妨利用这一点来优化。具体优化方式见蓝书P281-282。

2.区间DP

例题1:AcWing 282.石子合并
这题可以说是写区间DP的freshmen必写的一道题。这题很明显要以区间的长度为“阶段”,以区间的左端点和右端点为“状态”,以中转点k为“决策”。核心代码如下:

   	for(int len=2;len<=n;len++){
   
        for(int i=1;i+len-1<=n;i++){
   
            int l=i,r=i+len-1;
            f[l][r]=1e9;
            for(int k=l;k<r;k++)
                f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);//s是所给数组的前缀和数组
        }
    }
    cout<<f[1][n];

扩展:环形石子合并:NOI1995 石子合并NOIp2006提高组 能量项链
面对环状结构,一个通用的办法便是拆环为链。具体的实现方法就是将环复制两倍,然后枚举1~n的节点作为链的起点。
例题2:AcWing 283.多边形
有了上面的铺垫,我们就可以对这个题有一个大致的分析了。首先就可以想到把多边形这个“环”拆成“链”,然后设f[l][r]为将l~r合并后最终节点上的值可取得的最大值,这样貌似可以求出答案,其实不然。可以设想将l ~ k与 k+1 ~ r分别合并后它们的对应节点上取得了两个很小的负值,而且连接这两个节点的边上的符号是乘号,那么有可能这样的取法要比两个最大值乘起来还要大。
基于上述猜想,我们不妨将最小值也纳入状态蕴含的信息中。可以证明只需要有一个最大值和一个最小值就可以导出其他区间相应的极值。具体分析见蓝书P285
例题3:AcWing 284.金字塔
这题的遍历顺序有一个明显的特点:每一棵子树都对应着字符串序列中的一个区间。所以很容易想到用f[l][r]表示[l,r]对应的子树形状的可能数量。我们想到一分为二的划分法。但是区间[l,r]内含的子树可能不止2棵。进一步想:有没有可能划成两个区间,每个区间由若干个子树组成?当然可能,但是会导致重复计算。所以不妨换一种表示方式:依旧是一分为二的划分方法,但是是划分成第一棵子树以及剩余部分。只要每次改变第一棵子树的规模大小,就可以保证枚举的树形结构不同。那么就不会造成重复或是遗漏,这样状态转移方程也可以轻松写出了。详情见蓝书P287-288。
代码如下:

#include<iostream>
#include<cstring>
#include<cstdio>
#include<string>
using namespace std;
const int N=305,mod=1e9;
char str[N];
long long f[N][N];
int main(){
   
    cin>> str + 1;
    int n=strlen(str+1);
    if(n%2==0){
   
        puts("0");
        return 0;
    }
    for(int len=1;len<=n;len+=2){
   
        for(int l=1;l+len-1<=n;l++){
   
            int r=l+len-1;
            if(len==1) f[l][r]=1;
            else if(str[l]==str[r]){
   
                for(int k=l;k<r;k+=2){
   
                    if(str[k]==str[r])
                        f[l][r]=(f[l][r]+(long long)(f[l][k]*f[k+1][r-1]))%mod;
                }
            }
        }
    }
    printf("%d",f[1][n]);
    return 0;
}

例题4:AcWing 317.陨石的秘密(字符串类DP)
本题的设计思路其实一来就可以想到:即用dpi,j,k,d表示深度为d的、有i对()、j对[]、k对{}的SS串的个数。但关键在于找到计算状态的方法。在这里就要讲一个动态规划中的重要思想:寻找不同点(一般是最后一个),将状态涵盖的集合不重不漏地划分为若干个子集来计算(若是求极值可以不必要使得划分不重复)。 那么这题的不同点就是第一个形如(……)、[……]、{……}的括号序列。但是显然这样不太方便进行动规,所以要考虑对状态表示进行修正:将第4维深度d的意义进行扩充:改为深度小于等于d即可。
代码如下:

#include<iostream>
using namespace std;
const int N=12,mod=11380;
int f[31][11][11][11];
int main(){
   
    int l1,l2,l3,n;
    cin>>l1>>l2>>l3>>n;
    for(int i=0;i<=n;i++) f[i][0][0]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值