LeetCode刷题(六)——动态规划

Day9 2020.07.28&2020.07.29&2020.07.30

1.triangle

给出一个三角形,计算从三角形顶部到底部的最小路径和,每一步都可以移动到下面一行相邻的数字
例如,给出的三角形如下:
[↵ [2],↵ [3,4],↵ [6,5,7],↵ [4,1,8,3]↵]
最小的从顶部到底部的路径和是2 + 3 + 5 + 1 = 11。
注意:如果你能只用O(N)的额外的空间来完成这项工作的话,就可以得到附加分,其中N是三角形中的行总数。

  • 第一种解法:时O(n^2) 空O(n^2)
class Solution {
public:
    int minimumTotal(vector<vector<int> > &triangle) {
        //动态规划,从上到下计算经过最小路径
        //返回最后一行的最小值
        //时O(n^2)  空O(n^2)
        int n=triangle.size();
        vector<vector<int>> temp(n,vector<int>(n));
        temp[0][0]=triangle[0][0];
        for(int i=1;i<n;i++){
            temp[i][0]=temp[i-1][0]+triangle[i][0];
            for(int j=1;j<i;j++){
                //上面一行较小的路径值加上当前节点的值
                temp[i][j]=min(temp[i-1][j-1],temp[i-1][j])+triangle[i][j];
            }
            temp[i][i]=temp[i-1][i-1]+triangle[i][i];
        }
        return *min_element(temp[n-1].begin(),temp[n-1].end());
    }
};
  • 将第一种解法优化,空间复杂度为O(n)
class Solution {
public:
    int minimumTotal(vector<vector<int> > &triangle) {
        //动态规划+空间优化,使用两个一维数组就可以,上一行和当前行,之前的不需要存储
        //空间复杂度为O(n)
        int n=triangle.size();
        vector<vector<int>> temp(2,vector<int>(n));
        temp[0][0]=triangle[0][0];
        for(int i=1;i<n;i++){
            int cur=i%2;
            int pre=1-cur;
            temp[cur][0]=temp[pre][0]+triangle[i][0];
            for(int j=1;j<i;j++){
                上面一行较小的路径值加上当前节点的值
                temp[cur][j]=min(temp[pre][j-1],temp[pre][j])+triangle[i][j];
            }
            temp[cur][i]=temp[pre][i-1]+triangle[i][i];
        }
        return *min_element(temp[(n-1)%2].begin(),temp[(n-1)%2].end());
    }
};
2.minimum-path-sum

给定一个由非负整数填充的m x n的二维数组,现在要从二维数组的左上角走到右下角,请找出路径上的所有数字之和最小的路径。
注意:你每次只能向下或向右移动。
在这里插入图片描述
vec.end()和vec.rend()都是指向数组最后一个元素的后一个元素

class Solution {
public:
    int minPathSum(vector<vector<int> >& grid) {
    //空间复杂度为O(n)
        if(grid.size()==0||grid[0].size()==0) return 0;
        int row=grid.size();
        int col=grid[0].size();
        vector<int> temp(col,grid[0][0]);
        //temp中第一行(0行)当前元素的值等于面前元素的累加和
        for(int j=1;j<col;j++){
            temp[j]=temp[j-1]+grid[0][j];
        }
        for(int i=1;i<row;i++){
            //temp中第一列(0列)当前元素的值等于前面元素的累加和
            temp[0]+=grid[i][0];
            for(int j=1;j<col;j++){
                temp[j]=min(temp[j-1],temp[j])+grid[i][j];
            }
        }
        //在数组不为空的时候temp.rbegin()!=temp.end();
        return *temp.rbegin();
    }
};
3.unique-paths

