目录
- 引言
- 一、石子合并
- 二、环形石子合并
- 三、能量项链
- 四、加分二叉树
引言
关于区间DP,我其实觉得核心思想就是把一个区间拆分为任意两个区间,相当于是模拟枚举全部这种区间组合的过程,然后从中寻求最优解,本质上的思想不难,难的是跟其它知识点的一个组合,只是说用到了这种思想而已,那这就有挑战性了,不过还是继续加油吧!
一、石子合并
标签:动态规划、区间DP
思路:
整个区间
D
P
DP
DP 的模板就是如下的一种形式,先枚举长度,然后枚举左端点,并且让右端点不能超过整个的区间,然后去进行区间的拆分,找到最优解。状态定义就是
f
[
l
]
[
r
]
f[l][r]
f[l][r] 代表合并
l
∼
r
l \sim r
l∼r 所花费的最小代价。
题目描述:
设有 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
示例代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second
const int N = 310, M = N, INF = 0x3f3f3f3f;
int n, m;
int s[N], f[N][N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) cin >> s[i], s[i] += s[i-1];
memset(f, 0x3f, sizeof f);
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] = 0;
else
{
for(int k = l; k < r; ++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;
}
二、环形石子合并
标签:DP、区间DP、环形区间DP
思路:
对于环形区间问题其实就是在此基础上再加一条完全相等的区间,然后做长度为
n
n
n 的区间合并,最后枚举每个区间找最值即可。原因是环形的最后肯定会有一个缺口,这个缺口意味着两个相邻的点永远不会挨着合并,然后我们就枚举所有这样的缺口,最终展开的结果就是有
n
n
n 种链,
n
n
n 种石子合并问题。只要记住区间
D
P
DP
DP 的环形问题都是这样解决的就行了。
题目描述:
将 n 堆石子绕圆形操场排放,现要将石子有序地合并成一堆。
规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆的石子数记做该次合并的得分。
请编写一个程序,读入堆数 n 及每堆的石子数,并进行如下计算:
选择一种合并石子的方案,使得做 n−1 次合并得分总和最大。选择一种合并石子的方案,使得做 n−1 次合并得分总和最小。
输入格式
第一行包含整数 n,表示共有 n 堆石子。
第二行包含 n 个整数,分别表示每堆石子的数量。
输出格式
输出共两行:
第一行为合并得分总和最小值,
第二行为合并得分总和最大值。
数据范围
1≤n≤200
输入样例:
4
4 5 9 4
输出样例:
43
54
示例代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second
const int N = 410, M = N, INF = 0x3f3f3f3f;
int n, m;
int w[N], s[N], f[N][N], g[N][N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) cin >> w[i], w[i+n] = w[i];
for(int i = 1; i <= n * 2; ++i) s[i] += s[i-1] + w[i];
memset(f, 0x3f, sizeof f);
memset(g, -0x3f, sizeof g);
for(int len = 1; len <= n; ++len)
{
for(int l = 1; l + len - 1 <= n * 2; ++l)
{
int r = l + len - 1;
if(len == 1) f[l][r] = g[l][r] = 0;
else
{
for(int k = l; k < r; ++k)
{
f[l][r] = min(f[l][r], f[l][k] + f[k+1][r] + s[r] - s[l-1]);
g[l][r] = max(g[l][r], g[l][k] + g[k+1][r] + s[r] - s[l-1]);
}
}
}
}
int minv = INF, maxv = -INF;
for(int i = 1; i <= n; ++i)
{
minv = min(minv, f[i][i+n-1]);
maxv = max(maxv, g[i][i+n-1]);
}
cout << minv << endl << maxv << endl;
return 0;
}
三、能量项链
标签:DP、区间DP、破环成链
思路:
思路其实还是上一题的思路,在后面再开一串相同的链就行了,不一样的是该题一个石子的参数有两个,意味着我们合并的时候是三个三个的合并,并且最终的答案是
f
[
1
]
[
n
+
1
]
f[1][n+1]
f[1][n+1] ,也是要包括自己的,因为由于题目给出的性质肯定最后是如下图的一种方式:
所以最终的答案是包括自己的。再总结一下区间
D
P
DP
DP 的思路,先把所有的状态置为非法,然后从
1
∼
n
1\sim n
1∼n 枚举每一个区间,在此基础上将一些合法的区间初始化,比如这道题中区间长度小于
3
3
3 的值都为
0
0
0 ,然后就进行拆分取最值即可。
题目描述:
在 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
示例代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second
const int N = 210, M = N, INF = 0x3f3f3f3f;
int n, m;
int w[N], f[N][N];
int main()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) cin >> w[i], w[i+n] = w[i];
memset(f, -0x3f, sizeof f);
for(int len = 1; len <= n + 1; ++len)
{
for(int l = 1; l + len - 1 <= n * 2; ++l)
{
int r = l + len - 1;
if(len < 3) f[l][r] = 0;
else
{
for(int k = l + 1; k < r; ++k)
{
f[l][r] = max(f[l][r], f[l][k] + f[k][r] + w[l] * w[k] * w[r]);
}
}
}
}
int res = 0;
for(int i = 1; i <= n; ++i)
{
res = max(res, f[i][i+n]);
}
cout << res << endl;
return 0;
}
四、加分二叉树
标签:DP、区间DP
思路:
首先需要构造一个中序遍历的二叉树,也就意味着如果根结点确定为
k
k
k 的话,那么它的左子树编号就为
1
∼
k
−
1
1\sim k-1
1∼k−1 ,右子树的编号为
k
+
1
∼
n
k+1\sim n
k+1∼n ,那么也就意味着这道题是一道区间
D
P
DP
DP 的问题,我们需要枚举每一个区间,然后对每一个区间枚举它的根结点,然后求出这个区间的最大值。这里注意的是叶子结点就是本身的值,然后空树的加分为
1
1
1 这意味着是在算根结点的加分值,而不是它本身的值,这是两个值需要区分,然后在其中对于空树特判一下即可。对于前序遍历我们可以记录下最优解的方案,定义数组
g
[
l
]
[
r
g[l][r
g[l][r 代表区间
[
l
,
r
]
[l,r]
[l,r] 的根结点,然后去递归这个过程,先输出根结点然后输出左子树的根结点,再输出右子树的根结点即可。然后区间
D
P
DP
DP 整体的思路再说一遍,加深印象,先是将所有的状态赋为非法值,枚举区间长度,然后枚举左端点,并且右端点不能超过整个区间的长度,对于一些初始化值根据区间长度将其初始化,然后枚举区间划分的
k
k
k ,根据题目要求进行状态计算求最值。
题目描述:
设一个 n 个节点的二叉树 tree 的中序遍历为(1,2,3,…,n),其中数字 1,2,3,…,n 为节点编号。
每个节点都有一个分数(均为正整数),记第 i 个节点的分数为 di,tree 及它的每个子树都有一个加分,任一棵子树 subtree
(也包含 tree 本身)的加分计算方法如下:
subtree的左子树的加分 × subtree的右子树的加分 + subtree的根的分数
若某个子树为空,规定其加分为 1。
叶子的加分就是叶节点本身的分数,不考虑它的空子树。
试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树 tree。
要求输出:
(1)tree的最高加分
(2)tree的前序遍历
输入格式
第 1 行:一个整数 n,为节点个数。
第 2 行:n 个用空格隔开的整数,为每个节点的分数(0<分数<100)。
输出格式
第 1 行:一个整数,为最高加分(结果不会超过int范围)。
第 2 行:n 个用空格隔开的整数,为该树的前序遍历。如果存在多种方案,则输出字典序最小的方案。
数据范围
n<30
输入样例:
5
5 7 1 2 10
输出样例:
145
3 1 2 4 5
示例代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second
const int N = 35, M = N, INF = 0x3f3f3f3f;
int n, m;
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()
{
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
cin >> n;
for(int i = 1; i <= n; ++i) cin >> w[i];
memset(f, -0x3f, sizeof f);
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(score > f[l][r])
{
f[l][r] = score;
g[l][r] = k;
}
}
}
}
}
cout << f[1][n] << endl;
dfs(1,n);
return 0;
}