动态规划经典例题二

动态规划经典例题一

文章目录

    • 第7题 路径总数(Unique Paths II)
      • 动态规划方法
    • 第8题 最小路径和(Minimum Path Sum)
      • 动态规划方法
    • 第9题 背包问题
      • 动态规划方法
    • 第10题 回文串分割(Palindrome Partitioning)
      • 动态规划方法
    • 第11题 编辑距离(Edit Distance)
      • 动态规划方法
    • 第12题 不同子序列(Distinct Subsequences)
      • 动态规划方法

第7题 路径总数(Unique Paths II)

继续思考题目"Unique Paths":
如果在图中加入了一些障碍,有多少不同的路径?
分别用0和1代表空区域和障碍
例如
下图表示有一个障碍在3*3的图中央。
[↵ [0,0,0],↵ [0,1,0],↵ [0,0,0]↵]
有2条不同的路径
备注:m和n不超过100.

动态规划方法

状态:
子状态:从(0,0)到达(1,0),(1,1),(2,1),…(m-1,n-1)的路径数
F(i,j): 从(0,0)到达F(i,j)的路径数
状态递推:
F(i,j) = {F(i-1,j) + F(i,j-1)} OR {0, if obstacleGrid(i,j) = 1}
初始化:
特殊情况:第0行和第0列
F(0,i) = {1} OR {0, if obstacleGrid(0,j) = 1, j <= i}
F(i,0) = {1} OR {0, if obstacleGrid(j,0) = 1, j <= i}
返回结果:
F(m-1,n-1)

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int> > &obstacleGrid) {
      if(obstacleGrid.empty()||obstacleGrid[0].empty())
        return 0;
      int m=obstacleGrid.size();
      int n=obstacleGrid[0].size();
      vector<vector<int>> ret(m,vector<int>(n,0));
      for(int i=0;i<m;i++)
      {
        // 第0列中只要前面有障碍,后面都无法到达
        if(obstacleGrid[i][0])
          break;
        else
          ret[i][0]=1;
      }
      for(int i=0;i<n;i++)
      {
        // 第0行中只要前面有障碍,后面都无法到达
        if(obstacleGrid[0][i])
          break;
        else
          ret[0][i]=1;
      }
      for(int i=1;i<m;i++)
      {
        for(int j=1;j<n;j++)
        {
          // obstacleGrid[i][j] = 1 时,F(i,j)无法到达
          if (obstacleGrid[i][j]) 
            ret[i][j] = 0;
          else 
          {
            // F(i,j) = F(i-1,j) + F(i,j-1)
            ret[i][j] = ret[i - 1][j] + ret[i][j - 1];
          }
        }
      }
      return ret[m - 1][n - 1];
    }
};

第8题 最小路径和(Minimum Path Sum)

给定一个由非负整数填充的m x n的二维数组,现在要从二维数组的左上角走到右下角,请找出路径上的所有数字之和最小的路径。
注意:你每次只能向下或向右移动。

动态规划方法

状态:
子状态:从(0,0)到达(1,0),(1,1),(2,1),…(m-1,n-1)的最短路径
F(i,j): 从(0,0)到达F(i,j)的最短路径
状态递推:
F(i,j) = min{F(i-1,j) , F(i,j-1)} + (i,j)
初始化:
F(0,0) = (0,0)
特殊情况:第0行和第0列
F(0,i) = F(0,i-1) + (0,i)
F(i,0) = F(i-1,0) + (i,0)
返回结果:
F(m-1,n-1)

class Solution {
public:
    int minPathSum(vector<vector<int> > &grid) {
      if(grid.empty()||grid[0].empty())
        return 0;
      int m=grid.size();
      int n=grid[0].size();
      vector<vector<int>> ret(m,vector<int>(n,0));
      ret[0][0]=grid[0][0];
      for(int i=1;i<m;i++)
      {
        ret[i][0]=ret[i-1][0]+grid[i][0];
      }
      for(int j=1;j<n;j++)
      {
        ret[0][j]=ret[0][j-1]+grid[0][j];
      }
      for(int i=1;i<m;i++)
      {
        for(int j=1;j<n;j++)
        {
       	  // F(i,j) = min{F(i-1,j) , F(i,j-1)} + (i,j)
          ret[i][j]=min(ret[i-1][j],ret[i][j-1])+grid[i][j];
        }
      }
      return ret[m-1][n-1];
    }
};

第9题 背包问题

有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值.
问最多能装入背包的总价值是多大?

动态规划方法

