leetcode 经典动态规划DP算法题目(思路、方法、code)

leetcode 经典动态规划DP算法题目(思路、方法、code)

动态规划最重要的在于设计DP数组,找到相应的动态转移方程

文章目录

      • leetcode 经典动态规划DP算法题目(思路、方法、code)
        • [70. 爬楼梯](https://leetcode-cn.com/problems/climbing-stairs/)
        • [198. 打家劫舍](https://leetcode-cn.com/problems/house-robber/)
        • [213. 打家劫舍 II](https://leetcode-cn.com/problems/house-robber-ii/)
        • [337. 打家劫舍 III](https://leetcode-cn.com/problems/house-robber-iii/)
        • [53. 最大子序和](https://leetcode-cn.com/problems/maximum-subarray/)
        • [322. 零钱兑换](https://leetcode-cn.com/problems/coin-change/)
        • [120. 三角形最小路径和](https://leetcode-cn.com/problems/triangle/)
        • [300. 最长上升子序列](https://leetcode-cn.com/problems/longest-increasing-subsequence/)
        • [64. 最小路径和](https://leetcode-cn.com/problems/minimum-path-sum/)
        • [174. 地下城游戏](https://leetcode-cn.com/problems/dungeon-game/)
        • [32. 最长有效括号](https://leetcode-cn.com/problems/longest-valid-parentheses/)
        • [152. 乘积最大子数组](https://leetcode-cn.com/problems/maximum-product-subarray/)
        • [62. 不同路径](https://leetcode-cn.com/problems/unique-paths/)
        • [63. 不同路径 II](https://leetcode-cn.com/problems/unique-paths-ii/)

70. 爬楼梯

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?

**注意:**给定 n 是一个正整数。

分析:这个问题,可以看青蛙跳台阶(递归与数学归纳),实际上就是斐波那契数列的变体

在上述中,采用了递归或者正向遍历的方法,实际上动态规划就是一种正向遍历的方法。如果采用递归时,由于f(n)=f(n-1)+f(n-2),故直接递归即可,但是仔细思考,如果用该方法,计算f(n-1)时计算了f(n-2),而计算f(n)时又计算了f(n-2),因此存在大量的重复计算。故在此便考虑用一个数组存储对应的函数值,这便是动态规划思想。

令f(n)表示到达n阶的楼顶的方法数,则f(n)=f(n-1)+f(n-2),正向计算即可。

class Solution {
public:
    int climbStairs(int n) 
    {
         vector<int> dp(n+1);
         dp[0]=1;dp[1]=1;
         for(int i=2;i<=n;i++)
            dp[i]=dp[i-1]+dp[i-2];
        return dp[n];
    }
};

198. 打家劫舍

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:
输入: [1,2,3,1]
输出: 4
解释: 偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:
输入: [2,7,9,3,1]
输出: 12
解释: 偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12

分析:由于相邻的房间不能同时选择,因此在第k个房间时,有两种情况

  • 如果选取了第k个房间,说明第k-1个房间一定不能选,那说明这种情况的最大金额应该是前k-2个房间的最大金额加上第k个房间的金额
  • 如果没有选取第k个房间,说明这种情况的最大金额实际上就是前k-1个房间的最大金额
  • 因此,如果令 d p [ i ] dp[i] dp[i] 表示从0 到 $i $ 房间能盗窃的最高金额,动态转移方程则为 d p [ i ] = m a x ( d p [ i − 2 ] + a [ i ] , d p [ i − 1 ] ) dp[i]=max(dp[i-2]+a[i],dp[i-1]) dp[i]=max(dp[i2]+a[i],dp[i1]) ,其中 a [ i ] a[i] a[i]表示第i个房间的金额

根据此,可以遍历,最终结果应该为 d p [ n − 1 ] dp[n-1] dp[n1]

class Solution {
public:
    int rob(vector<int>& nums) 
    {
        int length=nums.size();
        if(length==0) return 0;
        if(length==1) return nums[0];
        vector<int> dp(length+1);
        dp[0]=nums[0];
        dp[1]=max(nums[0],nums[1]);
        for(int i=2;i<length;i++)
        {
            dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
        }
        return dp[length-1];
    }
};

213. 打家劫舍 II

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都围成一圈,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。

给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。

示例 1:
输入: [2,3,2]
输出: 3
解释: 你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2, 因为他们是相邻的。

示例 2:
输入: [1,2,3,1]
输出: 4
解释: 你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4

分析:这个题与上一个题的区别在于,房屋是环形的,这意味着,第一个房间和最后一个房间不能同时偷窃。这就意味着,**将原来的问题转化为两个子问题:偷窃1 ~ n-1个房间的最大值以及偷窃 2~n 个房间的最大值。最终,取二者的最大值即为解。**因此可以对上一个问题进行两次dp即可。

class Solution {
public:
    int rob(vector<int>& nums) 
    {
        int length=nums.size();
        if(length==0) return 0;
        if(length==1) return nums[0];
        vector<int> dp(length+1);
        dp[0]=nums[0];
        dp[1]=max(nums[0],nums[1]);
        for(int i=2;i<length-1;i++) //先对前n-1进行DP
        {
            dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
        }
        int result=dp[length-2]; //存储其值
        dp[0]=0;dp[1]=nums[1];
        for(int i=2;i<length;i++)  //对后n-1进行DP
        {
            dp[i]=max(dp[i-2]+nums[i],dp[i-1]);
        }
        //取最大值即可
        result=max(result,dp[length-1]);
        return result;
    }
};

337. 打家劫舍 III

在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。

计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。

示例 1:
输入: [3,2,3,null,3,null,1]

     3
    / \
   2   3
    \   \ 
     3   1

输出: 7 
解释: 小偷一晚能够盗取的最高金额 = 3 + 3 + 1 = 7.

示例 2:
输入: [3,4,5,1,3,null,1]

     3
    / \
   4   5
  / \   \ 
 1   3   1

输出: 9
解释: 小偷一晚能够盗取的最高金额 = 4 + 5 = 9.

分析:每个节点可选择偷或者不偷两种状态,根据题目意思,相连节点不能一起偷,因此父结点如果偷,则两个字节点就不能偷,父结点如果不偷,则子节点可以偷。注意这里说的是可以而不是一定,只需要父结点不偷的情况下两个子节点能够拿出最多的钱即可。因此有了递归的想法。

  • 当前节点选择不偷:当前节点能偷到的最大钱数 = 左孩子能偷到的最大钱 + 右孩子能偷到的最大钱
  • 当前节点选择偷:当前节点能偷到的最大钱数 = 左孩子选择自己不偷时能得到的钱 + 右孩子选择不偷时能得到的钱 + 当前节点的钱数
  • 考虑到在python中常用的返回多值的函数,在此处用了pair来存储结果,其中pair的第一个表示当前节点不偷获得的最大钱,pair的第二个表示当前节点偷的话获得的最大钱,根据此,便可以将上述逻辑轻松地表达出来。
class Solution {
public:
    int rob(TreeNode* root) 
	{
		pair<int,int> result=rob_helper(root);
		return max(result.first,result.second);
    }
    pair<int,int> rob_helper(TreeNode* root)
    {
    	pair<int,int> root_result;
    	pair<int,int> root_left;
    	pair<int,int> root_right;
    	if(root==NULL) return root_result;	
    	else
    	{
    		root_left=rob_helper(root->left);
    		root_right=rob_helper(root->right);
		}
			
  root_result.first=max(root_left.first,root_left.second)+max(root_right.first,root_right.second);
		root_result.second=root_left.first+root_right.first+root->val;
		return root_result;
	}
};

53. 最大子序和

给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。

输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6

分析:

动态规划法:动态规划的关键,就是设置dp变量,找到动态转移方程

对于该题而言,由于需要找连续子数组,因此设置 d p dp dp变量时,必须考虑如何设置才能够将连续考虑在内。在此用 d p [ i ] dp[i] dp[i] 表示以第 i i i 个元素结尾的数组的最大和,则 d p [ i ] = m a x ( d p [ i − 1 ] + n u m s [ i ] , n u m s [ i ] ) dp[i]=max(dp[i-1]+nums[i],nums[i]) dp[i]=max(dp[i1]+nums[i],nums[i]) ,因此也可以理解为如果 d p [ i − 1 ] > 0 dp[i-1]>0 dp[i1]>0, 则 d p [ i ] = d p [ i − 1 ] + n u m s [ i ] dp[i]=dp[i-1]+nums[i] dp[i]=dp[i1]+nums[i] ,否则 d p [ i ] = n u m s [ i ] dp[i]=nums[i] dp[i]=nums[i]

class Solution {
public:
    int maxSubArray(vector<int>& nums) 
    {
        int length=nums.size();
        if(length==0) return 0;
        if(length==1) return nums[0];
        vector<int> dp(length);
        dp[0]=nums[0];
        int result=dp[0];
        for(int i=1;i<length;i++)
        {
            dp[i]=dp[i-1]>0?dp[i-1]+nums[i]:nums[i];
            result=max(result,dp[i]);
        }
        return result;

    }
};

贪心法:

贪心的思想在于,每次尽可能地选取数字,但如果已经选取的数字和小于0了,则说明如果加上前面这些数字,只会令后面的连续子数组和变小,故重新开始选取数字。期间记录每次选取的连续子数组的和并更新最大值。

class Solution {
public:
    int maxSubArray(vector<int>& nums)
    {
        int max_sum=INT_MIN;  //初始化
        int cur_sum=0;  //当前选取的数字的和
        for(int i=0;i<nums.size();i++)
        {
            cur_sum+=nums[i];
            max_sum=max(cur_sum,max_sum);
            if(cur_sum<0)  //说明前面的部分应当抛弃
            {
                cur_sum=0;
            }
        }
        return max_sum;
    }
};

322. 零钱兑换

给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。

示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3 
解释: 11 = 5 + 5 + 1
   
示例 2:
输入: coins = [2], amount = 3
输出: -1

分析:首先要理解,贪心会出问题,例如硬币面额为[2,7,10],想要14元,则贪心会10+2+2,但是实际上7+7即可,因此我们可以发现,对于部分面额(日常生活中的1,2,5,10)贪心可行,但是一些情况下是不可行的。

动态规划法设置 d p [ i ] dp[i] dp[i] 表示金额 i 的最优解,则dp[i]是由哪些dp[j] 相关呢?如果面值为[1,2,5],则我们可以发现, dp[i]可以由dp[i-1],dp[i-2],dp[i-5]添加一枚硬币获得。因此,实际上,dp[i]是由任意一个可能添加一枚金币得到 i 面额的dp[i-j] 转移到达。因此可以用两重循环完成dp[i]的遍历,需要注意的是,所给的金钱可能无法组成i,此时应该记为-1.

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) 
    {
        vector<int> dp; //dp[i]表示i面额的最优解
        for(int i=0;i<=amount;i++)
            dp.push_back(-1); //表示还没有遍历到
        dp[0]=0;
        for(int i=1;i<=amount;i++)
        {    for(int j=0;j<coins.size();j++)
            {
                if(i-coins[j]>=0&&dp[i-coins[j]]!=-1) //说明可以组成
                    dp[i]=(dp[i]==-1)?(dp[i-coins[j]]+1):min(dp[i],dp[i-coins[j]]+1);
            }
        }
        return dp[amount];
    }
};

120. 三角形最小路径和

给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。

例如,给定三角形:

[
     [2],
    [3,4],
   [6,5,7],
  [4,1,8,3]
]

自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。

说明:如果你可以只使用 O(n) 的额外空间(n 为三角形的总行数)来解决这个问题,那么你的算法会很加分

分析:直观上,为每一个节点设置一个状态便可以较为轻松地用动态规划解决, d p [ i ] [ j ] dp[i][j] dp[i][j] 表示从起始点到达第i行第j列最小路径和,则 d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i − 1 , j − 1 ] ) dp[i][j]=min(dp[i-1][j],dp[i-1,j-1]) dp[i][j]=min(dp[i1][j],dp[i1,j1]) (考虑自己将三角形画图时左侧对齐,则每个点可以走向自己的下一个节点或者下右方节点),根据此,很容易解决问题。

这里我用到了一个技巧,由于路径节点的空间已经给定,所以设置dp数组时直接用原始数组进行了赋值,这样的拷贝会降低错误率并且更快。(实际上,可以直接在给定数组上进行修改,甚至都不需要再开dp数组,但是由于传入的是引用,为了避免修改原始数组,在此我拷贝了新的数组)

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) 
    {
        vector<vector<int>> dp=triangle;
        if(dp.size()==0) return 0;
        for(int i=1;i<dp.size();i++)
            dp[i][0]=dp[i-1][0]+triangle[i][0]; //将第一列初始化,减少判断时候的繁琐
        for(int i=1;i<dp.size();i++)
            for(int j=1;j<dp[i].size();j++)
                if(j==i) dp[i][j]=dp[i-1][j-1]+triangle[i][j];
                else dp[i][j]=min(dp[i-1][j-1],dp[i-1][j])+triangle[i][j];
        int result=dp[dp.size()-1][0];
        for(int j=1;j<dp.size();j++)
            result=min(result,dp[dp.size()-1][j]);
        return result;
    }
};

仅需要 O ( n ) O(n) O(n) 空间的dp算法解决该问题:

主要思路在于,用dp[i]表示当前的行时,到达第i个位置的最小和,也就是把dp 数组设置为动态更新的。每遍历一行,都会更新 dp数组,这样,在最后一行时,就会得到和上例一样的dp数组,取最小值即可。但是这里注意的是,**更新dp时候是从右向左更新,否则会导致左侧更新影响右侧的值。**可以画图感受一下

可以理解一下这个思路,没有改动原始数组以及并没有增加时间的情况下,将空间进行了压缩

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) 
    {
        if(triangle.size()==0) return 0;
        vector<int> dp(triangle.size());
        dp[0]=triangle[0][0];
        for(int i=1;i<triangle.size();i++)
        for(int j=i;j>=0;j--)  //注意从右侧向左侧更新
        {
            if(j==0) dp[j]=dp[j]+triangle[i][j];  //第一列时候
            else if(j==i) dp[j]=dp[j-1]+triangle[i][j]; //最右侧时候
            else dp[j]=min(dp[j],dp[j-1])+triangle[i][j];
        }
        int result=dp[0];
        for(int i=0;i<dp.size();i++)
            result=min(result,dp[i]);
        return result;
    }
};

300. 最长上升子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。
说明:

可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
你算法的时间复杂度应该为 O(n2) 。
进阶: 你能将算法的时间复杂度降低到 O(n log n)?

分析:

时间复杂度为 O ( n 2 ) O(n^2) O(n2)的方法:

设置DP数组,其中dp[i]表示以第i个元素结尾的最长上升子序列的长度,则思考dp[i]应该如何计算得到?

对于任意的 j < i jj<i ,可知,如果在 j j j 处的元素小于在第i处的元素值,则说明以j结尾的最长子序列,可以接上第i个元素,从而形成新的上升序列。故可知 如果 n u m s [ j ] < n u m s [ i ] nums[j]nums[j]<nums[i],则$dp[i]=max(dp[j]+1,dp[i]) $

基于此,便可以解决问题了。复杂度为 O ( n 2 ) O(n^2) O(n2)

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        if(nums.size()==0) return 0;
        vector<int> dp(nums.size());
        int result=0;
        for(int i=0;i<nums.size();i++)
        {   
            dp[i]=1; 
            for(int j=0;j<i;j++)
                if(nums[j]<nums[i]) 
                    dp[i]=max(dp[i],dp[j]+1);
            result=max(result,dp[i]);
        }    
        return result;
    }
};

时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn) 的方法

思路主要在于对于任意一个固定的子序列长度,为了达到尽可能地使得后面可以继续添加,我们要尽可能使末尾元素最小。设置dp[i] ,表示长度为i的子序列结尾的最小值。因此,遍历一遍数组,便可以更新完dp[i],最终最后一个dp[i]的 i i i 就是结果。

但是这样实际上仍旧是一个时间复杂度为O(n)的方法,只是每次在遍历新元素时候,比较的对象从该元素前面的元素变成了dp数组的元素。

在此有一个数学可证的内容,即dp数组是一个升序的,因为如果插入的新元素比较小,它一定会把dp数组中小于它的元素都更新了。 因为dp数组是增序的,所以可以采用二分查找,从而将比较的O(n)缩减为O(logn)

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) 
    {
        if(nums.size()==0)
            return 0;
        vector<int> dp;
        dp.push_back(nums[0]);
        for(int i=1;i<nums.size();i++)  //从左到右遍历
        {
            if(nums[i]>dp.back())  //说明可以组成更长的字串
                dp.push_back(nums[i]);
            else
            {
                //pos是找到第一个大于nums[i]的位置,
                int pos=lower_bound(dp.begin(),dp.end(),nums[i])-dp.begin();
                dp[pos]=nums[i];
            }
        }
        return dp.size(); //返回dp的长度即可
    }
};

64. 最小路径和

给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。

**说明:**每次只能向下或者向右移动一步。

示例:

输入:
[
  [1,3,1],
  [1,5,1],
  [4,2,1]
]
输出: 7
解释: 因为路径 13111 的总和最小。

分析:如果设 d p [ i ] [ j ] dp[i][j] dp[i][j]表示到达 [ i ] [ j ] [i][j] [i][j]位置的最小和,由于只能向下或者向右,因此DP方程很容易推出:

d p [ i ] [ j ] = m i n ( d p [ i − 1 ] [ j ] , d p [ i ] [ j − 1 ] ) + g r i d [ i ] [ j ] dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j] dp[i][j]=min(dp[i1][j],dp[i][j1])+grid[i][j] ,基于此进行遍历更新即可,最后返回最尾部的元素

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) 
    {
        if(grid.size()==0) return 0;
        vector<vector<int>> dp=grid;
        for(int i=1;i<dp[0].size();i++)
            dp[0][i]=dp[0][i-1]+grid[0][i]; //第一行初始化
        for(int i=1;i<dp.size();i++)
            dp[i][0]=dp[i-1][0]+grid[i][0]; //第一列初始化
        
        for(int i=1;i<dp.size();i++)  //将其他区域通过DP方程赋值
        for(int j=1;j<dp[0].size();j++)
        {
            dp[i][j]=min(dp[i-1][j],dp[i][j-1])+grid[i][j]; 
        }
        return dp.back().back();  //返回最后一个元素
    }
};

