leetcode题解(动态规划)

动态规划本质依然是递归算法,只不过是知足特定条件的递归算法;动态规划是一个设计感比较强,艺术感比较强的一种算法设计思想。ios

原文访问个人技术博客番茄技术小栈c++

什么是动态规划

定义

将原问题拆解成若干子问题,同时保存子问题的答案,使得每一个子问题只求解一次,最终得到原问题的答案。算法

paste image

  • 咱们先解决小数据量的问题,以后层层递推来解决更大数据量的问题,一般这个过程就叫作动态规划。这个时间和记忆化搜索的时间复杂度是至关的,不过动态规划没有递归的调用,不须要额外调用和栈空间。
  • 动态规划是一个设计感比较强,艺术感比较强的一种算法设计思想。

一个简单例子

#include <iostream>
    #include <ctime>
    
    using namespace std;
    
    int num = 0;
    
    int fib( int n ){
    
        num ++;
    
        if( n == 0 )
            return 0;
    
        if( n == 1 )
            return 1;
    
        return fib(n-1) + fib(n-2);
    }
    
    int main() {
    
        num = 0;
    
        int n = 42;
        time_t startTime = clock();
        int res = fib(n);
        time_t endTime = clock();
    
        cout<<"fib("<<n<<") = "<<res<<endl;
        cout<<"time : "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
        cout<<"run function fib() "<<num<<"times."<<endl;
    
        return 0;
    }
复制代码

分析

经过计时咱们会发现这个算法很慢,为何这个解法效率这么低呢?当咱们须要计算fib(5)时,它的递归树是:数组

从这个图能够看出这里面有大量的重复计算,咱们怎样避免呢,咱们能够在程序的外面作一个数组memo,其实memo[i]就记忆了第i个斐波那契数列。bash

#include <iostream>
    #include <ctime>
    #include <vector>
    using namespace std;
    
    vector<int> memo;
    int num = 0;
    
    // 记忆化搜索
    int fib( int n ){
    
        num ++;
    
        if( n == 0 )
            return 0;
    
        if( n == 1 )
            return 1;
    
        if( memo[n] == -1 )
            memo[n] = fib(n-1) + fib(n-2);
    
        return memo[n];
    }
    
    int main() {
    
        num = 0;
    
        int n = 42;
        memo = vector<int>(n+1,-1);
    
        time_t startTime = clock();
        int res = fib(n);
        time_t endTime = clock();
    
        cout<<"fib("<<n<<") = "<<res<<endl;
        cout<<"time : "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
        cout<<"run function fib() "<<num<<"times."<<endl;
    
        return 0;
    }
    
复制代码

咱们采用一个memo数组来记忆,因此叫作记忆化搜索。记忆化搜索其实就是在递归的过程当中添加计划化,是一种自上向下的解决问题,咱们假设基本的问题已经解决了,咱们已经会求fib(n-1)和fib(n-2)了,那么咱们就能求第n个数了。微信

若是咱们能自上而下解决问题,咱们也能自下而上解决问题,只不过不少时候咱们习惯于前者。函数

#include <iostream>
    #include <ctime>
    #include <vector>
    using namespace std;
    
    // 动态规划
    int fib( int n ){
    
        vector<int> memo(n+1, -1);
    
        memo[0] = 0;
        memo[1] = 1;
        for( int i = 2 ; i <= n ; i ++ )
            memo[i] = memo[i-1] + memo[i-2];
    
        return memo[n];
    }
    
    int main() {
    
        // 结果会溢出,这里只看性能
        int n = 1000;
    
        time_t startTime = clock();
        int res = fib(n);
        time_t endTime = clock();
    
        cout<<"fib("<<n<<") = "<<res<<endl;
        cout<<"time : "<<double(endTime-startTime)/CLOCKS_PER_SEC<<" s"<<endl;
    
        return 0;
    }
    
复制代码

第一个动态规划问题

leetcode 70. 爬楼梯

paste image

解题思路

咱们来看一下递归的思路,把一个大的问题分解成小的问题。性能

paste image

代码实现(递归)

