区间dp
1.石子合并
题目描述
设有 N 堆石子排成一排,其编号为 1,2,3,…,N。
每堆石子有一定的质量,可以用一个整数来描述,现在要将这 N 堆石子合并成为一堆。
每次只能合并相邻的两堆,合并的代价为这两堆石子的质量之和,合并后与这两堆石子相邻的石子将和新堆相邻,合并时由于选择的顺序不同,合并的总代价也不相同。
例如有 4 堆石子分别为 1 3 5 2, 我们可以先合并 1、2 堆,代价为 4,得到 4 5 2, 又合并 1,2 堆,代价为 9,得到 9 2 ,再合并得到 11,总代价为 4+9+11=24;
如果第二步是先合并 2,3 堆,则代价为 7,得到 4 7,最后一次合并代价为 11,总代价为 4+7+11=22。
问题是:找出一种合理的方法,使总的代价最小,输出最小代价。
输入格式
第一行一个数 N 表示石子的堆数 N。
第二行 N 个数,表示每堆石子的质量(均不超过 1000)。
输出格式
输出一个整数,表示最小代价。
数据范围
1≤N≤300
输入样例:
4
1 3 5 2
输出样例:
22
解题思路
划分的依据是
i
到j
之间的点k
,k
的取值是(i,j-1)
,因为把[i,j]
合并为一个,所以至少有两堆石子也就是将(i,k)
,(k+1,j)
合并,合并(i,k)
的代价是f[i,k]
,合并(k+1,j)
的代价是f[k+1,j]
,将(i,k)
,(k+1,j)
合并的代价是从i
到j
石子的重量,可以用前缀和提前求出s
,那么i~j
石子合并的代价就是s[j]-s[i-1]
,所以状态转移方程是f[i][j]]=f[i][k]+f[k+1][j]+s[j]-s[i-1]
代码实现
#include<iostream>
#include<cstring>
using namespace std;
const int N=310;
int s[N],f[N][N];
int main()
{
int n;
cin>>n;
memset(f,0x3f,sizeof(f));//记得初始化
for(int i=1;i<=n;i++){
cin>>s[i];
f[i][i]=0; //初始化
}
for(int i=1;i<=n;i++) s[i]+=s[i-1];
for(int len=2;len<=n;len++){//区间长度一共n堆石子,当n取一的时候就是一堆,不用合并
for(int l=1;l+len-1<=n;l++){//l~k,k~r-1//区间左端点
int r=l+len-1;//区间右端点
for(int k=l;k<r;k++){//k在区间左右端点之间
f[l][r]=min(f[l][r],f[l][k]+f[k+1][r]+s[r]-s[l-1]);
}
}
}
cout<<f[1][n]<<endl;
return 0;
}
2.环形石子合并
题目描述
将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。
规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:
选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。
选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。
输入格式
第一行包含整数 n,表示共有 n 堆石子。
第二行包含 n 个整数,分别表示每堆石子的数量。
输出格式
输出共两行:
第一行为合并得分总和最小值,
第二行为合并得分总和最大值。
数据范围
1≤n≤200
输入样例:
4
4 5 9 4
输出样例:
43
54
解题思路
将n堆环形的石子合并,其实就是在n个点之间连接n-1条边,最后会剩下一个缺口,我们可以通过计算这个缺口位置来计算出合并的代价,为了降低时间复杂度,将环形写成单链,长度是2n,这样在环形里缺口为1~8,在链式就是区间1~8,环形缺口为1~2,在链式就是区间2~1,然后就能像第一题的石子合并一样解题了,只不过,左端点枚举的区间应该是1~n2,因为现在是链长为2n ,最后的答案应该 在
f[i][i+n-1]
(i从1~n)里面找最大(小)
代码实现
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=410;
int f[N][N],s[N],w[N],g[N][N];//f用来求最小,g用来求最大
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>w[i];
w[i+n]=w[i];
}
memset(f,0x3f,sizeof f);
memset(g,-0x3f,sizeof g);
for(int i=1;i<=n*2;i++) s[i]=s[i-1]+w[i];
for(int len=1;len<=n;len++){//枚举长度是n
for(int i=1;i+len-1<=n*2;i++){//枚举区间是1~2n
int j=i+len-1;
if(len==1) f[i][j]=g[i][j]=0;
else{
for(int k=i;k<j;k++){
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]+s[j]-s[i-1]);
g[i][j]=max(g[i][j],g[i][k]+g[k+1][j]+s[j]-s[i-1]);
}
}
}
}
int minf=0x3f3f3f3f;
int maxf=-0x3f3f3f3f;
for(int i=1;i<=n;i++){
minf=min(f[i][i+n-1],minf);
maxf=max(maxf,g[i][i+n-1]);
}
cout<<minf<<endl<<maxf<<endl;
return 0;
}
3.能量项链
题目描述
在 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。
输入格式
输入的第一行是一个正整数 N,表示项链上珠子的个数。
第二行是 N 个用空格隔开的正整数,所有的数均不超过 1000,第 i 个数为第 i 颗珠子的头标记,当 i<N 时,第 i 颗珠子的尾标记应该等于第 i+1 颗珠子的头标记,第 N 颗珠子的尾标记应该等于第 1 颗珠子的头标记。
至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。
输出格式
输出只有一行,是一个正整数 E,为一个最优聚合顺序所释放的总能量。
数据范围
4≤N≤100,
1≤E≤2.1×109
输入样例:
4
2 3 5 10
输出样例:
710
解题思路
这道题可以将能量石看成矩阵,两个能量石合并释放的能量其实就是两个矩阵相乘的数乘次数,一个
pxq
的矩阵和一个qxr
的矩阵相乘,数乘次数是pxqxr
,连续n
个矩阵相乘,我们可以在n
个矩阵之间设置断点,这个断点左边的矩阵相乘的代价加上右边矩阵相乘的代价再加上断点左右两边的矩阵的代价就是设置在某个断点处的代价,断点可以取i+1\~j-1
之间的数,枚举所有断点,也就是计算出了所有可行解然后在里面找到最优解即可。这道题跟上一个一样,也是一个环形的,我们还是通过将一个长度为n
的环形链变成一个长度为2*n
的单链来计算,改变枚举的区间即可。
注意:输入样例2 3 5 10
表示的意思是,2x3 3x5 5x10 10x2
的四个能量石(矩阵),当前两个矩阵相乘时 由2x3 3x5
变成了2x5
其实就是消去了3
而且i
至少是3
才能表示两个矩阵相乘,否则 只有一个矩阵,数乘次数是0,也就是能量石合并不释放能量。k
(断点)的取值是i+1\~j-1
,因为k
其实就是两个矩阵相乘第一个矩阵的列数(第二个矩阵的行数),而第一个矩阵的行是i
,i\~k
是一个矩阵,k\~j
是一个矩阵,所以k
不能等于i
也不能等于j
.
最后的答案就是f[i][i+n]
(i
从1~n
)因为矩阵相乘(能量石的合并)是一个长度为n+1
的链,因为要在n
个点之间连n
条线
代码实现
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=220;
int f[N][N],w[N];
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++){
cin>>w[i];
w[i+n]=w[i];
}
for(int len=3;len<=n+1;len++){//len从3开始
for(int i=1;i+len-1<=2*n;i++){
int j=i+len-1;
for(int k=i+1;k<j;k++){//k的取值[i+1,j-1]
f[i][j]=max(f[i][j],f[i][k]+f[k][j]+w[i]*w[k]*w[j]);
}
}
}
int res=0;
for(int i=1;i<=n;i++) res=max(res,f[i][i+n]);
cout<<res<<endl;
return 0;
}
4.凸多边形的划分
题目描述
给定一个具有 N 个顶点的凸多边形,将顶点从 1 至 N 标号,每个顶点的权值都是一个正整数。
将这个凸多边形划分成 N−2 个互不相交的三角形,对于每个三角形,其三个顶点的权值相乘都可得到一个权值乘积,试求所有三角形的顶点权值乘积之和至少为多少。
输入格式
第一行包含整数 N,表示顶点数量。
第二行包含 N 个整数,依次为顶点 1 至顶点 N 的权值。
输出格式
输出仅一行,为所有三角形的顶点权值乘积之和的最小值。
数据范围
N≤50,
数据保证所有顶点的权值都小于109
输入样例:
5
121 122 123 245 231
输出样例:
12214884
解题思路
不管怎么划分,最后一定会有最后一个三角形是用旁边两个多边形的的边构成的,所以枚举左右两边的多边形,计算他们划分为三角形的代价再加上中间的三角形的代价。那么计算一个多边形就可以用f[l,r]来表示,表示这个多边形是从l到r的点的划分,划分只要是三个点就能组成一个三角形,和矩阵相乘类似,枚举l和r之间的断点k即可,然后可以发现,最后得到的状态转移方程是一样的
注意:数据很大,所以需要用到高精度!
代码实现
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long LL;
const int N=55,M=35,INF=1e9;
int w[N];
LL f[N][N][M];//M用来表示一个向量 将每一位都存下来
void add(LL a[],LL b[]){//高精度加
static LL c[M];
memset(c,0,sizeof c);
for(int i=0,t=0;i<M;i++){
t+=a[i]+b[i];
c[i]=t%10;
t/=10;
}
memcpy(a,c,sizeof(c));
}
void mull(LL a[],int b){//高精度乘
static LL c[M];
memset(c,0,sizeof c);
LL t=0;
for(int i=0;i<M;i++){
t+=a[i]*b;
c[i]=t%10;
t/=10;
}
memcpy(a,c,sizeof(c));
}
int cmp(LL a[],LL b[]){//比较两个高精度数的大小
for(int i=M-1;i>=0;i--){//注意,由于数是倒叙存放的 所以从后往前比较
if(a[i]>b[i]) return 1;
else if(a[i]<b[i]) return -1;
}
return 0;
}
void print(LL a[]){
int k=M-1;
while(k&&!a[k]) k--;
while(k>=0) cout<<a[k--];
cout<<endl;
}
int main()
{
int n;
cin>>n;
LL tmp[M];
for(int i=1;i<=n;i++) cin>>w[i];
for(int len=3;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
f[i][j][M-1]=1;//初始化第M-1位是1,就相当于正无穷,因为数是倒叙放在数组里的
for(int k=i+1;k<j;k++)
{
memset(tmp,0,sizeof tmp);
tmp[0]=w[i];
mull(tmp,w[k]);
mull(tmp,w[j]);
add(tmp,f[i][k]);
add(tmp,f[k][j]);
if(cmp(f[i][j],tmp)>0){
memcpy(f[i][j],tmp,sizeof tmp);
}
}
}
}
print(f[1][n]);
return 0;
}
5.加分二叉树
题目描述
解题思路
计算加分的时候,对于每一种前序遍历的二叉树,它的加分一定是左子树*右子树+根节点的分值(如果不存在左子树和右子树则直接等于根结点的分值),左边和右边是互不影响的,而且可以发现,前序遍历,每一颗子树的结点的遍历一定是一段连续的区间,左子树遍历完再递归回去遍历右子树,因为动态规划可以考虑计算一段区间
枚举根结点的位置,对于区间[L,R]
,根节点可以取L\~R
之间的数,对于不同的根结点,计算出所有的加分,取最大值,因为要输出前序遍历,所以多开一个数组记录根结点的转移,用g[l,r]
表示l
,r
这一段的根结点是谁,比如长度为n
,k=g[1,n]
表示1
到n
的根节点,左子树的根节点就是1
到k-1
即g[1,k-1]
,右子树的根结点就是k+1
到n
即g[k+1,n]
,再一直递归下去就行了.
代码实现
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=35;
int w[N];
int f[N][N],g[N][N];
void dfs(int l,int r){
if(l>r) return;
int k= g[l][r];
cout<<k<<' ';
dfs(l,k-1);
dfs(k+1,r);
}
int main()
{
int n;
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
for(int len=1;len<=n;len++){
for(int i=1;i+len-1<=n;i++){
int j=i+len-1;
for(int k=i;k<=j;k++){
int left=k==i?1:f[i][k-1];//如果左子树为空 加分为1
int right=k==j?1:f[k+1][j];//如果右子树为空 加分为1
int score=left*right+w[k];
if(i==j) score=w[k];//如果是叶子结点 就是它本身的分数
if(score>f[i][j]){
f[i][j]=score;
g[i][j]=k;//记录当前的根结点
}
}
}
}
cout<<f[1][n]<<endl;
dfs(1,n);
cout<<endl;
return 0;
}
初学区间dp,有错误的话,欢迎大家指出,对于题解有什么好的意见也欢迎提出,加油呀~