基础算法总结(一)Algorithm(动态规划 Dynamic Programming)

动态规划(Dynamic Programming)


DP定义:
  动态规划是分治思想的延伸,通俗一点来说就是大事化小,小事化无的艺术。在将大问题化解为小问题的分治过程中,保存对这些小问题已经处理好的结果,并供后面处理更大规模的问题时直接使用这些结果。
动态规划具备了以下三个特点:
  1. 把原来的问题分解成了几个相似的子问题。
  2. 所有的子问题都只需要解决一次。
  3. 储存子问题的解。
动态规划的本质:
  是对问题状态的定义和状态转移方程的定义(状态以及状态之间的递推关系)。
动态规划问题一般从以下四个角度考虑:
  1. 状态定义
  2. 状态间的转移方程定义
  3. 状态的初始化
  4. 返回结果


状态定义的要求:定义的状态一定要形成递推关系。
一句话概括:三特点四要素两本质
适用场景:最大值/最小值, 可不可行, 是不是,方案个数


题目目录:

第1题 Fibonacci
第2题 变态青蛙跳台阶(Climbing Stairs)
第3题 最大连续子数组和(Maximum Subarray)
第4题 字符串分割(Word Break)
第5题 三角矩阵(Triangle)
第6题 路径总数(Unique Paths)
第7题 路径总数(Unique Paths II)
第8题 最小路径和(Minimum Path Sum)
第9题 背包问题
第10题 回文串分割(Palindrome Partitioning)
第11题 编辑距离(Edit Distance)
第12题 不同子序列(Distinct Subsequences)



第1题 Fibonacci

/*
要求输入一个整数n,请输出斐波那契数列的第n项(从0开始,第0项为0)。
*/
#include <iostream>
#include <ctime>

using namespace std;

/* 
方法一:递归
    斐波那契数列定义:F(n)=F(n-1)+F(n-2)(n>=2,n∈N*),其中F(1)=1,F(2)=1
*/
class Solution
{
public:
	int Fibonacci(int n){
        // 初始值
		if (n <= 0){
			return 0;
		}
		else if (n == 1 || n == 2){
			return 1;
		}
		else{
            // F(n)=F(n-1)+F(n-2)
			return Fibonacci(n - 1) + Fibonacci(n - 2);
		}
	}
};
/*
递归的方法时间复杂度为O(2^n),随着n的增大呈现指数增长,效率低下
当输入比较大时,可能导致栈溢出
在递归过程中有大量的重复计算
*/

/* 
方法二:动态规划
	状态:F(n)
    状态递推:F(n)=F(n-1)+F(n-2)
    初始值:F(1)=F(2)=1
    返回结果:F(N)
*/
class Solution1
{
public:
	int Fibonacci(int n){
		//vector<int> record(n + 1, 0);
        //int* record = new int[n + 1];
        int record[n + 1] = {0};// 申请一个数组,保存子问题的解,题目要求从第0项开始
		record[1] = record[2] = 1;// 初始状态
        for(int i = 3; i <= n; ++i){
            record[i] = record[i - 1] + record[i - 2];// 状态转移 F(n)=F(n-1)+F(n-2)
        }
        return record[n];// 返回值
    }
};
/*
上述解法的空间复杂度为O(n)
其实F(n)只与它相邻的前两项有关,所以没有必要保存所有子问题的解
只需要保存两个子问题的解就可以
下面方法的空间复杂度将为O(1)
*/
class Solution2
{
public:
	int Fibonacci(int n){
        // 初始值
        if(n <= 0){
            return 0;
        }
        else if(n == 1 || n == 2){
            return 1;
        }
        else{
            int a = 1;
            int b = 1;
            int result = 0;
            for(int i = 3; i <= n; ++i){
                // F(n)=F(n-1)+F(n-2)
                result = a + b;
                // 更新值
                a = b;
                b = result;
            }
            return result;
        }
    }
};