174. 地下城游戏

一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。

骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。

有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。

为了尽快到达公主,骑士决定每次只向右或向下移动一步。

编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。

例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。

leetcode 经典动态规划DP算法题目(思路、方法、code)_第1张图片

说明:

骑士的健康点数没有上限。

任何房间都可能对骑士的健康点数造成威胁,也可能增加骑士的健康点数,包括骑士进入的左上角房间以及公主被监禁的右下角房间。

分析:这个题属于hard的主要问题在于,按照之前的常规思路考虑,即从左上到右下,我们会发现,当前的血量与最低初始健康点数没有直接关联。

按照从右下到左上的方式,让dp数组表示: d p [ i ] [ j ] dp[i][j] dp[i][j] 表示从 [ i ] [ j ] [i][j] [i][j] 位置到终点所需要的最低健康血量。则根据该定义,就可以找到动态转移方程: d p [ i ] [ j ] = m a x { 1 , m i n { d p [ i + 1 ] [ j ] , d p [ i ] [ j + 1 ] } − d u n g e o n [ i ] [ j ] } dp[i][j]=max\{1,min\{dp[i+1][j],dp[i][j+1]\}-dungeon[i][j]\} dp[i][j]=max{1,min{dp[i+1][j],dp[i][j+1]}dungeon[i][j]}

