动态规划问题

Posted ymz123_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划问题相关的知识,希望对你有一定的参考价值。

背包问题

问题描述:

有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值.
问最多能装入背包的总价值是多大?
A[i], V[i], n, m 均为整数
你不能将物品进行切分
你所挑选的要装入背包的物品的总大小不能超过 m
每个物品只能取一次
m <= 1000m<=1000
len(A),len(V)<=100len(A),len(V)<=100

解题思路:
1.状态:F(i,j):前i个物品放入大小为j的背包中所获得的最大价值。

2.状态递推:对于第i个商品,有一种例外:商品大小大于背包大小j,装不下。
若装得下则有两种选择:放或者不放。
(1)装不下:此时的价值与前i-1个的价值一样。
F(i,j) = F(i-1, j)
(2)装得下:需要在两种选择中找价值最大的
F(i, j) = maxF(i-1, j), F(i-1, j-A[i-1])+V[i-1]
F(i-1, j):表示不把第i个物品放入背包中,所以它的价值就是前i-1个物品放入大小为j的背包的最大价值。
F(i-1, j-A[i-1])+V[i-1]:表示把第i个物品放入背包中,价值增加V[i-1],但是需要腾出A[i-1]的大下放第i个商品。

3.初始化:第0行和第0列都为0,表示没有装物品时的价值都为0.
F(0,j)=F(i,0)=0
返回值:F(n,m)

