leetcode-动态规划

leetcode-动态规划


动态规划,Dynamic Programming,是运筹学的分支,是求解决策过程最优化的数学方法。

关键词:全局最优解,原问题与子问题,动态规划状态,边界状态结值,转台转移。

LeetCode 70 - Climbing Stairs - 爬楼梯 - easy

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

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

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

示例 1:

输入: 2
输出: 2
解释: 有两种方法可以爬到楼顶。
1.  1 阶 + 1 阶
2.  2 阶

示例 2:

输入: 3
输出: 3
解释: 有三种方法可以爬到楼顶。
1.  1 阶 + 1 阶 + 1 阶
2.  1 阶 + 2 阶
3.  2 阶 + 1 阶

递归超时解法:

其实就是斐波那契数列

class Solution {
public:
    int climbStairs(int n) {
        if (n == 1 || n == 2) return n;
        return climbStairs(n - 1) + climbStairs(n - 2);
    }
};

重复计算太多,时间复杂度趋近于2的n次方。

动态规划解法

思考到达第i阶的爬法有多少。答案是与i-1和i-2阶的爬法数量的和(因为只能一次爬1阶或2阶)。

步骤:

  1. 设置递推数组dp[n]={0},dp为动态规划缩写。dp[i]为到第i阶有几种爬法;
  2. dp[1] = 1; dp[2] = 2;
  3. 确定3到n阶有多少走法
#include <vector>

class Solution {
public:
    int climbStairs(int n) {
        std::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];
    }
};

之所以有n+3个元素,是因为数组至少要有0、1、2阶的走法。如果是n+1个元素,n==0是就越界了。

动态规划4大要素小结:

  1. 确认原问题与子问题。前者为n阶走法数量,后者为1到n-1阶走法数量;
  2. 确认状态。本体状态简单,第i个状态即第i阶走法数量;
  3. 边界状态。即第2个步骤。
  4. 状态转移。即循环里的语句。

LeetCode 198 - House Robber - 打家劫舍 - easy

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

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

示例 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 。

提示:

0 <= nums.length <= 100
0 <= nums[i] <= 400

超时解法

每个屋子有两种状态,时间复杂度为2的n次方。

贪心算法,不触发警报前提下,每次选择财宝最多的。肯定不行~

动态规划解法

选择i房间,就不能选择i-1;不选择i,就只考虑前i-1个房间。

4大要素分析:

  1. 原问题,n个房间最优解;子问题,求前1个、、n-1个房间的最优解。
  2. 状态。第i个状态为前i个房间获得的最大财宝。
  3. 边界状态。dp[1]为第一个房间财宝,dp[2]为1、2房间较大的财宝。
  4. 状态转移。
    • 选择i,dp[i]为第i个房间+前i-2房间最优解;
    • 不选择i,则取前i-1房间最优解
#include <vector>
#include <algorithm>
class Solution {
public:
    int rob(vector<int>& nums) {
        unsigned int nSize = nums.size();
        if(nSize == 0) return 0;
        if(nSize == 1) return nums[0];

        std::vector<int> dp(nSize + 3, 0);
        dp[1] = nums[0];
        dp[2] = std::max(nums[0], nums[1]);
        for(unsigned int i = 3; i <= nSize; ++i)
        {
            dp[i] = std::max(
                dp[i - 1],
                dp[i - 2] + nums[i-1]);
        }

        return dp[nSize];
    }
};

LeetCode 53 - Maximum Subarray - 最大子段和 - easy

非常经典的动态规划题。

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

示例:

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

暴力枚举的话,复杂度为n的2次方。

直接动态规划来解。关键是确定规划状态。思考用dp[i]代表前i个数字组成的最大子段和,推到dp[i], dp[i-1]的关系,发现不行,比如:

[-2, 1, 1, -3, 4]
dp[4]:[1,1]
dp[5]:[4]

也就是无法推导出状态转移关系。

为了让dp[i], dp[i-1]两个状态的最优解产生联系,也就是根据后者推导出前者,可以让dp[i]代表以第i个数字结尾的子段的最优解。