int main()
{
	int n = 40;
	clock_t startTime0, endTime0, startTime1, endTime1, startTime2, endTime2;

    int Fn = 0;
    Solution F;
	startTime0 = clock();//计时开始
    Fn = F.Fibonacci(n);
	endTime0 = clock();//计时结束
	cout << "recursion:\n" << Fn << endl;
	cout << "The run time is:" << (double)(endTime0 - startTime0) / CLOCKS_PER_SEC << "s" << endl;
    
    int Fn1 = 0;
    Solution1 F1;
    startTime1 = clock();//计时开始
    Fn1 = F1.Fibonacci(n);
	endTime1 = clock();//计时结束
	cout << "dynamic programming:\n" << Fn1 << endl;
	cout << "The run time is:" << (double)(endTime1 - startTime1) / CLOCKS_PER_SEC << "s" << endl;

    int Fn2 = 0;
    Solution2 F2;
    startTime2 = clock();//计时开始
    Fn2 = F2.Fibonacci(n);
	endTime2 = clock();//计时结束
	cout << "dynamic programming:\n" << Fn2 << endl;
	cout << "The run time is:" << (double)(endTime2 - startTime2) / CLOCKS_PER_SEC << "s" << endl;


	cout << "All run time is:" << (double)clock() / CLOCKS_PER_SEC << "s" << endl;

	return 0;
}

第2题 变态青蛙跳台阶(Climbing Stairs)

/*
一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶总共有多少种跳法。
*/
#include <iostream>

using namespace std;

/*
方法一:动态规划
    状态:
        子状态:跳上1级,2级,3级,...,n级台阶的跳法数
        f(n):还剩n个台阶的跳法数
    状态递推:
        n级台阶,第一步有n种跳法:跳1级、跳2级、到跳n级 
            跳1级,剩下n-1级,则剩下跳法是f(n-1)
            跳2级,剩下n-2级,则剩下跳法是f(n-2)
        f(n) = f(n-1)+f(n-2)+...+f(n-n)
            = f(n-1)+f(n-2)+...+f(0)
        f(n-1) = f(n-2)+...+f(0)
        则:f(n) = 2*f(n-1)
    初始值:
        f(1) = 1
        f(2) = 2*f(1) = 2
        f(3) = 2*f(2) = 4
        f(4) = 2*f(3) = 8
        所以它是一个等比数列
        f(n) = 2^(n-1)
    返回结果:f(n)
方法二:排列
    每个台阶看成一个位置,除过最后一个位置,其它位置都有两种可能性(要么跳,要么不跳)
    所以总的排列数为2^(n-1)*1 = 2^(n-1)
*/
class Solution
{
public:
    int JumpStair(int n){
        if(n  <= 0){
            return 0;
        }
        int total = 1;
        for(int i = 1; i < n; ++i){
            total *= 2;             // 2^(n-1)
        }
        return total;
    }
};
/*
扩展:降低时间复杂度
     上述实现的时间复杂度:O(N)
     O(1)的实现:使用移位操作
*/
class Solution1
{
public:
	int JumpStair(int n){
        if(n  <= 0){
            return 0;
        }
        return 1 << n - 1;      // 使用移位操作
    }
};
/*
总结:
    此题看似复杂,通过抽象和归纳,可以找出问题的内在规律
    定义问题的状态,以及状态间的递推关系,找到问题的答案
    
扩展1:
    上述问题为变态青蛙跳台阶,太疯狂,这只青蛙像是吃了大力丸
    身上充满了无穷的力量。现在让它变成一个正常的青蛙,限制它
    一次只能跳1阶或者2阶,现在该如何解答
    
扩展2:
    牛客网上另一个题目:矩形覆盖
    我们可以用2*1的小矩形横着或者竖着去覆盖更大的矩形。
    请问用n个2*1的小矩形无重叠地覆盖一个2*n的大矩形,总共有多少种方法?
*/
// 上述两个题目都可以用斐波那契数列求解:
class Solution2
{
public:
	int JumpStair(int n){
        // 初始值
        if(n <= 0){
            return 0;
        }
        else if(n == 1){
            return 1;
        }
        else if(n == 2){
            return 2;
        }
        else{
            int a = 1;
            int b = 2;
            int result = 0;
            for(int i = 3; i <= n; ++i){
                // F(n)=F(n-1)+F(n-2)
                result = a + b;
                // 更新值
                a = b;
                b = result;
            }
            return result;
        }
    }
};

int main()
{
	int n = 4;
    Solution F;
    cout << F.JumpStair(n) << endl;
    Solution1 F1;
    cout << F1.JumpStair(n) << endl;
    Solution2 F2;
    cout << F2.JumpStair(n) << endl;
	return 0;
}

第3题 最大连续子数组和(Maximum Subarray)

/*
在古老的一维模式识别中,常常需要计算连续子向量的最大和,当向量全为正数的时候,问题很好解决。
但是,如果向量中包含负数,是否应该包含某个负数,并期望旁边的正数会弥补它呢?
例如:{6,-3,-2,7,-15,1,2,2},连续子向量的最大和为8(从第0个开始,到第3个为止)。
给一个数组,返回它的最大连续子序列的和(子向量的长度至少是1)。
*/
#include <iostream>
#include <vector>

