当前位置: 首页 > news >正文

线性DP(动态规划)

线性DP的概念(视频)

学习线性DP之前,请确保已经对递推有所了解。

一、概念

1、动态规划

不要去看网上的各种概念,什么无后效性,什么空间换时间,会越看越晕。从做题的角度去理解就好了,动态规划就可以理解成一个 有限状态自动机,从一个初始状态,通过状态转移,跑到终止状态的过程。

2、线性动态规划

线性动态规划,又叫线性DP,就是在一个线性表上进行动态规划,更加确切的说,应该是状态转移的过程是在线性表上进行的。我们考虑有 0 到 n 这 n+1 个点,对于第 i 个点,它的值取决于 0 到 i-1 中的某些点的值,可以是求 最大值、最小值、方案数 等等。

很明显,如果一个点 i 可以从 i-1 或者 i-2 过来,求到达第 i 号点的方案数,就是我们之前学过的斐波那契数列了,具体可以参考这篇文章:递推。

二、例题解析

1、题目描述

给定一个 n,再给定一个 n(n ≤ 1000) 个整数的数组  cost, 其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦支付此费用,即可选择向上爬 1个 或者 2个 台阶。可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,请计算并返回达到楼梯顶部的最低花费。

2、算法分析

我们发现这题和之前的爬楼梯很像,只不过从原来的计算 方案数 变成了计算 最小花费。尝试用一个数组来表示状态:f[i] 表示爬到第 i 层的最小花费。

由于每次只能爬 1个或者 2个台阶,所以 f[i] 这个状态只能从 f[i-1] 或者 f[i-2] 转移过来:

1)如果从 i-1 层爬上来,需要的花费就是 f[i-1] + cost[i-1];

2)如果从 i-2 层爬上来,需要的花费就是 f[i-2] + cost[i-2];

没有其他情况了,而我们要 求的是最小花费,所以 f[i] 就应该是这两者的小者,得出状态转移方程:

f[i] = min(f[i-1] + cost[i-1], f[i-2] + cost[i-2])

然后考虑一下初始情况 f[0] 和 f[1],根据题目要求它们都应该是 0。

3、源码详解

int min(int a, int b) {return a < b ? a : b;                   // (1)
}int minCostClimbingStairs(int* cost, int n){int i;                                  // (2)int f[1001] = {0, 0};                   // (3)for(i = 2; i <= n; ++i) {               // (4)f[i] = min(f[i-1] + cost[i-1], f[i-2] + cost[i-2]);}return f[n];                            // (5)
}
  1. (1) 为了方便求最小值,我们实现一个最小值函数 min,直接利用 C语言 的 条件运算符 就可以了;
  2. (2) 然后开始动态规划的求解,首先定义一个循环变量;
  3. (3) 再定义一个数组 f[i] 代表从第 0 阶爬到第 i 阶的最小花费,并且初始化第 0 项 和 第 1 项;
  4. (4) 然后一个 for 循环,从第 2 项开始,直接套上状态转移方程就能计算每一项的值了;
  5. (5) 最后返回第 n 项即可;

三、再谈动态规划

经典的线性DP有很多,比如:最长递增子序列、背包问题 是非常经典的线性DP了。建议先把线性DP搞清楚以后再去考虑其它的动态规划问题。

而作为动态规划的通解,主要分为以下几步:

    1、设计状态

    2、写出状态转移方程

    3、设定初始状态

    4、执行状态转移

    5、返回最终的解

一、基本概念

学习动态规划,如果一上来告诉你:最优子结构、重叠子问题、无后效性 这些抽象的概念,那么你可能永远都学不会这个算法,最好的方法就是从一些简单的例题着手,一点一点去按照自己的方式理解,而不是背概念。

对于动态规划问题,最简单的就是线性动态规划,这堂课我们就利用一些,非常经典的线性动态规划问题来进行分析,从而逐个击破。

二、常见问题

1、爬楼梯

  1. 问题描述:有一个 n 级楼梯,每次可以爬 1 或者 2 级。问有多少种不同的方法可以爬到第 n 级。
  2. 状态:dp[i] 表示爬到第 i 级楼梯的方案数。
  3. 初始状态:dp[0] = dp[1] = 1
  4. 状态转移方程:dp[i] = dp[i-1] + dp[i-2]。 (对于爬到第 i 级,可以从 i-1 级楼梯爬过来,也可以从 i-2 级楼梯爬过来)
  5. 状态数:O(n)
  6. 状态转移消耗:O(1)
  7. 时间复杂度:O(n)
    class Solution {
    public:int climbStairs(int n) {vector<int> dp(n+1);dp[0] = dp[1] = 1;for (int i = 2; i < dp.size(); i++)dp[i] = dp[i - 1] + dp[i - 2];return dp[n];}
    };

