1563-石子游戏Ⅴ-区间DP问题

题目

分析

题意还是比较好理解的,每次将石子分成两大堆,抛弃总和大的那一堆,留下少的一堆并且总分数中加上少的一堆的和,直至只剩下一个石头,游戏结束。而题目给的数据规模在500,那么算法的复杂度要在O(n²)以内。
首先考虑一下暴力解法。当石子大于一块时,遍历每一个可能的分割位置,分别计算两边的和,留下少的一堆并在答案中加上少的那堆的和,比较所有的可能,找到最大的分数,其中计算部分和可以使用前缀和数组预处理来减少时间复杂度。而在暴力解法中,会出现很多区间重复计算的问题,动态规划显然是一个好的处理方式。

区间动态规划

状态定义

由于是区间上的动态规划问题,定义时,可以使用二维数组dp[i][j],枚举所有区间,也就是 i 表示区间头,j 表示区间尾,根据题意,可以想到dp数组的定义:dp[i][j]表示在区间(i, j)上,Alice能获得的最大分数,由此可以得到基础状态:

  • 对于长度为一的区间,只剩一块石子,游戏结束,即dp[i][i] = 0
状态转移

由于要找到所有的分割方法中的最优方法,所以状态转移时肯定需要遍历每个分割位置取最大值。然后对于每个分割位置,根据题意,取和小的那一堆,然后对小的那堆继续分割,小的那一堆也就相当于sum[j] - sum[i],而小的那堆继续分割的值在之前已经计算过,为dp[i][j],所以可以得到:

    if (sum[k + 1] - sum[i] > sum[j + 1] - sum[k + 1]) {
        dp[i][j] = Math.max(dp[i][j], sum[j + 1] - sum[k + 1] + dp[k + 1][j]);
    } else if (sum[k + 1] - sum[i] < sum[j + 1] - sum[k + 1]) {
        dp[i][j] = Math.max(dp[i][j], sum[k + 1] - sum[i] + dp[i][k]);
    }

这里的 i 表示区间头,j 表示区间尾,k 表示分割位置,sum 表示前缀和数组,下标要慎重考虑。
除了这样两种情况,还有两堆相等的情况,两堆相等并不能给出明确的选择给出哪个更好,所以应该计算左堆和右堆,取其中的最大值:

    dp[i][j] = Math.max(dp[i][j],
               Math.max(sum[j + 1] - sum[k + 1] + dp[k + 1][j], sum[k + 1] - sum[i] + dp[i][k]));

这里每次取dp[i][j]和另一个值比较是为了遍历分割位置 k 时取到所以结果中的最大值。

完整代码

有了状态定义、初始状态及状态转移,就可以写出DP代码了,通常情况的区间DP问题采用先循环遍历区间长度,再循环遍历区间头来遍历所有可能并且保证后边用到的值在先前已经得到。

class Solution {
    private int[][] dp;

    public int stoneGameV(int[] a) {
        int n = a.length;
        // 前缀和
        int[] sum = new int[a.length + 1];
        for (int i = 0; i < a.length; i++) {
            sum[i + 1] = sum[i] + a[i];
        }

        dp = new int[n][n];

        for (int len = 2; len <= n; len++) {
            for (int i = 0; i + len - 1 < n; i++) {
                int j = i + len - 1;
                for (int k = i; k < j; k++) {
                    if (sum[k + 1] - sum[i] > sum[j + 1] - sum[k + 1]) {
                        dp[i][j] = Math.max(dp[i][j], sum[j + 1] - sum[k + 1] + dp[k + 1][j]);
                    } else if (sum[k + 1] - sum[i] < sum[j + 1] - sum[k + 1]) {
                        dp[i][j] = Math.max(dp[i][j], sum[k + 1] - sum[i] + dp[i][k]);
                    } else {
                        dp[i][j] = Math.max(dp[i][j],
                                Math.max(sum[j + 1] - sum[k + 1] + dp[k + 1][j], sum[k + 1] - sum[i] + dp[i][k]));
                    }
                }
            }
        }
        return dp[0][n - 1];
    }
}

用时在500ms左右,也可以采用记忆化递归来处理,时间可以达到70ms左右,这里我也不太懂原因是什么,理论上循环计算要比递归调用用时短一些,可力扣上好多这样的题都是记忆化递归快,不是很懂他运行的机制,改写成递归还是比较简单的,只要把k部分的循环写到递归里即可,我这里就不在给出代码了。

总结

DP问题可真是老大难,各种各样的形式,思想差不多但是方法不同的解法,dp数组的定义,没有一样很容易掌握,但还是需要去学习啊。区间DP也是一个比较常见的DP问题类型,一点点努力,一点点进步~
文章如果有不正确的地方还请指出,感恩相遇~~

推荐阅读更多精彩内容