#include <vector>
using namespace std;
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        unsigned int nSize = nums.size();
        if(nSize == 0) return 0;
        
        int nRes = 0;
        vector<int> dp(nums.size(), 0);

        dp[0] = nums[0];
        nRes = dp[0];
        
        for(unsigned int i = 1; i < nSize; ++i)
        {
            dp[i] = max(dp[i - 1] + nums[i], nums[i]);

            nRes = nRes > dp[i]
                ? nRes
                : dp[i];
        }
        return nRes;
    }
};

LeetCode 322 - Coin Change - 找零钱 - medium

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

示例 1:

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

示例 2:

输入: coins = [2], amount = 3
输出: -1

说明:你可以认为每种硬币的数量是无限的。

其它解法

贪心思想:每次选用面值最大的硬币。

反例:[1 , 2 , 5 , 7 , 10], 14,正解为两个7,贪心则是[10, 2, 2],错了。。

动态规划解法

4大要素分析:

  1. 原问题,总额n最优解;子问题,金额1至n-1的最优解。
  2. 状态。第i个状态为金额i的最优解,即最少硬币数。
  3. 边界状态。各个面值对应状态为1,其余初始化为-1。
  4. 状态转移。以示例1为例,由dp[i-1]、dp[i-2], dp[i-5]与3种面值组合,即分别取最小值后,加1。
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        
        vector<int> dp(amount + 1, -1);
        unsigned int nSize = coins.size();
        
        //这段初始化可以没有,因为赋值dp[0]=0,推导时dp[硬币面值]会赋值为0+1
        //for(unsigned int i = 0; i < nSize; ++i)
        //{
        //    dp[coins[i]] = 1;
        //}
        
        dp[0] = 0;
        for(unsigned int i = 1; i <= amount; ++i)
        {
            for(unsigned int j = 0; j < nSize; ++j)
            {
                // 不能越界,且dp[i-coins[j]]有解
                if ( i < coins[j] || dp[i - coins[j]] == -1) continue;

                if ( dp[i] == -1 || dp[i] > dp[i-coins[j]] + 1)
                {
                    dp[i] = dp[ i - coins[j] ] + 1;
                }
            }
        }
        return dp[amount];
    }
};

LeetCode 120 - Triangle - 三角形 - medium

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

相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。

例如,给定三角形:

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

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

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


贪心算法肯定不行滴(比如下下层有个数字100000)。

从这个题开始,解除二维dp数组。

4大要素分析:

  1. 原问题,n层最优解;子问题,顶角到n-1层最优解。
  2. 状态。dp[i][j]代表三角形第i行、第j列距离最顶角的最优解。
  3. 边界状态。dp[0][0]为顶角值,其余为0。或者先初始化最后一行,倒推。
  4. 状态转移。两种推导方法,从顶角到下层,或从下层到顶角。

从上到下

显然要麻烦些。

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        unsigned int nRol = triangle.size();
        if(nRol == 0) return 0;

        vector<vector<int>> dp;

        // 初始化上面两条边
        for(unsigned int i = 0; i < nRol; ++i)
        {
            unsigned int nCol = triangle[i].size();
            dp.push_back(vector<int>(nCol, 0));
            
            if(nCol)
            {
                if(i == 0)
                {
                    // 顶角
                    dp[i][0] = triangle[0][0];
                }
                else
                {
                    dp[i][0] = triangle[i][0] + dp[i - 1][0];
                    dp[i][nCol - 1] = triangle[i][nCol - 1] + dp[i - 1][nCol - 2];
                }
            }
        }

        
        for(unsigned int rol = 1; rol <= nRol - 1; ++rol)
        {
            unsigned int nCol = triangle[rol].size();
            for(unsigned int col = 1; col <= nCol - 2; ++col)
            {
                dp[rol][col] = ( dp[rol - 1][col - 1] < dp[rol - 1][col]
                    ? dp[rol - 1][col - 1]
                    : dp[rol - 1][col] )
                    + triangle[rol][col];
            }
        }
        return *min_element(dp[nRol - 1].begin(), dp[nRol - 1].end());
    }
};