using namespace std;

/*
方法:动态规划
    状态:
        子状态:长度为1,2,3,...,n的子数组和的最大值
        F(i):长度为i的子数组和的最大值,这种定义不能形成递推关系,舍弃
        F(i):以array[i]为末尾元素的子数组和的最大值
    状态递推:
        F(i) = max(F(i-1) + array[i],array[i])
        F(i) = (F(i-1) > 0) ? (F(i-1) + array[i]) : array[i]
    初始值:
        F(0) = array[0]
    返回结果:
        maxsum:所有F(i)中的最大值
        maxsum = max(maxsum,F(i))
*/
class Solution
{
public:
    int MaxSubarraySum(vector<int> array){
        if(array.empty()){
            return 0;
        }
        // F(i)初始化
        int sum = array[0];
        // maxsum初始化
        int maxsum = array[0];
        for(int i = 1; i < array.size(); ++i){
            // F(i) = max(F(i-1) + array[i],array[i])
            sum = (sum > 0) ? (sum + array[i]) : array[i];
            // maxsum = max(maxsum,F(i))
            maxsum = maxsum > sum ? maxsum : sum;
        }
        return maxsum;
    }
};

int main(){
    vector<int> array;
    array.push_back(6);
    array.push_back(-3);
    array.push_back(-2);
    array.push_back(7);
    array.push_back(-15);
    array.push_back(1);
    array.push_back(2);
    array.push_back(2);
    Solution S;
    cout << S.MaxSubarraySum(array) << endl;
    return 0;
}

第4题 字符串分割(Word Break)

/*
给定一个字符串和一个词典dict,确定s是否可以根据词典中的词分成一个或多个单词。
比如,给定 s = "leetcode", dict = ["leet", "code"]
返回true,因为"leetcode"可以被分成"leet code"
*/
#include <iostream>
#include <string>
#include <vector>
#include <unordered_set>

using namespace std;

/*
方法:动态规划
    状态:
        子状态:前1,2,3,...,n个字符能否根据词典中的词被成功分词
        F(i): 前i个字符能否根据词典中的词被成功分词
    状态递推:
        F(i): true{j<i && F(j) && substr[j+1,i]能在词典中找到} OR false
        在j小于i中,只要能找到一个F(j)为true,并且从j+1到i之间的字符能在词典中找到,则F(i)为true
    初始值:
        对于初始值无法确定的,可以引入一个不代表实际意义的空状态,作为状态的起始
        空状态的值需要保证状态递推可以正确且顺利的进行,到底取什么值可以通过简单的例子进行验证
        F(0) = true
    返回结果:F(n)
*/
class Solution
{
public:
    bool WordBreak(string str, unordered_set<string> &dict){
        if(str.empty() || dict.empty()){
            return false;
        }
        // 获取词典中的单词的最大长度
        int max_length = 0;
        for(auto it = dict.begin(); it != dict.end(); ++it){
            max_length = max_length > (*it).size() ? max_length : (*it).size();
        }
        // F(i): 前i个字符能否根据词典中的词被成功分词
        vector<bool> F(str.size() + 1, false);
        // 初始化F(0) = true
        F[0] = true;
        for(int i = 1; i <= str.size(); ++i){
            for(int j = i - 1; j >= 0; --j ){
                // 如果最小子串长度大于max_length,则跳过
                if((i - j) > max_length){
                    break;
                }
                // F(i): true{j <i && F(j) && substr[j+1,i]能在词典中找到} OR false
                // 第j+1个字符的索引为j
                if(F[j] && dict.find(str.substr(j, i - j)) != dict.end()){
                    F[i] = true;
                    break;
                }
            }
        }
        return F[str.size()];
    }
};

int main()
{
    string str("iamachinesepeople");
    unordered_set<string> dict= {"men", "i", "am", "women", "a", "china", "chinese", "man", "people"};
    Solution s;
    cout << s.WordBreak(str, dict) << endl;
    return 0;
}

第5题 三角矩阵(Triangle)

/*
题目描述:
    给定一个三角矩阵,找出自顶向下的最短路径和,每一步可以移动到下一行的相邻数字。
    比如给定下面一个三角矩阵,自顶向下的最短路径和为11。2 + 3 + 5 + 1 = 11
    [
        [2],
       [3,4],
      [6,5,7],
     [4,1,8,3]
    ]
*/
#include <iostream>
#include <vector>

