线性区间 dp 与环形区间 dp

什么是区间dp

区间dp就是在区间上进行动态规划,求解一段区间上的最优解;该问题的求解思路主要是通过合并小区间的最优解进而得出整个大区间上最优解的dp算法。(实际上还是通过子问题的解来求出原问题的解)

核心思路

既然需要求解一个区间上的最优解,那么我们把这个区间分割成一个个小区间,求解每个小区间的最优解,再合并小区间得到大区间即可。

所以在代码实现上,我们可以枚举区间长度 len 为每次分割成的小区间长度,再在内层枚举该小区间的起点,自然终点也就可以通过起点和len求出了。然后在这个起点终点之间枚举分割点,求解这段小区间在某个分割点下的最优解。

例题 石子合并(弱化版) 

题目链接: P1775 石子合并(弱化版) - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
题目描述:
有N堆石子排成一排,其中第i堆的石子的重量为A i,现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆合并成新的一堆,形成的新石子堆的重量以及消耗的体力是两堆石子的重量之和。求把全部N堆石子合并成一堆最少需要消耗多少体力。

输入格式:
第一行一个正整数N(N<=300),表示石子的堆数N。
第二行N个正整数,表示每堆石子的质量(<=1000)。

输出格式:
一个正整数,表示最少需要消耗多少体力。

例题 石子合并(弱化版) 核心代码

//代码实现
//这就是区间dp的核心代码,注意在该代码前要先对dp数组进行
//初始化 (dp[i][i] = 0 只有一个数字,无需合并 )
for(int len = 2;len <= n;len++){ //列举区间长度
     for(int i = 1;i+len-1 <= n;i++){ //头的位置
          int j = i+len-1; //尾的位置
          for(int k = i;k < j;k++){ //分割点的位置
              //此处的min可以依题改为max
              dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
          }
     }
}

分析

这是 状态转移方程 :

 dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
dp[i][j]  指的是从 i 到 j 的区间的最优解
s 指的是存储大区间上数字的前缀和数组(因此在输入后要进行前缀和处理)
dp[i][k]+dp[k+1][j] 分割点 k 前的小区间的最优解加分割点 k 后的小区间的最优解
 

完整代码

#include<iostream>
#include<cstring>
using namespace std;
int a[305],s[305],dp[305][305];
int n,ans;
int main(){
    cin >> n;
    for(int i = 1;i <= n;i++){
        cin >> a[i];
    }
    for(int i = 1;i <= n;i++){
        s[i] = a[i] + s[i-1]; //前缀和处理
   }
   memset(dp,0x3f,sizeof(dp));
    for(int i = 1;i <= n;i++){
        dp[i][i] = 0;  //初始化
    }
    for(int len = 2;len <= n;len++){ //列举区间长度
        for(int i = 1;i+len-1 <= n;i++){ //头的位置
            int j = i+len-1; //尾的位置
            for(int k = i;k < j;k++){ //分割点的位置
               dp[i][j] = min(dp[i][j],dp[i][k]+dp[k+1][j]+s[j]-s[i-1]);
            }
        }
    }
    cout << dp[1][n]; //答案就是1到n的大区间的最优解
    return 0;
}

环形区间dp


例题 石子项链


题目描述
有N堆石子围成一圈,其中第i堆的石子的重量为 A_{i},现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆合并成新的一堆,形成的新石子堆的重量以及获得的价值都是两堆石子的重量和。求把全部N堆石子合并成一堆最多可以获得多少价值。

输入格式
第一行一个正整数N(N<=300),表示石子的堆数N。
第二行N个正整数,表示每堆石子的质量(<=1000)。

输出格式
一个正整数,表示最多获得多少价值。

做题思路


对于这种环形的区间,我们可以先用枚举的思想来分析
对于一个环:1-2-3-4-1-2...
我们可以将它看作:
1 2 3 4
2 3 4 1
3 4 1 2
4 1 2 3

上面四个线性区间对于环而言都是成立的(但不一定是最优解)
因此题目就转化为在上面四个线性区间共同的最优解只不过当环的长度变得很长后,这种想法会使我们很难堪。为了解决这个问题,我们可以将s数组复制,将它延长到2*n的长度,这样子我们依次向后遍历(就像是一个滑动窗口一样),就能得到枚举出来的几个区间。最后再通过线性区间dp的代码,我们就可以得到最终环形区间dp的结果。

例题 石子项链 完整代码

#include<iostream>
#include<cstring>
using namespace std;
long long a[605],s[605],dp[605][605];
int n;
int main(){
    cin >> n;
    int idx = 1;
    for(int i = 1;i <= n;i++)cin >> a[i];
    for(int i = n+1;i <= 2*n;i++)a[i] = a[idx++]; //复制
    for(int i = 1;i <= 2*n;i++)s[i] = s[i-1] + a[i];

    for(int len = 2;len <= n;len++){
        for(int l = 1;l+len-1 <= 2*n;l++){
            int r = l+len-1;
                for(int k = l;k < r;k++){
                    dp[l][r] = max(dp[l][r],dp[l][k]+dp[k+1][r]+s[r]-s[l-1]);
                }
        }
    }
    long long ans = dp[1][n];
    for(int l=1;l+n-1<=2*n;l++){ //比较枚举出来几个区间的解
        ans = max(dp[l][l+n-1],ans);
    } 
    cout << ans;
    return 0;
}