状态:
F(i, j): 看到前i个商品,包内物品大小为j的最大价值
状态递推:对于第i个商品,有一种例外,装不下,两种选择,放或者不放
如果装不下:此时的价值与前i-1个的价值是一样的
F(i,j) = F(i-1,j)
如果可以装入:需要在两种选择中找最大的
F(i, j) = max{F(i-1,j), F(i-1, j - A[i]) + V[i]}
F(i-1,j): 表示不把第i个物品放入背包中, 所以它的价值就是前i-1个物品放入大小为j的背包的最大价值
F(i-1, j - A[i]) + V[i]:表示把第i个物品放入背包中,价值增加V[i],但是需要腾出j - A[i]的大小放第i个商品
初始化:第0行和第0列都为0,表示没有装物品时的价值都为0
F(0,j) = F(i,0) = 0
返回值:F(n,m)

例如:

金条 钞票 钻石 古董 珠宝
重量 1 2 3 20 3
价值 24 1 1000 100 500

包大小为8

F(1,1):24 F(1,2)·················F(1,8):24
F(2,1):24 F(2,2):24 F(2,3):F(1,3-2)+1—>25·······F(2,8):25
F(3,1):24 F(3,2):24 F(3,3):F(2,3-3)+1000—>1000·········F(3,8):1025
···················
F(5,1):24 F(5,2):24 F(5,3):F(5,3-3)+1000—>1000 F(5,4):F(5,4-3-1)+1000+24—>1024·········F(5,8):F(5,8-3-3-1)+1000+500+24—>1524

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @param V: Given n items with value V[i]
     * @return: The maximum value
     */
    int backPackII(int m, vector<int> &A, vector<int> &V) {
        // write your code here
        if(A.empty())
            return 0;
        int row=A.size();
        vector<vector<int>> maxvalue(row+1,vector<int>(m+1,0));
        for(int i=1;i<=row;i++)
        {
            for(int j=1;j<=m;j++)
            {
                if(A[i-1]>j)
                    maxvalue[i][j]=maxvalue[i-1][j];
                else
                    maxvalue[i][j]=max(maxvalue[i-1][j-A[i-1]]+V[i-1],maxvalue[i-1][j]);
            }
        }
        return maxvalue[row][m];
    }
};

优化算法:
上面的算法在计算第i行元素时,只用到第i-1行的元素,所以二维的空间可以优化为一维空间
但是如果是一维向量,需要从后向前计算,因为后面的元素更新需要依靠前面的元素未更新(模拟二维矩阵的上一行的值)的值

class Solution {
public:
    /**
     * @param m: An integer m denotes the size of a backpack
     * @param A: Given n items with size A[i]
     * @param V: Given n items with value V[i]
     * @return: The maximum value
     */
    int backPackII(int m, vector<int> &A, vector<int> &V) {
        // write your code here
        if(A.empty())
            return 0;
        int row=A.size();
        vector<int> maxvalue(m+1,0);
        for(int i=1;i<=row;i++)
        {
            for(int j=m;j>0;j--)
            {
                if(A[i-1]>j)
                    maxvalue[j]=maxvalue[j];
                else
                    maxvalue[j]=max(maxvalue[j-A[i-1]]+V[i-1],maxvalue[j]);
            }
        }
        return maxvalue[m];
    }
};

第10题 回文串分割(Palindrome Partitioning)

给出一个字符串s,分割s使得分割出的每一个子串都是回文串
计算将字符串s分割成回文分割结果的最小切割数
例如:给定字符串s=“aab”,
返回1,因为回文分割结果[“aa”,“b”]是切割一次生成的。

动态规划方法

方法一

状态:
子状态:前i个字符的最小分割次数
F(i): 到第i个字符需要的最小分割数
状态递推:
F(i) = min{F(i), 1 + F(j)}, where j 上式表示如果从j+1到i判断为回文字符串,且已经知道从第1个字符到第j个字符的最小切割数,那么只需要再切一次,就可以保证1–>j, j+1–>i都为回文串。
初始化:
F(i) = i - 1
上式表示到第i个字符需要的最大分割数
比如单个字符只需要切0次,因为单子符都为回文串
2个字符最大需要1次,3个2次…
返回结果:
F(n)
遗留问题:如何判断一段字符串为回文串循环判断首尾元素是否相同,如果全部相同,则是回文串

例如:

F(0)=-1
F(1)=0
F(2)=s(2,2):1+F(1),s(1,2):1+F(0):0
F(3):F(2)+1
初始状态:F(i)=i-1最多分割i-1次
返回:F(n)

class Solution {
public:
    int minCut(string s) {
      if(s.empty())
        return 0;
      vector<int> minCut(s.size()+1,0);
      //F(i)=i-1
      for(int i=0;i<=s.size();i++)
      {
        minCut[i]=i-1;
      }
      for(int i=1;i<=s.size();i++)
      {
        for(int j=0;j<i;j++)
        {
          //1 ~ j F(j)   j+1 ~ i
          if(isPhrase(s,j,i-1))
          {
            minCut[i]=min(minCut[i],minCut[j]+1);
          }
        }
      }
      return minCut[s.size()];
    }
    bool isPhrase(string& s,int begin,int end)
    {
      while(begin<end)
      {
        if(s[begin]!=s[end])
          return false;
        ++begin;
        --end;
      }
      return true;
    }
};