从下到上

显然更简洁。

需要注意的是,循环时行列变量是递减的,不能用unsigned int类型。

class Solution {
public:
    int minimumTotal(vector<vector<int>>& triangle) {
        int nRol = triangle.size();
        if(nRol == 0) return 0;

        vector<vector<int>> dp;

        // 初始化底边
        for(int i = 0; i < nRol - 1; ++i)
        {
            int nCol = triangle[i].size();
            dp.push_back(vector<int>(nCol, 0));
        }
        dp.push_back(triangle[nRol - 1]);

        
        for(int rol = nRol - 2; rol >= 0; --rol)
        {
            int nCol = triangle[rol].size();
            for(int col = 0; col < nCol; ++col)
            {
                dp[rol][col] = ( dp[rol + 1][col] < dp[rol + 1][col + 1]
                    ? dp[rol + 1][col]
                    : dp[rol + 1][col + 1] )
                    + triangle[rol][col];
            }
        }
        return dp[0][0];
    }
};

LeetCode 300 - Longest Increasing Subsequence - 最长上升子序列 - Medium/Hard

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

示例:

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

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

暴力枚举的话,一共2的n次方种情况。


如果状态为前i个数字中的最优解,那返回dp[n-1]就行了。但一般这样是不行的,联系53题最大子段和。举个无法进行状态推导的例子:

[1, 3, 2, 3, 1, 4]

dp[0]:1, [1]
dp[1]:2, [1, 3]
dp[2]:2, [1, 3], [1, 2]
dp[3]:3, [1, 2, 3]
dp[4]:3, [1, 2, 3]
dp[5]:4, [1, 2, 3, 4]

和53题一样,状态i代表以第i个数字结尾的最优解就能推导了。

[1, 3, 2, 3, 1, 4]

dp[0]:1, [1]
dp[1]:2, [1, 3]
dp[2]:2, [1, 2]
dp[3]:3, [1, 2, 3]
dp[4]:1, [1]
dp[5]:4, [1, 2, 3, 4]

解法1

重新进行4大要素分析:

  1. 原问题,最长上升子序列;子问题,前1到n-1个数字最长上升子序列。
  2. 状态。dp[i]可代表以第i个数字结尾的最优解。
  3. 边界状态。均为1.
  4. 状态转移。遍历前i-1个解得出dp[i]

最终结果为状态数组的最大值。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int nSize = nums.size();
        if(nSize == 0) return 0;

        vector<int> dp(nSize, 1);
        dp[0] = 1;

        int nRes = dp[0];
        for(int i = 1; i < nSize; ++i)
        {
            for(int j = 0; j < i; ++j)
            {
                if( (nums[i] > nums[j] )
                    && (dp[j] + 1 > dp[i]) )
                {
                    dp[i] = dp[j] + 1;
                }
            }
            if( nRes < dp[i] ) nRes = dp[i];
        }

        return nRes;
    }
};

解法2

利用栈,,不太好想。

stack[i]代表长度为i+1的上升子段最后一个元素的最小可能取值,若要组成长度i+2的上升子序列,则需要一个大于stack[i]的数。最终栈的大小,即最优解。

这里不能用stack容器,因为它没有迭代器,所以还是用vector。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int nSize = nums.size();
        if(nSize == 0) return 0;

        vector<int> stack;
        stack.push_back(nums[0]);
        for(int i = 1; i < nSize; ++i)
        {
            if (nums[i] > stack.back())
            {
                stack.push_back(nums[i]);
            }
            else
            {
                int nStackSize = stack.size();
                for (int j = 0; j < nStackSize; ++j)
                {
                    if(stack[j] >= nums[i])
                    {
                        stack[j] = nums[i];
                        break;
                    }
                }
            }
        }

        return stack.size();
    }
};

然后借助二分查找将时间复杂度优化为nlogn。