然后从右下到左上更新dp数组, d p [ 0 ] [ 0 ] dp[0][0] dp[0][0]就是解

class Solution {
public:
    int calculateMinimumHP(vector< vector<int> >& dungeon) 
	{
		 if(dungeon.size()==0) return 0; 
		 vector< vector<int> > dp=dungeon;
		 int row=dungeon.size(),col=dungeon[0].size();
		 //先把最右和最下进行初始化
		 dp[row-1][col-1]=max(1,1-dungeon[row-1][col-1]); 
		 for(int i=col-2;i>=0;i--)
		 	dp[row-1][i]=max(1,dp[row-1][i+1]-dungeon[row-1][i]);
		 for(int i=row-2;i>=0;i--)
		 	dp[i][col-1]=max(1,dp[i+1][col-1]-dungeon[i][col-1]);
		//根据动态转移方程对DP数组进行更新 
        for(int i=row-2;i>=0;i--)
		 	for(int j=col-2;j>=0;j--)
		 	{
		 		int dp_min=min(dp[i+1][j],dp[i][j+1]);
		 		dp[i][j]=max(1,dp_min-dungeon[i][j]);
			 }
		return dp[0][0];		 
    }
};

32. 最长有效括号

给定一个只包含 ‘(’ 和 ‘)’ 的字符串,找出最长的包含有效括号的子串的长度。

