线性DP的概念(视频)
学习线性DP之前,请确保已经对递推有所了解。
不要去看网上的各种概念,什么无后效性,什么空间换时间,会越看越晕。从做题的角度去理解就好了,动态规划就可以理解成一个 有限状态自动机,从一个初始状态,通过状态转移,跑到终止状态的过程。
线性动态规划,又叫线性DP,就是在一个线性表上进行动态规划,更加确切的说,应该是状态转移的过程是在线性表上进行的。我们考虑有 0 到 n 这 n+1 个点,对于第 i 个点,它的值取决于 0 到 i-1 中的某些点的值,可以是求 最大值、最小值、方案数 等等。
很明显,如果一个点 i 可以从 i-1 或者 i-2 过来,求到达第 i 号点的方案数,就是我们之前学过的斐波那契数列了,具体可以参考这篇文章:递推。
给定一个 n,再给定一个 n(n ≤ 1000) 个整数的数组 cost, 其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦支付此费用,即可选择向上爬 1个 或者 2个 台阶。可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯,请计算并返回达到楼梯顶部的最低花费。
我们发现这题和之前的爬楼梯很像,只不过从原来的计算 方案数 变成了计算 最小花费。尝试用一个数组来表示状态: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。
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)
}
经典的线性DP有很多,比如:最长递增子序列、背包问题 是非常经典的线性DP了。建议先把线性DP搞清楚以后再去考虑其它的动态规划问题。
而作为动态规划的通解,主要分为以下几步:
1、设计状态
2、写出状态转移方程
3、设定初始状态
4、执行状态转移
5、返回最终的解
学习动态规划,如果一上来告诉你:最优子结构、重叠子问题、无后效性 这些抽象的概念,那么你可能永远都学不会这个算法,最好的方法就是从一些简单的例题着手,一点一点去按照自己的方式理解,而不是背概念。
对于动态规划问题,最简单的就是线性动态规划,这堂课我们就利用一些,非常经典的线性动态规划问题来进行分析,从而逐个击破。
class Solution {
public:
int climbStairs(int n) {
vector 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];
}
};
class Solution {
public:
int maxSubArray(vector& arr) {
vector dp(arr.size()+1);
dp[0] = arr[0];
int maxSum=dp[0];
for(int i=1;i& 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;
}
};
class Solution {
public:
int lengthOfLIS(vector& nums) {
vector 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;
}
};
(i,j)
的路径只能从两个方向来:从左上方来(即从 (i-1, j-1)
走到 (i,j)
)从上方来(即从 (i-1, j)
走到 (i,j)
)所以我们只需要比较这两个方向的最小值,加上当前位置的值即可。)
class Solution {
public:
int minimumTotal(vector>& triangle) {
int n = triangle.size();
vector> dp(n, vector(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());
}
};
力扣有一些非常经典的股票问题,可以自己尝试去看一下。
121. 买卖股票的最佳时机这个解法不是dp
class Solution {
public:
int maxProfit(vector& prices) {
int cost = INT_MAX, profit = 0;
for (int price : prices) {
cost = min(cost, price);
profit = max(profit, price - cost);
}
return profit;
}
};
122. 买卖股票的最佳时机 II
你需要在每一天决定是否 买、卖、或不操作 股票,最终获得 最大利润 。
限制条件: 任何时候最多只能持有一股股票(即必须先卖出才能再买)。
我们每天的状态只有两种可能:
我们用一个二维数组 dp[i][0]
和 dp[i][1]
来记录第 i
天结束后,这两种状态下的 最大利润 。
从第 i-1 天的状态推导第 i 天的状态
dp[i-1][0]
dp[i-1][1] + prices[i]
dp[i-1][1]
dp[i-1][0] - prices[i]
dp[0][0] = 0
(没有买,利润为 0)dp[0][1] = -prices[0]
(买了,但还没卖,利润为负)最后一天 不持有股票 的利润一定是最大的(因为持有股票还没卖的话,利润可能不是最大):return dp[n-1][0] // n 是天数
class Solution {
public:
int maxProfit(vector& prices) {
int n = prices.size();
vector> dp(n, vector(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& prices) {
int n = prices.size();
vector> dp(n, vector(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. 买卖股票的最佳时机含手续费
Dijkstra 本质也是一个动态规划问题。只不过通过不断更新状态,来实现状态转移。
0/1背包DP
完全背包DP
1、问题求什么,状态就尽量定义成什么,有了状态,再去尽力套状态转移方程。
2、动态规划的时间复杂度等于 状态数 x 状态转移 的消耗;
3、状态转移方程中的 i 变量导致数组下标越界,从而可以确定哪些状态是初始状态;
4、状态转移的过程一定是单向的,把每个状态理解成一个结点,状态转移理解成边,动态规划的求解就是在一个有向无环图上进行递推计算。
5、因为动态规划的状态图是一个有向无环图,所以一般会和拓扑排序联系起来。
接龙数列
数组切分
最大魅力值
使用最小花费爬楼梯
打家劫舍
删除并获得点数
买卖股票的最佳时机(带字幕版)
斐波那契数
第 N 个泰波那契数
剑指 Offer 10- II. 青蛙跳台阶问题
三步问题
剑指 Offer 10- I. 斐波那契数列
爬楼梯
剑指 Offer II 003. 前 n 个数字二进制中 1 的个数
旋转函数
访问完所有房间的第一天
使用最小花费爬楼梯
剑指 Offer II 088. 爬楼梯的最少成本
解决智力问题
打家劫舍
剑指 Offer II 089. 房屋偷盗
按摩师
打家劫舍 II
剑指 Offer II 090. 环形房屋偷盗
剑指 Offer 46. 把数字翻译成字符串
解码方法
1 比特与 2 比特字符
使序列递增的最小交换次数
恢复数组
秋叶收藏集
删除并获得点数
完成比赛的最少时间
单词拆分
分隔数组以得到最大和
最低票价
跳跃游戏 II
带因子的二叉树
剑指 Offer 42. 连续子数组的最大和
连续数列
最大子数组和
任意子数组和的绝对值的最大值
乘积最大子数组
乘积为正数的最长子数组长度
删除一次得到子数组最大和
最长数对链
最长递增子序列的个数
摆动序列
最长湍流子数组
最长递增子序列
最长字符串链
堆箱子
俄罗斯套娃信封问题
马戏团人塔
使数组 K 递增的最少操作次数
股票平滑下跌阶段的数目
买卖股票的最佳时机 II
买卖股票的最佳时机含手续费
最佳买卖股票时机含冷冻期
买卖股票的最佳时机 III
有效的山脉数组
将每个元素替换为右侧最大元素
买卖股票的最佳时机
最佳观光组合
数组中的最长山脉
适合打劫银行的日子
两个最好的不重叠活动
接雨水
移除所有载有违禁货物车厢所需的最少时间
接雨水 II
分割字符串的最大得分
哪种连续子字符串更长
翻转字符
将字符串翻转到单调递增
删掉一个元素以后全为 1 的最长子数组
和为奇数的子数组数目
两个非重叠子数组的最大和
K 次串联后最大子数组之和
找两个和为目标值且不重叠的子数组
生成平衡数组的方案数
三个无重叠子数组的最大和
统计特殊子序列的数目