间dp就是在区间上进行动态规划,求解一段区间上的最优解。主要是通过合并小区间的最优解进而得出整个大区间上最优解的 dp 算法。
既然让我求解在一个区间上的最优解,那么我把这个区间分割成一个个小区间,求解每个小区间的最优解,再合并小区间得到大区间即可。
所以在代码实现上,我可以枚举区间长度len为每次分割成的小区间长度(由短到长不断合并),内层枚举该长度下可以的起点,自然终点也就明了了。然后在这个起点终点之间枚举分割点,求解这段小区间在某个分割点下的最优解。
可能性展开的常见方式:
1.基于两侧端点讨论的可能性展开
2.基于范围上划分点的可能性展开
基于两侧端点讨论的可能性题目
题目一
暴力解法:
直接采用递归的方法,对两侧端点进行讨论,取min
int cmp(char* s, int r, int l)
{
if (r == l) //如果只有一个字符
return 0;
if (r + 1 == l) //如果只有两个字符
{
if (s[r] == s[l])
return 0;
else
return 1;
}
if (s[r] == s[l]) // 如果两个端点的字符相同
return cmp(s, l+1, r-1);
else
return min(cmp(s, l+1, r)+1, cmp(s, l, r-1)+1);
}
当两个端点的字符不相同时,例如aab,我们有两种方式进行插入,我们先满足 l 位置与后面的位置相同,也就是在 b 的后面插入 a 时,那下一步的 r 保持不变,l + 1,当我们也可以满足 r 位置与前面的位置相同,也就是在 a 的前面插入 b 时,则下一步的 l 保持不变,r - 1
两个操作都加一,取最小值。
暴力的递归的方法递归层数太多,那我们就可以通过记忆化搜索
优化一
int f2(char* s, int l, int r, int (*dp)[10])
{
if (dp[l][r] != -1)
return dp[l][r]
int ans;
if (l == r)
{
ans = 0;
}
else if (l + 1 == r)
{
if (s[l] = s[r])
{
ans = 0;
}
else
{
ans = 1;
}
}
else
{
if (s[l] == s[r])
ans = f2(s, l+1, r-1, dp);
else
ans = min(f2(s, l+1, r, dp), f2(s, l, r-1, dp)) + 1;
}
dp[l][r] = ans;
return ans;
}
也可以分析其严格依赖位置的动态规划
(分析可变参数的范围)
假设这个字符串长度为 5
那么我们就可以做出它的二维dp表(dp[i][j] 表示从 i 到 j 保持回文的最小操作次数)
因为 l 一定小于 r
所以它的表为:
当l == r的时候,操作次数一定为0
当l + 1 == r时
我们也可以直接求出对应的dp值
最终,我们要求的就是最右上角的格子(也就是dp[4][4])
那我们分析dp[i][j] 的位置依赖
从递归可知:
dp[i][j] 是由 dp[i+1][j-1],dp[i+1][j],dp[i][j-1] 三个位置得到
因此我们可以发现要求出dp[ i ][ j ]就必须先求出dp[ i+1 ][ j-1 ],dp[ i+1 ][ j ],dp[ i ][ j-1 ]
所以我们的求解顺序:
所以我们就可以直接求出dp表
# include <stdio.h>
int main()
{
char s[10];
int dp[10][10]; //表示从i到j位置保持回文的最少操作次数
int n = 10; //字符串长度
for (int i=0; i<n-1; ++i)
{
if (s[i] == s[i+1])
dp[i][i+1] = 0;
else
dp[i][i+1] = 1;
}
for (int l=n-3; l>=0; --l) //从小到上
for (int r = l+2; r<n; ++r) //从左到右
{
if (s[l] == s[r])
dp[l][r] = dp[l+1][r-1];
else
dp[l][r] = min(dp[l][r-1], dp[l+1][r]);
}
}
也可以用一维dp优化空间
代码类似
题目二
暴力递归:
# include <stdio.h>
//考虑num[l …r]范围上的数字进行游戏,轮到玩家1
//返回玩家1最终能获得多少分数,玩家1和玩家2都绝顶聪明
int cmp(int* num, int l, int r)
{
if (l == r)
return num[l];
if (l + 1 == r )
return max(num[l], num[r]);
//当玩家1,拿num[l]后,轮到玩家2了,他考虑num[l+1, r],因为两个人都是
//顶聪明,所以玩家2一定拿max(num[l+1], num[r]),然后轮到玩家1面对的情况
//一定是 min(cmp(num, l+2, r), cmp(num, l+1, r-1))
int p1 = num[l] + min(cmp(num, l+2, r), cmp(num, l+1, r-1));
//当玩家1,拿num[r]后 …
int p2 = num[r] + min(cmp(num, l+1, r-1), cmp(num, l, r-2));
return max(p1, p2);
}
int main()
{
int sum = 0;
int num[10];
for (int i=0; i<10; ++i)
{
scanf("%d", &num[i]);
sum = sum + num[i];
}
int n = 10;
int first = cmp(num, 0, n-1); //玩家1
int second = sum - first; //玩家2
}
记忆化搜索
# include <stdio.h>
int dp[10][10];
//考虑num[l …r]范围上的数字进行游戏,轮到玩家1
//返回玩家1最终能获得多少分数,玩家1和玩家2都绝顶聪明
int cmp(int* num, int l, int r)
{
int ans;
if (dp[l][r] != -1)
return dp[l][r];
if (l + 1 == r )
ans = max(num[l], num[r]);
else
{
int p1 = num[l] + min(cmp(num, l+2, r), cmp(num, l+1, r-1));
int p2 = num[r] + min(cmp(num, l+1, r-1), cmp(num, l, r-2));
ans = max(p1, p2);
}
return ans;
}
int main()
{
memset(dp, -1, sizeof(dp));
int sum = 0;
int num[10];
for (int i=0; i<10; ++i)
{
scanf("%d", &num[i]);
sum = sum + num[i];
}
int n = 10;
int first = cmp(num, 0, n-1); //玩家1
int second = sum - first; //玩家2
}
也可以分析其严格依赖位置的动态规划
(分析可变参数的范围)
dp[i][j] 是由 dp[i + 2][j], dp[l+1][r - 1], dp[l][r-2]得到
代码于上题类似
基于范围上划分点的可能性展开
题目一
假设有6个顶点
它的values数组为:
最左顶点为0, 最右顶点为5
那么我们就可以直接选择划分点进行枚举
以上的三角形(0,1,5)+ 三角形(1 ~ 5)的情况
以上的三角形(0,2,5)+ 三角形(0 ~ 2)+ 三角形(2 ~ 5)的情况
以上为三角形(0,3,5)+ 三角形(0 ~ 3)+ 三角形(3 ~ 5)的情况
还有一种情况就是三角形(0,4,5)+ 三角形(0 ~ 4)的情况
因此我们可以用记忆化搜索
代码:
# include <stdio.h>
int n;
int v[100];
int dp[100][100];
int cmp(int l, int r)
{
int a;
if (dp[l][r] != -1)
return dp[l][r];
if (l == r || l + 1 == r)
a = 0;
else
{
for (int i=l+1; i<r; ++i)
a = min(a, cmp(0, i) + cmp(i, r) + a[l]*a[r]*a[i]);
}
dp[l][r] = a;
return a;
}
int main()
{
scanf("%d", &n);
for (int i=0; i<n; ++i)
scanf("%d", &v[i]);
memset(dp, -1, sizeof(dp));
int ans = cmp(0, n-1);
}
然后我们就可以查看它严格依赖位置的动态规划
假设n = 4
我们做出它的dp表:
情况一,当以1为划分点时
dp[0][4] = dp[0][1]+dp[1][4]
情况二,当以2为划分点时
dp[0][4] = dp[0][2]+dp[2][4]
情况三,当以3为划分点时
dp[0][4] = dp[0][3]+dp[3][4]
因此我们发现,一个点的dp值是由该点的左方格子和下方的格子构成
所以代码:
# include <stdio.h>
int n;
int v[100];
int dp[100][100];
int main()
{
scanf("%d", &n);
for (int i=0; i<n; ++i)
scanf("%d", &v[i]);
memset(dp, -1, sizeof(dp));
for (int l=n-3; l>=0; --l)
for (int r = l+2; r<n; ++r)
{
dp[l][r] = 100000;
for (int m = l+1; m<r; ++m)
dp[l][r] = min(dp[l][r], dp[l][m]+dp[m][r] + a[l]*a[r]*a[m]);
}
}
题目二
首先我们要先预处理
在首尾都添加 1
这样我们就处理1~5位置的气球,1位置和6位置的气球永远不打爆
方便我们处理边界问题
本题和上题找划分点不一样
本题不能以先打爆某个气球作为划分点
例如:
因此如果我们以先打爆某个气球作为划分点时,我们没有足够的信息去结算下一个状态
因此我们不能以先打爆某个气球作为划分点
我们要以最后打爆某个气球作为划分点
代码:
# include <stdio.h>
int dp[100][100];
memset(dp, -1, sizeof(dp));
int num[100];
int n;
//num[l ... r]表示这些气球决定一个顺序,获得最大得分返回
//一定有:num[r+1]一定没爆
//一定有:num[l-1]一定没爆
//如果没有上述条件,就不要调用com函数
//尝试每个气球最后打爆
int com(int l, int r)
{
if (dp[l][r] != -1)
return dp[l][r];
int ans;
if (l == r)
//我们保证了l - 1 和 r + 1 的气球都没爆
ans = num[l]*num[l-1]*num[r+1];
else
{
ans = max(num[l-1]*num[r+1]*num[l]+com(l+1, r), num[l-1]*num[r+1]*num[r]+com(l, r-1));
for (int k=l+1; k<r; ++k)
ans = max(ans, num[l-1]*num[r+1]*num[k] + com(l, k-1) + com(k+1, r));
}
}
int main()
{
scanf("%d", &n);
for (int i=1; i<=n; ++i)
scanf("%d", &num[i]);
num[0] = 1;
num[n+1] = 1;
int ans = com(1, n);
}
更多
题目一:
我们分析出所有可能的情况:
当然,这两种情况可以结合在一起
因此我们在对字符串进行 dp 时,只用考虑这两种情况
代码:
# include <stdio.h>
# include <string.h>
char a[10];
int n = 10;
int dp[10][10];
int cmp(int l, int r)
{
if (l == r)
return 1;
if (l+1 == r)
{
if (a[l] == '(' && a[r] == ')' || a[l] == '[' && a[r] == ']')
return 0;
else
return 2;
}
if (dp[l][r] != -1)
return dp[l][r];
//情况一 : [l],[r]本来就是配对的
int p1 = 99999;
if (a[l] == a[r])
p1 = cmp(l+1, r-1);
//情况二 : 基于每一个点进行左右划分
int p2 = 99999;
for (int m=l; m<=r; ++m)
{
p2 = min(p2, cmp(l, m) + cmp(m+1, r));
}
int ans = min(p1, p2);
dp[l][r] = ans;
return ans;
}
int main()
{
memset(dp, -1, sizeof(dp));
scanf("%s", a);
printf("%d", cmp(0, n-1));
}
题目二
分析:
代码:
# include <stdio.h>
# include <string.h>
char a[10];
int n = 10;
int dp[10][10];
int cmp(int l, int r)
{
if (dp[l][r] != -1)
return dp[l][r];
int ans;
if (l == r)
ans = 1;
else if (l+1 = r)
{
if (a[l] == a[r])
ans = 1;
else
ans = 2;
}
else
{
if (a[l] == a[r])
ans = cmp(l, r-1);
else
{
for (int m=l, m<=r; ++m)
ans = min(ans, cmp(l, m)+cmp(m+1, r));
}
}
dp[l][r] = ans;
return ans;
}
int main()
{
memset(dp, -1, sizeof(dp));
scanf("%s", a);
printf("%d", cmp(0, n-1));
}
题目三
我们假设有一个理想队形【4,6,2,8】
我们要以最后一个进入作为划分点进行dp
我们假设有一个理想队形【2,6,4,1】
因此我们要设一个三维dp数组
dp[l][r][0]表示:a[l]最后进来
dp[l][r][1]表示:a[r]最后进来
代码:
//严格按照位置的dp
# include <stdio.h>
# include <string.h>
int num[10];
int n = 10;
int dp[10][10][2];
int main()
{
memset(dp, -1, sizeof(dp));
for(int i=0; i<n; ++i)
scanf("%d", &num[i]);
for (int i=1; i<n; ++i)
{
if (num[i] < num[i+1])
{
dp[i][i+1][0] = 1;
dp[i][i+1][1] = 1;
}
}
for (int l=n-2; l>=1; ++l)
{
for (int r=l+2; r<=n; ++r)
{
if (num[l] < num[l+1])
dp[l][r][0] = dp[l][r][0] + dp[l+1][r][0];
if (num[l] < num[r])
dp[l][r][0] = dp[l][r][0] + dp[l+1][r][0];
if (num[l] < num[r])
dp[l][r][1] = dp[l][r][1] + dp[l][r-1][1];
if (num[r-1] < num[r])
dp[l][r][1 = dp[l][r][1] + dp[l][r-1][1];
}
}
}