动态规划算法:
对每个决策都进行求解,放在表格里,最后选择最优解。
动态规划,英文:Dynamic Programming,简称DP。
如果某一问题有很多重叠子问题,使用动态规划是最有效的。
所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的,
动规思路五部曲:
1、确定dp数组及其下标意义。
2、确定递推公式。
3、确定dp数组如何初始化。
4、确定遍历顺序。
5、举例推导dp数组。
1、
1.1 斐波那契数
斐波那契数 (通常用
F(n)
表示)形成的序列称为 斐波那契数列 。该数列由0
和1
开始,后面的每一项数字都是前面两项数字的和。也就是:F(0) = 0,F(1) = 1 F(n) = F(n - 1) + F(n - 2),其中 n > 1给定
n
,请计算F(n)
。
class Solution {
public:
int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
vector<int> dp(n + 1);//dp[i]代表第i个数时的值
dp[0] = 0;//初始化
dp[1] = 1;//初始化
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];//状态转移方程
}
return dp[n];
}
};
1.2 爬楼梯
假设你正在爬楼梯。需要
n
阶你才能到达楼顶。每次你可以爬
1
或2
个台阶。你有多少种不同的方法可以爬到楼顶呢?这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。可转到 ==》 2.2.3 爬楼梯(进阶版)
class Solution {
public:
int climbStairs(int n) {
/*
dp[i-1],上i-1层楼梯时,有dp[i-1]种方法,那此时再上一层就是 dp[i] 了
dp[i-2],上i-2层楼梯时,有dp[i-2]种方法,那此时再上两层就是 dp[i] 了
因此:递推公式为:dp[i] = dp[i-1] + dp[i-2]
*/
if (n <= 1) return 1;//防止空指针
vector<int> dp(n + 1);//dp[i]代表爬第i个台阶时的方案数
dp[0] = 1;//在第0个台阶时,不动也是一种方法
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
};
1.3 使用最小花费爬楼梯
给你一个整数数组
cost
,其中cost[i]
是从楼梯第i
个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。你可以选择从下标为
0
或下标为1
的台阶开始爬楼梯。请你计算并返回达到楼梯顶部的最低花费。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
/*
将cost的size全走完,再走一步则到达终点,求走到终点的最少总花费。
注意!!!
从第0阶出发时最少花费为cost[0]
从第1阶出发时最少花费最便宜的那阶花费
但是!!!
走到第0阶时和走到第1阶时并不需要任何花费,因此0和1都初始化为0!!!!
*/
if (cost.size() < 0) return 0;
int n = cost.size();
vector<int> dp(n + 1, 0);//dp[i]表示走到第i阶时所花费的最少费用
dp[0] = 0;//走到第0阶时 并不需要任何花费
dp[1] = 0;// 1
for (int i = 2; i <= n; i++) {
dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
}
//for (int i : dp) cout << i << endl;
return dp[n];
}
};
优化空间复杂度版本:
dp[i]就是由前两位推出来的,因此可以不用dp数组,而是直接只用三个变量搞定。
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int dp0 = 0;
int dp1 = 0;
for (int i = 2; i <= cost.size(); i++) {
int dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2]);
dp0 = dp1;
dp1 = dpi;
}
return dp1;
}
};
1.4 不同路径
一个机器人位于一个
m x n
网格的左上角 (起始点在下图中标记为 “Start” )。机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
动态规划解法:
//法一:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
//将左边缘及上边缘全部元素都直接初始化为1,因为路径都只有一条。
for (int i = 0; i < m; i++) {
for (int j = 0; j <n; j++) {
if (i == 0) dp[i][j] = 1;//相当于 ==》(dp[0][j] = 1;//初始化)
else if (j == 0) dp[i][j] = 1;//相当于 ==》(dp[i][0] = 1;//初始化)
else if (i >= 1 && j >= 1) dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
}
};
//法二:
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
for (int i = 0; i < m; i++) dp[i][0] = 1;//初始化左边缘
for (int j = 0; j < n; j++) dp[0][j] = 1;//初始化上边缘
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
//每个位置的方案数都是其上一个方案数+其左一格的方案数之和
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];//返回右下角的方案数
}
};
深搜解法:(转化为求二叉树叶子节点的个数) 但力扣上提交会超时。
数论解法:
1.5
2、 背包问题(动态规划)
教程参考链接:
01:物品只能用一次,常是外层遍历物品,内层遍历价值(容量),遍历价值时需逆向(--);
完全:物品可用多次,遍历价值时需正向(++);
无顺序要求的组合则外层物品,内层价值;(常用的)
有顺序要求的排列则外层价值,内层物品。
dp[j]表示背包内物品总价值为j时可以获得的最大价值, 0<=j<=W
2.1 01背包(每个物品只能用一次)-逆向
二维dp数组:
dp[i][j]表示将前i件物品装进限重为j的背包可以获得的最大价值, 0<=i<=N, 0<=j<=W
- 不放物品i:由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j]。(其实就是当物品i的重量大于背包j的重量时,物品i无法放进背包中,所以背包内的价值依然和前面相同。)
- 放物品i:由dp[i - 1][j - weight[i]]推出,dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]的时候不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值。
一维dp数组:
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。
所以递归公式为:(求限定容量时的最大价值)
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
(求限定容量时的最大方案数,即组合类的最大方案数)实际应用看2.1.3目标和
dp[0] = 1;//求方案数时一定记得要先初始化第0号元素(啥都不放也是一种方案) dp[j] += dp[j - nums[i]];
2.1.1 分割等和子集
给你一个 只包含正整数 的 非空 数组
nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
是求 给定背包容量,能不能装满这个背包。
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
for (int n : nums) sum += n;
if (sum % 2 == 1) return false;//元素和必须是奇数
int w = sum / 2;//期望背包容量,此题中同时也是背包价值
vector<int> dp(w + 1, 0);//初始化dp数组,往里放进w+1个0
for (int i = 0; i < nums.size(); i++) {
for (int j = w; j >= nums[i]; j--) {
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);//获得每个容量对应的最大价值
}
}
if (dp[w] == w) return true;//若容量为元素和一半时,其价值刚好为元素和一半
else return false;
}
};
2.1.2 最后一块石头的重量 II
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
是求 给定背包容量,尽可能装,最多能装多少
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
/*
就是求出两堆石头的最小差值
即 将全部石头分成尽可能相等的两堆,求其差值并返回
*/
int sum = 0;
for (int s : stones) sum += s;
int w = sum / 2;
vector<int> dp(w + 1, 0);
for (int i = 0; i < stones.size(); i++) {
for (int j = w; j >= stones[i]; j--) {
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
}
return abs(sum - dp[w] - dp[w]);
}
};
2.1.3 目标和
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
是求 给定背包容量,装满背包有多少种方法。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
/*
思路一:
转换为子集求和问题:
sum(P) - sum(N) = target
sum(P) + sum(N) + sum(P) - sum(N) = target + sum(P) + sum(N)
2 * sum(P) = target + sum(nums)
即 找到nums的一个子集 P,使得sum(P) = (target + sum(nums)) / 2
且 target + sum(nums)必须是偶数,否则输出为0
-------------------------------------------------------------
思路二:
假设加法的总和为x,那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = target
x = (target + sum) / 2
此时问题就转化为,装满容量为x的背包,有几种方法。
这里的x,就是bagSize,也就是我们后面要求的背包容量w。
*/
int sum = 0;
for (int n : nums) sum += n;
//注意这里的target绝对值,一个是会导致后面的vectoe初始化下标越界,一个是要求的差值大于所有元素和
if ((target + sum) % 2 == 1 || sum < abs(target)) return 0;
int w = (target + sum) / 2;
vector<int> dp(w + 1, 0);
//容量为0时,啥都不放也是一种方案(容量为0的背包,拿第0件物品,有1种填满方式,那就是不填,因此第0号元素初始化为1)
dp[0] = 1;
/*
此代码是一维dp,如果是二维dp的话:
dp[0][0] = 1;//意义与一维dp中的 dp[0] = 1;同理
但 第一列也不能全部初始化为1,原因是 题目中物品的重量可以为0,所以容量为0的背包,不止一种放入方式。
*/
for (int i = 0; i < nums.size(); i++) {
for (int j = w; j >= nums[i]; j--) {
/*
遍历到i物品,此时dp[j]有两种来源:
不放入i物品,填满容量为j的背包的方式为,上一层的值:dp[j]
放入i物品,填满容量为j的背包的方式为,上一层减掉物品重量的值:dp[j - nums[i]]
不放第i件物品时,dp[j] = dp[j - 1]
因此,以下两条式子等价
dp[j] = dp[j - 1] + dp[j - nums[i]];
dp[j] = dp[j] + dp[j - nums[i]];
再简化就可以写成 dp[j] += dp[j - nums[i]];
*/
dp[j] += dp[j - nums[i]];//dp中每个元素都是背包容量为j时,可放物品的方案数
}
}
return dp[w];
}
};
2.1.4 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
是求 给定背包容量,装满背包最多有多少个物品。
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
/*
有 m 和 n 两个维度的01背包
而不同长度的字符串就是不同大小的待装物品。
dp[i][j]表示使用i个0和j个1时字符串的最大数量(i和j是两个容量,最大数量就是最大价值)
该题目中每个字符串的价值都为1,即其个数
*/
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));//初始化
for (string curStr : strs) {//遍历字符串数组(遍历物品)
int oneNum = 0;
int zeroNum = 0;
for (char c : curStr) {//遍历当前字符串中的所有字符
if (c == '1') oneNum++;
else zeroNum++;
}
//此时已知每个字符串中的 0 和 1 的数量
for (int i = m; i >= zeroNum; i--) {//遍历第一个容量m
for (int j = n; j >= oneNum; j--) {//遍历第二个容量n
dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
}
}
}
return dp[m][n];
}
};
2.2 完全背包(每个物品可用多次)-正向
**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。//平时常用的
**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。//需要排列顺序时用的
需要排列顺序时:
如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面!
2.2.1 零钱兑换 II
给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
假设每一种面额的硬币有无限个。
题目数据保证结果符合 32 位带符号整数。
class Solution {
public:
int change(int amount, vector<int>& coins) {
/*
钱币数量不限 ===》 完全背包
*/
vector<int> dp(amount + 1, 0);
dp[0] = 1;
//for (int i = 0; i < coins.size(); i++) {
for (int i : coins) {
for (int j = i; j <= amount; j++) {
dp[j] += dp[j - i];
}
}
return dp[amount];
}
};
2.2.2 组合总和 Ⅳ
给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。
题目数据保证答案符合 32 位整数范围。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
/*
背包容量为target,物品数量为nums.size(),每个物品价值结尾nums当前的元素
本题是 需要排列顺序,因此两层循环中:外层容量,内层物品
*/
vector<int> dp(target + 1, 0);
dp[0] = 1;//求的是组合的个数,即方案数。
for (int j = 0; j <= target; j++) {
for (int i : nums) {
if (j < i) continue;
if (dp[j] >= INT_MAX - dp[j - i]) continue;//C++测试用例有两个数相加超过int的数据,所以需要 加上 该句
dp[j] += dp[j - i];
}
}
return dp[target];
}
};
2.2.3 爬楼梯(进阶版)
卡码网:57. 爬楼梯(leetcode上并没有原题,是在卡码网上的)
假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?
注意:给定 n 是一个正整数。
#include<iostream>
#include<vector>
using namespace std;
int main() {
/*
总台阶数 为背包容量
每次可走的步数 就是物品价值
步数可重复走,即完全背包
1、2 和 2、1 是上三台阶的 两种不同的 方案,即求的是排列问题,有顺序要求
完全背包 ==》遍历容量时 为正向遍历
排列问题 ==》两层循环为 外层容量,内层物品
dp[0]时不走也是一种方案 ==》dp[0]初始化为1
*/
int n, m;
cin >> n >> m;
vector<int> dp(n + 1, 0);//dp[i]代表总共要走的i阶时的的方案数
dp[0] = 1;//要走0阶时,直接不走也是一种方案
for (int i = 0; i <= n; i++) {//遍历背包容量
for (int j = 1; j <= m; j++) {//遍历物品
if (i - j >= 0) dp[i] += dp[i - j];
}
}
cout << dp[n] << endl;
}
3、
3.1 无重复字符的最长子串
给定一个字符串
s
,请你找出其中不含有重复字符的 最长子串 的长度。
class Solution {
public:
int lengthOfLongestSubstring(string s) {
/*
对于第i个字符:
若在dp[i-1]所代表的子串中出现,
那么从所出现的位置j的下一个位置到i,构成了以i结尾的不重复子串。
即dp[i] = i-j;
若不在前面的dp[i-1]子串中出现,那么i-1子串加上i字符构成了i子串,
因此 dp[i] = dp[i-1]+1
无需担心j位置后是否又出现字符i,因为前面的子串本身就是不重复的,不可能存在两个字符i。
最大值: 返回 max(dp[i]) 即可
*/
if (s.size() <= 1) return s.size();
int maxSize = 1;
vector<int> dp(s.size(), 1);//dp[i]代表以第i个字符结尾的无重复字符字串的长度
for (int i = 1; i < s.size(); i++) {
bool flag = false;//当前字符在i-1的字串中是否出现过重复
for (int j = i - 1; j >= i - dp[i - 1] && j >= 0; j--) {
if (s[i] == s[j]) {
flag = true;
dp[i] = i - j;//重复,则i重构新子串
break;
}
}
if (!flag) {//没有重复,则i的子串长度为i-1长度的+1
dp[i] = dp[i - 1] + 1;
}
maxSize = max(maxSize, dp[i]);
//cout << i << " " << dp[i] << endl;
}
return maxSize;
}
};