上述方法两次循环时间复杂度是O(n^2),
判断回文串时间复杂度是O(n),
所以总时间复杂度为O(n^3)
对于过长的字符串,在OJ的时候会出现TLE(Time Limit Exceeded)
判断回文串的方法可以继续优化,使总体时间复杂度将为O(n^2)
判断回文串,这是一个“是不是”的问题,所以也可以用动态规划来实现
方法二

状态:
子状态:从第一个字符到第二个字符是不是回文串,第1-3,第2-5,…
F(i,j): 字符区间 [i,j] 是否为回文串
状态递推:
F(i,j): true->{s[i]==s[j] && F(i+1,j-1)} OR false
上式表示如果字符区间首尾字符相同且在去掉区间首尾字符后字符区间仍为回文串,
则原字符区间为回文串
从递推公式中可以看到第i处需要用到第i+1处的信息,所以i应该从字符串末尾遍历
初始化:
F(i,j) = false
返回结果:
矩阵F(n,n), 只更新一半值(i <= j),n^2 / 2

class Solution {
public:
    int minCut(string s) {
      if (s.empty()) return 0;
      int len = s.size();
      vector<int> cut;
      // F(i)初始化
      // F(0)= -1,必要项,如果没有这一项,对于重叠字符串“aaaaa”会产生错误的结果

      for (int i = 0; i < 1 + len; ++i) 
      {
        cut.push_back(i - 1);
      }
      vector<vector<bool>> mat = getMat(s);
      for (int i = 1; i < 1 + len; ++i) 
      {
        for (int j = 0; j < i; ++j) 
        {
          // F(i) = min{F(i), 1 + F(j)}, where j
          // 从最长串判断,如果从第j+1到i为回文字符串
          // 则再加一次分割,从1到j,j+1到i的字符就全部分成了回文字符串
          if (mat[j][i - 1]) {
            cut[i] = min(cut[i], 1 + cut[j]);
          }
        }
      }
      return cut[len];
    }
  vector<vector<bool>> getMat(string s) 
  {
    int len = s.size();
    vector<vector<bool> > mat = vector<vector<bool>>(len, vector<bool>(len,false));
    for (int i = len - 1; i >= 0; --i) 
    {
      for (int j = i; j < len; ++j) 
      {
        if (j == i) 
        {
          // 单字符为回文字符串
          mat[i][j] = true;
        }
        else if (j == i + 1) 
        {
          // 相邻字符如果相同,则为回文字符串
          mat[i][j] = (s[i] == s[j]);
        }
        else 
        {
          // F(i,j) = {s[i]==s[j] && F(i+1,j-1)
          // j > i+1
          mat[i][j] = ((s[i] == s[j]) && mat[i + 1][j - 1]);
        }
      }
    }
    return mat;
    }
};

上述方法判断回文串时间复杂度O(n^2)
主方法两次循环时间复杂度O(n^2)
总体时间复杂度O(n^2) ~ O(2 * n^2) = O(n^2) + O(n^2)
总结:

  • 简单的动态规划问题,状态,状态递推和状态初始化都比较直观
  • 对于复杂的动态规划问题,状态,状态递推和状态初始化都比较隐含,需要仔细推敲
  • 尤其是状态递推可能需要额外的辅助判断条件才能达成。

第11题 编辑距离(Edit Distance)

给定两个单词word1和word2,请计算将word1转换为word2至少需要多少步操作。
你可以对一个单词执行以下3种操作:
a)在单词中插入一个字符
b)删除单词中的一个字符
c)替换单词中的一个字符

动态规划方法

状态:
子状态:word1的前1,2,3,…m个字符转换成word2的前1,2,3,…n个字符需要的编辑距离
F(i,j):word1的前i个字符于word2的前j个字符的编辑距离
状态递推:
F(i,j) = min { F(i-1,j)+1, F(i,j-1) +1, F(i-1,j-1) +(w1[i]==w2[j]?0:1) }
上式表示从删除,增加和替换操作中选择一个最小操作数
F(i-1,j): w1[1,…,i-1]于w2[1,…,j]的编辑距离,删除w1[i]的字符—>F(i,j)
F(i,j-1): w1[1,…,i]于w2[1,…,j-1]的编辑距离,增加一个字符—>F(i,j)
F(i-1,j-1): w1[1,…,i-1]于w2[1,…,j-1]的编辑距离,如果w1[i]与w2[j]相同,
不做任何操作,编辑距离不变,如果w1[i]与w2[j]不同,替换w1[i]的字符为w2[j]—>F(i,j)
初始化:
初始化一定要是确定的值,如果这里不加入空串,初始值无法确定
F(i,0) = i :word与空串的编辑距离,删除操作
F(0,i) = i :空串与word的编辑距离,增加操作
返回结果:F(m,n)