using namespace std;

/*
方法一:动态规划
    状态:
        子状态:从(0,0)到(1,0),(1,1),(2,0),...(n,n)的最短路径和
        F(i,j): 从(0,0)到(i,j)的最短路径和
    状态递推:
        F(i,j) = min( F(i-1, j-1), F(i-1, j)) + triangle[i][j]
    初始值:
        F(0,0) = triangle[0][0]
    返回结果:
        min(F(n-1, i))
*/
class Solution
{
public:
    int minimumTotal(const vector<vector<int>> &triangle){
        if(triangle.empty()){
            return 0;
        }
        // F[i][j], F[0][0]初始化
        int N = triangle.size();
        vector<vector<int>> F(triangle);// 拷贝构造
        for(int i = 1; i < N; ++i){
            for(int j = 0; j <= i; ++j){
                // 处理左边界和右边界
                if(j == 0){
                    F[i][j] = F[i - 1][j] + F[i][j];
                    continue;
                }
                else if(j == i){
                    F[i][j] = F[i - 1][j - 1] + F[i][j];
                    break;
                }
                // F(i,j) = min( F(i-1, j-1), F(i-1, j)) + triangle[i][j]
                F[i][j] = min(F[i - 1][j - 1], F[i - 1][j]) + F[i][j];
            }
        }
        int result = F[N - 1][0];
        // min(F(n-1, i))
        for(int i = 1; i < N; ++i){
            result = min(result, F[N - 1][i]);
        }
        return result;
    } 
};
/*
方法二:动态规划(反向思维)
    状态:
        子状态:从(n,n),(n,n-1),...(1,0),(1,1),(0,0)到最后一行的最短路径和
        F(i,j): 从(i,j)到最后一行的最短路径和
    状态递推:
        F(i,j) = min( F(i+1, j), F(i+1, j+1)) + triangle[i][j]
    初始值:
        F(n-1,0) = triangle[n-1][0], F(n-1,1) = triangle[n-1][1],..., F(n-1,n-1) = triangle[n-1][n-1]
    返回结果:
        F(0, 0) 
这种逆向思维不需要考虑边界,也不需要最后寻找最小值,直接返回F(0,0)即可
*/
class Solution1
{
public:
    int minimumTotal(const vector<vector<int>> &triangle){
        if(triangle.empty()){
            return 0;
        }
        // F[n-1][n-1],...F[n-1][0]初始化
        int N = triangle.size();
        vector<vector<int>> F(triangle);// 拷贝构造
        // 从倒数第二行开始
        for(int i = N - 2; i >= 0; --i){
            for(int j = 0; j <= i; ++j){
                // F(i,j) = min( F(i+1, j), F(i+1, j+1)) + triangle[i][j]
                F[i][j] = min(F[i + 1][j], F[i + 1][j + 1]) + F [i][j];
            }
        }
        return F[0][0];
    } 
};
/*
注:
    易错点:只保留每一步的最小值,忽略其他路径,造成最终结果错误
            局部最小不等于全局最小
            
    总结:
        遇到关于矩阵、网格、字符串间的比较、匹配的问题,
        单序列(一维)动规解决不了的情况下,就需要考虑双序列(二维)动规。
*/

int main(){
    int N = 4;
    vector<vector<int>> triangle(N);
    triangle[0].push_back(2);
    triangle[1].push_back(3), triangle[1].push_back(4);
    triangle[2].push_back(6), triangle[2].push_back(5), triangle[2].push_back(7);
    triangle[3].push_back(4), triangle[3].push_back(1), triangle[3].push_back(8), triangle[3].push_back(3);
    Solution S;
    cout << S.minimumTotal(triangle) << endl;
    Solution S1;
    cout << S1.minimumTotal(triangle) << endl; 
    return 0;
}

第6题 路径总数(Unique Paths)

/*
题目描述:
    在一个m*n的网格的左上角有一个机器人,机器人在任何时候只能向下或者向右移动,
    机器人试图到达网格的右下角,有多少可能的路径。
*/
#include <iostream>
#include <vector>