示例 1:
输入: "(()"
输出: 2
解释: 最长有效括号子串为 "()"

示例 2:
输入: ")()())"
输出: 4
解释: 最长有效括号子串为 "()()"

分析:这个题目属于DP问题中非常复杂的

采用动态规划 令dp[i]表示以该字符起始,最远到字符串结尾的最长的有效括号字串长度。在这里从后向前进行处理(从前向后也可以的)

  • 设当前的坐标为index
  • 如果当前的位置是 ‘)’ ,则说明以该字符为起始的字串必定非法,因此dp[index]=0
  • 如果当前的位置是’(’,则需要判断
    • 如果右侧相邻处即str[index+1]是一个右括号,则 d p [ i n d e x ] = d p [ i n d e x + 2 ] + 2 dp[index]=dp[index+2]+2 dp[index]=dp[index+2]+2
    • 如果右侧相邻处是一个左括号,则需要找一下右边是否还有右括号,即dp[index+1]所指的字串的结尾处是否是‘)’,如果不是的话说明没办法将index处的字符与后面的字符连起来,故de[index]=0;但是如果那个位置有右括号,就说明至少是dp[index+1]+2,并且这种情况还要考虑,这样会将刚刚加上去的右括号右侧的有效括号字串能够连起来,也就是将index+1+dp[index+1]+1处的合法字符连了起来,即再加上 d p [ i n d e x + 1 + d p [ i n d e x + 1 ] + 1 ] dp[index+1+dp[index+1]+1] dp[index+1+dp[index+1]+1]
