LeetCode 初级动态规划问题
“动态规划”的步骤
- “动态规划”的两个步骤思考:“状态”以及“状态转移方程”。
- 有的资料又将“动态规划”分为 3 步:
base case
:思考问题规模最小的时候,是什么情况;update function
:自下而上思考这个问题,即上面的“状态转移方程”;goal
:重点强调了输出是什么,很多时候输出并不一定是最后一个状态。
分治法的精髓:
- Ref:分治法
- 分治法的精髓在于:
分
:将问题分解为规模更小的子问题;治
:将这些规模更小的子问题逐个击破;合
:将已解决的子问题合并,最终得出“母”问题的解;
- 分治法适用于:
- 该问题的规模缩小到一定的程度就可以容易地解决
- 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
- 利用该问题分解出的子问题的解可以合并为该问题的解;
- 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
基本思想与策略编辑:
- 由于动态规划解决的问题多数有重叠子问题这个特点,为减少重复计算,对每一个子问题只解一次,将其不同阶段的不同状态保存在一个二维数组中。(来自百度百科)
- 说实话,没有动态规划的基础很难看懂,但是也能从中看出一些信息,下面我翻译成人话:
- 首先是拆分问题,我的理解就是根据问题的可能性把问题划分成一步一步这样就可以通过递推或者递归来实现.
- 关键就是这个步骤,动态规划有一类问题就是从后往前推到,有时候我们很容易知道:
- 如果只有一种情况时,最佳的选择应该怎么做.然后根据这个最佳选择往前一步推导,得到前一步的最佳选择
- 然后就是定义问题状态和状态之间的关系,
- 我的理解是前面拆分的步骤之间的关系,用一种量化的形式表现出来,类似于高中学的推导公式,因为这种式子很容易用程序写出来,也可以说对程序比较亲和(也就是最后所说的状态转移方程式)
- 我们再来看定义的下面的两段,
- 我的理解是比如我们找到最优解,我们应该讲最优解保存下来,为了往前推导时能够使用前一步的最优解,在这个过程中难免有一些相比于最优解差的解,此时我们应该放弃,只保存最优解,这样我们每一次都把最优解保存了下来,大大降低了时间复杂度
爬楼梯 T:2019/10/24 W:0 12:50:21
- 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
- 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
- 注意:给定 n 是一个正整数。
- 示例
- 示例 1:
- 输入: 2
- 输出: 2
- 解释: 有两种方法可以爬到楼顶。
- 1 阶 + 1 阶
- 2 阶
- 示例 2:
- 输入: 3
- 输出: 3
- 解释: 有三种方法可以爬到楼顶。
- 1 阶 + 1 阶 + 1 阶
- 1 阶 + 2 阶
- 2 阶 + 1 阶
- 示例 1:
解
- 方法一: 暴力法
- 思路: 递归
- 当在第
i
步时,可以有两种方案:- 走一步到
i+1
- 走两步到
i+2
- 走一步到
- 递归函数:
- 输入:
- 下限:
int i
- 上限:
int n
- 下限:
- 逻辑
- 终止条件:
i == n; return 1
i > n; return 0;
- 返回:
递归函数(i+1)+递归函数(i+2)
- 终止条件:
- 输入:
- 当在第
- Code
class Solution { public: int climbStairs(int n) { if(n==1) return 1; return violence(0, n); } int violence(int i, int n){ if(i==n){return 1;} if(i>n){return 0;} return violence(i+1, n)+violence(i+2, n); } };
- 思路: 递归
- 方法二: 暴力+去重复
- 思路:
- 有如下暴力法递归如下:可以发现存在大量的重复计算,可以使用一个
mem
来记录,如果在mem
中存在则不用计算了.
- 有如下暴力法递归如下:可以发现存在大量的重复计算,可以使用一个
- 思路:
- Code
class Solution { public: vector<int> mem; int climbStairs(int n) { vector<int>(n+1).swap(mem); if(n==1) return 1; return violence(0, n); } int violence(int i, int n){ if(i==n){return 1;} if(i>n){return 0;} if(mem[i]>0){return mem[i];} mem[i]= violence(i+1, n)+violence(i+2, n); return mem[i]; } };
- 方法三: 动态规划问题
- 思路:
- 不难发现,这个问题可以被分解为一些包含最优子结构的子问题,即它的最优解可以从其子问题的最优解来有效地构建,我们可以使用
动态规划
来解决这一问题。 - 第 ii 阶可以由以下两种方法得到:
- 在第
(i-1)
阶后向上爬一阶。 - 在第
(i-2)
阶后向上爬二阶。
- 在第
- 所以到达第 ii 阶的方法总数就是到第
(i-1)
阶和第(i-2)
阶的方法数之和。 - 令
dp[i]
表示能到达第i
阶的方法总数:dp[i]=dp[i-1]+dp[i-2]
- 示例:
- 不难发现,这个问题可以被分解为一些包含最优子结构的子问题,即它的最优解可以从其子问题的最优解来有效地构建,我们可以使用
- 思路:
- Code
int dynamic_planning(int n){ if(n==1)return 1; vector<int>(n+1).swap(mem); mem[0] = 0; mem[1] = 1; mem[2] = 2; for(int i=3; i<=n;++i){ mem[i]=mem[i-1]+mem[i-2]; } return mem.back(); }
买卖股票的最佳时机 T:2019/10/24 W:0 12:50:21
- 给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。
- 如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。
- 注意你不能在买入股票前卖出股票。
- 示例
- 示例 1:
- 输入: [7,1,5,3,6,4]
- 输出: 5
- 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。
- 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。
- 示例 2:
- 输入: [7,6,4,3,1]
- 输出: 0
- 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。
- 示例 1:
解
- 方法一:暴力求解(略)
- 方法二:动态规划
- 子问题分解:
- 求
min
和max
- 求
- 子问题迭代:更新
min
和max
- Code
class Solution { public: int maxProfit(vector<int>& prices) { int minpri=INT_MAX, maxprofit=0; for(auto n:prices){ if(minpri>n){ minpri = n; } if(n-minpri > maxprofit){ maxprofit = n-minpri; } } return maxprofit; } };
- 子问题分解:
最大子序和 T:2019/10/25 W:5 10:19:17
- 给定一个整数数组
nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 - 示例:
- 输入:
[-2,1,-3,4,-1,2,1,-5,4]
, - 输出:
6
- 解释: 连续子数组
[4,-1,2,1]
的和最大,为6
。
- 输入:
- 进阶:
- 如果你已经实现复杂度为
O(n)
的解法,尝试使用更为精妙的分治法求解。
- 如果你已经实现复杂度为
解
- Ref
- 方法一: 动态规划
- 思路:
- 参照动态规划的步骤:
- 我们分别三个量:
状态
,状态转移
,输出
状态
:dp[i]
- 最方便的情况肯定是定义为
[0, i]
连续数组的最大和, - 但是考虑一下以上定义是很难进行更新的,因为
[0, i]
连续数组的最大和对应的数组的尾数不一定就在末尾,那要如何进行更新呢?
这是很难的!
- 所以我们定义为
[0, i]连续数组上一nums[i]结尾的最大和
- 最方便的情况肯定是定义为
状态转移
:if(dp[i]>0)
dp[i] = dp[i-1] + nums[i]
else
dp[i] = nums[i]
输出
:[0,n]
连续数组最大和, 其为dp[]
中的最大值嘛
- 我们分别三个量:
- Code
class Solution { public: int maxSubArray(vector<int>& nums) { long sum=0, result=INT_MIN; for(auto i:nums){ if(sum>0){ sum+=i; }else{ sum=i; } result = max(result, sum); } return result; } };
- 方法二: 分治法
- 思路:
- 分治法:按照分治法的三个重点来分析:
分
,治
,合
分
:将nums
数组从mid
进行分开[left, mid]
和[mid, right]
治
:if(right-left==1){return nums[left];}
合
:- 左子段和右子段需要合并只有一种情况:
- 左子段以
nums[mid-1]
(取不到mid
)为尾的最大合 - 右子段以
nums[mid]
为始的最大合 - 合并情况的最大合为以上两个相加
- 左子段以
- 左子段和右子段需要合并只有一种情况:
- Code
int divide_conquer(vector<int>& nums, int left, int right){ if(right-left==1){return nums[left];} int mid = left + floor((right-left)/2); // 左右分治结果 int left_s = divide_conquer(nums, left, mid); int right_s = divide_conquer(nums, mid, right); // 中间结果 // (1)以nums[mid-1]为结尾的最大和 long sum=0, lmax=INT_MIN, rmax=INT_MIN; for(int i=mid-1; i>=left; --i){ sum+=nums[i]; lmax=max(lmax, sum); } // (2)以nums[mid]为开始的最大和 sum=0; for(int i=mid; i<right; ++i){ sum+=nums[i]; rmax=max(rmax, sum); } int tmp = max(left_s, right_s); tmp = max(tmp, int(lmax+rmax)); return tmp; }
打家劫舍 T:2019/10/25 W:5 13:41:9
- 你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
- 给定一个代表每个房屋存放金额的非负整数数组,计算你在不触动警报装置的情况下,能够偷窃到的最高金额。
- 示例
- 示例 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 。
- 示例 1:
解
- 方法一:动态规划
- 思路:
- 参照动态规划的步骤: 三个量:
状态
,状态转移
,输出
状态
:dps[i]
保存的是偷到i
房间的收获;dp[0]
和dp[1]
需要单独计算mps
保存的是从0 ~ i-2
的最大收获
状态转移
:dps[i]=mps+nums[i]
输出
:max(dps)
- 参照动态规划的步骤: 三个量:
- 思路:
- Code
class Solution { public: int rob(vector<int>& nums) { if(nums.empty()){return 0;} if(nums.size()==1){return nums[0];} vector<long> dps(nums.size(), 0); dps[0] = nums[0]; dps[1] = nums[1]; long profit=max(dps[0], dps[1]); long mps=dps[0]; for(int i=2; i<nums.size(); ++i){ if(dps[i-2]>mps){mps=dps[i-2];} dps[i]=mps+nums[i]; profit = max(profit, dps[i]); } return profit; } };
来源:https://blog.csdn.net/zzxiaozhao/article/details/102699557