C++ 算法篇 动态规划----区间动态规划

区间动态规划的含义与模板解释

区间DP,其实求的就是一个区间内的最优值.
一般这种题目,在设置状态的时候,都可以设f[i][j]为区间i-j的最优值
而f[i][j]的最优值,这有两个小区间合并而来的,为了划分这两个更小的区间,我们则需用用一个循环变量k来枚举,而一般的状态转移方程便是:

f[i][j] = max/min (f[i][j] , f[i][k] + f[k][j] + something)   

我们则需要根据这个题目的实际含义进行变通即可.
而区间dp的大致模板是:

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++)
            f[i][j]=max/min(f[i][j],f[i][k]+f[k][j]+something)
    }

len枚举区间的长度,i和j分别是区间的起点和终点,k的作用是用来划分区间.

 

例1、石子合并

题目描述

在一个圆形操场的四周摆放 N 堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。

试设计出一个算法,计算出将 N 堆石子合并成 1 堆的最小得分和最大得分。

输入格式

数据的第 1 行是正整数 N,表示有 N 堆石子。

第 2 行有 N 个整数,第 i 个整数 ai​ 表示第 i 堆石子的个数。

输出格式

输出共 2 行,第 1行为最小得分,第 2 行为最大得分。

输入输出样例

输入

4
4 5 9 4

输出

43
54

说明/提示

1≤N≤100,0≤ai​≤20。

分析

一、确定算法

第一眼看见:最小得分与最大得分,便想到要用贪心或dp。贪心能用吗?不能,因为只有相邻两堆才能合并,这样就无法贪心啦!

所以,此题正解就是  区间dp

二、前置操作

当一条链来做, 最后一次合并一定是两堆石子合并,从第一个元素开始,这两堆石子有可能的是:

第1种可能性:(4),(5,9,4) 4是一堆石子,(5,9,4)合并的一堆石子  结果为:14+18+22 =54                

第2种可能性:(4,5),(9,4)    结果为:9+13+21=43

第3种可能性:(4,5,9),(4)    结果为:14+18+22=54

所以,我们更加抽象一点来思考,设dp(i,j)表示第i堆石子到第j堆石子合并成一堆石子的最大得分,a(i,j)表示第i到第j的和:

所以一条链的石子合并就OK了,关键环形是怎么做。

首先,发现要在一个首尾相连的环上dp并不方便,于是可以断环为链  ,

假设输入的数是a[1],a[2],……,a[n],那么使a[n+1]=a[1],a[n+2]=a[2],……,a[2*n]=a[n]  。

你会发现n个数的环上的每一段都在2*n个数的链上了 ,于是就在链上dp等价于在环上dp 。

三、动态规划三部曲:

1、状态

 设dp1[i][j]表示把从i到j的石子合并为一堆的最小得分,dp2[i][j]表示把从i到j的石子合并为一堆的最大得分(i<j)

2、转移方程

以最小值为例:

 假设存在k使得 将i到k合为一堆的最小得分(a堆)+将k+1到j合为一堆的最小得分(b堆)+将a堆与b堆合并的得分 < 将i到j合为一堆的最小得分 时,更新dp[i][j]  

 即dp1[i][j]=min(dp1[i][k]+dp1[k+1][j]+将a堆与b堆合并的得分,dp1[i][j])

所以现在唯一的未知量就是将a堆与b堆合并的得分了 。 

观察到,a堆的石子个数使a[i]+a[i+1]+……+a[k],b堆的石子个数是a[k+1]+a[k+2]+……+a[j]  , 所以将a堆与b堆合并的得分=a[i]+a[i+1]+……+a[j]
 
 故最小值  dp1[i][j]=min(dp1[i][j],dp1[i][k]+dp1[k+1][j]+a[i]+a[i+1]+……+a[j])
 
    最大值  dp2[i][j]=max(dp2[i][j],dp2[i][k]+dp2[k+1][j]+a[i]+a[i+1]+……+a[j])

3、初始化:           dp[i][i]=0/other  (i=1->n)

     为什么这么做呢?很好理解,就是只包含自己本身的时候,也就是区间长度为1,值肯定是确定的。

4、答案输出

最小值:  ans1=min(ans1,dp1[i][i+n-1])

最大值:  ans2=max(ans2,dp2[i][i+n-1]) (1<=i<=n)

四、注意细节

1、初始化

 ans1=0x7fffffff,     ans2=-0x7fffffff
 
  最小值dp1[l][r]=0x7fffffff;  最大值dp2[l][r]=-0x7fffffff;

2、前缀和优化