进阶运用

正常的区间dp时间复杂度是O(n^{3}),但是在部分特殊题目中分割点不需要遍历,也就是时间复杂度只有O(n^{2})。接下来我们看个例子。

例题 奶牛的零食

题目描述

约翰经常给产奶量高的奶牛发特殊津贴,于是很快奶牛们拥有了大笔不知该怎么花的钱。为此,约翰购置了 𝑁 份美味的零食来卖给奶牛们。每天约翰售出一份零食。当然约翰希望这些零食全部售出后能得到最大的收益,这些零食有以下这些有趣的特性:

  • 零食按照 1,…,𝑁 编号,它们被排成一列放在一个很长的盒子里。盒子的两端都有开口,约翰每天可以从盒子的任一端取出最外面的一个。

  • 与美酒与好吃的奶酪相似,这些零食储存得越久就越好吃。当然,这样约翰就可以把它们卖出更高的价钱。

  • 每份零食的初始价值不一定相同。约翰进货时,第i份零食的初始价值为 𝑉𝑖(1≤𝑉≤1000)。

  • 第i份零食如果在被买进后的第 𝑎 天出售,则它的售价是 𝑉𝑖×𝑎。

𝑉𝑖Vi​ 的是从盒子顶端往下的第i份零食的初始价值。约翰告诉了你所有零食的初始价值,并希望你能帮他计算一下,在这些零食全被卖出后,他最多能得到多少钱。

输入格式

第一行一个整数 𝑡,代表有 𝑡 组样例,1 ≤ 𝑡 ≤ 50

每组样例:

第一行一个整数 𝑁,代表有 𝑁 件零食,

第2~N+1行,第 i + 1 行代表第 i 份零食的价值 𝑉𝑖

保证𝑠𝑢𝑚(𝑁) ≤ 2000。

输出格式

每组样例输出约翰最多能卖出多少钱

样例输入 

2 5 1 3 1 5 2 2 8 9

样例输出 

43 26

提示

第一组样例有5件零食,第一天约翰可以选择第1件或第5件

最后约翰卖出零食(零食的价值为1,3,1,5,2)的顺序可以是1,5,2,3,4,也就是先卖第1件,再卖第5件,再卖第2件,再卖第3件,再卖第4件。总和 = 1*1 + 2*2 + 3*3 + 4*1 + 5*5 = 43.

第二组样例有2件零食。

做题思路 

这一道题我们仍然使用区间dp的方法来做。但是这一次我们发现这道题似乎和板子题有那么一内内不一样。这一道题的分割点不需要我们遍历,本题的分割点就是区间的最前端和最后端,所以状态定义和状态转移方程如下。

状态定义

dp[i][j] 表示还剩下区间 i 到 j 的零食可以得到的最大价值。

状态转移方程

dp[i][j] = max(dp[i+1][j]+a[i]*days,dp[i][j-1]+a[j]*days) 

此处的days是零食储存的天数                                                                                                            

以此可得代码:

for(int len = 1;len <= n;len++){ //区间长度
		for(int i = 1;i+len-1 <= n;i++){ //枚举左端点
			int j = i + len - 1; //右端点
			int d = n + i - j; //存储的天数
			if(len == 1)dp[i][j] = a[i]*n; //当长度为1时记录当前该零食的价值
			else dp[i][j] = max(dp[i+1][j]+a[i]*d,dp[i][j-1]+a[j]*d);//状态转移方程
		}
	}
}

  完整代码如下:

#include<bits/stdc++.h>
using namespace std;
int t,n,a[2005],dp[2005][2005];
int main(){
	cin >> t;
	while(t--){
		memset(dp,0,sizeof(dp)); //初始化
		cin >> n;
		for(int i = 1;i <= n;i++)cin >> a[i];
        //区间dp关键部分
		for(int len = 1;len <= n;len++){
			for(int i = 1;i+len-1 <= n;i++){
				int j = i + len - 1;
				int d = n + i - j;
				if(len == 1)dp[i][j] = a[i]*n;
				else dp[i][j] = max(dp[i+1][j]+a[i]*d,dp[i][j-1]+a[j]*d);
			}
		}
        //答案
		cout << dp[1][n] << endl;
	}
	return 0;
}

练习习题

[NOIP2006 提高组] 能量项链 - 洛谷     [普及+/提高]

[NOI1995] 石子合并 - 洛谷                   [普及+/提高]

[HAOI2008] 玩具取名 - 洛谷                 [提高+/省选-]

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值