算法-动态规划

动态规划

概念

动态规划解决的问题通常满足三个特征:

  • 重叠子问题:你算出结果的过程,需要反复用到前面算过的结果。
  • 最优子结构:大问题的最优解,包含了小问题的最优解。
  • 无后效性:前面的状态一旦确定,就不受后面决策的影响。
    通俗理解:想象你在爬楼梯。你想知道爬到第 10 层有多少种方法。你需要知道爬到第 9 层的方法数(再跨 1 步就到了)和爬到第 8 层的方法数(再跨 2 步就到了)。公式就是: f ( 10 ) = f ( 9 ) + f ( 8 ) f(10) = f(9) + f(8) f(10)=f(9)+f(8)。为了不重复计算 f ( 9 ) f(9) f(9) 和 f ( 8 ) f(8) f(8) 里的重叠部分,我们用一个数组 dp[] 把以前算过的楼层结果存起来,这就是动态规划。

DP解题模版

  1. 定义状态

    我们要解决什么问题?

    dp[i] 代表什么含义?(比如:凑满金额 i 所需的最少硬币数)。

  2. 推导状态转移方程

    这是最难的一步。思考:dp[i] 是怎么由之前的状态(比如 dp[i-1], dp[i-k])推导出来的?

    通常是 Math.min, Math.max 或者求和。

  3. 初始化

    最开始的情况是什么?dp[0] 是多少?

    数组其他位置默认填什么值(0?还是无穷大?)

  4. 确定遍历顺序

    是从小到大遍历,还是从大到小?

    如果是二维 DP,先遍历行还是先遍历列?

零钱兑换

零钱兑换

cpp 复制代码
#include <vector>
#include <algorithm>
#include <iostream>

using namespace std;

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        // 1. 定义 dp 数组
        // 大小为 amount + 1,因为要算到 dp[amount]
        // 初始值设为 amount + 1,这相当于无穷大。
        // 为什么是 amount + 1?因为只要有解,硬币数一定 <= amount(全是1块钱的情况)
        vector<int> dp(amount + 1, amount + 1);

        // 2. Base Case 初始化
        dp[0] = 0; 

        // 3. 遍历(外层遍历背包容量,内层遍历物品,或者反过来都可以,这里用外层遍历金额)
        for (int i = 1; i <= amount; ++i) {
            // 尝试每一枚硬币
            for (int coin : coins) {
                // 如果当前金额 i 能容下这枚硬币
                if (i - coin >= 0) {
                    // 状态转移方程:取当前值 和 (凑足剩余金额所需个数 + 1) 的较小值
                    dp[i] = min(dp[i], dp[i - coin] + 1);
                }
            }
        }

        // 4. 返回结果
        // 如果 dp[amount] 还是初始值,说明没有硬币组合能凑出这个金额
        if (dp[amount] > amount) {
            return -1;
        }
        
        return dp[amount];
    }
};

最长递增子序列

最长递增子序列

最长递增子序列(序列类 DP)

关注的是"位置"。

dp[i] 的 i 代表数组下标。

逻辑:数值大小不是状态索引,数值只是判断条件。我在乎的是**"我是排在第几个的人"以及"我能不能接在别人后面"**。

回头看:回头看 0 到 i-1 的所有下标(位置回溯)。

cpp 复制代码
class Solution {
public:
    int lengthOfLIS(vector<int>& nums) {
        int n = nums.size();
        if (n == 0) return 0;

        // 1. 定义状态
        // dp[i]:以 nums[i] 结尾的最长递增子序列长度
        // 既然是求长度,初始化为 1 (因为每个数字本身就是长度 1 的序列)
        vector<int> dp(n, 1);
        
        int maxAns = 1; // 用于记录全局最大值

        // 2. 外层循环:i 从 1 到 n-1
        // 这一步是在确定:我们现在要解决哪一个数字的"归属"问题
        for (int i = 1; i < n; ++i) {
            
            // 3. 内层循环:j 从 0 到 i-1 (回顾历史)
            // 这一步是在做:"选秀"。i 看着前面的 j,挑选能接在谁后面
            for (int j = 0; j < i; ++j) {
                
                // 4. 核心判断
                // 只有当 nums[i] 比 nums[j] 大,才能排在 j 后面
                if (nums[i] > nums[j]) {
                    
                    // 5. 状态转移
                    // 我有两种选择:
                    // A. 保持现在的 dp[i] (可能我之前已经跟了一个很长的大哥了)
                    // B. 跟随 nums[j],长度变成 dp[j] + 1
                    // 我要选更长的那个
                    dp[i] = max(dp[i], dp[j] + 1);
                }
            }
            // 每算完一个人的 dp[i],都跟全局最大值比一下
            maxAns = max(maxAns, dp[i]);
        }

        return maxAns;
    }
};

俄罗斯套娃

先排序,在dp