观察到,每次状态转移时,都要计算a[i]+a[i+1]+……+a[j],有些麻烦,所以我们可以与计算前缀和,则a[i]+a[i+1]+……+a[j]=a[j]-a[i-1] 

3、数组大小

无论是存每堆的石子数量的sum数组,还是dp1,dp2数组都要开到201!

4、注意

dp过程的循环要先枚举dp区间的长度,再枚举左端点,右端点也可以求出来了,再枚举k,具体的看代码

#include<bits/stdc++.h>
using namespace std;
const int INF=0x7fffffff;
const int maxn=200+10;
int a[maxn],dp1[maxn][maxn],dp2[maxn][maxn];
//sum[i]表示从第一堆到第i堆石子之和(前缀和),则sum[i]-sum[j-1]表示从第j~i堆石子之和,方便状态转移 
//dp[i][j]存放的是从i~j的最值,dp1为最小值,dp2为最大值 
int main()
{   int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]),a[i+n]=a[i];
    for(int i=1;i<n*2;i++) a[i]+=a[i-1];//预处理前缀和 
    for(int x=2;x<=n;x++)	//最外层循环枚举区间长度,因为dp[i][j]={dp[i][k]+dp[k+1][r]+sum[r]-sum[l-1]},转移的时候需要用到比当前区间小的区间 
        for(int l=1;l+x-1<n*2;l++)
        {   int r=l+x-1;
            dp1[l][r]=INF;dp2[l][r]=-INF;
            for(int k=l;k<r;k++)
                dp1[l][r]=min(dp1[l][r],dp1[l][k]+dp1[k+1][r]+a[r]-a[l-1]),
                dp2[l][r]=max(dp2[l][r],dp2[l][k]+dp2[k+1][r]+a[r]-a[l-1]);
        }
    int ans1=INF,ans2=-INF;
    for(int i=1;i<=n;i++) ans1=min(ans1,dp1[i][i+n-1]),ans2=max(ans2,dp2[i][i+n-1]);
    //因为是环形的,可从任意两堆间分开,所以需要枚举一遍起点从1~n 
    printf("%d\n%d",ans1,ans2);
    return 0;
}

 

例2、能量项链【NOIP 2006 提高组 第一题】

题目描述

在Mars星球上,每个Mars人都随身佩带着一串能量项链。在项链上有N颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为r,尾标记为n,则聚合后释放的能量为m×r×n(Mars单位),新产生的珠子的头标记为m,尾标记为n。

需要时,Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设N=4,4颗珠子的头标记与尾标记依次为(2,3)(3,5)(5,10)(10,2)。我们用记号⊕表示两颗珠子的聚合操作,(j⊕k)表示第j,k两颗珠子聚合后所释放的能量。则第4、1两颗珠子聚合后释放的能量为:

(4⊕1)=10×2×3=60。

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为:

((4⊕1)⊕2)⊕3)=110×2×3+10×3×5+10×5×10=710。

输入格式

第一行是一个正整数N(4≤N≤100),表示项链上珠子的个数。第二行是N个用空格隔开的正整数,所有的数均不超过1000。第i个数为第i颗珠子的头标记(1≤i≤N),当i<N时,第i颗珠子的尾标记应该等于第i+1颗珠子的头标记。第N颗珠子的尾标记应该等于第1颗珠子的头标记。

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。

输出格式

一个正整数E(E≤2.1×(10)^9),为一个最优聚合顺序所释放的总能量。

输入输出样例

输入 

4
2 3 5 10

输出 

710

【问题分析】

