【算法提高:动态规划】1.6 区间DP

文章目录

  • 前言
  • 例题列表
    • 1068. 环形石子合并(前缀和 + 区间DP + 环形转换成线性⭐)
      • 如何把环转换成区间?⭐
      • 实现代码
      • 补充:相关题目——282. 石子合并
    • 320. 能量项链(另一种计算价值的石子合并)
    • 479. 加分二叉树好题!⭐
      • 解法与代码
        • 如果求解最大值
        • 如果保留状态转移的过程
        • 代码
    • 1069. 凸多边形的划分(区间DP + 高精度计算)
      • 补充:相似题目——1039. 多边形三角剖分的最低得分
    • 321. 棋盘分割(二维前缀和 + 区间DP)好题!
  • 相关链接

前言

根据笔者的经验,区间DP一般使用记忆化搜索会更好写一些。

例题列表

1068. 环形石子合并(前缀和 + 区间DP + 环形转换成线性⭐)

https://www.acwing.com/problem/content/1070/

【算法提高:动态规划】1.6 区间DP_第1张图片

可以认为是 https://www.acwing.com/problem/content/284/ 的进阶版,从普通数组变成了环形数组。

如何把环转换成区间?⭐

【算法提高:动态规划】1.6 区间DP_第2张图片

将数组复制一份变成长度为 2 ∗ n 2*n 2n
这样对这个 2 ∗ n 2*n 2n 求一下非环形数组的石子合并,最后枚举每个长度为 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/

【算法提高:动态规划】1.6 区间DP_第3张图片

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/

【算法提高:动态规划】1.6 区间DP_第4张图片

这道题给出了中序遍历的二叉树,一个中序遍历并不能确定一个唯一的二叉树,因此我们要找到这些所有可能中,分值最大的那个二叉树。

根据题意,任一棵树的分值为:w[u] + tree(l) + tree®。

解法与代码

如果求解最大值

中序遍历中,每棵子树在中序遍历结果中都是连续的。
首先考虑 dp 数组的定义:dp[l][r] 表示所有中序遍历是 [l, r] 这一段的二叉树的集合,属性是 最大分值。

状态转移可以通过 枚举根节点是 [l, r] 中的哪一位来表示。

【算法提高:动态规划】1.6 区间DP_第5张图片

到这儿我们其实就知道如何求出最大值了,但是如何得到对应最大值时的前序遍历结果呢?(且题目要求输出字典序最小的方案——即根节点尽可能靠左)。

如果保留状态转移的过程

开设一个新数组 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/

【算法提高:动态规划】1.6 区间DP_第6张图片

这道题的数值范围太大,需要使用大数计算或者字符模拟高精度计算。
这里我选择使用 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/

【算法提高:动态规划】1.6 区间DP_第7张图片

这道题目看起来很吓人,但是很简单。

逐个分析,定义 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 (xix)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)⭐

你可能感兴趣的:(算法,算法,动态规划,区间DP,前缀和,DP,记忆化搜索,dfs)