线性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、源码详解

cpp 复制代码
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)

    java 复制代码
    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)
java 复制代码
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)
java 复制代码
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)
java 复制代码
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];
                else
                    dp[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

java 复制代码
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 是天数

java 复制代码
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

java 复制代码
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 次串联后最大子数组之和

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

生成平衡数组的方案数

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

统计特殊子序列的数目

相关推荐
csdn_aspnet17 分钟前
C# 检查某个点是否存在于圆扇区内(Check whether a point exists in circle sector or not)
算法·c#
梁下轻语的秋缘1 小时前
C/C++滑动窗口算法深度解析与实战指南
c语言·c++·算法
iFulling1 小时前
【数据结构】第八章:排序
数据结构·算法
一只鱼^_1 小时前
力扣第448场周赛
数据结构·c++·算法·leetcode·数学建模·动态规划·迭代加深
寂空_2 小时前
【算法笔记】动态规划基础(二):背包dp
笔记·算法·动态规划
搏博2 小时前
神经网络在专家系统中的应用:从符号逻辑到连接主义的融合创新
人工智能·深度学习·神经网络·算法·机器学习
Eric.Lee20213 小时前
数据集-目标检测系列- 印度人脸 检测数据集 indian face >> DataBall
人工智能·算法·目标检测·计算机视觉·yolo检测·印度人脸检测
vibag3 小时前
启发式算法-禁忌搜索算法
java·算法·启发式算法·禁忌搜索
白露秋483 小时前
数据结构——算法复杂度
数据结构·算法·哈希算法