『进阶DP专题:序列区间DP与环形区间DP』

版权声明:随你转载,你开心就好(记得评论和注明出处哦) https://blog.csdn.net/Prasnip_/article/details/81087949

<更新提示>

<第一次更新>


<正文>

区间DP

区间动态规划是动态规划中一个重要的分支。当然,它也是动态规划的进阶,理解起来更具有难度,所以我们单独作为一个专题来讲解。

区间动态规划最大的特征为对于某段区间的最优值,它都是由几段更小的区间的最优值得到的。也就是说,它依然满足动态规划的最优子结构以及无后效性的特征。那么这类问题的解决策略就是以长度划分阶段,通过将一个区间问题不断分解为几个更小的区间,并递归地花费或直接求解,在合并推导出大区间的最优解。也就是分治思想的运用。

区间dp的状态设置与普通的线性dp不同,由于区间的最优解是由更小的区间推导而来,所以状态的设置一般是f[i][j]代表由第i个元素到第j个元素的区间最优值。

序列

序列区间dp是区间dp的基本模型,一下主要讲解序列区间dp的例题。

1

石子合并
题目描述
在操场上沿一直线排列着n堆石子。现要将石子有次序地合并成一堆。
规定每次只能选相邻的两堆石子合并成新的一堆,并将新的一堆石子数计为该次合并的得分。 我们希望这n-1次合并后得到的得分总和最小。
输入格式
第一行有一个正整数n(n<=300),表示石子的堆数; 第二行有n个正整数,表示每一堆石子的石子数,每两个数之间用一个空格隔开。它们都不大于10000。
输出格式
一行,一个整数,表示答案。
样例数据
input
3
1 2 9
output
15
数据规模与约定
区间dp第一题
时间限制:
1s
空间限制:
256MB

分析

这是一道序列区间dp模板题,用于入门。
我们根据区间dp的思想,设置状态f[i][j]代表元素i到元素j之间的区间最优解。首先,区间dp的实现需要利用三重循环来枚举,第一重:枚举区间长度len,第二重:枚举区间起始点i(l),然后根据区间长度计算区间结束点j(r),第三重:枚举i(l),j(r)间某一个断点k,以分割大区间为两个小区间。那么,我们可以推导出状态转移方程如下: f [ i ] [ j ] = m a x ( f [ i ] [ j ] , f [ i ] [ k ] + f [ k + 1 ] [ j ] + c o s t [ i ] [ j ] ) ,意为区间将ik合并,得到区间ik的最优解和将区间k+1j合并,得到区间k+1j的最优解,再将这两堆合并,加上本次合并的花费,得到区间f[i][j]的解,当然,如果有更优的分割点k能得到更优的解,要将上一个解更新。
对于状态转移方程中的cost[i][j],其意为合并区间i到j之间的石子需要的花费,由题意可以很快的推出cost[i][j]即a[i]到a[j]的区间求和,我们利用前缀和实现即可。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,stone[100080]={},cost[10080][10080]={},sum[100080]={},f[10080][10080]={};
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
inline int spend()
{
    for(int i=1;i<=n;i++)
    {
        for(int j=i;j<=n;j++)
        {
            cost[i][j]=sum[j]-sum[i-1];
        }
    } 
} 
int main()
{
    n=read();
    for(int i=1;i<=n;i++)
    {
        stone[i]=read();
        sum[i]=sum[i-1]+stone[i];
    }
    spend();
    for(int p=1;p<=n;p++)
    {
        for(int i=1;i<=n;i++)
        {
            int j=i+p-1;
            if(j>n)continue;
            for(int k=i;k<j;k++)
            {
                if(f[i][j]>f[i][k]+f[k+1][j]+cost[i][j]||!f[i][j])
                {
                    f[i][j]=f[i][k]+f[k+1][j]+cost[i][j];
                }
            }
        }
    }
    cout<<f[1][n]<<endl;
    return 0;
}
2

矩阵连乘
题目描述
给定 n个矩阵{A1,A2,…,An},考察这 n个矩阵的连乘积 A1A2…An。由于矩阵乘法满足结合律,故计算矩阵的连乘积可以有许多不同的计算次序,这种计算次序可以用加括号的方式来确定。
矩阵连乘积的计算次序与其计算量有密切关系。例如,考察计算 3 个矩阵{A1,A2,A3}连乘积的例子。 设这3个矩阵的维数分别为
10∗100,100∗5,和5∗50
10∗100,100∗5,和5∗50

若按(A1A2)A3 计算,3 个矩阵连乘积需要的数乘次数为
10∗100∗5+10∗5∗50=7500
10∗100∗5+10∗5∗50=7500