一个机器人在m×n大小的地图的左上角(起点,下图中的标记“start"的位置)。机器人每次向下或向右移动。机器人要到达地图的右下角。(终点,下图中的标记“Finish"的位置)。可以有多少种不同的路径从起点走到终点?

  • 解法一:数学方法
    C(n+m-2,m-1)=(从n+m-2开始乘,乘m-1个/(m-1)!
    ** 在连乘的时候一定要注意数据类型是否越界**
class Solution {
public:
    int uniquePaths(int m, int n) {
        long int k1=1,k2=1;
        //计算(m-1)!,(n+m-2)!,(n-1)!,注意0!=1
        //!!!需要注意的是,如果循环直接乘的话会超过int的范围,所以不能直接乘
        //所以若m>=n只能计算从(m+n-2)开始乘(n-1)个以及(n-1)!
        if(m<n){
            int temp=n;
            n=m;
            m=temp;
        }
        //此时m>=n
        for(int i=n+m-2,j=1;j<=n-1;i--,j++){
            k1*=i;
            k2*=j;
        }
        return k1/k2;
    }
};
  • 解法二:动态规划
class Solution {
public:
    int uniquePaths(int m, int n) {
        //使用O(n)的时间复杂度
        vector<int> temp(n,1);
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++)
                temp[j]+=temp[j-1];
        }
        return *temp.rbegin();
    }
};
4.unique-paths-ii

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

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int> >& obstacleGrid) {
        int row=obstacleGrid.size();
        int col=obstacleGrid[0].size();
        if(obstacleGrid[0][0]==1||obstacleGrid[row-1][col-1]==1) return 0;
        //也要注意第一行和第一列,如果其中有1,则1后面的路径数都为0
        //特殊情况:开始和结尾都为障碍,则只有0条路径
        //此时便将上述特殊情况包含在内
        //但上面的语句还是保留了,为的是可以提前结束,减少特殊情况的运行时间
        vector<int> temp(col,0);
        for(int j=0;j<col;j++){
            if(obstacleGrid[0][j]==0) temp[j]=1;
            else break;
        }
        for(int i=1;i<row;i++){
            if(obstacleGrid[i][0]==1) temp[0]=0;
            for(int j=1;j<col;j++){
                if(obstacleGrid[i][j]==1) temp[j]=0;
                else temp[j]+=temp[j-1];
            }
        }
        return *temp.rbegin();
    }
};
5.climbing-stairs

你在爬楼梯,需要n步才能爬到楼梯顶部
每次你只能向上爬1步或者2步。有多少种方法可以爬到楼梯顶部?

class Solution {
public:
    int climbStairs(int n) {
        int d[n+1];
        d[0]=1;
        d[1]=1;
        for(int i=2;i<=n;i++){
            d[i]=d[i-1]+d[i-2];
        }
        return d[n];
    }
};
6.jump-game

给出一个非负整数数组,你最初在数组第一个元素的位置,数组中的元素代表你在这个位置可以跳跃的最大长度,判断你是否能到达数组最后一个元素的位置。
例如:
A =[2,3,1,1,4], 返回 true.
A =[3,2,1,0,4], 返回 false.

class Solution {
public:
    bool canJump(int* A, int n) {
        vector<int> B(n,0);
        B[0]=1;
        for(int i=0;i<n&&1==B[i];i++){
            if(i+A[i]>=n-1) return true;
            else for(int j=i+1;j<=i+A[i];j++){B[j]=1;}
        }
        if(0==*B.rbegin()) return false;
        else return true;
    }
};
7.jump-game-ii

给出一个非负整数数组,你最初在数组第一个元素的位置,数组中的元素代表你在这个位置可以跳跃的最大长度,你的目标是用最少的跳跃次数来到达数组的最后一个元素的位置。
例如:给出数组 A =[2,3,1,1,4]
最少需要两次才能跳跃到数组最后一个元素的位置。(从数组下标为0的位置跳长度1到达下标1的位置,然后跳长度3到数组最后一个元素的位置)

