动态规划概述
动态规划是运筹学的一个分支,是求解决策过程最优化的数学方法。它是一种最优化原理,利用各阶段之间的关系逐个求解,最终求得全局最优解。在设计动态规划算法时,需要确认原问题与子问题、动态规划状态、边界状态结值、状态转移方程等关键要速。
动态规划原理
- 确认原问题与子问题
- 确认状态
- 确认边界状态的值
- 确定状态转移方程
例1:爬楼梯(LeetCode70-简单)
题目:
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
示例:
输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶
思想:
递归回溯法:
上一步要不是走1步,要不走2步;递归即可!
class Solution {
public:
int climbStairs(int n) {
if(n==1 || n==2){
return n;
}
return climbStairs(n-1)+class(n-2);
}
};
但这样做会超时!!!!因为我们重复算了大量的值!
动态规划:
1.确认原问题与子问题:
原问题是求N阶台阶的所有走法;子问题是求1..2....n-1阶台阶的走法
2.确认状态:
第i个状态就是i阶台阶的所有走法数量
3.确认边界状态的值:
边界状态为1阶和2阶台阶的走法,dp[1]=1,dp[2]=2
4.确定状态转移方程:
dp[i]=dp[i-1]+dp[i-2]
代码:
class Solution {
public:
int climbStairs(int n) {
//注意:n+3是为了防止传进来0时运行出错
vector<int> dp(n+3,0);
dp[1]=1;
dp[2]=2;
for(int i=3;i<=n;i++){
dp[i]=dp[i-1]+dp[i-2];
}
return dp[n];
}
};
例2:打家劫舍(LeetCode 198-简单)
题目:
你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例:
输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
思想:
1.原问题与子问题:
原问题是求n个房间的最优解,子问题是求前1...2....n-1个房间的最优解
2.状态:
dp[i]表示前i个房间的最优解
3.确认边界值:
dp[1]=nums[0];
dp[2]= max(nums[0] , nums[1] )
4.转移方程:
dp[i]=max(dp[i-1] , dp[i-2]+nums[i]) i>=3;
代码:
class Solution {
public:
int rob(vector<int>& nums) {
if(nums.size() ==0 ){
return 0;
}
if(nums.size() ==1){
return nums[0];
}
vector<int> dp(nums.size(),0);
dp[0]=nums[0];
dp[1]=std::max(nums[0],nums[1]);
for(int i=2;i<nums.size();i++){
dp[i]=std::max(nums[i]+dp[i-2] , dp[i-1]);
}
return dp[nums.size()-1];
}
};
例3:最大子段和(LeetCode 53-简单)
题目:
给定一个整数数组 nums
,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。
示例:
输入: [-2,1,-3,4,-1,2,1,-5,4],
输出: 6
解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。
思想:
1.原问题与子问题:
原问题是n个数中连续子段最大和,子问题是以 1..2..n-1为结尾的子段的和的最大值
2.状态:
dp[i]表示以i为结尾的连续字串中和的最大值
3.边界条件:
dp[0]=nums[0]
dp[1]=max(nums[1] , nums[1]+dp[0] )
4.转移方程:
dp[i]=max( dp[i-1] + nums[i] , nums[i])
代码:
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int result=0;
vector<int> dp(nums.size(),0);
dp[0]=nums[0];
result=dp[0];
for(int i=1;i<nums.size();i++){
dp[i]=std::max(dp[i-1]+nums[i],nums[i]);
if(dp[i]>result){
result=dp[i];
}
}
return result;
}
};
例4:找零钱(LeetCode 322-中等)
题目;
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例:
输入: coins =[1, 2, 5]
, amount =11
输出:3
解释: 11 = 5 + 5 + 1
输入: coins =[2]
, amount =3
输出: -1
思想:
1.原问题和子问题:
原问题是凑成n的最小数量,子问题是凑成1...2...n-1用的最小数量
2.状态
dp[i]表示凑成i用的最少数量
3.边界条件:
dp[ coins[*] ]=1
4.转移方程:
dp[i]=min( dp[i-coins[0]] ,.....,dp[ i-coins[n-1] ] ) +1
代码:
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount+1,-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 ){
if(dp[i]==-1 || dp[i]> (dp[i-coins[j]]+1) ){
dp[i]=dp[i-coins[j]] +1;
}
}
}
}
return dp[amount];
}
};
例5:三角形最小路径和(LeetCode 120-中等)
题目:
给定一个三角形,找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。
相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。
示例:
例如,给定三角形:
[
[2],
[3,4],
[6,5,7],
[4,1,8,3]
]
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
思想:
1.原问题和子问题:
原问题是自顶向下的最小路径和,子问题是自顶到当前结点的最小路径和,或者是自底到当前结点的最小路径和。
2.状态:
dp[i][j]表示到 i,j位置时最小的路径和
3.边界条件
dp[0][0]=triangle[0][0]或者最底层的值为初始也可
4.转移方程:
自底向上递推时:
dp[i][j]=min(dp[i+1][j],dp[i+1][j+1]) + triangle[i][j]
自顶向下递推时:
dp[i][j] = min(dp[i-1][j] , dp[i-1][j-1] )+triangle[i][j]
代码:
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
if(triangle.size()==0){
return 0;
}
//创建dp
int n = triangle.size();
vector<vector<int>> dp;
for(int i=0;i<n;i++){
dp.push_back(vector<int>(i+1,0));
}
//边界条件:把最底下一层赋值
for(int i=0;i<n;i++){
dp[n-1][i]= triangle[n-1][i];
}
//开始递推
for(int i=n-2;i>=0;i--){
for(int j=0;j<dp[i].size();j++){
dp[i][j]=std::min(dp[i+1][j],dp[i+1][j+1])+triangle[i][j];
}
}
return dp[0][0];
}
};
例6:最长上升子序列(LeetCode 300-中等)
题目:
给定一个无序的整数数组,找到其中最长上升子序列的长度。
示例:
输入:[10,9,2,5,3,7,101,18]
输出: 4 解释: 最长的上升子序列是[2,3,7,101],
它的长度是4
思想:
动态规划解决:
1.原问题和子问题:
原问题是整个数组中最长上升子序列得长度;子问题是以1....2...n-1为结尾得数组中最长上升子序列得长度
2.状态:
dp[i]为以第i个为结尾得
3.边界条件:
dp[0]=1
4.转移方程:
i前面比nums[i]小得最大Index值
dp[i]= dp[index]+1
贪心思想:
代码:
O(n^2)得动态规划
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size()==0){
return 0;
}
int result=1;
vector<int> dp(nums.size(),1);
for(int i=1;i<nums.size();i++){
for(int j=i-1;j>=0;j--){
if(nums[i]>nums[j] && (dp[j]+1)>dp[i]){
dp[i]=dp[j]+1;
}
}
if(result< dp[i]){
result=dp[i];
}
}
return result;
}
};
贪心策略;
代码中得查找可以用二分查找来优化,O(nlogn)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
if(nums.size()==0){
return 0;
}
vector<int> stack;
stack.push_back(nums[0]);
for(int i=1;i<nums.size();i++){
if(nums[i]>stack.back()){
stack.push_back(nums[i]);
}
else{
for(int j=0;j<stack.size();j++){
if(stack[j]>=nums[i]){
stack[j]=nums[i];
break;
}
}
}
}
return stack.size();
}
};
例7:最小路径和(LeetCode 64-中等)
题目:
给定一个包含非负整数的 m x n 网格,请找出一条从左上角到右下角的路径,使得路径上的数字总和为最小。
说明:每次只能向下或者向右移动一步。
示例:
输入:
[
[1,3,1],
[1,5,1],
[4,2,1]
]
输出: 7
解释: 因为路径 1→3→1→1→1 的总和最小。
思想:
1.原问题与子问题:
原问题是左上到右下得最小路径和,子问题是左上到 i,j 位置得最短路径和。
2.状态
dp[i][j]表示左上到i j位置得最短路径和
3.边界条件
dp[0][0]=grid[0][0]
4.转移方程:
dp[i][j]=min(dp[i-1][j], dp[i][j-1]) + grid[i][j];
代码:
class Solution {
public:
int minPathSum(vector<vector<int>>& grid) {
if(grid.size()==0){
return 0;
}
vector<vector<int>> dp(grid.size(),vector<int>(grid[0].size(),-1));
dp[0][0]=grid[0][0];
for(int i=0;i<dp.size();i++){
for(int j=0;j<dp[0].size();j++){
if(j-1>=0){
dp[i][j]=dp[i][j-1]+grid[i][j];
}
if(i-1>=0){
if( !(dp[i][j]!=-1 && (dp[i-1][j]+grid[i][j])>dp[i][j] )){
dp[i][j]=dp[i-1][j]+grid[i][j];
}
}
}
}
return dp[dp.size()-1][dp[0].size()-1];
}
};
例8:地牢游戏(LeetCode 174-困难)
题目:
一些恶魔抓住了公主(P)并将她关在了地下城的右下角。地下城是由 M x N 个房间组成的二维网格。我们英勇的骑士(K)最初被安置在左上角的房间里,他必须穿过地下城并通过对抗恶魔来拯救公主。
骑士的初始健康点数为一个正整数。如果他的健康点数在某一时刻降至 0 或以下,他会立即死亡。
有些房间由恶魔守卫,因此骑士在进入这些房间时会失去健康点数(若房间里的值为负整数,则表示骑士将损失健康点数);其他房间要么是空的(房间里的值为 0),要么包含增加骑士健康点数的魔法球(若房间里的值为正整数,则表示骑士将增加健康点数)。
为了尽快到达公主,骑士决定每次只向右或向下移动一步。
编写一个函数来计算确保骑士能够拯救到公主所需的最低初始健康点数。
示例:
例如,考虑到如下布局的地下城,如果骑士遵循最佳路径 右 -> 右 -> 下 -> 下,则骑士的初始健康点数至少为 7。
-2 (K) -3 3
-5 -10 1
10 30 -5 (P)
思想:
代码:
class Solution {
public:
int calculateMinimumHP(vector<vector<int>>& dungeon) {
int n = dungeon.size(), m = dungeon[0].size();
vector<vector<int>> dp(n + 1, vector<int>(m + 1, INT_MAX));
dp[n][m - 1] = dp[n - 1][m] = 1;
for (int i = n - 1; i >= 0; --i) {
for (int j = m - 1; j >= 0; --j) {
int minn = min(dp[i + 1][j], dp[i][j + 1]);
dp[i][j] = max(minn - dungeon[i][j], 1);
}
}
return dp[0][0];
}
};