#include <iostream>
    #include <vector>
    
    using namespace std;
    
    // 记忆化搜索
    class Solution {
    private:
        vector<int> memo;
    
        int calcWays(int n){
    
            if( n == 0 || n == 1)
                return 1;
    
            if( memo[n] == -1 )
                memo[n] = calcWays(n-1) + calcWays(n-2);
    
            return memo[n];
        }
    public:
        int climbStairs(int n) {
    
            memo = vector<int>(n+1,-1);
            return calcWays(n);
        }
    };
复制代码

代码实现(动态规划)

咱们会发现和上面斐波那契同样,很轻易能够转化为动态规划解法。大数据

#include <iostream>
    #include <vector>
    
    using namespace std;
    
    // 动态规划
    class Solution {
    
    public:
        int climbStairs(int n) {
    
            vector<int> memo(n+1, -1);
            memo[0] = 1;
            memo[1] = 1;
    
            for ( int i = 2; i <= n; i++ ) {
                memo[i] = memo[i-1] + memo[i-2];
            }
    
            return memo[n];
        }
    };

复制代码

类似问题

  • leetcode 120
  • leetcode 64

发现重叠子问题

leetcode 343. 整数拆分

paste image

解题思路

paste image

对于一个问题若是没有思路时,咱们能够先考虑暴力解法。话句话说,咱们使用什么样的方式,才能把正整数n的全部分割枚举出来,咱们没法知道有几重循环,一般咱们须要使用递归的手段。
暴力解法:回溯遍历将一个数作分割的全部可能性。O(2^n)ui

之因此递归树存在,是由于它有最优子结构
经过求子问题的最优解,能够得到原问题的最优解。

最优子结构

paste image

  • 经过求子问题的最优解, 能够得到原问题的最优解

代码实现

实现1
#include <iostream>
    #include <cassert>
    
    using namespace std;
    
    class Solution {
    private:
        int max3( int a , int b , int c ){
            return max( a , max(b,c) );
        }
    
        // 将n进行分割(至少分割两部分), 能够得到的最大乘积
        int breakInteger( int n ){
    
            if( n == 1 )
                return 1;
    
            int res = -1;
            for( int i = 1 ; i <= n-1 ; i ++ )
                res = max3( res , i*(n-i) , i * breakInteger(n-i) );
            return res;
        }
    public:
        int integerBreak(int n) {
            assert( n >= 1 );
            return breakInteger(n);
        }
    };
    
    
复制代码
实现2

它包含重叠子问题,下面是记忆化搜索版本:

class Solution {
    private:
        vector<int> memo;
    
        int max3( int a , int b , int c ){
            return max( a , max(b,c) );
        }
    
        // 将n进行分割(至少分割两部分), 能够得到的最大乘积
        int breakInteger( int n ){
    
            if( n == 1 )
                return 1;
    
            if( memo[n] != -1 )
                return memo[n];
    
            int res = -1;
            for( int i = 1 ; i <= n-1 ; i ++ )
                res = max3( res , i*(n-i) , i * breakInteger(n-i) );
            memo[n] = res;
            return res;
        }
    public:
        int integerBreak(int n) {
            assert( n >= 1 );
            memo = vector<int>(n+1, -1);
            return breakInteger(n);
        }
    };
    
复制代码
实现3 动态规划

下面咱们使用自底向上的方法,也就是动态规划解决这个问题

class Solution {
    
    private:
        int max3( int a , int b , int c ){
            return max(max(a,b),c);
        }
    public:
        int integerBreak(int n) {
    
            // memo[i] 表示将数字i分割(至少分割成两部分)后获得的最大乘积
            vector<int> memo(n+1, -1);
    
            memo[1] = 1;
            for ( int i = 2; i <= n; i++ ) {
                // 求解memo[i]
                for ( int j = 1; j <= i-1; j++ ) {
                    // j + (i-j)
                    memo[i] = max3( memo[i], j*(i-j), j*memo[i-j] );
                }
            }
    
            return memo[n];
    
        }
    };
    
复制代码

类似问题

  • leetcode 279
  • leetcode 91
  • leetcode 62
  • leetcode 63

状态的定义和状态转移

leetcode 198. 打家劫舍

paste image

状态的定义

考虑偷取[x...n-1]范围里的房子(函数定义)

状态的转移