using namespace std;
/*
方法:动态规划
    状态:
        子状态:从(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)
    初始化:
        特殊情况:第0行和第0列
        F(0,i) = 1
        F(i,0) = 1
    返回结果:
        F(m-1,n-1)
*/
class Solution
{
public:
    int uniquePaths(int m, int n){
        if(m < 1 || n < 1){
            return 0;
        }
        // else if(m == 1 || n == 1){
        //     return 1;
        // }
        // 申请F(i,j)空间,初始化
        vector<vector<int>> F(m, vector<int>(n, 1));
        for(int i = 1; i < m; ++i){
            for(int j = 1; j < n; ++j){
                // F(i, j) = F(i, j - 1) + F(i - 1, j)
                F[i][j] = F[i][j - 1] + F[i - 1][j];
            }
        }
        return F[m - 1][n - 1];
    }
};

int main(){
    Solution S;
    cout << S.uniquePaths(3, 7) << endl;
    return 0;
}

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

/*
题目描述:
    在一个m*n的网格的左上角有一个机器人,机器人在任何时候只能向下或者向右移动,
    机器人试图到达网格的右下角,有多少可能的路径。
    机器人还是要从网格左上角到达右下角,但是网格中添加了障碍物,障碍物用1表示
*/
#include <iostream>
#include <vector>

using namespace std;
/*
方法:动态规划
    状态:
        子状态:从(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 uniquePaths1(const vector<vector<int>> &arr){
        if(arr.empty() || arr[0].empty()){
            return 0;
        }
        const int m = arr.size();
        const int n = arr[0].size();
        // 申请F(i,j)空间,初始值为0
        vector<vector<int>> F(m, vector<int>(n, 0));
        for(int i = 0; i < m; ++i){
            if(arr[i][0] == 1){// 第0列中只要前面有障碍,下面都无法到达
                break;
            }
            F[i][0] = 1;
        }
        for(int j = 0; j < n; ++j){
            if(arr[0][j] == 1){// 第0行中只要前面有障碍,后面都无法到达
                break;
            }
            F[0][j] = 1;
        }
        for(int i = 1; i < m; ++i){
            for(int j = 1; j < n; ++j){
                if(arr[i][j] == 1){// 如果有障碍物,则将该位置的方法数填为0
                    F[i][j] = 0;
                }
                else{
                    // F(i, j) = F(i, j - 1) + F(i - 1, j)
                    F[i][j] = F[i][j - 1] + F[i - 1][j];
                }
            }
        }
        return F[m - 1][n - 1];
    }
};

int main(){
    vector<vector<int>> arr(3, vector<int>(3, 0));
    arr[1][1] = 1;
    Solution S;
    cout << S.uniquePaths1(arr) << endl;
    return 0;
}

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

/*
题目描述:
    给定一个m*n的网格,网格用非负数填充,找到一条从左上角到右下角的最短路径。
    注:每次只能向下或者向右移动。
*/
#include <iostream>
#include <vector>

using namespace std;
/*
方法:动态规划
    状态:
        子状态:从(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 minimumPathSum(const vector<vector<int>> &arr){
        // 如果为空或者只有一行,返回0
        if(arr.empty() || arr[0].empty()){
            return 0;
        }
        // 获取行和列大小,并申请F(i,j)空间
        const int m = arr.size();
        const int n = arr[0].size();
        vector<vector<int>> F(m, vector<int>(n, 0));
        for(int i = 0; i < m; ++i){
            for(int j = 0; j < n; ++j){
                if(i == 0 && j == 0){
                    F[0][0] = arr[0][0]; 
                }
                else if(i == 0){
                    F[0][j] = F[0][j - 1] + arr[0][j];// 因为是求和,所以不要忘记加上当前位置的值
                }
                else if(j == 0){
                    F[i][0] = F[i - 1][0] + arr[i][0];// 因为是求和,所以不要忘记加上当前位置的值
                }
                else{
                    F[i][j] = min(F[i][j - 1], F[i - 1][j]) + arr[i][j];// 因为是求和,所以不要忘记加上当前位置的值
                }
            }
        }
        return F[m - 1][n - 1];
    }
};
// 以上解法简洁,但容易出错
// 在解决复杂问题是,同时考虑多种情况,逻辑容易混乱。
// 因此,分解思考每一种情况,逻辑简明,不易出错。
class Solution1
{
public:
    int minimumPathSum(const vector<vector<int>> &arr){
        // 如果为空或者只有一行,返回0
        if(arr.empty() || arr[0].empty()){
            return 0;
        }
        // 获取行和列大小,并申请F(i,j)空间
        const int m = arr.size();
        const int n = arr[0].size();
        vector<vector<int>> F(m, vector<int>(n, 0));
        // F(0,0)初始化
        F[0][0] = arr[0][0];
        // 初始化第一列
        for(int i = 1; i < m; ++i){
            F[i][0] = F[i - 1][0] + arr[i][0];
        }
        // 初始化第一行
        for(int j = 1; j < n; ++j){
            F[0][j] = F[0][j - 1] + arr[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)
                F[i][j] = min(F[i][j - 1], F[i - 1][j]) + arr[i][j];
            }
        }
        return F[m - 1][n - 1];
    }
};
int main(){
    vector<vector<int>> arr = {{2, 1, 9}, {5, 3, 6}, {7, 8, 4}};
    Solution S;
    cout << S.minimumPathSum(arr) << endl;
    Solution1 S1;
    cout << S1.minimumPathSum(arr) << endl;
    return 0;
}

第9题 背包问题

/*
题目描述:
    有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值.
    问最多能装入背包的总价值是多大?
*/
#include <iostream>
#include <vector>