若按 A1(A2A3)计算,则总共需要
100∗5∗50+10∗100∗50=75000
100∗5∗50+10∗100∗50=75000
次数乘。
现在你的任务是给出一个矩阵连乘式,计算其需要的最少乘法次数。
输入格式
输入数据由多组数据组成。每组数据格式如下:第一行是一个整数n(1≤n≤100),表示矩阵的个数。 接下来 n 行,每行两个整数 a,b,分别表示该矩阵的行数和列数,其中1 < a,b<100。
输出格式
对于每组数据,输出仅一行包含一个整数,即将该矩阵连乘方案需要的最少乘法次数。
样例数据
input
3
10 100
100 5
5 50
output
7500
数据规模与约定
时间限制:
1s

空间限制:
256MB

分析

这道题的策略仍然相同,我们定义f[i][j]代表区间i,j的最优解。依然三重循环枚举区间及断点。这里,我们需要对输入数据进行处理,我们知道,可以进行乘法操作的矩阵一定有一维相同。那么我们将每一个矩阵的第一维储存下来即可,对于最后一个矩阵,将其的第二维储存再a[n+1]的位置即可。根据以上的处理,状态转移方程如下: f [ l ] [ r ] = m a x ( f [ l ] [ r ] , f [ l ] [ k ] + f [ k + 1 ] [ r ] + a [ l ] a [ k + 1 ] a [ r + 1 ] ) 。那么,最终的答案为f[1][n]。
对于区间dp的转移,枚举时的边界等细节问题,我们可以采用如下的解决方法:先建立模型和大概思路,确认时区间dp后,在处理细节问题时套入f[1][1]模型检测方程及循环的推导是否合法。然后确定后手动模拟程序来过样例,要是在推时不出什么问题,一切都合法,那么基本思路就没问题了。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int a[100080]={},n,t,f[1080][1080]={};
int main()
{
    cin>>n;
    memset(f,10,sizeof(f));
    for(int i=1;i<n;i++)cin>>a[i]>>t,f[i][i]=0;
    cin>>a[n]>>a[n+1];f[n][n]=f[n+1][n+1]=0;
    for(int len=2;len<=n;len++)
    {
        for(int l=1;l+len-1<=n;l++)
        {
            int r=l+len-1;
            for(int k=l;k<r;k++)
            {
                f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+a[l]*a[k+1]*a[r+1]);
            }
        }
    }
    cout<<f[1][n]<<endl;
}
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
数据规模与约定
时间限制:
1s
空间限制:
256MB

分析

首先,我们需要分析这道题目的数学模型。写在这里,当然使用区间dp来求解,但中在做题时能否灵活转换并想到解决方案,以及用区间dp的方法。
我们先设置状态:f[i][j]代表区间i,j的最优解。这是基本套路,但设下后,你有没有立刻发现状态无法转移呢?这里,我们需要灵活变通,由题面可知,关灯的操作是由一个人进行的,而关完区间i,j的灯后,人的位置也很重要,考虑时间最短,花费最小的最小的情况,在关完区间i,j的所有灯后,人一定在灯i或者灯j处,即区间的最左端或最右端,由于人的位置关乎到花费以及下一次的转移,我们将位置加入状态中。即:f[i][j][0/1]代表区间i,j的最小花费,第三维为0代表人在灯i处(左边),为1代表人在灯j处(右边)。
那么,我们考虑状态转移方程。在写方程前,我们知道每一次的转移一定要累加关灯途中的花费,那么,花费这么计算呢?由题可知,每一盏灯每秒都有自己的花费。所以,我们模拟人每一次关灯操作时所增加的花费就是关灯的时间乘上除了关掉的灯等外所有其他灯的花费。这涉及到大量的求和操作,我们可以利用前缀和预处理处cost[i][j]数组,代表从i到j关灯时其他灯的花费。
花费的问题解决后,我们分类讨论就行了:

f [ i ] [ j ] [ 0 ] f r o m { f [ i + 1 ] [ j ] [ 0 ] f [ i + 1 ] [ j ] [ 1 ] f [ i ] [ j ] [ 1 ] f r o m { f [ i ] [ j 1 ] [ 0 ] f [ i ] [ j 1 ] [ 1 ]

然后利用花费数组处理新增的花费,即可列出状态转移方程:
f [ i ] [ j ] [ 0 ] = m i n { f [ i + 1 ] [ j ] [ 0 ] + ( l i g h t [ i + 1 ] . d i s l i g h t [ i ] . d i s ) c o s t [ i + 1 ] [ j ] f [ i + 1 ] [ j ] [ 1 ] + ( l i g h t [ j ] . d i s l i g h t [ i ] . d i s ) c o s t [ i + 1 ] [ j ] f [ i ] [ j ] [ 1 ] = m i n { f [ i ] [ j 1 ] [ 0 ] + ( l i g h t [ j ] . d i s l i g h t [ i ] . d i s ) c o s t [ i ] [ j 1 ] f [ i ] [ j 1 ] [ 1 ] + ( l i g h t [ j ] . d i s l i g h t [ j 1 ] . d i s ) c o s t [ i ] [ j 1 ]

套用区间dp基本模板,去暴力转移就行了。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int n,begin,f[1080][1080][2]={},cost[1080][1080]={};
//f代表关闭第i到j盏灯的最小花费,0表示在左边,1表示在右边 
struct LIGHT
{
    int dis,spend;
}light[100080]={};
inline int read()
{
    int w=0,x=0;char ch;
    while(!isdigit(ch)){w|=ch=='-';ch=getchar();}
    while(isdigit(ch)){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return w?-x:x;
}
int main()
{
    n=read();
    begin=read();
    for(int i=1;i<=n;i++)
    {
        light[i].dis=read();
        light[i].spend=light[i-1].spend+read();
    }
    for(int i=1;i<=n;i++)
    {
        for(int j=i;j<=n;j++)
        {
            cost[i][j]=light[n].spend-light[j].spend+light[i-1].spend;
        }
    }
    memset(f,10,sizeof(f));
    f[begin][begin][1]=f[begin][begin][0]=0;
    for(int l=2;l<=n;l++)
    {
        for(int i=1;i<=n-l+1;i++)
        {
            int j=i+l-1;
            f[i][j][0]=min(f[i+1][j][0]+(light[i+1].dis-light[i].dis)*cost[i+1][j],f[i+1][j][1]+(light[j].dis-light[i].dis)*cost[i+1][j]);
            f[i][j][1]=min(f[i][j-1][0]+(light[j].dis-light[i].dis)*cost[i][j-1],f[i][j-1][1]+(light[j].dis-light[j-1].dis)*cost[i][j-1]);
        }
    }
    cout<<min(f[1][n][0],f[1][n][1])<<endl;
    return 0;
}

环形

环形区间dp是序列区间dp的延伸,其特点在于dp的范围是一个环。一般的解决办法由两种。
1)将环作为一个序列,并复制一份放置在原序列后面,得到一个长度为2*n的序列。
2)在状态转移时对数组下标进行取模操作(类似于滚动数组)
下面主要引入一道例题来讲解,大体解法与序列区间dp相同,不再深入探讨。

能量项链

【问题描述】
在 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)=10×2×3+10×3×5+10×5×10=710
【输入文件】
输入文件 energy.in 的第一行是一个正整数
N(4≤N≤100),表示项链上珠子的个数。第二行是 N个用空格隔开的正整数,所有的数均不超过 1000。第 i个数为第i颗珠子的头标记(1≤i≤N),当 i < N时,第 i颗珠子的尾标记应该等于第 i+1颗珠子的头标记。第 N颗珠子的尾标记应该等于第 1颗珠子的头标记。
至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。
【输出文件】
输出文件 energy.out 只有一行,是一个正整数 E(E≤2.1×109),为一个最优聚合顺序所释放的总能量。
【样例 1】
ex_energy1.in
4
2 3 5 10
ex_energy1.ans
710
时间限制:
1s
空间限制:
10MB

分析

重点在怎么处理环。我们选择第一种方法,在输入时,我们将序列复制一份放在原序列后。那么,我们在状态转移时也需要有所改动。先定义状态,这题的状态比较奇怪,由于奇妙的数据输入方式,合并相邻的珠子i,i+1还需要调用到第i+1颗珠子的尾标记,即a[i+2],为了方便状态的转移,我们需要在定义状态时做小的改动。f[l][r]代表合并第l到r-1颗珠子的最大能量值。那么,在循环转移时,我们这样处理。第一重循环枚举长度len,范围2~n+1,第二重循环枚举起始点l,初值为1,满足r=l+len-1<2*n,第三重循环枚举断点k,范围l+1~r-1。由此,状态转移方程就是 f [ l ] [ r ] = m a x ( f [ l ] [ r ] , f [ l ] [ k ] + f [ k ] [ r ] + a [ l ] a [ k ] a [ r ] ) ,当然,如果不能充分的理解状态转移方程,仍然可以使用之前说的方法,带入数据和特殊的情况检验,推导即可。
转移完所有的状态后,我们需要继续选择答案。我们循环i=1~n。在每一个f[i][i+n]中找到最大值作为答案。为什么这样呢?原因是对应复制序列的操作,在环中找到每一个长度为n+1的区间,代表环中n颗珠子合并的每一种可能。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
int f[250][250]={},n,Energy[280]={};
int main()
{
    cin>>n;
    for(int i=1;i<=n;i++)cin>>Energy[i],Energy[i+n]=Energy[i];
    for(int len=2;len<=n+1;len++)
    {
        for(int l=1;l+len-1<=2*n;l++)
        {
            int r=l+len-1;
            for(int k=l+1;k<=r-1;k++)
            {
                f[l][r]=max(f[l][r],f[l][k]+f[k][r]+Energy[l]*Energy[k]*Energy[r]);
            }
        }
    } 
    int ans=0;
    for(int i=1;i<=n;i++)
    {
        ans=max(ans,f[i][i+n]);
    }
    cout<<ans<<endl;
} 

提高与综合运用

对于区间dp,变化还有很多,需要我们熟练地掌握,不是只会背模板,这里放一道noip2013提高组的题目作为例题进行综合讲解。

矩阵游戏

题目描述
帅帅经常更同学玩一个矩阵取数游戏:对于一个给定的n*m的矩阵,矩阵中的每个元素a[i][j]据为非负整数。游戏规则如下:
  1. 每次取数时须从每行各取走一个元素,共n个。m次后取完矩阵所有的元素;
  2. 每次取走的各个元素只能是该元素所在行的行首或行尾;
  3. 每次取数都有一个得分值,为每行取数的得分之和;每行取数的得分 = 被取走的元素值*2^i,其中i表示第i次取数(从1开始编号);
游戏结束总得分为m次取数得分之和。
  帅帅想请你帮忙写一个程序,对于任意矩阵,可以求出取数后的最大得分。
输入格式
包括n+1行;
第一行为两个用空格隔开的整数n和m。
第2~n+1行为n*m矩阵,其中每行有m个用单个空格隔开
输出格式
仅包含1行,为一个整数,即输入矩阵取数后的最大的分。
样例数据
input
2 3
1 2 4
3 4 2
output
90
数据规模与约定
60%的数据满足:
1<=n,m<=30,答案不超过1016
100%的数据满足:1<=n, m<=80,0<=a[i][j]<=1000
时间限制:
1s
空间限制:
256MB

分析

先分析题目,大意如下:给出大小为n*m的矩阵,在每行依次取数,每次取数的得分为被取走的元素值*2^i,其中i表示第i次取数(从1开始编号)。累加即为该行的得分。求矩阵的最大得分和。
那么显然每一行之间是没有制约关系的。我们可以求解每一行的最优解,再累加就能得到全局最优解。
考虑如何求解每行最优解。设置状态f[st][en]代表区间[st,en]的最优解。既然是最优解,也就是说区间[st,en]中的每一个数都取了。那么,我们分两种情况讨论:
①由f[st+1][en]转移来。
②由f[st][en-1]转移来。
那么,怎么计算得到当前这个区间新的最优解呢?以①为例,如果认为先取区间[st+1][en],再取a[st](暂时定义a[i]为该行第I个元素),那么在转移时就要加上a[st]en-st+1,难道用快速幂,显然不现实,时空全炸。我们可以认为先取a[st],再取区间[st+1][en],那么计算时,只需将f[st+1][en]乘2,即指数加1,再加上a[st]*21即两倍a[st]即可。②同理,我们认为先取a[en],再取区间[st][en-1],那么将f[st][en-1]乘2,再加上两倍a[en]就能得到f[st][en]。
故状态转移方程为:

f [ s t ] [ e n ] = m a x { f [ s t + 1 ] [ e n ] 2 + a [ s t ] 2 f [ s t ] [ e n 1 ] 2 + a [ e n ] 2

那么对每一行进行转移,再累加即可。
显然,大量的指数运算会爆longlong,这里利用高精度类解决这个问题。
代码实现如下:

#include<bits/stdc++.h>
using namespace std;
#define l(i,st,en) for(int i=st;i<=en;i++)
#define r(i,st,en) for(int i=st;i>=en;i--)
class bign    
{  
    public:  
    static const int base=100000000;  
    static const int width=8;  
    vector<int>s;  
    bign(long long num=0){*this=num;}    //构造函数  
    bign operator=(long long num)  
    {  
        s.clear();  
        do
        {  
            s.push_back(num%base);  
            num=num/base;  
        }
        while(num>0);  
        return *this;  
    }  
    bign operator=(const string &str)      //重载=号  
    {  
        s.clear();  
        int x,len=(str.length()-1)/width+1;  
        for(int i=0;i<len;i++)
        {  
            int end=str.length()-i*width;  
            int start=max(0,end-width);  
            sscanf(str.substr(start,end-start).c_str(),"%d",&x);    //格式符%d是读入十进制整数  
            s.push_back(x);  
        }  
        return *this;  
    }  
    friend ostream & operator<<(ostream &out,const bign& x)   //重载输出号  
    {  
        out<<x.s.back();  
        for(int i=x.s.size()-2;i>=0;i--)
        {  
            char buf[20];  
            sprintf(buf,"%08d",x.s[i]);  
            for(int j=0;j<int(strlen(buf));j++)  
                out<<buf[j];  
        }  
    return out;  
    }  
    friend istream & operator>>(istream &in,bign& x)   //重载输入号  
    {  
        string s;  
        if(!(in>>s)) return in;  
        x=s;  
        return in;  
    }  
    bign operator+(const bign& b)const   //重载加号  
    {  
        bign c;  
        c.s.clear();  
        for(int i=0,g=0;;i++)
        {  
            if(g==0&&i>=s.size()&&i>=b.s.size()) break;  
            int x=g;  
            if(i<s.size()) x+=s[i];  
            if(i<b.s.size()) x+=b.s[i];  
            c.s.push_back(x%base);  
            g=x/base;  
        }  
        return c;  
    }  
    bign operator-(const bign& b)   //重载减号,默认前面大于后面  
    {  
        bign c;  
        c.s.clear();  
        if(*this>b)
        {  
            int i,g;  
            for(i=0,g=0;;i++)
            {  
                if(g==0&&i>=b.s.size()) break;  
                int x=g;  
                if(s[i]<b.s[i])
                {  
                    s[i+1]-=1;  
                    s[i]=s[i]+base;  
                }  
                if(i<s.size()) x+=s[i];  
                if(i<b.s.size()) x-=b.s[i];  
                c.s.push_back(x%base);  
                g=x/base;  
            }  
            int x=0;  
            for(;i<s.size();i++)
            {  
                x+=s[i];  
                c.s.push_back(x%base);  
                x=x/base;  
            }  
        }  
        return c;  
    }  
    bool operator<(const bign& b)const   //重载小于号  
    {  
        if(s.size()!=b.s.size()) return s.size()<b.s.size();  
        for(int i=s.size()-1;i>=0;i--)  
            if(s[i]!=b.s[i]) return s[i]<b.s[i];  
        return false;  
    }  
    bool operator>(const bign& b)const   //重载大于号  
    {  
        return b<*this;  
    }  
    bool operator<=(const bign& b)const  
    {  
        return !(b<*this);  
    }  
    bool operator>=(const bign& b)const  
    {  
        return !(*this<b);  
    }  
    bool operator==(const bign& b)const  //重载等于号  
    {  
        return !(b<*this)&&!(*this<b);  
    }  
    bign operator+=(const bign& b)  
    {  
        *this=(*this+b);  
        return *this;  
    }  
    bign operator-=(const bign& b)  
    {  
        *this=(*this-b);  
        return *this;  
    }
};
bign max(bign a,bign b)
{
    if(a>b)return a;
    else return b;
}
bign mul(bign a,int b)
{
    bign res=0;
    for(int i=1;i<=b;i++)res+=a;
    return res;
}
//f[i][j]代表取区间[i,j]的最大值
bign score[100][100]={},f[100][100]={},sum=0;
int n,m;
bign dp(bign temp[])
{
    l(i,0,99) l(j,0,99)f[i][j]=0;
    for(int len=0;len<=m;len++)
    {
        for(int st=1;st+len<=m;st++)
        {
            int en=st+len;
            f[st][en]=max(mul(f[st+1][en],2)+mul(temp[st],2),mul(f[st][en-1],2)+mul(temp[en],2));
        }
    }
    return f[1][m];
}
int main()
{
    cin>>n>>m;
    l(i,1,n) l(j,1,m) cin>>score[i][j];
    l(i,1,n)sum+=dp(score[i]);
    cout<<sum<<endl;
    return 0;
}

小结

区间dp是动态规划中很难的一个分支,也是极其灵活多变的一个分支。很多时候,它没有固定的模板(例如矩阵游戏中小区间一定为长度为1,在边缘,故循环没有枚举断点k),需要我们根据题目建模,自己制定转移方程和计划。所以,区间dp的题实现难度不大,但往往会有很大的思想难度和很多的细节问题。这就是区间dp的难度所在。这类题目需要我们多加练习,深刻理解。


<后记>


<废话>

猜你喜欢

转载自blog.csdn.net/Prasnip_/article/details/81087949
今日推荐