//区间动规 
//重点就是将整体划分为区间,小区间之间合并获得大区间
//状态转移方程的推导如下
//一、将珠子划分为两个珠子一个区间时,这个区间的能量=左边珠子*右边珠子*右边珠子的下一个珠子
//二、区间包含3个珠子,可以是左边单个珠子的区间+右边两珠子的区间,或者左边两珠子的区间右边+单个珠子的区间 
//即,先合并两个珠子的区间,释放能量,加上单个珠子区间的能量(单个珠子没有能量。。)
//Energy=max(两个珠子的区间的能量+单个珠子区间的能量,单个珠子的区间的能量+两个珠子的区间的能量 ) 
//三、继续推4个珠子的区间,5个珠子的区间。
//于是可以得到方程:Energy=max(不操作的能量,左区间合并后的能量+右区间合并后的能量+两区间合并产生能量)
//两区间合并后产生的能量=左区间第一个珠子*右区间第一个珠子*总区间后面的一个珠子 
#include<bits/stdc++.h>
using namespace std;
int n,e[300],s[300][300],maxn=-1;
int main(){
    cin>>n;
    for(int i=1;i<=n;i++){cin>>e[i];e[i+n]=e[i];}                       //珠子由环拆分为链,重复存储一遍    
    for(int i=2;i<2*n;i++)
	{  for(int j=i-1;i-j<n&&j>=1;j--)                                  //从i开始向前推
       {   for(int k=j;k<i;k++)                                        //k是项链的左右区间的划分点 
           s[j][i]=max(s[j][i],s[j][k]+s[k+1][i]+e[j]*e[k+1]*e[i+1]);  //状态转移方程:max(原来能量,左区间能量+右区间能量+合并后生成能量)  
           if(s[j][i]>maxn)maxn=s[j][i];                               //求最大值 
        }
    } 
    cout<<maxn;
    return 0;
}
#include <bits/stdc++.h>
using namespace std;
int f[405][405];
int n,a[205]; 
int main()
{   cin >> n;
    for(int i=1;i<=n;i++)  //***对环形问题的处理技巧***
    {   cin >> a[i];  a[n+i]=a[i];    } 
    for(int i=2;i<=n+1;i++)
    {   for(int l=1;l+i-1<=2*n;l++)  //如果采取了上述策略,一定要将2*n个点都更新 
        {   int r=l+i-1;
            for(int k=l+1;k<=l+i-2;k++)
                f[l][r]=max(f[l][r],f[l][k]+f[k][r]+a[l]*a[k]*a[r]); 
        }
    }
    int res=0;
    for (int i=1;i<=n;i++) res=max(res,f[i][n+i]);
    cout << res;
    return 0;
}

例3、关灯

题目描述
宁智贤得到了一份有趣而高薪的工作。每天早晨她必须关掉她所在村庄的街灯。所有的街灯都被设置在一条直路的同一侧。 宁智贤每晚到早晨5点钟都在晚会上,然后她开始关灯。开始时,她站在某一盏路灯的旁边。 每盏灯都有一个给定功率的电灯泡,因为宁智贤有着自觉的节能意识,她希望在耗能总数最少的情况下将所有的灯关掉。 宁智贤因为太累了,所以只能以1m/s的速度行走。关灯不需要花费额外的时间,因为当她通过时就能将灯关掉。 编写程序,计算在给定路灯设置,灯泡功率以及宁智贤的起始位置的情况下关掉所有的灯需耗费的最小能量。
输入格式
第一行包含一个整数N,2≤N≤1000,表示该村庄路灯的数量。
第二行包含一个整数V,1≤V≤N,表示宁智贤开始关灯的路灯号码。
接下来的N行中,每行包含两个用空格隔开的整数D和W,用来描述每盏灯的参数,其中0≤D≤1000,0≤W≤1000。
D表示该路灯与村庄开始处的距离(用米为单位来表示),W表示灯泡的功率,即在每秒种该灯泡所消耗的能量数。路灯是按顺序给定的。
输出格式
第一行即唯一的一行应包含一个整数,即消耗能量之和的最小值。注意结果小超过1,000,000,000。
样例数据
input

4
3
2 2
5 8
6 1
8 7

output
56

显然,这道题目同样也是区间动态规划,即我们无法用贪心(以往一边关到底再回来这一策略)显然不可行,若使用暴搜则同样可以拿到部分分.因此,我们考虑使用区间动态规划来解决这个问题.我们可以设f[i][j]为关掉区间i到区间j的所有灯的最小消耗数目,我们不难得知:
f[i][j]这个状态的转移只和f[i+1][j]与f[i][j-1]有关,因为我们知道,要使一个区间内的状态最优便必然要关完一个区间内的等必然结束在该区间的两端,因此我们f[i][j]在状态结束的时候必然结尾在位置i和位置j,那么必然与位置i和位置j旁边的点相关.
为了能够分清状态的转移与每一个结束点位置的关系,我们可以选择再增加一维地状态,即:
1.设f[i][j][0]表示区间i-j且最后停留在左边的最小耗费
2.设f[i][j][1]表示巨剑i-j且最后停留在右边的最小耗费
跟据上述两个状态,我们便可以得出显然性的结论:f[i][j][0]必然是由f[i+1][j][0/1]转移过来的,f[i][j][1]必然是由f[i][j-1][0/1]转移过来的,那么我们便可以得到状态转移方程:
f[i][j][0] = max ( f[i][j][0] , f[i+1][j][0] + sum(i,i+1,i+1,j) , f[i+1][j][1] + sum(i,j,i+1,j) )


f[i][j][1] = max ( f[i][j][1] , f[i][j−1][1] + sum(j−1,j,i,j−1) , f[i][j−1][0] + sum(i,j,i,j−1) )