2、最大子数组和(最大子段和)

  1. 问题描述:给定一个 n 个元素的数组 arr[],求一个子数组,并且它的元素和最大,返回最大的和。
  2. 状态:dp[i] 表示以第 i 个元素结尾的最大子数组和。
  3. 初始状态:dp[0] = arr[0](可以为负数)
  4. 状态转移方程:dp[i] = arr[i] + max(dp[i-1], 0)。 (因为是以第i个元素结尾,所以 arr[i]必选, dp[i-1] 这部分是以第 i-1 个元素结尾的,可以不选或者选,完全取决于它是否大于0,所以选和不选取大者)
  5. 状态数:O(n)
  6. 状态转移消耗:O(1)
  7. 时间复杂度:O(n)
class Solution {
public:int maxSubArray(vector<int>& arr) {vector<int> dp(arr.size()+1);dp[0] = arr[0];int maxSum=dp[0];for(int i=1;i<arr.size();i++){dp[i] = arr[i] + max(dp[i-1], 0);maxSum = max(maxSum, dp[i]);}return maxSum;}
};还有一个双O(1)的方法
class Solution {
public:int maxSubArray(vector<int>& arr) {if (arr.empty()) return 0;int currentSum = arr[0];int maxSum = arr[0];for (int i = 1; i < arr.size(); ++i) {currentSum = max(currentSum + arr[i], arr[i]);maxSum = max(maxSum, currentSum);}return maxSum;}
};

3、最长递增子序列

  1. 问题描述:给定一个 n 个元素的数组 arr[],求一个最大的子序列的长度,序列中元素单调递增。
  2. 状态:dp[i] 表示以第 i 个元素结尾的最长递增子序列的长度。
  3. 初始状态:dp[0] = 1
  4. 状态转移方程:dp[i] = max(dp[i], dp[j] + 1)。(arr[j] < arr[i]) (对于所有下标比 i 小的下标 j,并且满足 arr[j] < arr[i] 的情况,取所有这里面 dp[j] 的最大值 加上 1 就是 dp[i] 的值,当然可能不存在这样的 j,那么这时候 dp[i] 的值就是 1)
  5. 状态数:O(n)
  6. 状态转移消耗:O(n)
  7. 时间复杂度:O(n^2)
class Solution {
public:int lengthOfLIS(vector<int>& nums) {vector<int> dp(nums.size(), 1);int maxlength = 1;for (int i = 1; i < nums.size(); i++) {for (int j = 0; j < i; j++) {if (nums[j] < nums[i]) {dp[i] = max(dp[i], dp[j] + 1);maxlength = max(maxlength, dp[i]);}}}return maxlength;}
};

4、数字三角形

  1. 问题描述:给定一个 n 行的三角形 triangle[][],找出自顶向下的最小路径和。每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1。
  2. 状态:dp[i][j] 表示从顶部走到 (i, j) 位置的最小路径和。
  3. 初始状态:dp[0][0] = triangle[0][0];起点就是顶部,路径和只能是它自己。
  4. 状态转移方程:dp[i][j] = max(dp[i-1][j-1], dp[i-1][j]) + triangle[i][j]。(走到 (i,j) 的路径只能从两个方向来:从左上方来(即从 (i-1, j-1) 走到 (i,j))从上方来(即从 (i-1, j) 走到 (i,j))所以我们只需要比较这两个方向的最小值,加上当前位置的值即可。)
  5. 状态数:O(n^2)
  6. 状态转移消耗:O(1)
  7. 时间复杂度:O(n^2)

class Solution {
public:int minimumTotal(vector<vector<int>>& triangle) {int n = triangle.size();vector<vector<int>> dp(n, vector<int>(n, 0));int minsum = 0;dp[0][0] = triangle[0][0];for (int i = 1; i < triangle.size(); i++) {for (int j = 0; j < triangle[i].size(); j++) {if (j == 0)dp[i][j] = dp[i - 1][j] + triangle[i][j];else if (j == i)dp[i][j] = dp[i - 1][j - 1] + triangle[i][j];elsedp[i][j] =min(dp[i - 1][j - 1], dp[i - 1][j]) + triangle[i][j];}}return *min_element(dp[n - 1].begin(), dp[n - 1].end());}
};