class Solution {
public:
    int jump(int* A, int n) {
        //使用贪心算法求解
        int maxPos=0;//指向下一步最大可跳跃的位置
        int jump=0;//记录再次起跳的位置
        int step=0;//记录需要的步数
        //不考虑从最后一个位置起跳的情况
        for(int i=0;i<n-1;i++){
            if(maxPos>=i){
                maxPos=max(maxPos,i+A[i]);
                if(i==jump){
                    jump=maxPos;
                    ++step;
                }
            }
        }
        return step;
    }
};
8.palindrome-partitioning-ii

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

  • 第一种解法:运行超时!!!
class Solution {
public:
    int minCut(string s) {
        int n=s.length();
        if(n<2) return 0;
        vector<int> dp(n);
        for(int i=0;i<n;i++){
            dp.at(i)=i;
        }
        for(int i=0;i<n;i++){
            //dp[i]表示前缀子串s[0:i]分割成若干个回文子串所需要的最小分割次数
            //单个字符一定是回文数,所以dp[0]=0;
            if(checkPalindrome(s,0,i)){//保证s[0:i]是回文串,才能对后面的进行分割
                dp[i]=0;
                continue;
            }
            //当j==i时,s[i]就一个字符,一定时回文
            //所以枚举到i-1即可
            for(int j=0;j<i;j++){
                if(checkPalindrome(s,j+1,i)){
                    dp[i]=min(dp[i],dp[j]+1);
                }
            }
        }
        return dp[n-1];
    }

    bool checkPalindrome(string s,int left,int right){
        while(left<right){
            if(s[left]!=s[right]) return false;
            left++;
            right--;
        }
        return true;
    }
};
  • 优化:
class Solution {
public:
    int minCut(string s) {
        int n=s.length();
        if(n<2) return 0;
        vector<int> dp(n);
        for(int i=0;i<n;i++){
            dp.at(i)=i;
        }
        //用于记录子串s[a:b]是否为回文串,一开始全部初始化为false
        //用一次遍历,记录所有子串是否为回文串
        vector<vector<bool>> checkPalindrome(n,vector<bool>(n,false));
        for(int right=0;right<n;right++){
            for(int left=0;left<=right;left++){
                if(s[left]==s[right]&&(right-left<=2||checkPalindrome[left+1][right-1])){
                    checkPalindrome[left][right]=true;
                }
            }
        }
        for(int i=0;i<n;i++){
            //dp[i]表示前缀子串s[0:i]分割成若干个回文子串所需要的最小分割次数
            //单个字符一定是回文数,所以dp[0]=0;
            if(checkPalindrome[0][i]){//保证s[0:i]是回文串,才能对后面的进行分割
                dp[i]=0;
                continue;
            }
            //当j==i时,s[i]就一个字符,一定时回文
            //所以枚举到i-1即可
            for(int j=0;j<i;j++){
                if(checkPalindrome[j+1][i]){
                    dp[i]=min(dp[i],dp[j]+1);
                }
            }
        }
        return dp[n-1];
    }
};
9.longest-increasing-subsequence

给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入: [10,9,2,5,3,7,101,18]
输出: 4
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n) 吗?

  • 第一种解法:时间复杂度为O(n^2)
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n=nums.size();
        if(n==0) {
            return 0;
        }
        vector<int> dp(n,1);
        for(int i=0;i<n;i++){
            //j遍历i之前的所有数
            for(int j=0;j<i;j++){
                //若nums[j]<nums[i]则nums[i]更新为max(dp[i],dp[j]+1)
                if(nums[j]<nums[i]){
                    dp[i]=max(dp[i],dp[j]+1);
                }
            }
        }
        return *max_element(dp.begin(),dp.end());
    }
};
  • 第二种解法:贪心+二分查找(时间复杂度为O(nlong),空间复杂度为O(n))
    整个算法流程为:
    设当前已求出的最长上升子序列的长度为len(初始时为 1),从前往后遍历数组 nums,在遍历到nums[i] 时:
    如果nums[i] >d[len] ,则直接加入到 d 数组末尾,并更新len=len+1;
    否则,在 d 数组中二分查找,找到第一个比nums[i] 小的数 d[k] ,并更新d[k+1]=nums[i]。
    以输入序列[0,8,4,12,2] 为例:
    第一步插入 0,d=[0];
    第二步插入 8,d=[0,8];
    第三步插入 4,d=[0,4];
    第四步插入 12,d=[0,4,12];
    第五步插入 2,d=[0,2,12]。
    最终得到最大递增子序列长度为 3。
    来源:力扣(LeetCode)
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n=nums.size();
        int len=1;
        if(n==0) {
            return 0;
        }
        vector<int> d(n+1,0);
        d[len]=nums[0];
        for(int i=1;i<n;i++){
            if(nums[i]>d[len]) d[++len]=nums[i];
            else{//二分查找
                int left=0,right=len,pos=0;
                // 如果找不到说明所有的数都比nums[i]大,此时要更新 d[1],所以这里将pos设为0
                while(left<=right){
                    int mid=(left+right)>>1;
                    if(d[mid]<nums[i]){
                        pos=mid;
                        left=mid+1;
                    }else right=mid-1;
                }
                d[pos+1]=nums[i];
            }
        }
        return len;
    }
};
10.word-break