注:
放得下且要放入时,又分两种情况:背包剩余空间足够,可以直接放入第i个物品,价值必定增加;背包剩余空间不够,但取出前面某个物品再放入第i个物品价值将增加。
F(i-1, j)是本就没加上i的空间的价值,就等于F(i-1, j-A[i-1])(腾出i的空间)的价值。
即F(i-1, j-A[i-1]) = F(i-1, j)
所以F(i-1, j-A[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() || V.empty() || m < 1)
            return 0;
        vector<vector<int> > result;
        const int N = A.size() + 1;
        const int M = m + 1;
        result.resize(N);
        //初始化
        for(int i = 0; i < N; i++)
        
            result[i].resize(M, 0);
        
        for(int i = 1; i < N; i++)
        
            //j为背包大小
            for(int j = 1; j < M; j++)
            
                if(A[i-1] > j)
                
                    result[i][j] = result[i-1][j];
                
                else
                
                    result[i][j] = max(result[i-1][j], result[i-1][j-A[i-1]] + V[i-1]);
                
            
        

        return result[N-1][M-1];
    
;

优化算法:
上面的算法在计算第i行元素时,只用到第i-1行的元素,所以二维空间可以优化为一维空间。由于后面的元素更新需要依靠前面的元素未更新,j需要从后向前

    int backPackII(int m, vector<int> &A, vector<int> &V) 
        // write your code here
        if(A.empty() || V.empty() || m < 1)
            return 0;
        const int N = A.size() + 1;
        const int M = m + 1;
        vector<int> result;
        result.resize(M,0);
        //这里商品的索引位置不需要偏移
        for(int i = 0; i < N; i++)
        
            for(int j = M -1; j > 0; j--)
            
                if(A[i-1] > j)
                    result[j] = result[j];
                else
                    result[j] = max(result[j], result[j-A[i-1]] + V[i-1]);
            
        

        return result[m];
    

分割回文串

问题描述:

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

解题思路:
1.状态:
子状态:第1,2,3,…,n个字符需要的最小分割数。
F(i):到第i个字符需要的最小分割数。

2.状态递推:
F(i) = minF(i), 1+F(j), j<i && j+1到i是回文串
上式表示如果从j+1到i判断为回文字符串,且已知从第1个字符到第j个字符的最小切割数,那么只需要再切一次,就可以保证1–>j, j+1–>i都为回文串。

3.初始化:
F(i)=i-1
上式表示到第i个字符需要的最大分割数。
比如单个字符只需要切0次,2个字符最大需要1次。。。

4.返回结果:
F(n)

代码实现:

class Solution 
public:
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    bool isPalindrome(string s, int i, int j)
    
        while(i < j)
        
            if(s[i] != s[j])
                return false;
            i++;
            j--;
        
        
        return true;
    
    int minCut(string s) 
        // write code here
        if(s.empty())
            return 0;
        int len = s.size();
        vector<int> result;
        //初始化 给每个子串的最大切割次数 
        //F(0) = -1;必要,否则
        for(int i = 0; i <= len; i++)
            result.push_back(i-1);
        
        //F(i) = minF(i), 1+F(j), j<i && j+1到i是回文串
        for(int i = 1; i <= len; i++)
            for(int j = 0; j < i; j++)
                if(isPalindrome(s, j, i-1))
                    //如果result[j]+1的值比i个字符串的最大(i-1)小,则更新值
                    result[i] = min(result[i],result[j]+1);
                
            
        
        
        return result[len];
    
;

上述方法两次循环时间复杂度是O(n^2),判断回文串时间复杂度是O(n),总时间复杂度是O(n ^3)
对于过长的字符串,在OJ时可能会出现TLE(Time Limit Exceeded)

可将总体时间时间复杂度优化为O(n^2) 用空间换时间
判断回文串,这是一个“是不是”的问题,所以也可以用动态规划实现。
1.状态:
子状态:从第一个字符到第二个字符是不是回文串,第1-3,第2-5,…
F(i, j):字符区间[i, j] 是否为回文串

2.状态递推:
F(i, j):true->s[i]==s[j] && F(i+1, j-1) or false
上式表示如果字符串区间首尾字符相同且在去掉区间首尾字符后字符区间仍为回文串,则原字符区间为回文串。
从递推公式中可看到第i处需要用到第i+1处的信息,所以i应该从字符串末尾遍历。

3.初始化:
F(i, j) = false

4.返回结果:
矩阵F(n, n),只更新一半值(i <= j),n^2 / 2

class Solution 
public:
    /**
     * 
     * @param s string字符串 
     * @return int整型
     */
    vector<vector<bool> > getMat(string s)
    
        int len = s.size();
        vector<vector<bool> > mat(len, vector<bool>(len, false));
        for(int i = len - 1; i >= 0; i--)
            for(int j = i; j < len; j++)
                if(i == j) //单字符
                    mat[i][j] = true;
                else if(i + 1 == j) //相邻字符
                    mat[i][j] = (s[i] == s[j]);
                else
                    mat[i][j] = (s[i] == s[j] && mat[i+1][j-1]);
            
        
        
        return mat;
    
    int minCut(string s) 
        // write code here
        if(s.empty())
            return 0;
        int len = s.size();
        vector<int> result;
        //初始化 给每个子串的最大切割次数 
        //F(0) = -1;必要,否则
        for(int i = 0; i <= len; i++)
            result.push_back(i-1);
        
        vector<vector<bool> > mat = getMat(s);
        
        //F(i) = minF(i), 1+F(j), j<i && j+1到i是回文串
        for(int i = 1; i <= len; i++)
            for(int j = 0; j < i; j++)
                if(mat[j][i-1])
                    result[i] = min(result[i], result[j]+1);
            
        
        
        return result[len];
    
;

编辑距离

问题描述:

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

解题思路:
F(i, j): word1的前i个字符于word2的前j个字符的编辑距离

1.状态递推:
F(i, j) = minF(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)

2.初始化:
初始化一定要是确定的值,如果这里不加入空串,初始值无法确定。
F(i,0)=i:word与空串的编辑距离,删除操作
F(0,i)=i:空串与word的编辑距离,增加操作

3.返回结果:F(m, n)

代码实现:

class Solution 
public:
    /**
     * 
     * @param word1 string字符串 
     * @param word2 string字符串 
     * @return int整型
     */
    int minDistance(string word1, string word2) 
        // write code here
        if(word1.empty() || word2.empty())
            return max(word1.size(),word2.size());
        int len1 = word1.size();
        int len2 = word2.size();
        vector<vector<int> > result(1+len1,vector<int>(1+len2,0));
        //初始化
        for(int i = 0; i <= len1; i++)
            result[i][0] = i;
        
        for(int i = 0; i <= len2; i++)
            result[0][i] = i;
        
        for(int i = 1; i <= len1; i++)
            for(int j = 1; j <= len2; j++)
                result[i][j] = 1 + min(result[i-1][j],result[i][j-1]);
                if(word1[i-1] == word2[j-1])
                    //字符相等,编辑距离不变 (替换)
                    result[i][j] = min(result[i][j],result[i-1][j-1]);
                else
                    result[i][j] = min(result[i][j],1 + result[i-1][j-1]);
            
        
        
        return result[len1][len2];
    
;

不同子序列

问题描述:

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

S有多少个不同的子串与T相同
S[1:m]中的子串与T[1:n]相同的个数
由S的前m个字符组成的子串与T的前n个字符相同的个数
解题思路:
1.状态:
子状态:由S的前1,2,…,m个字符组成的子串与T的前1,2,…,n个字符相同的个数
F(i,j):S[1:i]中的子串与T[1:j]相同的个数

2.状态递推:
在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)