class Solution {
public:
    int longestValidParentheses(string s) 
    {
        int len=s.length();
        vector<int> dp(len+1);
        for(int i=len-1;i>=0;i--)
        {
            int sy=i+1+dp[i+1];  //找到i+1+dp[i+1]的位置
            if(s[i]=='('&&s[i+1]==')')  //相邻为右括号则与其组成合法
                dp[i]=2+dp[i+2];
            else if(s[i]=='('&&s[i+1]=='('&&s[sy]==')')
            {
                dp[i]=dp[i+1]+2;
                if(i+1+dp[i+1]+1<=len-1)
                    dp[i]+=dp[i+1+dp[i+1]+1];     
            }   
        }
        int ans=0;
        for(int i=0;i<len;i++)
            ans=max(ans,dp[i]);
        return ans;
    }
};

152. 乘积最大子数组

给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积。

示例 1:
输入: [2,3,-2,4]
输出: 6
解释: 子数组 [2,3] 有最大乘积 6。

示例 2:
输入: [-2,0,-1]
输出: 0
解释: 结果不能为 2, 因为 [-2,-1] 不是子数组。

分析:该题的难点在于,由于存在负数元素,因此如果只是每一步选取最大值的话,会将负数忽略,但是两个负数相乘会产生正数,故在此我的解决办法是开设两个数组,MAX和MIN。分别存储从数组头开始,包含当前元素的乘积最大的连续子数组。每次在遇到正数负数时要分开考虑:

  • 如果当前存储的是正数,则 M A X [ i ] = m a x { M A X [ i − 1 ] , 1 } ∗ n u m s [ i ] MAX[i]=max\{MAX[i-1],1\}*nums[i] MAX[i]=max{MAX[i1],1}nums[i]

    M I N [ i ] = m i n { M I N [ i − 1 ] , 1 } ∗ n u m s [ i ] MIN[i]=min\{MIN[i-1],1\}*nums[i] MIN[i]=min{MIN[i1],1}nums[i]

  • 如果当前存储的是负数,则 M A X [ i ] = m i n { M I N [ i − 1 ] , 1 } ∗ n u m s [ i ] ; MAX[i]=min\{MIN[i-1],1\}*nums[i]; MAX[i]=min{MIN[i1],1}nums[i];

    M I N [ i ] = m a x { M A X [ i − 1 ] , 1 } ∗ n u m s [ i ] MIN[i]=max\{MAX[i-1],1\}*nums[i] MIN[i]=max{MAX[i1],1}nums[i]

