区间DP详细解析
1.定义与性质
区间类动态规划是线性动态规划的扩展,它在分阶段地划分问题时,与阶段中元素出现的顺序和由前一阶段的哪些元素合并而来有很大的关系。
令状态 \(dp_{(i,j)}\) 表示将下标位置 \(i\) 到 \(j\) 的所有元素合并能获得的价值的最大值,那么 \(dp_{(i,j)}=max\{dp_{(i,k)}+dp_{(k+1,j)}+w\}\),\(w\) 为将这两组元素合并起来的代价。
区间 \(dp\) 有如下特点:
-
1.合并:即将两个或多个部分进行整合,当然也可以反过来;
-
2.特征:能将问题分解为能两两合并的形式;
-
3.求解:对整个问题设最优值,枚举合并点,将问题分解为左右两个部分,最后合并两个部分的最优值得到原问题的最优值。
2.模板题
在一条链上有 \(n\) 个数 \(a_1,a_2,\dots,a_n\) 进行 \(n-1\) 次合并操作,每次操作将相邻的两堆合并成一堆,能获得新的一堆中的石子数量的和的得分。你需要最大化你的得分。
思路
令 \(dp_{(i,j)}\) 表示将区间 \([i,j]\) 内的所有石子合并到一起的最大得分。
写出状态转移方程:
然后我们用前缀和 \(sum\) 数组消掉求和公式,得:
由于计算 \(dp_{(i,j)}\) 的值时需要知道所有 \(dp_{(i,k)}\) 和 \(dp_{(k+1,j)}\) 的值,而这两个中包含的元素的数量都小于 \(dp_{(i,k)}\),所以我们以 \(len=j-i+1\) 作为 \(DP\) 的阶段。
首先从小到大枚举 \(len\),然后枚举 \(i\) 的值,根据 \(len\) 和 \(i\) 用公式计算出 \(j\) 的值,然后枚举 \(k\),时间复杂度为 \(O(n^3)\)
代码十分简单,这里不再摆出。
3.区间DP拓展知识之一:如何处理带环的区间DP?
针对此问,一般有两种做法:
-
1.既然是带环的区间 \(DP\),说明一定有 \(n\) 条连边,而题中又只能连 \(n-1\) 次,说明有一条边是用不上的,于是我们可以跑 \(n\) 遍区间 \(dp\),枚举当每条边不被使用时的最优值。(时间复杂度 \(O(n^4)\))
-
2.可以将这条链延长一倍,即将这条链复制下来接在其后面,最终结果为 \(dp_{(1,n)},dp_{(2,n+1)},dp_{(3,n+2)}...dp_{(n-1,2n-2)}\) 中的最优值,时间复杂度 \(O(n^3)\)。
4.区间DP拓展知识之二:区间DP如何记录方案?
例题
设一个 \(n\) 个节点的二叉树 \(tree\) 的中序遍历为\((1,2,3,…,n)\),其中数字 \(1,2,3,…,n\) 为节点编号。
每个节点都有一个分数(均为正整数),记第 \(i\) 个节点的分数为 \(d_i\),\(tree\) 及它的每个子树都有一个加分。
任一棵子树 \(subtree\)(也包含 \(tree\) 本身)的加分计算方法如下:
\(subtree\) 的左子树的加分 × \(subtree\) 的右子树的加分 + \(subtree\) 的根的分数
若某个子树为空,规定其加分为 \(1\)。
叶子的加分就是叶节点本身的分数,不考虑它的空子树。
试求一棵符合中序遍历为\((1,2,3,…,n\) 且加分最高的二叉树 \(tree\)。
要求输出:\(tree\) 的最高加分 和 \(tree\) 的前序遍历
思路
采用 \(**闫式DP分析法**\):
状态表示 \(dp[l][r]\)
-
1.集合:所有中序遍历是 \([L,R]\) 这一段的二叉树集合
-
2.属性:\(Max\)
状态计算
我们先把 \([L,R]\) 这一段区间划分为数个小区间,设分割点为 \(k\),这里不妨令 \(k\) 为这棵二叉树的根节点。
那么这棵树的价值就是其左,右子树的最大价值与根节点的价值之和,即:
然后最终答案就是 \(dp_{(1,n)}\)。
但是问题又来了:我们求出了最大结果,但是我们又要如何求出其对应加分二叉树的最大值呢?很简单,其实我们只需定义一个新数组 \(g_{(l,r)}\) 存储 \(l\) 到 \(r\) 这一段二叉树的根节点编号即可。
代码
#include <iostream>
#include <algorithm>
#include <cstring>
//define int long long
using namespace std;
#define N 35
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(),cin.tie(),cout.tie()
int n, w[N], dp[N][N], g[N][N];
void dfs (int l, int r) {
if (l > r) return ; //如果输出完了,就退出
int root = g[l][r]; //g[l][r]代表当前子树的根
cout << root << ' '; //输出根
dfs (l, root - 1); //递归左子树
dfs (root + 1, r); //递归右子树
}
int main () {
IOS;
cin >> n;
For (i, 1, n) {
cin >> w[i];
}
For (len, 1, n) {
for (int l = 1; l + len - 1 <= n; l ++) {
int r = l + len - 1; //区间DP,存储左端点和右端点
if (len == 1) { //如果只有一个点,就依题意赋值
dp[l][r] = w[l];
g[l][r] = l;
} else {
For (k, l, r) { //枚举分界点
int le_sc = (k == l ? 1 : dp[l][k - 1]); //左边的分数
int ri_sc = (k == r ? 1 : dp[k + 1][r]); //右边的分数
int sum_sc = le_sc * ri_sc + w[k]; //总分
if (dp[l][r] < sum_sc) { //如果得到了更高的分数
dp[l][r] = sum_sc;
g[l][r] = k; //将这一段的根更新
}
}
}
}
}
cout << dp[1][n] << endl; //输出最大分值
dfs (1, n); //递归输出前序遍历
return 0;
}
4.区间DP拓展知识之三:如何处理二维区间DP?
例题
思路
看到这样一大坨公式,我们首先想到整理它:
才怪,我们要求均方差最小,所以当然是整理均方差啦!
首先去掉根号,不影响最小值:
然后展开完全平方式:
再把括号也展开:
然后我们发现 \(\sum_{i=1}^{n} \times 2x_i\) 实际上就等于 \(2n\bar{x}\),于是我们继续:
让它变得直观一点:
然后我们把分子合并同类项,得:
再用分子两项分别除以分母,得:
因为 \(\bar{x}\) 是个定值,所以我们只需要使前面这个分数最小即可,即使每一部分的平方和最小。
分析
状态表示 \(dp[x1,y1,x2,y2,k]\)
-
1.集合:将子矩阵 \((x1,y1)(x2,y2)\) 切成 \(k\) 部分的所有方案
-
2.属性:\(\bar{x}= \frac{\sum_{i=1}^{k} \times x_i}{n}\) 的最小值
状态计算
首先考虑这道题有多种情况讨论:对于每次切割,有横切和纵切两种切法。
在两种切法下各有 \(7\) 种选切的位置,对于横切的每种位置有选择上面切和选择下面切两种情况,纵切同理。
假如我们在 \((x1,y1)(x2,y2)\) 这个矩阵沿第 \(i\) 行切了一刀,且选择了上半部分,则上半部分最小值为 \(dp_{(x1,y1,i,y2)}\)。
然后下面的因为已经不会再更改了,即 \(\frac{x_i- \bar{x}^2}{n}\)。
注意事项
-
1.因为 每一刀丢弃的部分的和是一个固定值,所以可以使用 二维前缀和 记录。
-
2.因为本题需要用到五维空间,写循环肯定会超时,所以需采用 记忆化搜索 解决。
代码
#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>
using namespace std;
#define N 15
#define M 9
#define For(i,j,k) for(int i=j;i<=k;i++)
#define IOS ios::sync_with_stdio(),cin.tie(),cout.tie()
const double INF = 1e9;
int n, m = 8, s[N][N];
double X/*bar{x}*/, dp[M][M][M][M][N];
//用大写X存储平均数
double get (int x1, int y1, int x2, int y2) { //套公式算丢掉部分的和
double sum = s[x2][y2] - s[x2][y1 - 1] - s[x1 - 1][y2] + s[x1 - 1][y1 - 1] - X;
return sum * sum / n;
}
double Dp (int x1, int y1, int x2, int y2, int k) {
double &v = dp[x1][y1][x2][y2][k];
if (v >= 0) return v; //如果这个矩阵已经被算过了,就直接返回
if (k == 1) return v = get (x1, y1, x2, y2); //如果不能再切了,就将这个矩阵 “丢掉”
v = INF;
For (i, x1, x2 - 1) { //横着切
v = min (v, Dp (x1, y1, i, y2, k - 1) + get (i + 1, y1, x2, y2)); //取上面
v = min (v, Dp (i + 1, y1, x2, y2, k - 1) + get (x1, y1, i, y2)); //取下面
}
For (i, y1, y2 - 1) { //竖着切
v = min (v, Dp (x1, y1, x2, i, k - 1) + get (x1, i + 1, x2, y2)); //取左边
v = min (v, Dp (x1, i + 1, x2, y2, k - 1) + get (x1, y1, x2, i)); //取右边
}
return v;
}
int main () {
IOS;
cin >> n;
For (i, 1, m) {
For (j, 1, m) {
cin >> s[i][j];
s[i][j] = s[i][j] + s[i - 1][j] + s[i][j - 1] - s[i - 1][j - 1];
//二维前缀和公式
}
}
memset (dp, -1, sizeof dp); //先将dp数组初始化
X = (double) s[m][m] / n; //将平均值赋值,一定要注意精度!!!!!!!!!!
printf ("%.3lf\n", sqrt (Dp (1, 1, 8, 8, n))); //输出整个矩阵切n刀的最大价值
return 0;
}

浙公网安备 33010602011771号