给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:拆分时可以重复使用字典中的单词。你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
示例 3:
输入: s = “catsandog”, wordDict = [“cats”, “dog”, “sand”, “and”, “cat”]
输出: false

class Solution {
public:
    bool wordBreak(string s, vector<string>& wordDict) {
        auto wordDictSet=unordered_set<string>();
        for(auto word:wordDict){
            wordDictSet.insert(word);
        }
        vector<bool> dp(s.size()+1);
        dp[0]=true;
        for(int i=1;i<=s.size();i++){
            for(int j=0;j<i;j++){
                if(dp[j]&&(wordDictSet.find(s.substr(j,i-j))!=wordDictSet.end()))
                {
                    dp[i]=true;
                    break;
                }
            }
        }
        return dp[s.size()];
    }
};
11.longest-common-subsequence

给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。
一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。
例如,“ace” 是 “abcde” 的子序列,但 “aec” 不是 “abcde” 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。
若这两个字符串没有公共子序列,则返回 0。

  • 解法一:空间复杂度为O(n^2)
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int row = text1.length();
        int col = text2.length();
        if (row == 0 || col == 0) return 0;
        vector<vector<int>> dp(row+1,vector<int>(col+1,0));
        for(int i=1;i<=row;i++){
            for(int j=1;j<=col;j++){
                if(text1[i-1]==text2[j-1]) dp[i][j]=dp[i-1][j-1]+1;
                else dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
            }
        }
        return dp[row][col];
    }
};
  • 解法二:空间复杂度为O(n)
    因为状态转移是O(i)(j)=O(i-1)(j-1)+1,所以要记得保存O(i-1)(j-1)的值
class Solution {
public:
    int longestCommonSubsequence(string text1, string text2) {
        int row = text1.length();
        int col = text2.length();
        if (row == 0 || col == 0) return 0;
        //让索引为0的表示空串
        //所以下面的遍历中i和j分别要遍历到row和col
        vector<int> dp(col + 1, 0);
        int i = 1, j = 1;//使用两个指针分别遍历数组
        int last = 0;//因为只用了O(n)的空间,last记录dp[i-1][j-1]未更新之前的值
        for (i = 1; i <= row; i++,last=0) {//还要注意,每一次计算每一行的第一个元素的时候last需要初始化
            for (j = 1; j <= col; j++) {
                int t = dp[j];
                if (text1[i-1] == text2[j-1]) dp[j] = last + 1;//这里若仍为dp[j]=dp[j-1]+1则会出错,因为dp[j+1]已经被更新
                else {
                    dp[j] = max(dp[j], dp[j-1]);
                }
                last = t;
            }
         }
        return dp[col];
    }
};
12.edit-distance

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

