动态规划--区间Dp

197 阅读2分钟

《算法竞赛进阶指南》这本书中讲到: DP也属于线性DP中的一种,它以 区间长度 作为DP的阶段,使用两个坐标(区间的左右端点),描述每个维度。

区间DP中一个,状态有若干个比他更小,且包含于它的区间所代表的状态转移而来。因此区间DP的决策往往就是划分区间的方法。区间DP的状态,一般由长度为一的元区间构成,这种向下划分、再向上递推的模式与某些树形结构的线段数有很大相似之处。同时借助区间dp这种与树形相关的结构,我们也将提及记忆化搜索,本质是动态规划的递归实现方法。

leetcode312. 戳气球

有 n 个气球,编号为0 到 n - 1,每个气球上都标有一个数字,这些数字存在数组 nums 中。

现在要求你戳破所有的气球。戳破第 i 个气球,你可以获得 nums[i - 1] * nums[i] * nums[i + 1] 枚硬币。 这里的 i - 1 和 i + 1 代表和 i 相邻的两个气球的序号。如果 i - 1或 i + 1 超出了数组的边界,那么就当它是一个数字为 1 的气球。

求所能获得硬币的最大数量。

示例 1:

输入: nums = [3,1,5,8]
输出: 167
解释:
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

分析: image.png

不妨设f[l, r]为消去[l + 1, r - 1]这段区间的气球(两个端点不消去)的最大分值.然后我们看最近一次的状态转移。应当注意到,无论我们我们怎么消法,最后一定是在[l, r]之间留下一个气球(设为k),把k消去后恰好得到当前状态。因此可得状态转移 f[l][r] = max(f[l][k] + f[k][r] + score[k] * score[l] * score[r]) (l < k < r)

class Solution {
public:
    int maxCoins(vector<int>& nums) {
        // 区间dp
        int n=nums.size();
        vector<int> a(n+2,1);
        for(int i=1;i<=n;i++) a[i]=nums[i-1]; // 首尾加1
        vector<vector<int>> f(n+2,vector<int>(n+2));

        for(int len=3;len<=n+2;len++) // 阶段:枚举区间长度len
        {
            for(int i=0;i+len-1<=n+1;i++)  // 左端点
            {
                int j=i+len-1;  // 右端点
                for(int k=i+1;k<j;k++)    // 决策
                    f[i][j]=max(f[i][j],f[i][k]+f[k][j]+a[i]*a[j]*a[k]);
            }
        }
        return f[0][n+1];

    }
};

375. 猜数字大小 II

我们正在玩一个猜数游戏,游戏规则如下:

  1. 我从 1 ****到 n 之间选择一个数字。
  2. 你来猜我选了哪个数字。
  3. 如果你猜到正确的数字,就会 赢得游戏 。
  4. 如果你猜错了,那么我会告诉你,我选的数字比你的 更大或者更小 ,并且你需要继续猜数。
  5. 每当你猜了数字 x 并且猜错了的时候,你需要支付金额为 x 的现金。如果你花光了钱,就会 输掉游戏 。

给你一个特定的数字 n ,返回能够 确保你获胜 的最小现金数,不管我选择那个数字 。

dp分析:

image.png

设f[i][j]表示的集合为[i, j]区间内所有target以及其对应的所有选法,状态表示这些target和选法的组合所用钱数最坏(多)情况下的最小值。

可以根据选[i, j]内的哪个数进行猜测,进而对集合进行划分。假设选的数是k,则在选k的前提条件下, 最坏情况所用钱数为max(f[i, k - 1], f[k + 1, j]) + k。我们遍历区间[i,j]范围内的每个k,求出每个对应的最坏(多)情况下的钱数,对这些值取最小值,即为题目所求。

需要理解的是:最坏情况下的最小值

class Solution {
public:
// 区间dp

    int getMoneyAmount(int n) {
        vector<vector<int>> f(n+2,vector<int>(n+2));

        for(int len=2;len<=n;len++)    // 枚举区间长度
        {
            for(int i=1;i+len-1<=n;i++) // 左端点
            {
                int j=i+len-1;       // 右端点
                f[i][j]=INT_MAX;
                for(int k=i;k<=j;k++)       // 决策
                {
                    f[i][j]=min(f[i][j],max(f[i][k-1],f[k+1][j])+k);
                }
            }
        }
        return f[1][n];

    }
};

leetcode516. 最长回文子序列

给你一个字符串 s ,找出其中最长的回文子序列,并返回该序列的长度。

子序列定义为:不改变剩余字符顺序的情况下,删除某些字符或者不删除任何字符形成的一个序列。

示例 1:

输入: s = "bbbab"
输出: 4
解释: 一个可能的最长回文子序列为 "bbbb" 。

示例 2:

输入: s = "cbbd"
输出: 2
解释: 一个可能的最长回文子序列为 "bb" 。

分析:

image.png

class Solution {
public:
    int longestPalindromeSubseq(string s) {
        int n=s.size();
        vector<vector<int>> f(n,vector<int>(n));

        for(int len=1;len<=n;len++)  // 枚举区间长度len
        {
            for(int i=0;i+len-1<n;i++) // 左端点
            {
                int j=i+len-1;  //右端点
                if(len==1) f[i][j]=1;
                else
                {
                    if(s[i]==s[j]) f[i][j]=f[i+1][j-1]+2;
                    f[i][j]=max(f[i][j],max(f[i+1][j],f[i][j-1]));
                }
            }
        }
        return f[0][n-1];

    }
};

从上面的几道题可以看出,区间dp也属于线性dp的一种,它是以区间长度作为DP的阶段,使用两个坐标(区间的左端点和区间的右端点)来描述每一个维度。区间长度一般从一开始。