一、简述
区间dp,顾名思义,在区间上dp,大多数题目的状态都是由区间构成的,就是我们可以把大区间转化成小区间来处理,然后对小区间处理后再回溯的求出大区间的值,主要的方法有两种,记忆化搜索和递推。
在用递推来求解时,关键在于递推是for循环里面的顺序,以及dp的关键:状态转移方程。
当然大部分的区间dp都是有特点的,我们可以考虑符合什么条件下,大区间可以转化成小区间,然后找出边界条件,进行dp求解。
for(int len=2;len<=n;len++){//枚举区间长度
for(int i=1;i<n;++i){//枚举区间的起点
int j=i+len-1;//根据起点和长度得出终点
if(j>n) break;//符合条件的终点
for(int k=i;k<=j;++k)//枚举最优分割点,k为分隔位置
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k+1][j]+w[i][j]);
//状态转移方程
}
}
基础模板有了,一般的简单题都可以上手,但有些题可能会超时这时候就要用到一个经典的优化可以把它优化到:O(n^2),证明很难理解。
优化思路大概为:多开一个数组s,在枚举最优分割点时,再缩小一下枚举范围,经典的用空间换时间的做法。称为四边形优化。
for(int len=2;len<=n;len++){
for(int i = 1;i<=n;i++){
int j = i+len-1;
if(j>n) break;
for(int k = s[i][j-1];k<=s[i+1][j];k++){
//缩小枚举范围
if(dp[i][j]>dp[i][k]+dp[k+1][j]+w[i][j]){
dp[i][j]=dp[i][k]+dp[k+1][j]+w[i][j];
s[i][j]=k;
}
}
}
}
二、石子合并(链状)
大区间包含小区间,我们解决了小区间才能解决大区间。我们 跑遍一个区间的所有可能组合 ,就可以取出这个区间的最优组合,知晓这个区间的最优解。在 for 循环中的最外层放置L(length),使得小区间会在大区间之前被处理完
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int dp[300][300],num[300],s[300];
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> num[i];
s[i] = s[i - 1] + num[i];
}
for (int len = 2; len <= n; len++) { //遍历长度
for (int i = 1,j=i+len-1; j<=n;i++,j=len+i-1) { //遍历左端点
dp[i][j] = 99999999;
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] << endl;
}
三、石子合并(环形)
在一个圆形操场的四周摆放 N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。试设计出一个算法,计算出将 N堆石子合并成 1堆的最小得分和最大得分。
题目地址:石子合并
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int f1[300][300], f2[300][300],num[300],s[300];
int d(int i, int j) { return s[j] - s[i - 1]; }
//返回区间[i,j]的分数
//转移方程:f[i][j] = max(f[i][k]+f[k+1][j]+d[i][j]);
int main()
{
int n;
cin >> n;
for (int i = 1; i <= n; i++)
{//因为是一个环,所以需要两倍再枚举分界线,
cin >> num[i];
num[i + n] = num[i];
}
for (int i = 1; i < 2 * n; i++)
s[i] = s[i - 1] + num[i];
for (int p = 1; p <= n; p++)//枚举区间
{
for (int i = 1, j = i + p; (i < 2 * n) && (j < 2 * n); i++, j = i + p)
{//i为区间开头
f2[i][j] = 999999999;
for (int k = i; k < j; k++)
{
f1[i][j] = max(f1[i][j], f1[i][k] + f1[k + 1][j] + d(i, j));
f2[i][j] = min(f2[i][j], f2[i][k] + f2[k + 1][j] + d(i, j));
}
}
}
int minl = 999999, maxl = 0;
for (int i = 1; i <= n; i++)
{//再遍历出最大最小
maxl = max(maxl, f1[i][i + n - 1]);
minl = min(minl, f2[i][i + n - 1]);
}
cout << minl << endl << maxl << endl;
}
四、能量项链
如果前一颗能量珠的头标记为m ,尾标记为r ,后一颗能量珠的头标记为r,尾标记为n ,则聚合后释放的能量为m×r×n,新产生的珠子的头标记为m,尾标记为n 。请你设计一个聚合顺序,使一串项链释放出的总能量最大。
题目地址: 能量项链
分析:其实就是把加变成乘,思路还是不变,注意是环状,最后需要扫描一遍最大值。
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
using namespace std;
int dp[300][300],num[300];
int main()
{
int n,maxn=0;
cin >> n;
for (int i = 1; i <= n; i++)
{
cin >> num[i];
num[i + n] = num[i];
}
for (int j= 2; j < 2*n; j++) { //遍历长度
for (int i =j-1; j-i<n&&i>=1;i--) { //遍历左端点
for (int k =i; k<j; k++) { //遍历合并点
dp[i][j] = max(dp[i][j],dp[i][k]+dp[k+1][j]+num[i]*num[k+1]*num[j+1]);
//状态转移方程:max(原来能量,左区间能量+右区间能量+前一颗头*尾*后一颗尾)
if (dp[i][j] > maxn)maxn = dp[i][j];//求最大值
}
}
}
cout << maxn << endl;
}
五、括号匹配
Hecy又接了个新任务:BE处理。BE中有一类被称为GBE。
以下是GBE的定义:
空表达式是GBE
如果表达式A是GBE,则[A]与(A)都是GBE
如果A与B都是GBE,那么AB是GBE
下面给出一个BE,求至少添加多少字符能使这个BE成为GBE。
思路:因为A是GBE-------->[A]和(A)是GBE 则若x、y两个位置可以构成一对括号则dp[x][y]=dp[x+1][y-1]+2(因为是两个已经匹配过的括号);
(2)因为A、B是GBE--------->AB是GBE 则枚举xy中区间断点k并用dp[x][k]+dp[k+1][y]更新dp[x][y];
#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
#include<string.h>
using namespace std;
char s[300];
int dp[300][300];
int main()
{
cin >> s+1;
int n = strlen(s+1);
for (int i = 2; i <= n; i++)
{
for (int j = 1; j + i - 1 <= n; j++)
{
int l = j, r = j + i - 1;
if ((s[l] == '(' && s[r] == ')') || (s[l] == '[' && s[r] == ']'))
dp[l][r] = max(dp[l][r], dp[l + 1][r - 1] + 2);
for (int k = l; k < r; k++) dp[l][r] = max(dp[l][r], dp[l][k] + dp[k + 1][r]);
}
}
cout << n - dp[1][n] << endl;
//最后给所有没有匹配的再匹配即可
}