f(0) = max{ v(0) + f(2), v(1) + f(3), v(2) + f(4), … , v(n-3) + f(n-1), v(n-2), v(n-1)}(状态转移方程)

解题思路

首先依然是若是没有思路的话,先考虑暴力解法。检查全部的房子,对每一个组合,检查是否有相邻的房子,若是没有,记录其价值,找最大值。O((2^n)*n)

注意其中对状态的定义:
考虑偷取[x…n-1]范围里的房子(函数的定义)

根据对状态的定义,决定状态的转移:
f(0) = max{ v(0) + f(2), v(1) + f(3), v(2) + f(4), … , v(n-3) + f(n-1), v(n-2), v(n-1)}(状态转移方程)

实际上咱们的递归函数就是在实现状态转移。

198. House Robber

实现代码

class Solution {
    private:
        // memo[i] 表示考虑抢劫 nums[i...n) 所能得到的最大收益
        vector<int> memo;
    
        // 考虑抢劫nums[index...nums.size())这个范围的全部房子
        int tryRob( vector<int> &nums, int index){
    
            if( index >= nums.size() )
                return 0;
    
            if( memo[index] != -1 )
                return memo[index];
    
            int res = 0;
            for( int i = index ; i < nums.size() ; i ++ )
                res = max(res, nums[i] + tryRob(nums, i+2));
            memo[index] = res;
            return res;
        }
    public:
        int rob(vector<int>& nums) {
    
            memo = vector<int>(nums.size(), -1);
            return tryRob(nums, 0);
        }
    };
    
复制代码

动态规划解法

class Solution {
    
    public:
        int rob(vector<int>& nums) {
    
            int n = nums.size();
    
            if( n == 0 ) {
                return 0;
            }
    
            // memo[i] 表示考虑抢劫 nums[i...n) 所能得到的最大收益
            vector<int> memo(n, 0);
            memo[n-1] = nums[n-1];
            for( int i = n-2 ; i >= 0 ; i -- ) {
                for (int j = i; j < n; j++) {
                    memo[i] = max(memo[i], nums[j] + (j + 2 < n ? memo[j + 2] : 0) );
                }
            }
    
            return memo[0];
        }
    };
    
复制代码

状态的另外一种定义

咱们所强调的是对于动态规划来讲,咱们要清晰本身对状态的定义,在咱们以前的定义咱们是去考虑偷取[x…n-1]范围里的房子(函数的定义)。对于一样的问题,不少时候咱们能够设立不一样的状态获得一样正确的答案。

改变对状态的定义:
考虑偷取[0…x]范围里的房子(函数的定义)。实现以下:

记忆化搜索代码实现

class Solution {

private:
    vector<int> memo;
    //考虑偷取[0..x]范围里的房子
    int tryRob(vector<int>&nums, int index){
        if (index < 0){
            return 0;
        }
        
        if (memo[index] != -1){
            return memo[index];
        }
        
        int res = 0;
        for( int i = index; i >= 0; i--){
            res = max(res, nums[i] + tryRob(nums, i - 2));
        }
        memo[index] = res;
        return res;
    }

public:
    
    int rob(vector<int>& nums) {
        int n = nums.size();
        memo = vector<int>(n + 1, -1);
        if (n == 0){
            return 0;
        }
        
        return tryRob(nums, n-1);
    }
};

复制代码

动态规划代码实现

class Solution {

public:
    
    //考虑偷取[0..x]范围里的房子
    int rob(vector<int>& nums) {
        int n = nums.size();
        vector<int> memo(n, -1);
        
        if (n == 0){
            return 0;
        }
        
        memo[0] = nums[0];
        
        for(int i = 1; i < n; i++){
            for(int j = i; j >= 0; j --){
                memo[i] = max(memo[i], nums[j] + (j-2 >= 0? memo[j-2]: 0));
            }
        }
        
        return memo[n-1];
        
    }
};
复制代码

类似问题

  • leetcode 213
  • leetcode 337
  • leetcode 309

-------------------------华丽的分割线--------------------

看完的朋友能够点个喜欢/关注,您的支持是对我最大的鼓励。

我的博客番茄技术小栈掘金主页

想了解更多,欢迎关注个人微信公众号:番茄技术小栈

番茄技术小栈
相关文章
相关标签/搜索