using namespace std;
/*
状态:
    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)
*/
class Solution
{
public:
    int backPack(int m, int n, const vector<int> &A, const vector<int> &V){
        if(A.empty() || V.empty() || m == 0){
            return 0;
        }
        // 多加一行一列,用于设置初始条件
        const int N = n + 1;
        const int M = m + 1;
        // 初始化所有位置为0,第一行和第一列都为0,初始条件
        vector<vector<int>> F(N, vector<int> (M, 0));
        for(int i = 1; i < N; ++i){
            for(int j = 1; j < M; ++j){
                //第i个商品在A中对应的索引为i-1: i从1开始
                //如果第i个商品大于j,说明放不下, 所以(i,j)的最大价值和(i-1,j)相同
                if(A[i - 1] > j){
                    F[i][j] = F[i - 1][j];
                }
                //如果可以装下,分两种情况,装或者不装
                //如果不装,则即为(i-1, j)
                //如果装,需要腾出放第i个物品大小的空间:j - A[i-1], 装入之后的最大价值即为(i - 1, j - A[i-1]) + 第i个商品的价值V[i - 1]
                //最后在装与不装中选出最大的价值
                else{
                    // 这里需要着重理解:F(i-1, j - A[i]) + V[i]:表示把第i个物品放入背包中,价值增加V[i],但是需要腾出j - A[i]的大小放第i个商品
                    int newValue = F[i - 1][j - A[i - 1]] + V[i - 1];
                    F[i][j] = max(F[i - 1][j], newValue);
                }
            }
        }
        //返回装入前N个商品,物品大小为m的最大价值
        return F[N - 1][M - 1];
    }
};
/*
优化算法:
    上面的算法在计算第i行元素时,只用到第i-1行的元素,所以二维的空间可以优化为一维空间
    但是如果是一维向量,需要从后向前计算,因为后面的元素更新需要依靠前面的元素未更新(模拟二维矩阵的上一行的值)的值
*/
class Solution1
{
public:
    int backPack(int m, int n, const vector<int> &A, const vector<int> &V){
        if(A.empty() || V.empty() || m == 0){
            return 0;
        }
        const int N = n;
        //多加一列,用于设置初始条件,因为第一件商品要用到前面的初始值
        const int M = m + 1;
        //初始化所有位置为0,第一行都为0,初始条件
        vector<int> F(M, 0);
        //这里商品的索引位置不需要偏移,要和未优化的方法区分开
        //这里的i-1理解为上一行,或者未更新的一维数组值
        for(int i = 0; i < N; ++i){
            for(int j = m; j > 0; --j){
                //如果第i个商品大于j,说明放不下, 所以(i,j)的最大价值和(i-1,j)相同
                if(A[i] > j){
                    F[j] = F[j];
                }
                //如果可以装下,分两种情况,装或者不装
                //如果不装,则即为(i-1, j)
                //如果装,需要腾出放第i个物品大小的空间: j - A[i],装入之后的最大价值即为(i - 1, j - A[i-1]) + 第i个商品的价值V[i]
                //最后在装与不装中选出最大的价值
                else{
                    int newValue = F[j - A[i]] + V[i];
                    F[j] = max(F[j], newValue);
                }
            }
        }
        //返回装入前N个商品,物品大小为m的最大价值
        return F[m];
    }
};

int main(){
    int m = 8;
    int n = 5;
    vector<int> A = {3, 6, 1, 2, 4, 5};
    vector<int> V = {1200, 2000, 1000, 500, 1000, 1500};
    Solution S;
    cout << S.backPack(m, n, A, V) << endl;
    Solution S1;
    cout << S1.backPack(m, n, A, V) << endl;
    return 0;
}

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

