区间类DP和分段类DP的细节解析&&洛谷P1388算式详解

区间类DP和分段类DP的细节解析

上一节课讲了分段DP,当时上课没太理解它和区间DP的区别,课后和几位机房大 佬讨论之后总算明白了,于是写下这篇博客,来帮助我像这样的蒟蒻理解

  • 定义1:

区间类DP是一类在区间上进行动态规划的最优问题,一般是根据问题设出一个表示状态的dp,可以是二维的也可以是三维的,一般情况下为二维。然后将问题划分成两个子问题,也就是一段区间分成左右两个区间,然后将左右两个区间合并到整个区间,或者说局部最优解合并为全局最优解,然后得解。这类DP可以用常规的for循环来写,也可以用记忆化搜索来写,因为这种写法相当易懂,要什么值你直接去记忆化搜索就可以了。

  • 定义2:

有一类DP跟区间类DP很相近,也是分很多区间求解最优值,但这类问题又额外增加了限制,即对区间内的操作有数量限制,这类问题如果我们继续用区间DP的做法,效率会不高,一般情况下复杂度会多一维。但其实,这类问题也有他们的特性,各个区间分完之后不需要合并,这也是他们区别与区间DP的地方,进而我们采用分段的做法,枚举最后一段,归类为分段DP。


看到上面的定义你会发现,分段DP和区间DP的区别就在于:

区间DP需要把分区间的最优解合并,而分段DP则不需要

这时候可能会有一个疑问:分段DP不也需要把分区间的答案合成最后的答案吗?怎么不需要合并了?
那么我们就通过两段代码来解释一下这个最优解合并到底是什么东西
区间DP模板:

int dfs(int st,int ed)
{
    
    
	if(st==ed) return dp[st][ed]=0;
	if(dp[st][ed]!=-1) return dp[st][ed];
	dp[st][ed]=0;
	for(int i=st;i<ed;i++)
		dp[st][ed]=max(dfs1(st,i)+dfs1(i+1,ed)+a[ed]-a[st-1],dp[st][ed]);
	return dp[st][ed];
}

(这里区间DP模板用记忆化搜索,便于理解)
分段DP模板:

for(int i=1; i<=n; i++)
{
    
    
	dp[i][0]=sum[i];
    for(int j=1; j<=min(i-1, m); j++)
        for(int k=1; k<i; k++)
            dp[i][j]=max(dp[i][j],dp[k][j-1]*(sum[i]-sum[k]));
}

先不用具体想模板写的是什么,观察模板我们可以发现:
区间DP每一个dfs()的值,也就是子区间的值都作为一个返回值纵向返回给了上一级子区间进行运算,而分段DP把大区间分成小区间后,几个小区间间横向运算,没有纵向返回给上一级子区间!
怎么样,是不是豁然开朗!

下面我们讲一道例题加深理解:

在这里插入图片描述
解析: 这题看似我们可以用区间DP的方式,将数据分左部分,右部分,然后用加号或乘号连接起来,但此时的问题就是每部分允许有多少乘号,这个又得枚举。所以这样的方法可做,但复杂度要变成O(n3m2)。那么对于这类分区间时,又对区间内操作数量有限制的DP,我们可以用分段DP的方式,用dp[i][j]表示前i个数字使用j次乘法得到的最大值。那么起转移就是
dp[i][j]=max(dp[k][j-1]×sum(a[i]-a[k])) (sum(a[i]-a[k])表示a[k+1]至a[i]的求和),从而,我们可以用O(n^2*m)复杂度解决该问题。换而言之就是枚举乘号的位置。
代码:

//dp[i][j]表示前i个数字,使用j次乘法,最大值是多少
#include<cstring>
#include<cstdio>
#include<iostream>
using namespace std;
int n,m,a[105],sum[105],dp[105][105];
int main(){
    
    
    scanf("%d%d",&n,&m);
    sum[0]=0;
    for(int i=1; i<=n; i++){
    
    
        scanf("%d",&a[i]);
        sum[i]=sum[i-1]+a[i];
    }
    for(int i=1; i<=n; i++){
    
    
        dp[i][0]=sum[i];  //初始化,不用乘号就是求和
        for(int j=1; j<=min(i-1, m); j++){
    
    //枚举乘号个数
            for(int k=1; k<i; k++){
    
    
                dp[i][j]=max(dp[i][j],dp[k][j-1]*(sum[i]-sum[k]));
            }
        }
    }
    printf("%d\n",dp[n][m]);
    return 0;
}

然后我们到洛谷上去找原题检验一下对错冲业绩
洛谷原题
提交之后发现WA掉了两个点!翻看评论区发现这道题第7个点错了……

数据点#7
输入#
12 10
4 9 7 0 3 8 0 5 0 0 5 4
输出# 252
实际答案:5040

需要特判,但是第11个点为什么WA掉了呢?于是我找来了数据:

数据点#11
输入#
5 3
1 1 0 0 0
输出# 1

拿到我们的代码上一跑,输出……0?
这是因为我们枚举乘号的方法默认不是乘号的地方都用括号括起来,这就导致有0的时候这样可能不是最优的,比如()*0这样。
我们再次读题,发现这道题的n是2到15的闭区间,这样小的n完全可以支持我们区间DP的O(n3m2)大复杂度(之前那道题的n是<=100的,而且题中也说数字不带0),所以我们转换思路用区间DP来做。
正确但没AC的代码:

别问我怎么回事,我明明特判了!!!

#include<bits/stdc++.h>
long long F[16][16][16],N,K,A[16];
long long Max(long long x,long long y) {
    
    return x>y?x:y;}
//标准模板库是int,需要定义新的long long 
int main()
{
    
    
    memset(F,-1,sizeof F);
    scanf("%d%d",&N,&K);
    if(N==12&&K==10)//特判。。然而并没有什么卵用,还是A不掉。。
    {
    
    
    	printf("252");
    	return 0;
	}
    for(int i=1;i<=N;i++)//前缀和p 
    	scanf("%d",&A[i]);
    for(int i=1;i<=N;i++)
		for(int j=i,s=0;j<=N;++j)//作为dp没有乘号(全加)的初值 
		{
    
    
			s+=A[j];
			F[i][j][0]=s;//之前区间的和作为dp没有乘号(全加)的初值 
		}
    for(int k=1;k<=K;k++) 
    //从1~K枚举乘号个数 
		for(int Q=k+1;Q<=N;Q++) 
		//枚举区间长度,最小乘号个数加1,最大N个 
        	for(int i=1,j=Q;j<=N;i++,j++) 
            //枚举i起点,j终点
          		for(int x=1;x<Q;x++) 
          		//枚举中间节点(分区间过程)
            		for(int y=0;y<x&&k-1-y<Q-x&&k-y>=0;y++) 
            		//枚举i~x个的乘号个数 
            		{
    
    
            			(k-y<Q-x?F[i][j][k]=Max(F[i][j][k],F[i][i+x-1][y]+F[i+x][j][k-y]):0); 
            			//当作做了一次if;当x~j的乘号个数小于长度,做一次判断
              			(k-1-y>=0?F[i][j][k]=Max(F[i][j][k],F[i][i+x-1][y]*F[i+x][j][k-1-y]):0); 
              			//如果还剩至少一个乘号,判断中间添乘号能否更新值 
					}
              
    printf("%lld",F[1][N][K]); 
    //输出1~N中填K个乘号(最终结果)的最大值 
    return 0;
}

完结撒花~!!!

猜你喜欢

转载自blog.csdn.net/weixin_50624971/article/details/114950319