5、股票系列

力扣有一些非常经典的股票问题,可以自己尝试去看一下。 

121. 买卖股票的最佳时机这个解法不是dp

class Solution {
public:int maxProfit(vector<int>& prices) {int cost = INT_MAX, profit = 0;for (int price : prices) {cost = min(cost, price);profit = max(profit, price - cost);}return profit;}
};

122. 买卖股票的最佳时机 II

 你需要在每一天决定是否 买、卖、或不操作 股票,最终获得 最大利润 。
限制条件: 任何时候最多只能持有一股股票(即必须先卖出才能再买)。

我们每天的状态只有两种可能:

  • 状态 0: 手里没有股票(可以买)
  • 状态 1: 手里有股票(可以卖)

我们用一个二维数组 dp[i][0]dp[i][1] 来记录第 i 天结束后,这两种状态下的 最大利润

从第 i-1 天的状态推导第 i 天的状态 

(1) 当前状态 0(手里没有股票)
  • 可能来源:
    • 昨天也没股票(今天没买),利润不变:dp[i-1][0]
    • 昨天有股票(今天卖了),利润 = 昨天有股票的利润 + 今天卖出的价格:dp[i-1][1] + prices[i]
  • 取最大值:dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
(2) 当前状态 1(手里有股票)
  • 可能来源:
    • 昨天也有股票(今天没卖),利润不变:dp[i-1][1]
    • 昨天没股票(今天买了),利润 = 昨天没股票的利润 - 今天买入的价格:dp[i-1][0] - prices[i]
  • 取最大值:dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])

4. 初始状态

  • 第 0 天(第一天):
    • dp[0][0] = 0(没有买,利润为 0)
    • dp[0][1] = -prices[0](买了,但还没卖,利润为负)

5. 最终答案

最后一天 不持有股票 的利润一定是最大的(因为持有股票还没卖的话,利润可能不是最大):return dp[n-1][0] // n 是天数

class Solution {
public:int maxProfit(vector<int>& prices) {int n = prices.size();vector<vector<int>> dp(n, vector<int>(2, 0));// 初始状态dp[0][0] = 0;dp[0][1] = -prices[0];for (int i = 1; i < n; ++i) {dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]);dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]);}return dp[n-1][0];  // 返回最后一天不持有股票的最大利润}
};

123. 买卖股票的最佳时机 III

class Solution {
public:int maxProfit(vector<int>& prices) {int n = prices.size();vector<vector<int>> dp(n, vector<int>(4));dp[0][0] = -prices[0], dp[0][1] = 0, dp[0][2] = -prices[0],dp[0][3] = 0;for (int i = 1; i < n; i++) {dp[i][0] = max(-prices[i], dp[i - 1][0]);dp[i][1] = max(dp[i - 1][0] + prices[i], dp[i - 1][1]);dp[i][2] = max(dp[i - 1][1] - prices[i], dp[i - 1][2]);dp[i][3] = max(dp[i - 1][2] + prices[i], dp[i - 1][3]);}return max(dp[n - 1][1], dp[n - 1][3]);}
};

188. 买卖股票的最佳时机 IV

fucking-algorithm/动态规划系列/团灭股票问题.md at master · labuladong/fucking-algorithm

309. 买卖股票的最佳时机含冷冻期

714. 买卖股票的最佳时机含手续费

6、最短路径问题

Dijkstra 本质也是一个动态规划问题。只不过通过不断更新状态,来实现状态转移。

7、背包问题

0/1背包DP

完全背包DP

三、细节剖析

1、问题求什么,状态就尽量定义成什么,有了状态,再去尽力套状态转移方程。

2、动态规划的时间复杂度等于 状态数 x 状态转移 的消耗;

3、状态转移方程中的 i 变量导致数组下标越界,从而可以确定哪些状态是初始状态;

4、状态转移的过程一定是单向的,把每个状态理解成一个结点,状态转移理解成边,动态规划的求解就是在一个有向无环图上进行递推计算。

5、因为动态规划的状态图是一个有向无环图,所以一般会和拓扑排序联系起来。

 题目

接龙数列

数组切分

最大魅力值

0、自然语言视频题解

  1. 接龙数列
  2. 数组切分
  3. 最大魅力值

3、C++视频题解

  1. 接龙数列
  2. 数组切分
  3. 最大魅力值

