算法学习笔记Day9——动态规划基础篇

一、介绍

本文解决几个问题:动态规划是什么?解决动态规划问题有什么技巧?如何学习动态规划?

1. 动态规划问题的一般形式就是求最值。动态规划其实是运筹学的一种最优化方法,只不过在计算机问题上应用比较多,比如说让你求最长递增子序列呀,最小编辑距离呀等等。

  1. 动态规划的核心思想就是穷举求最值,但只有列出正确的「状态转移方程」 ,才能正确地穷举。你需要判断算法问题是否具备「最优子结构」 ,是否能够通过子问题的最值得到原问题的最值。另外,动态规划问题存在「重叠子问题」,如果暴力穷举的话效率会很低,所以需要你使用「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。

以上提到的重叠子问题、最优子结构、状态转移方程就是动态规划三要素。

  1. 思维框架:明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义

递归是自顶向下,动态规划是自底向上

  1. 带备忘录的递归和动态规划实际上是等价的,动态规划是从底层开始,一步一步完成对数组的完善,所以不需要备忘录,或者说dp数组本身就是备忘录,递归会遇到很多重复的子问题,所以需要备忘录来简化。

二、例题

例题1:斐波那契数

分析

写出状态转移方程,写出基底,就可以开始自底向上构造了。

代码

思路1:自底向上解法

cpp 复制代码
class Solution {
public:
    int fib(int n) {
        if(n == 0 || n == 1){
            return n;
        }
        int fib_1 = 1, fib_2 = 0;
        int fib_i;
        for(int i = 2; i<= n; i++){
            fib_i = fib_1 + fib_2;
            fib_2 = fib_1;
            fib_1 = fib_i;
        }
        return fib_i;
    }
};

思路2:自顶向下解法

cpp 复制代码
class Solution {
public:
    vector<int> diary;
    int recursion(int n){
        //基地
        if(n == 0 || n == 1){
            return n;
        }
        //查日记
        if(diary[n] != -1){
            return diary[n];
        }
        //日记没查到,更新日记,用递归更新它
        diary[n] = recursion(n-1) + recursion(n-2);
        //再查找日记本
        return diary[n];
    }
    int fib(int n) {
        diary.resize(n+1, -1);
        return recursion(n);
    }
};

例题2:零钱兑换

分析

写出状态方程就可以了

代码

思路1:带备忘录的递归

cpp 复制代码
class Solution {
public:
    vector<int> diary;
    int dp(vector<int>& coins, int amount){
        if(amount < 0){
            return -1;
        }
        if(amount == 0){
            return 0;
        }
        if(diary[amount] != 0){
            return diary[amount];
        }
        int ans = INT_MAX;
        for(int coin : coins){
            int subsolution = dp(coins, amount - coin);
            if(subsolution != -1){
                ans = min(subsolution+1, ans);
            }
        }
        diary[amount] = ans==INT_MAX ? -1:ans;
        return diary[amount];
    }
    int coinChange(vector<int>& coins, int amount) {
        diary.resize(amount+1, 0);
        return dp(coins, amount);
    }
};

不知道为什么把diary初始化为-1就会超时,推测是-1表示不可能的情况,有很多正数diary也是-1,就容易进入循环,但是0只有0这个情况。

思路2:dp迭代

cpp 复制代码
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        vector<int> dp(amount + 1, INT_MAX-1);
        //base situation
        dp[0] = 0;
        for(int i  =0; i<=amount; i++){
            for(int coin : coins){
                if(i - coin < 0){
                    continue;
                }
                dp[i] = min(dp[i], dp[i-coin] + 1);
            }
        }
        return dp[amount]==INT_MAX-1?-1:dp[amount];
    }
};

例题3:最长递增子序列

分析

首先要明确dp数组代表什么,这里是以 位置i数字 结尾的最长字序列长度,对于每个位置,比较前面的位置,只要它大于某个元素,就可以和那个元素的最长子序列组成新的最长子序列。

代码

cpp 复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        vector<int> dp(nums.size(), 1);
        for(int i  = 0; i< nums.size(); i++){
            for(int j = 0; j<i; j++){
                if(nums[i] > nums[j])
                    dp[i] = max(dp[i], dp[j] + 1);
            }
        }
        return *max_element(dp.begin(), dp.end());
    }
};

例题4:俄罗斯套娃信封问题

代码

cpp 复制代码
class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        int n = envelopes.size();
        sort(envelopes.begin(), envelopes.end(), [](vector<int>& a, vector<int>& b)->bool{
            return a[0] == b[0]? a[1] > b[1] : a[0] < b[0];
        });
        vector<int> dp(n, 1);
        for(int i = 0; i<n; i++){
            for(int j = 0; j< i; j++){
                if(envelopes[i][1] > envelopes[j][1]){
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
        }
        return *max_element(dp.begin(), dp.end());
    }
};

三、总结

i)斐波那契数列的问题,解释了如何通过「备忘录」或者「dp table」的方法来优化递归树,并且明确了这两种方法本质上是一样的,只是自顶向下和自底向上的不同而已。

ii)凑零钱的问题,展示了如何流程化确定「状态转移方程」,只要通过状态转移方程写出暴力递归解,剩下的也就是优化递归树,消除重叠子问题而已。

iii)二维数组也可以排序,要传入一个lamda表达式来说明排序的方式,第四题套娃问题,同样长的信封必须按宽的逆序排列,因为同样大小是不可以嵌套的,如果顺序排列,求最长递增子序列的时候就会多一个。

相关推荐
XH华4 小时前
初识C语言之二维数组(下)
c语言·算法
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
不想当程序猿_4 小时前
【蓝桥杯每日一题】求和——前缀和
算法·前缀和·蓝桥杯
sanguine__4 小时前
Web APIs学习 (操作DOM BOM)
学习
落魄君子4 小时前
GA-BP分类-遗传算法(Genetic Algorithm)和反向传播算法(Backpropagation)
算法·分类·数据挖掘
冷眼看人间恩怨4 小时前
【Qt笔记】QDockWidget控件详解
c++·笔记·qt·qdockwidget
菜鸡中的奋斗鸡→挣扎鸡4 小时前
滑动窗口 + 算法复习
数据结构·算法
Lenyiin5 小时前
第146场双周赛:统计符合条件长度为3的子数组数目、统计异或值为给定值的路径数目、判断网格图能否被切割成块、唯一中间众数子序列 Ⅰ
c++·算法·leetcode·周赛·lenyiin
郭wes代码5 小时前
Cmd命令大全(万字详细版)
python·算法·小程序
scan7245 小时前
LILAC采样算法
人工智能·算法·机器学习