解题思路:dp[i][j]代表 word1 到 i 位置转换成 word2 到 j 位置需要最少步数
所以,当 word1[i] == word2[j],dp[i][j] = dp[i-1][j-1];
当 word1[i] != word2[j],dp[i][j] = min(dp[i-1][j-1], dp[i-1][j], dp[i][j-1]) + 1
其中,dp[i-1][j-1] 表示替换操作,dp[i-1][j] 表示删除操作,dp[i][j-1] 表示插入操作。
针对第一行,第一列要单独考虑,我们引入 ‘’ 下图所示
在这里插入图片描述
第一行,是 word1 为空变成 word2 最少步数,就是插入操作
第一列,是 word2 为空,需要的最少步数,就是删除操作
作者:powcai 力扣(LeetCode)
链接:https://leetcode-cn.com/problems/edit-distance/solution/zi-di-xiang-shang-he-zi-ding-xiang-xia-by-powcai-3/

class Solution {
public:
    int minDistance(string word1, string word2) {
        int row=word1.length();
        int col=word2.length();
        if(row*col==0) return row+col;
        vector<vector<int>> dp(row+1,vector<int>(col+1,0));
        //处理第一行和第一列
        for(int i=1;i<=row;i++){
            dp[i][0]=i;
        }
        for(int j=1;j<=col;j++){
            dp[0][j]=j;
        }
        for(int i=1;i<=row;i++){
            for(int j=1;j<=col;j++){
                if(word1[i-1]==word2[j-1]) dp[i][j]=dp[i-1][j-1];
                else dp[i][j]=min(dp[i-1][j-1],min(dp[i-1][j],dp[i][j-1]))+1;
            }
        }
        return dp[row][col];
    }
};
13.coin-change

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
说明:
你可以认为每种硬币的数量是无限的。
在这里插入图片描述

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        int Max=amount+1;
        //自下而上动态规划
        //F(i)=minF(i-cj)+1;cj代表第j枚硬币的面值
        vector<int> dp(amount+1,Max);
        dp[0]=0;
        for(int i=1;i<=amount;i++){
            for(int j=1;j<coins.size();j++){
                if(coins[j]<=i){
                    dp[i]=min(dp[i],dp[i-coins[j]]+1);
                }
            }
        }
        return dp[amount]>amount?-1:dp[amount];
    }
};
14.coin-change

在n个物品中挑选若干物品装入背包,最多能装多满?假设背包的大小为m,每个物品的大小为A[i]
示例1:输入: [3,4,8,5], backpack size=10 输出: 9
示例2:输入: [2,3,5,7], backpack size=12 输出: 12
挑战
O(n x m) 的时间复杂度 and O(m) 空间复杂度,如果不知道如何优化空间O(n x m) 的空间复杂度也可以通过.

class Solution {
public:
    int backPack(int m, vector<int>& A) {
        int n = A.size();
        vector<int> dp(m+1, 0);
        //自顶向下
        for (int i = 1; i <= n; i++) {
            for (int j = m; j>=A[i-1] ; j--) {
                    dp[j] = max(dp[j], dp[j-A[i-1]]+ A[i-1]); 
            }
        }
        return dp[m];
    }
};
15.coin-change-ii

有 n 个物品和一个大小为 m 的背包. 给定数组 A 表示每个物品的大小和数组 V 表示每个物品的价值.问最多能装入背包的总价值是多大?
示例1:
输入: m = 10, A = [2, 3, 5, 7], V = [1, 5, 2, 4]
输出: 9
解释: 装入 A[1] 和 A[3] 可以得到最大价值, V[1] + V[3] = 9
示例2:
输入: m = 10, A = [2, 3, 8], V = [2, 5, 8]
输出: 10
解释: 装入 A[0] 和 A[2] 可以得到最大价值, V[0] + V[2] = 10
挑战:O(nm) 空间复杂度可以通过, 不过你可以尝试 O(m) 空间复杂度吗?
注意事项:
A[i], V[i], n, m 均为整数
你不能将物品进行切分;你所挑选的要装入背包的物品的总大小不能超过 m;每个物品只能取一次.

在这里插入代码片
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值