文章目录
- 前言
- 例题列表
- 1068. 环形石子合并(前缀和 + 区间DP + 环形转换成线性⭐)
- 如何把环转换成区间?⭐
- 实现代码
- 补充:相关题目——282. 石子合并
- 320. 能量项链(另一种计算价值的石子合并)
- 479. 加分二叉树🐂好题!⭐
- 解法与代码
- 如果求解最大值
- 如果保留状态转移的过程
- 代码
- 1069. 凸多边形的划分(区间DP + 高精度计算)
- 补充:相似题目——1039. 多边形三角剖分的最低得分
- 321. 棋盘分割(二维前缀和 + 区间DP)🐂好题!
- 相关链接
前言
根据笔者的经验,区间DP一般使用记忆化搜索会更好写一些。
例题列表
1068. 环形石子合并(前缀和 + 区间DP + 环形转换成线性⭐)
https://www.acwing.com/problem/content/1070/
可以认为是 https://www.acwing.com/problem/content/284/ 的进阶版,从普通数组变成了环形数组。
如何把环转换成区间?⭐
将数组复制一份变成长度为
2
∗
n
2*n
2∗n。
这样对这个
2
∗
n
2*n
2∗n 求一下非环形数组的石子合并,最后枚举每个长度为 n 的区间的答案即可。
而不需要对每个长度为 n n n 的区间执行一次非环形数组的石子合并。
这样时间复杂度的差异就是 O ( n 3 ) O(n^3) O(n3) 和 O ( n 4 ) O(n^4) O(n4) 之间的差异。(因为普通数组类型的石子合并的时间复杂度是 O ( n 3 ) O(n^3) O(n3))
实现代码
import java.io.BufferedInputStream;
import java.util.*;
public class Main {
final static int N = 205;
static int n;
static int[] stones = new int[2 * N], sum = new int[2 * N];
static int[][] mn = new int[2 * N][2 * N], mx = new int[2 * N][2 * N];
static {
for (int[] value : mn) Arrays.fill(value, Integer.MAX_VALUE);
for (int[] ints : mx) Arrays.fill(ints, Integer.MIN_VALUE);
}
public static void main(String[] args) {
Scanner sin = new Scanner(new BufferedInputStream(System.in));
n = sin.nextInt();
for (int i = 0; i < n; ++i) {
stones[i] = stones[i + n] = sin.nextInt();
}
// 计算前缀和
for (int i = 0; i < 2 * n; ++i) sum[i + 1] = sum[i] + stones[i];
dfs(0, 2 * n - 1);
int mnV = Integer.MAX_VALUE, mxV = Integer.MIN_VALUE;
// 枚举所有长度为 n 的区间,更新答案
for (int i = 0; i < n; ++i) {
mnV = Math.min(mnV, mn[i][i + n - 1]);
mxV = Math.max(mxV, mx[i][i + n - 1]);
}
System.out.println(mnV);
System.out.println(mxV);
}
// 返回值是 {最小值,最大值}
static int[] dfs(int l, int r) {
if (l == r) return new int[]{0, 0};
if (mn[l][r] != Integer.MAX_VALUE) return new int[]{mn[l][r], mx[l][r]};
int mnV = Integer.MAX_VALUE, mxV = Integer.MIN_VALUE;
for (int i = l; i < r; ++i) {
// 拆成左右两部分计算,最后加上合并这整个区间的花费
int[] res1 = dfs(l, i), res2 = dfs(i + 1, r);
mnV = Math.min(mnV, sum[r + 1] - sum[l] + res1[0] + res2[0]);
mxV = Math.max(mxV, sum[r + 1] - sum[l] + res1[1] + res2[1]);
}
mn[l][r] = mnV;
mx[l][r] = mxV;
return new int[]{mnV, mxV};
}
}
补充:相关题目——282. 石子合并
282. 石子合并
见:【算法】区间DP (从记忆化搜索到递推DP)⭐
320. 能量项链(另一种计算价值的石子合并)
https://www.acwing.com/problem/content/322/
import java.io.BufferedInputStream;
import java.util.*;
public class Main {
final static int N = 101 * 2;
static int n;
static int[][] memo = new int[N][N];
static int[] v = new int[N]; // 记录头标记
public static void main(String[] args) {
Scanner sin = new Scanner(new BufferedInputStream(System.in));
n = sin.nextInt();
for (int i = 0; i < n; ++i) {
v[i] = v[i + n] = sin.nextInt();
}
dfs(0, 2 * n - 2);
int ans = 0;
for (int i = 0; i < n; ++i) {
ans = Math.max(ans, memo[i][i + n - 1]);
}
System.out.println(ans);
}
static int dfs(int l, int r) {
if (l == r) return 0;
if (memo[l][r] != 0) return memo[l][r];
int res = 0;
for (int i = l; i < r; ++i) {
res = Math.max(res, dfs(l, i) + dfs(i + 1, r) + v[l] * v[i + 1] * v[r + 1]);
}
return memo[l][r] = res;
}
}
对于拆分成 (l, i) 和 (i + 1, r) ,三个相乘的值应该是 v[l],v[i + 1],v[r+1]。
479. 加分二叉树🐂好题!⭐
https://www.acwing.com/activity/content/problem/content/1299/
这道题给出了中序遍历的二叉树,一个中序遍历并不能确定一个唯一的二叉树,因此我们要找到这些所有可能中,分值最大的那个二叉树。
根据题意,任一棵树的分值为:w[u] + tree(l) + tree®。
解法与代码
如果求解最大值
中序遍历中,每棵子树在中序遍历结果中都是连续的。
首先考虑 dp 数组的定义:dp[l][r] 表示所有中序遍历是 [l, r] 这一段的二叉树的集合,属性是 最大分值。
状态转移可以通过 枚举根节点是 [l, r] 中的哪一位来表示。
到这儿我们其实就知道如何求出最大值了,但是如何得到对应最大值时的前序遍历结果呢?(且题目要求输出字典序最小的方案——即根节点尽可能靠左)。
如果保留状态转移的过程
开设一个新数组 g[l][r] 存一下这个区间的根节点是哪个节点。
这样输出前序遍历结果时,就是先输出 g[1][n],假设此时的根节点为 x,那么就递归地输出 g[1][x - 1] 和 g[x + 1][n]。
注意题目要求字典序最小的方案。
代码
dp 数组和 g 数组的定义见上文分析。
import java.io.BufferedInputStream;
import java.util.*;
public class Main {
final static int N = 31;
static int[][] dp = new int[N][N], g = new int[N][N];
static int n;
static int[] score = new int[N];
public static void main(String[] args) {
Scanner sin = new Scanner(new BufferedInputStream(System.in));
n = sin.nextInt();
for (int i = 1; i <= n; ++i) score[i] = sin.nextInt();
System.out.println(dfs(1, n));
dfs2(1, n);
}
static int dfs(int l, int r) {
if (l > r) return 1; // 子树为空,规定分数为1
if (l == r) return score[l]; // 叶子的加分就是节点本身的分数
if (dp[l][r] != 0) return dp[l][r];
int res = 0;
// 枚举 i 作为 l ~ r 之间的根节点
for (int i = l; i <= r; ++i) {
int v = dfs(l, i - 1) * dfs(i + 1, r) + score[i];
if (v > res) {
res = v;
g[l][r] = i;
}
}
return dp[l][r] = res;
}
// 前序遍历
static void dfs2(int l, int r) {
if (l == r) System.out.print(l + " ");
else if (l < r) {
int x = g[l][r];
System.out.print(x + " ");
dfs2(l, x - 1);
dfs2(x + 1, r);
}
}
}
1069. 凸多边形的划分(区间DP + 高精度计算)
https://www.acwing.com/activity/content/problem/content/1300/
这道题的数值范围太大,需要使用大数计算或者字符模拟高精度计算。
这里我选择使用 Java 的 BigInteger 类型。
关于 Java 大数的相关链接可见:
Java【大数类】整理。
https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/math/BigInteger.html
import java.io.BufferedInputStream;
import java.math.BigInteger;
import java.util.*;
public class Main {
final static int N = 51;
static BigInteger[][] dp = new BigInteger[N][N];
static int n;
static BigInteger[] score = new BigInteger[N];
static String s = "10000000000000000000000000000000";
static BigInteger inf = new BigInteger(s);
public static void main(String[] args) {
Scanner sin = new Scanner(new BufferedInputStream(System.in));
n = sin.nextInt();
for (int i = 1; i <= n; ++i) score[i] = BigInteger.valueOf(sin.nextInt());
for (int i = 0; i <= n; ++i) Arrays.fill(dp[i], new BigInteger(s));
System.out.println(dfs(1, n));
}
static BigInteger dfs(int l, int r) {
if (!dp[l][r].equals(inf)) return dp[l][r];
if (l + 1 == r) return BigInteger.ZERO;
for (int i = l + 1; i < r; ++i) {
dp[l][r] = dp[l][r].min(dfs(l, i).add(dfs(i, r)).add(score[l].multiply(score[i]).multiply(score[r])));
}
return dp[l][r];
}
}
补充:相似题目——1039. 多边形三角剖分的最低得分
1039. 多边形三角剖分的最低得分
题解见:【算法】区间DP (从记忆化搜索到递推DP)⭐
321. 棋盘分割(二维前缀和 + 区间DP)🐂好题!
https://www.acwing.com/activity/content/problem/content/1301/
这道题目看起来很吓人,但是很简单。
逐个分析,定义 dp 数组为 dp[x1][y1][x2][y2][k]
表示:将(x1,y1)(x2,y2)分成k部分的所有方案 的 均方差的平方的最小值。
为了求解均方差,我们可以求解均方差的平方,这样就在 dp 推导的过程中去掉了开根号的过程。
平均值 X 是可以提前计算的,就是整个棋盘的总分除以 n。
对于每一个小棋盘,它所贡献的均方差的平方就是 ( x i − x ) 2 / n (x_i - x)^2/n (xi−x)2/n,其中 x i x_i xi 就是这个小棋盘的总分,这个总分可以通过二维前缀和来求。关于前缀和可见:【算法基础】1.5 前缀和与差分
整体代码如下:
import java.io.BufferedInputStream;
import java.math.BigInteger;
import java.util.*;
public class Main {
final static int N = 15, M = 9;
final static double INF = 1e9;
static int n, m = 8;
static int[][] sum = new int[M][M]; // 二维前缀和数组
static double[][][][][] dp = new double[M][M][M][M][N]; // 将(x1,y1)(x2,y2)分成k部分的所有方案 的 均方差的平方的最小值
static double x;
public static void main(String[] args) {
Scanner sin = new Scanner(new BufferedInputStream(System.in));
n = sin.nextInt(); // 分成n个棋盘
// 计算二维前缀和数组
for (int i = 0; i < m; ++i) {
for (int j = 0; j < m; ++j) {
sum[i + 1][j + 1] = sin.nextInt() + sum[i + 1][j] + sum[i][j + 1] - sum[i][j];
}
}
x = (double)sum[m][m] / n;
System.out.printf("%.3f\n", Math.sqrt(dfs(1, 1, 8, 8, n)));
}
static double dfs(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 dp[x1][y1][x2][y2][k] = get(x1, y1, x2, y2);
double res = INF;
// 枚举横着切
for (int i = x1; i < x2; ++i) {
res = Math.min(res, get(x1, y1, i, y2) + dfs(i + 1, y1, x2, y2, k - 1));
res = Math.min(res, get(i + 1, y1, x2, y2) + dfs(x1, y1, i, y2, k - 1));
}
// 枚举竖着切
for (int i = y1; i < y2; ++i) {
res = Math.min(res, get(x1, y1, x2, i) + dfs(x1, i + 1, x2, y2, k - 1));
res = Math.min(res, get(x1, i + 1, x2, y2) + dfs(x1, y1, x2, i, k - 1));
}
return dp[x1][y1][x2][y2][k] = res;
}
// 得到这个棋盘的 (xi - x)^2 / n
static double get(int x1, int y1, int x2, int y2) {
double sum = getSum(x1, y1, x2, y2) - x;
return sum * sum / n;
}
// 根据前缀和数组 得到一个矩形内的总分
static int getSum(int x1, int y1, int x2, int y2) {
return sum[x2][y2] - sum[x1 - 1][y2] - sum[x2][y1 - 1] + sum[x1 - 1][y1 - 1];
}
}
相关链接
【算法】区间DP (从记忆化搜索到递推DP)⭐