由于时间复杂度是O(N2)导致超时
需要用贪心加二分查找,后续学习

cpp 复制代码
class Solution {
public:
    int maxEnvelopes(vector<vector<int>>& envelopes) {
        int n = envelopes.size();
         if(n == 0) return 0;

         sort(envelopes.begin(),envelopes.end(),[](const vector<int>& a,const vector<int>& b){
            if(a[0] !=  b[0]){
                return a[0]<b[0];
            }
            return a[1]<b[1];
         });

         vector<int> dp(n,1);
         int max_res = 1;

         for(int i = 1;i<n;i++){
            for(int j = 0;j<i;j++){
                if(envelopes[i][0]>envelopes[j][0]&&
                    envelopes[i][1]>envelopes[j][1]){
                        dp[i] = max(dp[i],dp[j]+1);
                    }
            }
            max_res = max(max_res,dp[i]);
         }
         return max_res;

    }
};

目标和

目标和

01背包问题,和前文的零钱问题(无限背包,零钱无限用)和最长递增子序列(序列dp,关注下标)不同

问题拆解:
sum ( P ) − sum ( N ) = target \text{sum}(P) - \text{sum}(N) = \text{target} sum(P)−sum(N)=target我们可以利用总和 sum ( nums ) = sum ( P ) + sum ( N ) \text{sum}(\text{nums}) = \text{sum}(P) + \text{sum}(N) sum(nums)=sum(P)+sum(N) 来变形: sum ( P ) − sum ( N ) = target sum ( P ) − sum ( N ) + sum ( nums ) = target + sum ( nums ) sum ( P ) − sum ( N ) + ( sum ( P ) + sum ( N ) ) = target + sum ( nums ) 2 × sum ( P ) = target + sum ( nums ) sum ( P ) = target + sum ( nums ) 2 \begin{aligned} \text{sum}(P) - \text{sum}(N) &= \text{target} \\ \text{sum}(P) - \text{sum}(N) + \text{sum}(\text{nums}) &= \text{target} + \text{sum}(\text{nums}) \\ \text{sum}(P) - \text{sum}(N) + (\text{sum}(P) + \text{sum}(N)) &= \text{target} + \text{sum}(\text{nums}) \\ 2 \times \text{sum}(P) &= \text{target} + \text{sum}(\text{nums}) \\ \text{sum}(P) &= \frac{\text{target} + \text{sum}(\text{nums})}{2} \end{aligned} sum(P)−sum(N)sum(P)−sum(N)+sum(nums)sum(P)−sum(N)+(sum(P)+sum(N))2×sum(P)sum(P)=target=target+sum(nums)=target+sum(nums)=target+sum(nums)=2target+sum(nums)

转换之后的问题就是:请在 nums 中挑出一些数字,让它们的和恰好等于 (target + sum) / 2。问有多少种挑法?

值得注意的一点就是必须要第二个for循环要倒序排列,因为01背包问题一个数字只能用一次

如果顺序排列的话就会导致元素的重复使用(我之前用这个数填过一次了,后来我继续使用这个数字去填)

cpp 复制代码
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum  = 0;
        for(int n : nums){
            sum += n;
        }
        if(abs(target)>sum) return 0;
        if((target+sum)%2 == 1) return 0;

        int bagsize = (target+sum) / 2;

        vector<int> dp(bagsize+1,0);
        dp[0] =1;
        for(int num :nums){
            for(int j = bagsize;j>=num;j--){
                dp[j] = dp[j] + dp[j-num];
            }
        }
        return dp[bagsize];


    }
};

最大子数组和

最大子数组和

cpp 复制代码
class Solution {
public:
    int maxSubArray(vector<int>& nums) {
        int cur = nums[0];
        int max_res = nums[0];

        for(int i = 1;i<nums.size();i++){

            cur = max(nums[i],cur + nums[i]);
            if(cur>max_res){
                max_res = cur;
            }
        }
        return max_res;
    }
};
相关推荐
霑潇雨2 小时前
题解 | 分析每个商品在不同时间段的销售情况
数据库·sql·算法·笔试
季明洵2 小时前
Java中哈希
java·算法·哈希
jaysee-sjc2 小时前
【练习十】Java 面向对象实战:智能家居控制系统
java·开发语言·算法·智能家居
cici158742 小时前
基于MATLAB实现eFAST全局敏感性分析
算法·matlab
gihigo19982 小时前
MATLAB实现K-SVD算法
数据结构·算法·matlab
dyyx1112 小时前
C++编译期数据结构
开发语言·c++·算法
Swift社区2 小时前
LeetCode 384 打乱数组
算法·leetcode·职场和发展
running up that hill2 小时前
日常刷题记录
java·数据结构·算法
Loo国昌2 小时前
【LangChain1.0】第十四阶段:Agent最佳设计模式与生产实践
人工智能·后端·算法·语言模型·架构