class Solution {
public:
    int maxProduct(vector<int>& nums) 
    {
        vector<int> MAX(nums.size());
        vector<int> MIN(nums.size());
        MAX[0]=MIN[0]=nums[0];
        for(int i=1;i<nums.size();i++)
        {
            if(nums[i]>=0)
            {
                MAX[i]=max(MAX[i-1],1)*nums[i];
                MIN[i]=min(MIN[i-1],1)*nums[i];
            }
            else
            {
                MAX[i]=min(MIN[i-1],1)*nums[i];
                MIN[i]=max(MAX[i-1],1)*nums[i];
            }
        }
        int result=MAX[0];
        for(int i=0;i<nums.size();i++)
            result=max(result,MAX[i]);
        return result;
    }
};

62. 不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。

leetcode 经典动态规划DP算法题目(思路、方法、code)_第2张图片
问总共有多少条不同的路径?

示例 1:

输入: m = 3, n = 2
输出: 3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向右 -> 向下
2. 向右 -> 向下 -> 向右
3. 向下 -> 向右 -> 向右

分析:这是一个典型的排列组合问题,实际上问题的答案就是 C m n C_m^n Cmn

现用另一种思路,动态规划求解

每一个位置只可以从上或者左到达,故如果用 d p [ i ] [ j ] dp[i][j] dp[i][j]表示到达 [ i ] [ j ] [i][j] [i][j]处的路径,则到达该位置的路径数量等于到达上方的数量加上到达左方的路径数量, d p [ i ] [ j ] = d p [ i − 1 ] [ j ] + d p [ i ] [ j − 1 ] dp[i][j]=dp[i-1][j]+dp[i][j-1] dp[i][j]=dp[i1][j]+dp[i][j1]

