目录
一、概述
之前介绍的线性dp一般从初态开始,沿着某个方向扩展,直到计算出目标状态。区间dp也是线性dp的一种,不过区间dp以“区间长度”为方向,使用两个坐标(左端点和右端点)来描述每个维度。区间dp的一个状态由某个比他更小且包含于他的区间所代表的状态转移而来。因此,区间dp的决策就是划分区间的方法。
区间dp的初态一般是长度为1的“元区间”,这种向下划分,再向上递推的模式与树形结构类似,因此也会使用记忆化搜索等方式——本质是动态规划的递归实现方法。
思考类似dp问题,感觉和分治思想有一定联系。
二、环形区间dp
遇到环形,一个常用的处理方法是,将环拓展为两倍,在拆成链。这样在任意选取1~n一个节点k,往下走n步,就是在环形中以k为起点走一圈的走法。
(1)环形石子合并
将环扩大一倍展成链,求出这条链上的长度为n的石子合并代价最大最小值即可。
具体实现回顾之前写过的一维石子合并代码即可。
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
//环展成两倍链
const int N =410;
int f[N][N];
int g[N][N];
int n;
int s[N];
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>s[i],s[i+n]=s[i];
int nn=2*n;
memset(f,0x3f,sizeof f);
memset(g,0xcf,sizeof g);
for(int i=1;i<=nn;i++) s[i]+=s[i-1],f[i][i]=0,g[i][i]=0;
for(int len=2;len<=n;len++)
for(int l=1;l+len-1<=nn;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]);
g[l][r]=max(g[l][r],g[l][k]+g[k+1][r]);
}
f[l][r]+=s[r]-s[l-1];
g[l][r]+=s[r]-s[l-1];
}
int res1=0x3f3f3f3f;
int res2=-0x3f3f3f3f;
for(int i=1;i<=n;i++) res1=min(res1,f[i][i+n-1]),res2=max(res2,g[i][i+n-1]);
cout<<res1<<endl<<res2;
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4157290/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
(2)能量项链(矩阵合并)
和石子合并类似,下面仅列举不同点。
- 将这下标记存到数组中,由于我们只存下这些数,观看样例我们可以知道,每个数都被用了两遍,因此我们的len最大是n+1,最小是3。(为了最后一次再合并用上第一个矩阵的行数)
- 合并石子是两堆合成一堆,只需要两个数,合并之后变为一个石子堆的数量,能量释放是一个行一个列,和另一个矩阵的列数。需要三个数,合并之后上下一个矩阵,包含行列两个参数。
- 由第二点造成的影响,k的范围不可取到l,r。由于每个点都会用两次,因此转移方程是:
代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N =210;
int f[N][N];//与合并石子不同 本题f更像是个矩阵
int n;
int w[N];
int main()
{
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++)//开头的点需要算两次
for(int l=1;l+len-1<=n*2;l++)
{
int r=l+len-1;
for(int k=l+1;k<r;k++)//注意是k+1开始遍历,因为矩阵合并要三个参数 合并石子只需要两堆合一堆
f[l][r]=max(f[l][r],f[l][k]+f[k][r]+w[l]*w[k]*w[r]);//注意不是k+1了
}
int res=0;
for(int i=1;i<=n;i++) res=max(res,f[i][i+n]);
cout<<res;
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4158010/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
三、推公式,与高精度结合
(1)凸多边形划分
我们对任意一个凸多边形都可以如此划分(以六边形为例)。划分为以1,6为底边的一个三角形和三角形左右两边两部分,划分点可以是2~5。左右两边没有关系,所以可以分别计算。此时,多边形权值之和分为了三部分:左半边,右半边,和划分点与左右端点构成的三角形。
运用区间dp的思想,我们可以用f[l.r]存储划分下标为l~r所组成的凸多边形的所有方案的最小值。因此状态转移方程为:
补充一个思维误区:有可能会提到,如果以1,5为端点划分,那么多边形就划分为了四部分:划分三角形的三条边隔出的区域,和它本身。
但是仔细观察发现,这应该是属于以1,6为底边,划分点为5的划分方案中的一种,以我们之前所说的方法来做的话这种方案是在决策集合里面被考虑过的。
综上,不难想到我们的决策集合覆盖了所有可能的方案,因此最后的答案是正确的。
此外,因为本题三角形端点的权值给的非常大,因此需要用高精度来存储答案。
四、与记忆化搜索结合(递归实现方法)
(1)加分二叉树
为了保证是中序遍历,则应该构成一颗二叉搜索树。影响树的加分的因素是每个树的根节点的选择。
因此我们容易想到用f[l,r]表示编号为l~r区间内的点构成的最大加分。状态划分依据是根节点的选择。因此枚举l~r内所有点做根节点。(特别地,如果划分点k恰好是l或者r,则左子树或右子树的加分应该是1)状态转移方程为:
此外,需要考虑怎么输出方案。依照中序遍历的知识,我们用一个数组g存储一颗树编号区间是l~r内的根节点。输出根节点之后dfs搜索左右节点即可。
保证字典序最小只需要枚举k时是从l~r的顺序枚举,只有当枚举的状态所求的值严格大于当前值时才更新状态,即可。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N =32;
int f[N][N];
int g[N][N];
int n;
int w[N];
void dfs(int l,int r)
{
if(l>r) return;
int root=g[l][r];
cout<<root<<" ";
dfs(l,root-1);
dfs(root+1,r);
}
int main()
{
cin>>n;
for(int i=1;i<=n;i++) cin>>w[i];
for(int len=1;len<=n;len++)
for(int l=1;l+len-1<=n;l++)
{
int r=l+len-1;
if(len==1) f[l][r]=w[l],g[l][r]=l;
else
{
for(int k=l;k<=r;k++)//枚举根节点
{
int left=k==l?1:f[l][k-1];
int right=k==r?1:f[k+1][r];
int score=left*right+w[k];
if(f[l][r]<score)//让根节点最左
{
g[l][r]=k;
f[l][r]=score;
}
}
}
}
cout<<f[1][n]<<endl;
dfs(1,n);
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4160572/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
(2)棋盘分割(二维区间)
综合我们学过的知识。不难想到如下做法:
- 首先可以确定,更大的范围由更小的范围得来。先计算出每个小棋盘均方差,在递推出大范围内的均方差。
- 接下来确定在状态转移方程,一块大面积的棋盘,要切割成小范围的和标准的棋盘,就只能在x或y方向上,选取某个位置,一刀切去一整行或一整列,得到两部分。继续细分时需要分别分治两部分,取两者最小值。
- 每个区域的均方差都是可以存储下来的,因此用记忆化搜索比较方便。
- 限定了分块的次数,那么需要多加一维状态即可、
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 15, M = 9;
const double INF = 1e9;
int n, m = 8;
int s[M][M];
double f[M][M][M][M][N];
double X;
int get_sum(int x1,int y1,int x2,int y2)//得到部分和
{
return s[x2][y2]-s[x2][y1-1]-s[x1-1][y2]+s[x1-1][y1-1];
}
double get(int x1, int y1, int x2, int y2)//得到这部分均方差^2
{
double sum = get_sum(x1, y1, x2, y2) - X;
return (double)sum * sum / n;
}
double dp(int x1,int y1,int x2,int y2,int k)//某部分切成k块的最小均方差和^2
{
double &v=f[x1][y1][x2][y2][k];
if(v>=0) return v;
if(k==1) return v=get(x1,y1,x2,y2);
v=INF;
for(int i=x1;i<x2;i++)//横切
{
v=min(v,get(x1,y1,i,y2)+dp(i+1,y1,x2,y2,k-1));//切出来两块 分别分治
v=min(v,get(i+1,y1,x2,y2)+dp(x1,y1,i,y2,k-1));
}
for(int i=y1;i<y2;i++)
{
v=min(v,get(x1,y1,x2,i)+dp(x1,i+1,x2,y2,k-1));
v=min(v,get(x1,i+1,x2,y2)+dp(x1,y1,x2,i,k-1));
}
return v;
}
int main()
{
cin>>n;
for(int i=1;i<=m;i++)
for(int j=1;j<=m;j++)
{
cin>>s[i][j];
s[i][j] += s[i-1][j]+s[i][j-1]-s[i-1][j-1];
}
X=(double)s[m][m]/n;//平均数
memset(f,-1,sizeof f);
printf("%.3lf\n",sqrt(dp(1,1,m,m,n)));
return 0;
}
作者:yankai
链接:https://www.acwing.com/activity/content/code/content/4162261/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。