  使用最小花费爬楼梯

  打家劫舍

  删除并获得点数

  买卖股票的最佳时机(带字幕版)

递推

斐波那契数

第 N 个泰波那契数

剑指 Offer 10- II. 青蛙跳台阶问题

三步问题

剑指 Offer 10- I. 斐波那契数列

爬楼梯

剑指 Offer II 003. 前 n 个数字二进制中 1 的个数

旋转函数

访问完所有房间的第一天

线性DP / 状态转移 O(C)

使用最小花费爬楼梯

剑指 Offer II 088. 爬楼梯的最少成本

解决智力问题

打家劫舍

剑指 Offer II 089. 房屋偷盗

按摩师

打家劫舍 II

剑指 Offer II 090. 环形房屋偷盗

剑指 Offer 46. 把数字翻译成字符串

解码方法

1 比特与 2 比特字符

使序列递增的最小交换次数

恢复数组

秋叶收藏集

删除并获得点数

完成比赛的最少时间

线性DP / 状态转移 O(n)

单词拆分

分隔数组以得到最大和

最低票价

跳跃游戏 II

带因子的二叉树

最大子数组和

剑指 Offer 42. 连续子数组的最大和

连续数列

最大子数组和

任意子数组和的绝对值的最大值

乘积最大子数组

乘积为正数的最长子数组长度

删除一次得到子数组最大和

最长递增子序列

最长数对链

最长递增子序列的个数

摆动序列

最长湍流子数组

最长递增子序列

最长字符串链

堆箱子

俄罗斯套娃信封问题

马戏团人塔

使数组 K 递增的最少操作次数

股票问题

股票平滑下跌阶段的数目

买卖股票的最佳时机 II

买卖股票的最佳时机含手续费

最佳买卖股票时机含冷冻期

买卖股票的最佳时机 III

前缀最值

有效的山脉数组

将每个元素替换为右侧最大元素

买卖股票的最佳时机

最佳观光组合

数组中的最长山脉

适合打劫银行的日子

两个最好的不重叠活动

接雨水

移除所有载有违禁货物车厢所需的最少时间

接雨水 II

前缀和

分割字符串的最大得分

哪种连续子字符串更长

翻转字符

将字符串翻转到单调递增

删掉一个元素以后全为 1 的最长子数组

和为奇数的子数组数目

两个非重叠子数组的最大和

K 次串联后最大子数组之和

找两个和为目标值且不重叠的子数组

生成平衡数组的方案数

三个无重叠子数组的最大和

统计特殊子序列的数目

相关文章:

  • flask 获取各种请求数据:GET form-data x-www-form-urlencoded JSON headers 上传文件
  • 物联网智能项目之——智能家居项目的实现!
  • 开源项目实战学习之YOLO11:ultralytics-cfg-models-rtdetr(十一)
  • 循环缓冲区
  • 实验-组合电路设计1-全加器和加法器(数字逻辑)
  • 大数据:驱动技术创新与产业转型的引擎
  • 节流 和 防抖的使用
  • 【C语言练习】018. 定义和初始化结构体
  • ai之paddleOCR 识别PDF python312和paddle版本冲突 GLIBCXX_3.4.30
  • 提升办公效率的PDF转图片实用工具
  • 学习黑客资产威胁分析贴
  • 《MATLAB实战训练营:从入门到工业级应用》趣味入门篇-用声音合成玩音乐:MATLAB电子琴制作(超级趣味实践版)
  • NocoDB:开源的 Airtable 替代方案
  • 二叉树最近公共祖先(后序遍历,回溯算法)
  • springboot war包tomcat中运行报错,启动过滤器异常,一个或多个筛选器启动失败。
  • 关于Python:7. Python数据库操作
  • 经典算法 求解硬币组成问题
  • 基于大模型的肾结石诊疗全流程风险预测与方案制定研究报告
  • 软件测评如何保障质量与提升体验?从五方面详细说说
  • JSON 处理笔记
  • 俄乌互相空袭、莫斯科机场关闭,外交部:当务之急是避免局势紧张升级
  • 探索人类的心灵这件事,永远也不会过时
  • 央行:上市公司回购增持股票自有资金比例要求从30%下调至10%
  • 川大全职引进考古学家宫本一夫,他曾任日本九州大学副校长
  • 我国科研团队发布第四代量子计算测控系统
  • 抗战回望19︱《中国工程师学会四川考察团报告》:“将来重工业所在,以四川为最适宜之地点”