动态规划
动态规划基本思想
动态规划(dynamic programming)中每一个状态是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优。
动态规划的步骤:
1.确定dp数组(dp table)以及下标的含义
2.确定递推公式
3.dp数组如何初始化
4。确定遍历顺序
5.举例推导dp数组
动态规划是自底向上,递归是自顶向下
动态规划一般都脱离了递归,而是由循环迭代完成计算。
关键是找到状态转移方程
剑指 Offer 10- I. 斐波那契数列
首先根据f(0)和f(1)计算ff(2),然后根据f(1)和f(2)计算f(3)
class Solution {
public:
int fib(int n) {
int MOD=1000000007;
if(n<2) return n;
int fibOne=0;
int fibTwo=1;
int fibN=0;
for(int i=2;i<=n;i++){
fibN=(fibOne+fibTwo)%MOD;
fibOne=fibTwo;
fibTwo=fibN;
}
return fibN;
}
};
剑指 Offer II 088. 爬楼梯的最少成本
数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。
每当爬上一个阶梯都要花费对应的体力值,一旦支付了相应的体力值,就可以选择向上爬一个阶梯或者爬两个阶梯。
请找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。
示例 1:
输入:cost = [10, 15, 20]
输出:15
解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。
示例 2:
输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1]
输出:6
解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。
dp[i]表示达到下标i的最小花费
class Solution {
public:
int minCostClimbingStairs(vector<int>& cost) {
int n=cost.size();
vector<int> dp(n+1);
dp[0]=0;// 默认第一步都是不花费体力的
dp[1]=0;
for(int i=2;i<=n;i++){
dp[i]=min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);//从i-1和i-2里选一个小的就行
}
return dp[n];
}
};
剑指 Offer 14- I. 剪绳子
给你一根长度为 n 的绳子,请把绳子剪成整数长度的 m 段(m、n都是整数,n>1并且m>1),每段绳子的长度记为 k[0],k[1]…k[m-1] 。请问 k[0]k[1]…*k[m-1] 可能的最大乘积是多少?例如,当绳子的长度是8时,我们把它剪成长度分别为2、3、3的三段,此时得到的最大乘积是18。
解题思路
dp[i]表示将长度为i的绳子至少剪乘两段后,这些绳子长度的最大乘积
确定状态转移方程
假设对长度为i的绳子剪出第一段绳子长度为j,有两种情况:
i剪成j和i-j,i-j不剪,则乘积为j*(i-j);
i剪成j和i-j, i-j继续剪, 乘积为j*dp[i-j]。
class Solution {
public:
int cuttingRope(int n) {
vector<int> dp(n+1);
dp[2]=1;
for(int i=3;i<=n;i++){
for(int j=1;j<=i-1;j++){
dp[i]=max(dp[i],max(j*(i-j),j*dp[i-j]));
}
}
return dp[n];
}
};
剑指 Offer 13. 机器人的运动范围
地上有一个m行n列的方格,从坐标 [0,0] 到坐标 [m-1,n-1] 。一个机器人从坐标 [0, 0] 的格子开始移动,它每次可以向左、右、上、下移动一格(不能移动到方格外),也不能进入行坐标和列坐标的数位之和大于k的格子。例如,当k为18时,机器人能够进入方格 [35, 37] ,因为3+5+3+7=18。但它不能进入方格 [35, 38],因为3+5+3+8=19。请问该机器人能够到达多少个格子?
解题思路
设v[i][j]表示是否可以走到这一格
先判断边界条件是否不会超过k,然后要走到这里的只能从(i-1,j)或(i,j-1)
class Solution {
public:
int get(int x){
int res=0;
while(x>0){
res+=x%10;
x=x/10;
}
return res;
}
int movingCount(int m, int n, int k) {
if (!k) return 1;
vector<vector<int> > vis(m, vector<int>(n, 0));
int ans = 1;
vis[0][0] = 1;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if ((i == 0 && j == 0) || get(i) + get(j) > k) continue;
// 边界判断
if (i - 1 >= 0) vis[i][j] |= vis[i - 1][j];
if (j - 1 >= 0) vis[i][j] |= vis[i][j - 1];
ans += vis[i][j];
}
}
return ans;
}
};
剑指 Offer II 089. 房屋偷盗
一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响小偷偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。
给定一个代表每个房屋存放金额的非负整数数组 nums ,请计算 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。
示例 1:
输入:nums = [1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
偷窃到的最高金额 = 1 + 3 = 4 。
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if(n == 1)
return nums[0];
vector<int> dp(n + 1);
dp[0] = nums[0];
dp[1] = max(nums[0], nums[1]);
for(int i = 2; i < n; i ++)
dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]);
return dp[n - 1];//这里返回的不是dp[n]
}
};
剑指 Offer II 098. 路径的数目
一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。
问总共有多少条不同的路径?
解题思路:
dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。
当前路径数由它的左边或上边决定。
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int> >dp(m,vector<int>(n));
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];
}
};
面试题 08.02. 迷路的机器人
设想有个机器人坐在一个网格的左上角,网格 r 行 c 列。机器人只能向下或向右移动,但不能走到一些被禁止的网格(有障碍物)。设计一种算法,寻找机器人从左上角移动到右下角的路径。
网格中的障碍物和空位置分别用 1 和 0 来表示。
返回一条可行的路径,路径由经过的网格的行号和列号组成。左上角为 0 行 0 列。如果没有可行的路径,返回空数组。
示例 1:
输入:
[
[0,0,0],
[0,1,0],
[0,0,0]
]
输出: [[0,0],[0,1],[0,2],[1,2],[2,2]]
解释:
输入中标粗的位置即为输出表示的路径,即
0行0列(左上角) -> 0行1列 -> 0行2列 -> 1行2列 -> 2行2列(右下角)
class Solution {
public:
vector<vector<int>> pathWithObstacles(vector<vector<int>>& obstacleGrid) {
int m=obstacleGrid.size(),n=obstacleGrid[0].size();
vector<vector<int> >dp(m,vector<int>(n,0));
vector<vector<int> >res;
for(int i=0;i<m && obstacleGrid[i][0]==0;i++) dp[i][0]=1;//如果[i][0]这条边有障碍,后面的位置都走不到了
for(int j=0;j<n && obstacleGrid[0][j]==0;j++) dp[0][j]=1;
for(int i=1;i<m;i++){
for(int j=1;j<n;j++){
if(obstacleGrid[i][j]==1) continue;
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);//这里不用保存和,保存最大即可
}
}
if(dp[m-1][n-1]==0) return res;
int r=m-1,c=n-1;//从终点反推回起点
while(r!=0||c!=0){
res.push_back({r,c});
int up=0;
if(r>0) up=dp[r-1][c];
int left=0;
if(c>0) left=dp[r][c-1];
if(up>=left) r--;
else c--;
}
res.push_back({0,0});
reverse(res.begin(),res.end());
return res;
}
};
120. 三角形最小路径和
给定一个三角形 triangle ,找出自顶向下的最小路径和。
每一步只能移动到下一行中相邻的结点上。相邻的结点 在这里指的是 下标 与 上一层结点下标 相同或者等于 上一层结点下标 + 1 的两个结点。也就是说,如果正位于当前行的下标 i ,那么下一步可以移动到下一行的下标 i 或 i + 1 。
示例 1:
输入:triangle = [[2],[3,4],[6,5,7],[4,1,8,3]]
输出:11
解释:如下面简图所示:
2
3 4
6 5 7
4 1 8 3
自顶向下的最小路径和为 11(即,2 + 3 + 5 + 1 = 11)。
class Solution {
public:
int minimumTotal(vector<vector<int>>& triangle) {
int n=triangle.size();
vector<vector<int> >dp(n+1,vector<int>(n+1,0));
for(int i=n-1;i>=0;i--){//这里要从最后一层开始向上推
for(int j=0;j<=i;j++){
dp[i][j]=min(dp[i+1][j],dp[i+1][j+1])+triangle[i][j];
}
}
return dp[0][0];
}
};
最长上升子序列
输入:nums = [10,9,2,5,3,7,101,18]
输出:4
解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n=nums.size();
if(n==0) return 0;
vector<int> dp(n,1);
for(int i=0;i<n;i++){
for(int j=0;j<i;j++){
if(nums[j]<nums[i])
dp[i]=max(dp[j]+1,dp[i]);
}
}
sort(dp.begin(),dp.end());
return dp[n-1];
}
};
最长公共子序列
输入:text1 = “abcde”, text2 = “ace”
输出:3
解释:最长公共子序列是 “ace” ,它的长度为 3 。
首先确定边界情况当i=0或j=0时,dp[i][j]=0.
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int n1=text1.size();
int n2=text2.size();
vector<vector<int> >dp(n1+1,vector<int>(n2+1,0));
for(int i=1;i<=n1;i++){
for(int j=1;j<=n2;j++){
if(text1[i-1]==text2[j-1])
dp[i][j]=dp[i-1][j-1]+1;
else
dp[i][j]=max(dp[i-1][j],dp[i][j-1]);
}
}
return dp[n1][n2];
}
}
剑指 Offer II 096. 字符串交织
输入:s1 = “aabcc”, s2 = “dbbca”, s3 = “aadbbcbcac”
输出:true
解题思路
f(i,j)表示s1[0]~s1[i]前i+1个字符和s2[0] ~s2[j]前j+1个字符能否交织成s3[0] ~ s[i+1+j]共i+j+2个字符。
如果s3[i+1+j]==s1[i]而且 s1[0] ~ s1[i - 1] 和 s2[0] ~ s2[j] 可以交织成 s3[0] ~ s3[i + j]即f(i, j) = f(i - 1, j),那么 s1[0] ~ s1[i] 和 s2[0] ~ s2[j] 也可以交织成 s3[0] ~ s3[i + j + 1]。
初始条件很重要。当s1,s2都为空时,可以组成一个空的字符串。另外当s1为空时,s2恒等于s3时,也可以组成字符串。
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int n1=s1.size(),n2=s2.size();
if(s1.size() + s2.size() != s3.size()) {
return false;
}
vector<vector<bool> >dp(n1+1,vector<bool>(n2+1,false));
dp[0][0]=true;
for (int j = 0; j < s2.size() && s2[j] == s3[j]; ++j) {
dp[0][j + 1] = true;
}
for (int i = 0; i < s1.size() && s1[i] == s3[i]; ++i) {
dp[i + 1][0] = true;
}
for(int i=0;i<n1;i++){
for(int j=0;j<n2;j++){
dp[i+1][j+1]=((s3[i+1+j]==s1[i]) && dp[i][j+1]) || ((s3[i+1+j]==s2[j]) && dp[i+1][j]);
}
}
return dp[n1][n2];
}
};
背包问题
1.01背包
一共有N件物品,第i个物品重w[i],价值v[i],问在不超过背包限重W的情况下,能够装入背包的最大价值。
解题思路:
dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
dp[i][j]分成两种情况:
第i件装入背包:dp[i-1][j-w[i]]+v[i]
第i件不装入背包:dp[i-1][j]
状态转移方程
dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]) //j>=w[i]
初始化dp
如果背包容量j为0,即dp[i][0],那么无论选取哪些物品,背包总价值都为0;
dp[0][j]:存放编号0的物品时,各个容量的背包所能存放的最大价值就是v[0]
确定遍历顺序
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
滚动数组(一维dp)
在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
可以用一个一维数组来表示dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
dp[0]表示容量为0的背包总价值最大是多少,这里是0.其他下标应该初始化多少呢?dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
为什么呢?倒序遍历是为了保证物品i只被放入一次!
分割等和子集
输入:nums = [1,5,11,5]
输出:true
解释:nums 可以分割成 [1, 5, 5] 和 [11] 。
解题思路
设f(i,j)表示能否从前i个物品(从0~i-1)中选择若干物品放入容量为j的背包。
对于f(i,j)有两种选择:
将标号i-1的物品放入背包,如果能从前i-1个物品里选择若干物品放入容量为j-num[i-1]的背包,那么f(i,j)为true;
不放入标号i-1的物品,如果能从前i-1个物品里选择若干物品放入容量为j的背包,那么f(i,j)为true。
当 j 等于 0 时,即背包容量为空,只要不选择物品就可以,所以 f(i, 0) 为 true。当 i 等于 0 时,即物品数量为 0,那么除了空背包都无法装满,所以当 j 大于 0 时,f(0, j) 为 fasle。
状态转移方程
f(i,j)=f(i-1,j)||f(i-1,j-nums[i-1])
class Solution {
public:
bool canPartition(vector<int>& nums) {
int sum = 0;
// dp[i]中的i表示背包内总和
// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200
// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了
vector<int> dp(10001, 0);
for (int i = 0; i < nums.size(); i++) {
sum += nums[i];
}
if (sum % 2 == 1) return false;
int target = sum / 2;
// 开始 01背包
for(int i = 0; i < nums.size(); i++) {
for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历
dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
}
}
// 集合中的元素正好可以凑成总和target
if (dp[target] == target) return true;
return false;
}
};
剑指 Offer II 102. 加减的目标值
给定一个正整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
解题思路:
假设相加的和为x,相减的和为sum-x,那么x+(sum-x)=target,即x=(target+sum)/2,此时题目就转化为,装满容量为x背包,有几种方法。
dp[j]:填满容量为j的背包,有dp[j]种方法。
确定递推公式
不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]种方法。
那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]] 种方法。
所以求组合类问题的公式,都是类似这种:
dp[j] += dp[j - nums[i]]
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum=0;
for(int i=0;i<nums.size();i++){
sum+=nums[i];
}
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 i=0;i<nums.size();i++){
for(int j=bagSize;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[bagSize];
}
};
2.完全背包
完全背包与01背包不同就是每种物品可以有无限多个:一共有N种物品,每种物品有无限多个,第i(i从1开始)种物品的重量为w[i],价值为v[i]。在总重量不超过背包承载上限W的情况下,能够装入背包的最大价值是多少?
不装入第i种物品,即dp[i−1][j],同01背包;
装入第i种物品,此时和01背包不太一样,因为每种物品有无限个(但注意书包限重是有限的),所以此时不应该转移到dp[i−1][j−w[i]]而应该转移到dp[i][j−w[i]],即装入第i种商品后还可以再继续装入第种商品。
我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。
而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即:
// 先遍历物品,再遍历背包
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = weight[i]; j <= bagWeight ; j++) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
零钱兑换(完全背包)
计算最少硬币数
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
dp[j]:凑足总额为j所需钱币的最少个数为dp[j]
得到dp[j](考虑coins[i]),只有一个来源,dp[j - coins[i]](没有考虑coins[i])。
凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j](考虑coins[i])
所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。
递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]);
class Solution {
public:
int coinChange(vector<int>& coins, int amount) {
int n=coins.size();
int Max=amount+1;
vector<int> dp(amount+1,Max);
dp[0]=0;
for(int i=1;i<=n;i++){
for(int j=coins[i-1];j<=amount;j++){
dp[j]=min(dp[j],dp[j-coins[i-1]]+1);
}
}
return dp[amount]==Max ? -1:dp[amount];
}
};