/*
回文串:正读和反读都一样的字符串,比如noon,level,字符串左右对称
题目描述:
    给定一个字符串 s, 把 s 分割成一系列的子串,分割的每一个子串都为回文串
    返回最小的分割次数
    比如,给定 s = "aab",
    返回1,因为一次cut就可以产生回文分割["aa","b"]
*/
#include <iostream>
#include <string>
#include <algorithm>
#include <vector>

using namespace std;
/*
方法:动态规划
    状态:
        子状态:到第1,2,3,...,n个字符需要的最小分割数
        F(i): 到第i个字符需要的最小分割数
    状态递推:
        F(i) = min{F(i), 1 + F(j)}, where j<i && j+1到i是回文串
        上式表示如果从j+1到i判断为回文字符串,且已经知道从第1个字符到第j个字符的最小切割数
        那么只需要再切一次,就可以保证 1-->j, j+1-->i 都为回文串。
    初始化:
        F(i) = i - 1
        上式表示到第i个字符需要的最大分割数, 比如单个字符只需要切0次,因为单子符都为回文串
        2个字符最大需要1次,3个2次......
    返回结果:
        F(n)
    遗留问题:如何判断一段字符串为回文串
        循环判断首尾元素是否相同,如果全部相同,则是回文串
*/
class Solution
{
public:
    //判断是否回文串
    bool isPalindrome(const string &str){
        string str1(str);
        reverse(str1.begin(), str1.end());
        if (str1 == str){
            return true;
        }
        return false;
    }
    int minCut(const string &str){
        if (str.empty()){
            return 0;
        }
        int len = str.size();
        vector<int> F;
        // F(i)初始化
        // F(0)= -1,必要项,如果没有这一项,对于重叠字符串“aaaaa”会产生错误的结果
        for (int i = 0; i < len + 1; ++i){
            F.push_back(i - 1);
        }
        for (int i = 1; i < len + 1; ++i){
            for (int j = 0; j < i; ++j){
                // F(i) = min{F(i), 1 + F(j)}, where j<i && j+1到i是回文串
                // 从最长串判断,如果从第j+1到i为回文字符串
                // 则再加一次分割,从1到j,j+1到i的字符就全部分成了回文字符串
                if (isPalindrome(str.substr(j, i - j))){
                    F[i] = min(F[j] + 1, F[i]);
                }
            }
        }
        return F[len];
    }
};
/*
上述方法两次循环时间复杂度是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 Solution1
{
public:
    //判断是否回文串
    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;
    }
    int minCut(const string &str){
        if (str.empty()){
            return 0;
        }
        int len = str.size();
        vector<int> F;
        // F(i)初始化
        // F(0)= -1,必要项,如果没有这一项,对于重叠字符串“aaaaa”会产生错误的结果
        for (int i = 0; i < len + 1; ++i){
            F.push_back(i - 1);
        }
        vector<vector<bool>> mat = getMat(str);
        for (int i = 1; i < len + 1; ++i){
            for (int j = 0; j < i; ++j){
                // F(i) = min{F(i), 1 + F(j)}, where j<i && j+1到i是回文串
                // 从最长串判断,如果从第j+1到i为回文字符串
                // 则再加一次分割,从1到j,j+1到i的字符就全部分成了回文字符串
                if (mat[j][i - 1]){
                    F[i] = min(F[j] + 1, F[i]);
                }
            }
        }
        return F[len];
    }
};
/*
上述方法判断回文串时间复杂度O(n^2)
主方法两次循环时间复杂度O(n^2)
总体时间复杂度O(n^2) ~ O(2 * n^2) = O(n^2) + O(n^2)
总结:
    简单的动态规划问题,状态,状态递推和状态初始化都比较直观
    对于复杂的动态规划问题,状态,状态递推和状态初始化都比较隐含,需要仔细推敲
    尤其是状态递推可能需要额外的辅助判断条件才能达成。
*/

int main()
{
    string str("aaaaaaa");
    Solution S;
    cout << S.minCut(str) << endl;
    Solution S1;
    cout << S1.minCut(str) << endl;
    return 0;
}

第11题 编辑距离(Edit Distance)

/*
题目描述:
    给定两个单词word1和word2,找到最小的修改步数,把word1转换成word2
    每一个操作记为一步
    允许在一个word上进行如下3种操作:
    a) 插入一个字符
    b) 删除一个字符
    c) 替换一个字符
编辑距离(Edit Distance):是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。
*/
#include <iostream>
#include <string>
#include <vector>

