本蒟蒻通过这两个题入门区间DP,就分享一下心得,也算是复习一下了。
首先来看看题:
线形石子合并
N 堆石子排成一排,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 NN堆石子合并成 1 堆的最小得分。
#include<iostream>
using namespace std;
#define MAX 200
#define INF 0x3f3f3f3f //用这个表示无穷大
long long int f[MAX][MAX], sum[MAX], stone;
//f是dp数组,表示从第i个到j个的最优解
//sum是前缀和(本蒟蒻也是才知道还有这种操作
//stone是石头的值
int main()
{
int n;
cin >> n;
for(int i = 1; i <= n; i ++)
{
cin >> stone;
sum[i] = stone + sum[i - 1];
//前缀和,用起来老方便了
}
for(int i = n - 1; i >= 1; i --)
{
//i代表的是每次讨论的长度,
//我们要求从第1堆合并到第n堆的结果,还得分步来
//比如要求1~5堆那么先讨论1~2,2~3,3~4,4~5,
//再讨论1~3,2~4,3~5,然后再1~4,2~5,最后就有了1~5;
//只不过这里是倒过来的
for(int j = i + 1; j <= n; j ++)
{
//j是终点,i是起点,表示从i合并到j的最优结果
f[i][j] = INF;
//求最小结果,就先初始为最大
for(int k = i; k < j; k ++)
{
//精髓就是中间状态K,也就是分步讨论
//讨论的过程--比如:对于f[1][5]的最优解,
//可能是来自f[1][3]与f[4][5]合并,也可能是来自f[1][2]与f[3][5]合并,
//所以我们需要通过这样一个判断找出它的最优解:
if(f[i][j] > f[i][k] + f[k + 1][j] + (sum[j] - sum[i - 1]))
f[i][j] = f[i][k] + f[k + 1][j] + (sum[j] - sum[i - 1]);
//由于每个f[i][k]和f[k+1][j]代表两堆石子,
//那么要变成一堆,还得合并它们
//也就是还得求从i到j的所有石子的值,即sum[j] - sum[i - 1];
}
}
}
cout << f[1][n];
return 0;
}
也许我的描述会有一点问题,毕竟蒟蒻啊
但是大致上应该是这样的
现在,了解了上面这个题的思想,再来看看下面这个题:
环形石子合并
在一个圆形操场的四周摆放 N堆石子,现要将石子有次序地合并成一堆.规定每次只能选相邻的2堆合并成新的一堆,并将新的一堆的石子数,记为该次合并的得分。
试设计出一个算法,计算出将 N堆石子合并成 1堆的最小得分和最大得分。
输入格式
数据的第 1行是正整数 N,表示有 N堆石子。
第 2 行有 N 个整数,第 i个整数 ai表示第 i堆石子的个数。
输出格式
输出共 2行,第 1行为最小得分,第 2 行为最大得分。
输入
4
4 5 9 4
输出
43
54
传送门:https://www.luogu.com.cn/problem/P1880
这个题的不一样之处在于首尾可以相连,难点就在于如何去构建这样一个环形,
思前想后 发现问题很大 发现好像高中数学老师教给我们一个方法叫做 “化曲为直”
果然数学基础决定上层建筑,蒟蒻
比如
我们可以把它用一位数组存储:
stone[] ={1,2,3,4,5,1,2,3,4,5};
举个例子,首先1与2(stone[0]与stone[1])合并为3*(加 * 区别于开始就有的3),然后5在逻辑上与这个3 * 相临,
如果5与这个3 * 要合并的话,在此之前会有stone[5]与stone[6]的合并也是得到3*,
这样一来5与3 * 就在一维数组中相邻了。
有了这个操作,结合上一题的思想,代码实现也就容易了:
#include<iostream>
//#include<stdlib.h>
#include<string.h>
#include<math.h>
using namespace std;
#define INF 0X3f3f3f3f
int sum[210], stone[210], dpmax[210][210], dpmin[210][210];
int main()
{
int n;
cin >> n;
memset(dpmax, -INF, sizeof(dpmax));
memset(dpmin, INF, sizeof(dpmin));
//从1开始避免讨论混乱,也有利于第一个前缀和的处理
for(int i = 1; i <= n; i ++)
{
cin >> stone[i];
stone[i + n] = stone[i];
}
for(int i = 1; i <= 2 * n; i ++)
{
//前缀和,sum[0]本身就是0不需要特殊处理--从1开始的好处
sum[i] += sum[i - 1] + stone[i];
//代表自己与自己合并,当然不计入
//其实可以省略这一步,因为全局的数组默认就是0
dpmax[i][i] = 0;
dpmin[i][i] = 0;
}
for(int i = 1; i < n; i ++)//每次讨论的长度(长度为i+1)
{
for(int l = 1; l + i <= 2 * n; l ++)//左边界,右边界(l + i)不超过2n
{
int r = l + i;//右边界
for(int k = l; k < r; k ++)
{
dpmax[l][r] = max(dpmax[l][r], dpmax[l][k] + dpmax[k + 1][r] + sum[r] - sum[l - 1]);
dpmin[l][r] = min(dpmin[l][r], dpmin[l][k] + dpmin[k + 1][r] + sum[r] - sum[l - 1]);
}
}
}
int MaxA = -INF, MinA = INF;
for(int i = 1; i <= n; i ++)
{
//相当于考虑环形的起点是哪里,取最优的一种情况
MaxA = max(MaxA, dpmax[i][i + n - 1]);
MinA = min(MinA, dpmin[i][i + n - 1]);
}
cout << MinA << endl << MaxA << endl;
return 0;
}
蒟蒻笔记,码字不易,如有错误,欢迎指正~~