3.初始化:引入空串进行初始化
F(i,0) = 1 —> S的子串与空串相同的个数(只有空串和空串相同)

4.返回结果:F(m, n)

代码实现:

class Solution 
public:
    /**
     * 
     * @param S string字符串 
     * @param T string字符串 
     * @return int整型
     */
    int numDistinct(string S, string T) 
        // write code here
        if(T.empty())
            return 1;
        int len1 = S.size();
        int len2 = T.size();
        if(len1 < len2)
            return 0;
        //初始化
        vector<vector<int> > result(len1 +1, vector<int>(len2+1,0));
        //空串和空串相同的个数为1
        result[0][0] = 1;
        for(int i = 1; i <= len1; i++)
            result[i][0] = 1;
            for(int j = 1; j <= len2; j++)
                if(S[i-1] == T[j-1])
                    result[i][j] = result[i-1][j] + result[i-1][j-1];
                else
                    result[i][j] = result[i-1][j];
            
        
        
        return result[len1][len2];
    
;

可将空间复杂度优化为O(n)
F[i][j]只和F[i - 1][j],F[i - 1][j - 1]有关
类似于背包问题,可用一维数组保存上一行的结果,每次哦才能够最后一列更新元素值

class Solution 
public:
    /**
     * 
     * @param S string字符串 
     * @param T string字符串 
     * @return int整型
     */
    int numDistinct(string S, string T) 
        // write code here
        if(T.empty())
            return 1;
        int len1 = S.size();
        int len2 = T.size();
        if(len1 < len2)
            return 0;
        vector<int > ret(len2+1, 0);
        ret[0] = 1;
        
        for(int i = 1; i <= len1; i++)
            for(int j = len2; j > 0; j--)
                if(S[i-1] == T[j-1])
                    ret[j] = ret[j-1] + ret[j];
                else
                    ret[j] = ret[j];
            
        
        
        return ret[len2];
    
;

以上是关于动态规划问题的主要内容,如果未能解决你的问题,请参考以下文章

算法动态规划 ③ ( LeetCode 62.不同路径 | 问题分析 | 自顶向下的动态规划 | 自底向上的动态规划 )

算法动态规划 ③ ( LeetCode 62.不同路径 | 问题分析 | 自顶向下的动态规划 | 自底向上的动态规划 )

算法动态规划 ⑧ ( 动态规划特点 )

算法动态规划 ⑧ ( 动态规划特点 )

递归与动态规划

ACM动态规划题