sum(Pos1,Pos2,Left,Right) = ∣D[Pos2] − D[Pos1]∣ ∗ ( S[n] − S[Right] + S[Left] )


1.函数sum的含义:表示这个人从位置Pos1(或Pos2)走到位置Pos2或(Pos1)的位置时除了Left到Right区间外的灯耗费的电费,两者被染相乘.D表示位置,在体面上便有所说明;而S则是前缀和的意思,即S[i]=w[1…i]
2.状态转移方程的含义:表示上一个阶段的花费加上走过来时灯的花费的最小值.
因此应该很好理解了吧!
代码如下:

#include<bits/stdc++.h>
using namespace std;
int N,V;
int D[100000];
int W[100000];
int s[100000];
int f[3000][3000][2];
inline int sum(int p1,int p2,int d1,int d2)//分别表示从走的距离和已经关了灯的区间编号,则消耗的量为距离*区间之外的等的消耗总和 
{	return abs(D[p1]-D[p2])*(s[N]-s[d2]+s[d1-1]);  }
inline int Min(int a,int b,int c)
{   return min(a,min(b,c));  } 
int main()
{
	cin>>N>>V;
	for (int i=1;i<=N;i++)
	{
	    cin>>D[i]>>W[i];
	    s[i]=s[i-1]+W[i];
	}
	memset(f,100,sizeof(f));
	f[V][V][0]=f[V][V][1]=0;//0在左边,1在右边 
	for (int len=2;len<=N;len++)
	    for (int i=1;i+len-1<=N;i++)
	    {
	    	int j=i+len-1;
	    	f[i][j][0]=Min(f[i][j][1],f[i+1][j][0]+sum(i,i+1,i+1,j),f[i+1][j][1]+sum(i,j,i+1,j));
	    	f[i][j][1]=Min(f[i][j][1],f[i][j-1][1]+sum(j-1,j,i,j-1),f[i][j-1][0]+sum(i,j,i,j-1));
		}
	cout<<min(f[1][N][1],f[1][N][0]);
	return 0;
} 

 

区间动态规划练习:

1、P5336 [THUSC2016]成绩单

题目描述

期末考试结束了,班主任 L 老师要将成绩单分发到每位同学手中。L老师共有n份成绩单,按照编号从1到n 的顺序叠放在桌子上,其中编号为i的的成绩单分数为Wi​。
成绩单是按照批次发放的。发放成绩单时,L 老师会从当前的一叠成绩单中抽取连续的一段,让这些同学来领取自己的成绩单。当这批同学领取完毕后,L 老师再从剩余的成绩单中抽取连续的一段,供下一批同学领取。经过若干批次的领取后,成绩单将被全部发放到同学手中。
然而,分发成绩单是一件令人头痛的事情,一方面要照顾同学们的心理情绪,不能让分数相差太远的同学在同一批领取成绩单;另一方面要考虑时间成本,尽量减少领取成绩单的批次数。对于一个分发成绩单的方案,我们定义其代价为:

其中k是分发的批次数,对于第i披分发的成绩单,maxi​是最高分数,mini​是最低分数,a和b是给定的评估参数。 现在,请你帮助 L 老师找到代价最小的分发成绩单的方案,并将这个最小的代价告诉 L 老师。当然,分发成绩单的批次数k是你决定的。

输入格式

第一行包含一个正整数n,表示成绩单的数量。 第二行包含两个非负整数a,b,表示给定的评估参数。 第三行包含n个正整数,wi​表示第ii张成绩单上的分数。

输出格式

仅一个正整数,表示最小的代价是多少。

输入输出样

输入 

10
3 1
7 10 9 10 6 7 10 7 1 2

输出

15

说明/提示

n≤50,a≤1500,b≤10,wi​≤1000

 

2、字符合并

链接:https://ac.nowcoder.com/acm/problem/19997
来源:牛客网
 

题目描述

有一个长度为 n 的 01 串,你可以每次将相邻的 k 个字符合并,得到一个新的字符并获得一定分数。得到的新字符和分数由这 k 个字符确定。你需要求出你能获得的最大分数。

输入描述:

第一行两个整数n,k。

接下来一行长度为n的01串,表示初始串。

接下来2k行,每行一个字符ci和一个整数wi,ci 表示长度为k的01串连成二进制后按从小到大顺序得到的第i种合并方案得到的新字符,wi表示对应的第i种方案对应获得的分数。

1 ≤ n ≤ 300,0 ≤ ci ≤ 1,wi ≥ 1, k ≤ 8

输出描述:

输出一个整数表示答案

示例1

输入

3 2
1 0 1
1 10
1 10
0 20
1 30

输出

40

 

 

  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值