class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int nSize = nums.size();
        if(nSize == 0) return 0;

        vector<int> stack;
        stack.push_back(nums[0]);
        for(int i = 1; i < nSize; ++i)
        {
            if (nums[i] > stack.back())
            {
                stack.push_back(nums[i]);
            }
            else
            {
                auto it = lower_bound(stack.begin(), stack.end(), nums[i]);
                *it = nums[i];
            }
        }

        return stack.size();
    }
};

LeetCode 64 - Minimum Path Sum - 最小路径和 - Mudium

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

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

示例:

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

直接解:

class Solution {
public:
    int minPathSum(vector<vector<int>>& grid) {
        int nRow = grid.size();
        if (nRow == 0) return 0;
        int nCol = grid[0].size();

        vector<vector<int>> dp;
        dp.push_back(vector<int>(nCol, 0));
        dp[0][0] = grid[0][0];
        for (int i = 1; i < nRow; ++i)
        {
            dp.push_back(vector<int>(nCol, 0));

            dp[i][0] = dp[i - 1][0] + grid[i][0];
        }
        for (int j = 1; j < nCol; ++j)
        {
            dp[0][j] = dp[0][j - 1] + grid[0][j];
        }


        for (int i = 1; i < nRow; ++i)
        {
            for (int j = 1; j < nCol; ++j)
            {
                dp[i][j] = (dp[i - 1][j] < dp[i][j - 1]
                        ? dp[i - 1][j]
                        : dp[i][j - 1])
                        + grid[i][j];
            }
        }

        return dp[nRow - 1][nCol - 1];
    }
};

LeetCode 174 - Dungeon Game - 地下城游戏 - Hard

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

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

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

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

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

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

-2 (K)	-3	3
-5	-10	1
10	30	-5 (P)

说明:

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

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

这个题的坑是,如果从左上往右下递推,无法推导出初始时最少有多少血量,也就是推不出题干的“至少”。

如果从右下往左上推,每个状态就代表想到达这里,至少有多少血量。

4大要素分析:

  1. 原问题,从左上到达右下至少多少血量;子问题,从[1~ m][1-n]到达右下,至少多少血量(1+x).
  2. 状态。dp[i][j]代表从这里到达右下角至少多少血量。
  3. 边界状态。dp[m-1][n-1],对比1和1-右下角,取最大值,并初始化最后一行/列。
  4. 状态转移。对比右/下dp-格子的最小值,再和1对比,选择最大值。

比如:

1*3
dungeon: 3, -5, 2
dp:
?, ?, 1 > 2 ? 1 : 2
?, 1 > 1-(-5) 1 : 1-(-5) , 1
1 > 6-3 ? 1 : 3, 6, 1
3, 6, 1
class Solution {
public:
    int calculateMinimumHP(vector<vector<int>>& dungeon) {
        int nRow = dungeon.size();
        if(nRow == 0) return 0;

        vector<vector<int>> dp;
        int nCol = dungeon[0].size();
        int nTmp = 0;

        for(int i = 0; i < nRow; ++i)
        {
            dp.push_back(vector<int>(nCol, 0));
        }

        nTmp = 1 - dungeon[nRow - 1][nCol - 1];
        dp[nRow - 1][nCol - 1] = 1 > nTmp ? 1 : nTmp;

        // 最后一行
        for( int i = nCol - 2; i >= 0; --i)
        {
            nTmp = dp[nRow - 1][i + 1] - dungeon[nRow - 1][i];
            dp[nRow - 1][i] = 1 > nTmp
                    ? 1
                    : nTmp;
        }
        // 最后一列
        for( int i = nRow - 2; i >= 0; --i )
        {
            nTmp = dp[i + 1][nCol - 1] - dungeon[i][nCol - 1];
            dp[i][nCol - 1] = 1 > nTmp
                    ? 1
                    : nTmp;
        }


        for(int i = nRow - 2; i >= 0; --i)
        {
            for(int j = nCol - 2; j >= 0; --j)
            {
                dp[i][j] = max({
                    1, 
                    // 右边和下边,哪个需要的血量少
                    min({
                        dp[i + 1][j] - dungeon[i][j],
                        dp[i][j + 1] - dungeon[i][j]
                    })
                });
            }
        }

        return dp[0][0];
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值