例如:word1:“abcde” word2:“12345”

F(1,1):“abcde”—>“1bcde”
F(2,2):F(1,1):“1bcde”+ 替换:b—>2 “12cde”
F(2,1):“ab” —>“1”—>“1cde”
F(2,2):F(2,1):“1cde”+‘2’—>“12cde”

class Solution {
public:
    int minDistance(string word1, string word2) {
      int len1=word1.size();
      int len2=word2.size();
      vector<vector<int>> ret(len1+1,vector<int>(len2+1,0));
      //初始化
      //第一行
      for(int i=0;i<=len2;i++)
      {
        ret[0][i]=i;
      }
      //第一列
      for(int i=0;i<=len1;i++)
      {
        ret[i][0]=i;
      }
      for(int i=1;i<=len1;i++)
      {
        for(int j=1;j<=len2;j++)
        {
          ret[i][j]=min(ret[i-1][j]+1,ret[i][j-1]+1);
          if(word1[i-1]==word2[j-1])
          {
            ret[i][j]=min(ret[i][j],ret[i-1][j-1]);
          }
          else
          {
            ret[i][j]=min(ret[i][j],ret[i-1][j-1]+1);
          }
        }
      }
      return ret[len1][len2];
    }
};

第12题 不同子序列(Distinct Subsequences)

给定一个字符串S和一个字符串T,计算S中的T的不同子序列的个数。
字符串的子序列是由原来的字符串删除一些字符(也可以不删除)在不改变相对位置的情况下的剩余字符(例如,"ACE"is a subsequence of"ABCDE"但是"AEC"不是)
例如:
S =“rabbbit”, T =“rabbit”
返回3

动态规划方法

状态:
子状态:由S的前1,2,…,m个字符组成的子串与T的前1,2,…,n个字符相同的个数
F(i,j): S[1:i]中的子串与T[1:j]相同的个数
状态递推:
在F(i,j)处需要考虑S[i] = T[j] 和 S[i] != T[j]两种情况
当S[i] = T[j]:
1>: 让S[i]匹配T[j],则
F(i,j) = F(i-1,j-1)
2>: 让S[i]不匹配T[j],则问题就变为S[1:i-1]中的子串与T[1:j]相同的个数,则F(i,j) = F(i-1,j)
故,S[i] = T[j]时,F(i,j) = F(i-1,j-1) + F(i-1,j)
当S[i] != T[j]:问题退化为S[1:i-1]中的子串与T[1:j]相同的个数
故,S[i] != T[j]时,F(i,j) = F(i-1,j)
初始化:引入空串进行初始化
F(i,0) = 1 —> S的子串与空串相同的个数,只有空串与空串相同
返回结果:
F(m,n)

例如:

F(1,1):“r” “r”:“r” 1
F(2,1):“ra” “r”:“r” 1
F(2,2):“ra” “ra”:“ra” 1
F(3,3):“rab” “rab”:“rab” 1
F(4,3):“rabb” “rab”:“rab” “ra b”
F(4,2):“rabb” “ra”:“ra” 1
F(5,3):“rabbb” “rab”:“rab” ,“ra b”,“ra b”

class Solution {
public:
    int numDistinct(string S, string T) {
      int len1 = S.size();
      int len2 = T.size();
      // S的长度小于T长度,不可能含有与T相同的子串
      if (len1 < len2) 
        return 0;
      // T为空串,只有空串与空串相同,S至少有一个子串,它为空串
      if (T.empty()) 
        return 1;
      // F(i,j),初始化所有的值为0
      vector<vector<int> > numDis(len1 + 1, vector<int>(len2 + 1, 0));
      // 空串与空串相同的个数为1
      numDis[0][0] = 1;
      for (int i = 1; i <= len1; ++i) 
      {
        // F(i,0)初始化
        numDis[i][0] = 1;
        for (int j = 1; j <= len2; ++j) 
        {
          // S的第i个字符与T的第j个字符相同
          if (S[i-1] == T[j-1]) 
          {
            numDis[i][j] = numDis[i-1][j] + numDis[i-1][j-1];
          }
          else 
          {
            // S的第i个字符与T的第j个字符不相同
            // 从S的前i-1个字符中找子串,使子串与T的前j个字符相同
            numDis[i][j] = numDis[i-1][j];
          }
        }
      }
      return numDis[len1][len2];
    }
};

你可能感兴趣的:(基础算法)