class Solution {
public:
    int uniquePaths(int m, int n) 
    {
        int dp[n][m];
        for(int i=0;i<m;i++)
            dp[0][i]=1;
        for(int i=1;i<n;i++)
            dp[i][0]=1;
        for(int i=1;i<n;i++)
        for(int j=1;j<m;j++)
            dp[i][j]=dp[i-1][j]+dp[i][j-1];
        return dp[n-1][m-1];    
    }
};

63. 不同路径 II

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

示例 1:
输入:
[
  [0,0,0],
  [0,1,0],
  [0,0,0]
]
输出: 2
解释:
3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

分析:在上一题基础上,增加了障碍物。实际上,是类似的,如果一个位置的左侧有障碍物,则它只能由上方到达,如果一个位置的上方有障碍物,则它只能由左侧到达。如果上方左方没有障碍物,则可以从上方或者左方到达。路径的方法传递类似。

class Solution {
public:
    int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) 
    {
        if(obstacleGrid.size()==0) return 0;
        int row=obstacleGrid.size();
        int col=obstacleGrid[0].size();
        int dp[row][col];
        for(int i=0;i<row;i++)
        for(int j=0;j<col;j++)
            dp[i][j]=0;
        dp[0][0]=1;
        for(int i=0;i<row;i++)
            for(int j=0;j<col;j++)
            {
                if(i-1>=0&&obstacleGrid[i-1][j]!=1) dp[i][j]+=dp[i-1][j];
                if(j-1>=0&&obstacleGrid[i][j-1]!=1) dp[i][j]+=dp[i][j-1];
                if(obstacleGrid[i][j]==1) dp[i][j]=0;
            }
        return dp[row-1][col-1];
    }
};

你可能感兴趣的:(数据结构与算法)