using namespace std;
/* 
方法:动态规划
    状态:
        子状态: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)
注:字符串类的动态规划,可引入空串进行初始化
*/
class Solution
{
public:
    int editDistance(string s, string s1){
        if(s.empty() && s1.empty()){
            return 0;
        }
        else if(s.empty()){
            return s1.size();
        }
        else if(s1.empty()){
            return s.size();
        }
        const int m = s.size() + 1;
        const int n = s1.size() + 1;
        vector<vector<int>> F(m, vector<int>(n, 0));
        for(int i = 0; i < m; ++i){
            F[i][0] = i;
        }
        for(int j = 0; j < n; ++j){
            F[0][j] = j;
        }
        for(int i = 1; i < m; ++i){
            for(int j = 1; j < n; ++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) } 
                // 判断word1的第i个字符是否与word2的第j个字符相等
                if (s[i - 1] == s1[j - 1]) {
                    F[i][j] = 1 + min(F[i][j - 1], F[i - 1][j]);
                    // 字符相等,F(i-1,j-1)编辑距离不变
                    F[i][j] = min(F[i][j], F[i - 1][j - 1]);
                }
                else {
                    F[i][j] = 1 + min(F[i][j - 1], F[i - 1][j]);
                    // 字符不相等,F(i-1,j-1)编辑距离 + 1
                    F[i][j] = min(F[i][j], 1 + F[i - 1][j - 1]);
                }
            }
        }
        return F[m - 1][n - 1];
    }
};
// 简化版:
class Solution1
{
public:
    int editDistance(string s, string s1){
        if(s.empty() || s1.empty()){
            return max(s.size(), s1.size());
        }
        const int m = s.size() + 1;
        const int n = s1.size() + 1;
        vector<vector<int>> F(m, vector<int>(n, 0));
        for(int i = 0; i < m; ++i){
            F[i][0] = i;
        }
        for(int j = 0; j < n; ++j){
            F[0][j] = j;
        }
        for(int i = 1; i < m; ++i){
            for(int j = 1; j < n; ++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][j] = min(min(F[i - 1][j] + 1, F[i][j - 1] + 1), F[i - 1][j - 1] + (s[i - 1] == s1[j - 1] ? 0 : 1));
            }
        }
        return F[m - 1][n - 1];
    }
};

int main(){
    Solution S;
    cout << S.editDistance("edit", "distance") << endl;
    Solution S1;
    cout << S1.editDistance("edit", "distance") << endl;
    return 0;
}

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

/*
题目描述:
    给定两个字符串S和T,求S有多少个不同的子串与T相同。
    S的子串定义为在S中任意去掉0个或者多个字符形成的串。
    子串可以不连续,但是相对位置不能变。
    比如“ACE”是“ABCDE”的子串,但是“AEC”不是。
*/
#include <iostream>
#include <string>
#include <vector>

using namespace std;
/*
问题翻译:S有多少个不同的子串与T相同
    S[1:m]中的子串与T[1:n]相同的个数
    由S的前m个字符组成的子串与T的前n个字符相同的个数
方法:动态规划
    状态:
        子状态:由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)
*/
class Solution
{
public:
    int distinctSubsequences(string s, string t){
        if(s.size() < t.size()){    // S的长度小于T长度,不可能含有与T相同的子串
            return 0;
        }
        else if(t.empty()){         // T为空串,只有空串与空串相同,S至少有一个子串,它为空串
            return 1;
        }
        const int m = s.size() + 1;
        const int n = t.size() + 1;
        vector<vector<int>> F(m, vector<int>(n, 0));
        // F(i,0)初始化
        for(int i = 0; i < m; ++i){
            F[i][0] = 1;
        }
        for(int i = 1; i < m; ++i){
            for(int j = 1; j < n; ++j){
                if(s[i -1] == t[j - 1]){    // S的第i个字符与T的第j个字符相同
                    F[i][j] = F[i - 1][j - 1] + F[i- 1][j];
                }
                else{
                    // S的第i个字符与T的第j个字符不相同
                    // 从S的前i-1个字符中找子串,使子串与T的前j个字符相同
                    F[i][j] = F[i - 1][j];
                }
            }
        }
        return F[m - 1][n - 1];
    }
};

int main(){
    Solution S;
    cout << S.distinctSubsequences("ababcac", "abc") << endl;
    return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值