DP中需要用到子问题外信息的情况分析

DP中需要用到子问题外信息的情况分析


问题描述

In this case, I shall call that the definition of the subproblem is not self-contained and its solution relies on information external to the subproblem itself.

来自leetcode的一句话。
通常传统的dp问题都会具有self-contained 的性质,就是母问题的最优解会涉及到子问题的最优解,也就是说通过将问题拆分成很多子问题,求得它们的最优解,再通过递推关系使用子问题的最优解就可以找到原问题的最优解。
但是在有些情况下,子问题的解并不能提供我们需要解决原问题的所有信息,因此我们需要通过一些方式,在保留子问题最优解的同时也保留额外的信息。


546. Remove Boxes

Given several boxes with different colors represented by different positive numbers.
You may experience several rounds to remove boxes until there is no box left. Each time you can choose some continuous boxes with the same color (composed of k boxes, k >= 1), remove them and get k*k points.
Find the maximum points you can get.

拿到这道题首先还是用dp的思想去分析,很显然想到用dp[i][j]来代表拿走[i,j]数组的最大得分,但是进一步分析发现这样拆分出的子结构并不能直接用于原问题,因为这一段数组拿走之后的得分不仅仅取决于当前这一段子数组,也取决于这一段之外数组被改变的状况:
[3, 2, 1, 4, 4, 2, 4, 4]
比如一这个数组的[0,3]部分为例:
[3, 2, 1, 4]这个数组的最优解无法直接求出,因为取决于a[5]是否已经被拿走;换句话说我们可以求出[3, 2, 1, 4]这样一个独立数组的解,但是这个解无法被用做解决更大数组的解。因此这里出现了该类问题的特征:子问题的最优解不足以解决原问题。
基于这个特点,很显然我们需要给子问题增加一个新的维度来传输更多的信息,使子问题的解可以用于解决更大的问题。
考虑数组[3,3,2,1,4,3,2,3,4]
很显然我们有两种处理方式,第一种直接拿第一个,这里的子问题的解就是dp[i+1,j]
还有一种选择就是我们尝试找和第一个相同的箱子m,将中间的箱子挪走让i和m相邻来增加得分,这里我们找到a[4] 是3,那么该问题就变成了[2,1,4] 和 [3,3–3,2,3,4]两个子问题。从这个例子可以看出,给定一个数组,我们可以把它按这种方式拆成两个数组A和B,这两个数组有如下特征:
A数组左右两侧的箱子颜色相同,且与 A自己左右两个端点的箱子不同,那么A部分的值很简单就是dp[aleft,aright];
B数组可以看作两部分的组合,前半部分是若干个颜色相同的箱子,后半部分是之前数组的一部分,对于这样一个拼接数组,我们无法直接用一个dp[i][j]的形式来描述,但这个信息对于求解后半部分又是必要的,所以我们这里给dp增加一个维度[k],用来描述当前数组前方还有多少个和首项颜色相同的箱子;在增加这个维度后我们尝试用dp[i][j][k]来描述递归过程,这里设拿法2中找到的同色箱子序号为m,则:

d p [ i ] [ j ] [ k ] = 1 : ( ( k + 1 ) ( k + 1 ) + d p [ i + 1 ] [ j ] [ 0 ] 2 : d p [ i + 1 , m 1 , 0 ] + d p [ m , j , k + 1 ]

注意拿法2中,为了遍历所有可能性,我们需要尝试所有和a[i]颜色相同的色块作为a[m],但是对于[2,1,2,2,3,3]这种情况,虽然第二、第三项都是同色的,但是很明显我们知道第三项作为m是更优解,所以编码中可以手动完成这一步降低迭代次数。
那么基于这个公式就可以完成编码,编码方式可以分成top-down 和 bottom-up
注意top-down会设计很多重复的子问题,所以要用memorization

//bottom-up
class Solution {
    int[] a;
    int[][][] dp;
    public int removeBoxes(int[] b) {
        a = b;
        dp = new int[a.length][a.length][a.length];
        return remove(0,a.length-1,0);
    }


    public int remove(int i,int j,int k){
        if(i==j) return (k+1)*(k+1);
        if(i>j) return 0;
        if(dp[i][j][k]!=0) return dp[i][j][k];
        int res = (k+1)*(k+1) + remove(i+1,j,0);
        for(int m = i+1;m<=j;m++){
            if(a[i]==a[m]){
                int s = m;
                while(m+1<=j && a[m+1]==a[i]) m++;
                res = Math.max(res, remove(i+1,s-1,0) + remove(m,j,m-s+k+1));
            }
        }
        dp[i][j][k] = res;
        return res;
    }
}
class Solution {
    int[] a;
    int[][][] dp;
    //memo
    public int removeBoxes(int[] b) {
        a = b;
        dp = new int[a.length][a.length][a.length];
        return remove(0,a.length-1,0);
    }


    public int remove(int i,int j,int k){
        if(i==j) return (k+1)*(k+1);
        if(i>j) return 0;
        if(dp[i][j][k]!=0) return dp[i][j][k];
        int res = (k+1)*(k+1) + remove(i+1,j,0);
        for(int m = i+1;m<=j;m++){
            if(a[i]==a[m]){
                int s = m;
                while(m+1<=j && a[m+1]==a[i]) m++;
                res = Math.max(res, remove(i+1,s-1,0) + remove(m,j,m-s+k+1));
            }
        }
        dp[i][j][k] = res;
        return res;
    }
}

312.Burst Balloon

Given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by array nums. You are asked to burst all the balloons. If the you burst balloon i you will get nums[left] * nums[i] * nums[right] coins. Here left and right are adjacent indices of i. After the burst, the left and right then becomes adjacent.
Find the maximum coins you can collect by bursting the balloons wisely.

Input: [3,1,5,8]
Output: 167
Explanation: nums = [3,1,5,8] –> [3,5,8] –> [3,8] –> [8] –> []
coins = 3*1*5 + 3*5*8 + 1*3*8 + 1*8*1 = 167

最直接的思想就是dp,还是和前一题类似,举个例子:
[3,1,4,5,6,7]
对于这个数组,我们可以求出[4,5,6]这样一个子问题的最优解,但是原问题在这个问题的基础上还需要知道该数组两端的值才可以进行计算,所以还是需要更多的信息。直观的想法就是创建一个4d数组dp[i][j][left][right] ,来记录子数组[i,j]在左右两端的值分别为left,right的情况下的最优解。观察递推式:

r e s = M a t h . m a x ( r e s , d p [ l a s t ] [ l a s t ] [ i 1 ] [ j + 1 ] + d p [ i ] [ l a s t 1 ] [ i 1 ] [ l a s t ] + d p [ l a s t + 1 ] [ j ] [ l a s t ] [ j + 1 ] ) ;

递推的起点就是dp[i][i][left][right],这个值就是 a [ i ] a [ j ] a [ l ] ,基于这种设计我们会发现递归起点其实不需要进行存储。
式子中除了递归起点之外,还用到的所有递推公式都是可以写成dp[i][j][i-1][j+1]的形式,也就是意味着我们每个子问题在母问题中需要的额外信息只是 当前数组两端的值
基于这一点我们就可以改写dp的定义,dp[i][j]用来表示以[i+1,j-1]的最优解就已经保存了两端的值。这样就大大降低了空间复杂度,新的公式:
d p [ i ] [ j ] = M a t h . m a x ( a [ x ] a [ I ] a [ j ] + d p [ i ] [ x ] + d p [ x ] [ j ] ) ( i < x < j ) ) ;

注意这里递归起点的寻找很tricky,其实是用到了一个反向思考的思路。我们可以发现当前的最高分数只和剩余的气球有关,已经爆炸的气球不影响结果,所以自然可以想到bottom-up的起点就是只有一个气球的情况,dp[i][j] 中间如果只剩下一个气球,那么dp[i][j] 就是 a[x] * a[i] * a[j] , 所以基于这一条件,我们可以轻易的写出以x为最后一个气球时候的得分,遍历所有x求最高分就可以了。
常规的dp思路我们都是从大问题往下分割,也就是上述等式从右往左寻找,我们尝试用小问题的解来构建大问题的解。也就是从最大的问题开始,一步步拆分成最小的问题。但这里我们先从最小的思路开始考虑,也就是说一个大问题拆分到最简单的情况是什么样的,然后我们逆向从这个最简单的情况往复杂的推进。

扫描二维码关注公众号,回复: 2616501 查看本文章

猜你喜欢

转载自blog.csdn.net/qq_24436311/article/details/81360437
今日推荐