十、动态规划算法学习(代码随想录学习)

1. 动态规划理论基础

代码随想录:动态规划理论基础

(1)什么是动态规划(Dynamic Programming)
如果某一问题有很多重叠子问题,使用动态规划是最有效的。所以动态规划中每一个状态一定是由上一个状态推导出来的,这一点就区分于贪心,贪心没有状态推导,而是从局部直接选最优的。

(2) 动态规划的解题步骤
a. 确定dp数组(dp table)以及下标的含义
b. 确定递推公式
c. dp数组如何初始化
d. 确定遍历顺序
e. 举例推导dp数组

2. 斐波那契数列

leetcode链接

思路: 本题利用动态规划,且题目自带状态转移表达式,通过表达式计算即可。

class Solution {
public:
	int fib(int n) {
		if (n == 0 || n == 1) return n;
		vector<int> dp(n + 1);
		dp[0] = 0;
		dp[1] = 1;
		for (int i = 2; i <= n; i++)
			dp[i] = dp[i - 1] + dp[i - 2];
		return dp[n];
	}
};

时间复杂度: O(n)
空间复杂度: O(n)

3. 爬楼梯

leetcode链接

思路: 与上题类似,利用动态规划。本题要到达第n阶有两种方法,从第n-1阶跨一步 和从第n-2阶跨两步。因此总共的方法数dp[n] = dp[n-1] + dp[n-2],初始化dp[1] = 1,dp[2] = 2

class Solution {
public:
	int climbStairs(int n) {
		if (n == 1 || n == 2) return n;
		vector<int> dp(n + 1); // dp[n]表示到第n阶的方法的种类数
		dp[0] = 0;
		dp[1] = 1;
		dp[2] = 2;
		// dp[i] = dp[i-1] + dp[i-2]
		for (int i = 3; i <= n; i++)
			dp[i] = dp[i - 1] + dp[i - 2];
		return dp[n];
	}
};

时间复杂度: O(n)
空间复杂度: O(n)

4. 使用最小花费爬楼梯

leetcode链接

思路: 利用动态规划,确定dp[i]表示爬到第i阶的最小花费,状态转移方程为dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2])

class Solution {
public:
	int minCostClimbingStairs(vector<int>& cost) {
		// dp[i]表示到达第i阶花费的最低费用
		vector<int> dp(cost.size() + 1);
		dp[0] = 0;
		dp[1] = 0;
		for (int i = 2; i <= cost.size(); i++)
			dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]);
		return dp[cost.size()];
	}
};

时间复杂度: O(n)
空间复杂度: O(n)

5. 动态规划周总结1

代码随想录:动态规划周总结1

6. 不同路径

leetcode链接

思路: 确定状态转移方程,由于机器人只能向右或向下移动,因此dp[i][j] = dp [i-1][j]+dp[i][j-1]

初始化dp数组: 第一行和第一列均为1,因为到达该位置的路径只有直走一条路径

class Solution {
public:
	int uniquePaths(int m, int n) {
		// dp[x][y] 表示到达坐标为x,y 的方法数
		vector<vector<int>> dp(m, vector<int>(n));
		// 由于机器人只能向右移或者下移,因此到第一行或第一列均只有一种方法
		for (int i = 0; i < n; i++)  // 将第一行变为1
			dp[0][i] = 1;
		for (int i = 0; i < m; i++)  // 将第一列变为1
			dp[i][0] = 1;
		for (int i = 1; i < m; i++) {
			for (int j = 1; j < n; j++) {
				// 更新dp
				dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
			}
		}
		return dp[m - 1][n - 1];
	}
};

时间复杂度: O(mn)
空间复杂度: O(m
n)

7. 不同路径Ⅱ

leetcode链接

思路: 与上题类似,只不过多了路障的判断。将所有值均初始化0,将第一行和第一列中为被路障阻挡的位置修改为1,其他均与上题相同。

class Solution {
public:
	int uniquePathsWithObstacles(vector<vector<int>>& obstacleGrid) {
		int m = obstacleGrid.size(), n = obstacleGrid[0].size();
		vector<vector<int>> dp(m, vector<int>(n,0));
		for (int i = 0; i < n; i++) {  // 初始化行
			if (obstacleGrid[0][i] == 1) // 路障后面全为0
				break;
			dp[0][i] = 1;
		}
		for (int i = 0; i < m; i++) {  // 初始化列
			if (obstacleGrid[i][0] == 1) // 路障后面全为0
				break;
			dp[i][0] = 1;
		}
		for (int i = 1; i < m; i++) {
			for (int j = 1; j < n; j++) {
				if (obstacleGrid[i][j] == 1)
					continue;
				dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
			}
		}
		return dp[m - 1][n - 1];
	}
};

时间复杂度: O(mn)
空间复杂度: O(m
n)

8. 整数拆分

leetcode链接

思路: 动态规划,依次判断每个值符合条件的最大乘积,双重循环遍历,依次取符合条件的最大值

写法1:

class Solution {
public:
	int integerBreak(int n) {
		// dp[i]表示 i符合条件的数
		vector<int> dp(n + 1, 0);
		dp[1] = 1;
		dp[2] = 1;
		for (int i = 1; i < n; i++) {
			for (int j = 2; j <= n && i+j<=n; j++) {
				dp[i + j] = max(max(dp[i], i)*max(dp[j], j),dp[i+j]);
			}
		}
		return dp[n];
	}
};

写法2:

class Solution {
public:
	int integerBreak(int n) {
		// dp[i]表示 i符合条件的数
		vector<int> dp(n + 1, 0);
		dp[2] = 1;
		for (int i = 2; i <= n; i++)
			for (int j = 1; j <= i/2; j++)
				dp[i] = max(dp[i], max(dp[i - j] * j, (i - j)*j));
		return dp[n];
	}
};

问: 为什么j不需要拆分?
答: 由于j是从1开始遍历的,因此,拆分的情况在前面已经考虑到了。

时间复杂度: O(n^2)
空间复杂度: O(n)

9. 不同的二叉搜索树

leetcode链接

思路: dp[i]表示节点数为 i 时的 二叉搜索树的类别数。循环遍历1-n,表示中间节点为第几个节点,判断其左右孩子的节点数。通过dp可知其左右孩子的二叉搜索树的类别,二者相乘即可得到当前节点的二叉搜索树总数,累计相加即可得到结果。

class Solution {
public:
	int numTrees(int n) {
		// dp[i]表示i个节点的二叉搜索树数量
		vector<int> dp(n + 1, 0);
		dp[0] = 1;
		dp[1] = 1;
		for (int i = 2; i <= n; i++) {
			// l表示左孩子节点数,r表示右孩子节点数(-1是减中间节点)
			int l = 0, r = i - l - 1; 
			while (l < i) {
				dp[i] += dp[l] * dp[r];
				l++;
				r--;
			}
		}
		return dp[n];
	}
};

时间复杂度: O(n^2)
空间复杂度: O(n)

10. 动态规划周总结2

代码随想录:动态规划周总结

11. 0-1背包问题理论基础(一)

在这里插入图片描述

  1. 确定dp数组以及下标的含义
    dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少

  2. 确定递推公式
    当遍历到下标为(i,j)时,有两种情况,放入物品i 和不放物品i。如果放物品i就要预先留出物品i的空间。
    不放物品i:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。
    放物品i:背包空出物品i的容量后,背包容量为j - weight[i],dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值

  3. dp数组如何初始化
    第一列应全初始化为0,因为第一列的空间为0,必定放不下任何物品,价值为0。
    第一行中,当空间大小大于物品0所用空间时,价值为物品0的价值;否则,为0。

  4. 确定遍历顺序
    遍历顺序从第二行开始遍历和第二列开始遍历均可,因为使用的均是左上方和正上方的dp数组。

  5. 举例推导dp数组
    做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!

题目链接

#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;

int main() {
	int n , space ;  // space为最大空间大小
	cin >> n >> space;
	vector<int> weights(n), value(n);
	// dp[i][j]表示物品0-i中,尽量填满空间为j的最大价值,行号为物品,列号为空间
	vector<vector<int>> dp(n, vector<int>(space + 1, 0));
	for (int i = 0; i < n; i++)
		cin >> weights[i];
	for (int i = 0; i < n; i++)
		cin >> value[i];
	// 初始化 dp:第一列均为0(因为空间为0时,价值必定他为0)
	// 第一行中空间大于物品0所需空间,则赋值为物品0的价值
	for (int i = 0; i <= space; i++)
		if (weights[0] <= i)
			dp[0][i] = value[0];

	// 状态转移方程为:dp[i][j] = max(dp[i-1][j],dp[i-1][j-weights[i]]+value[i])
	for (int i = 1; i < n; i++){
		for (int j = 1; j <= space; j++) {
			if (j >= weights[i])
				dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weights[i]] + value[i]);
			else
				dp[i][j] = dp[i - 1][j];
		}
	}	
	cout << dp[n - 1][space];
	return 0;
}

11. 0-1背包理论基础(二)

整体思想与二维数组类似,将上一层的二维数组进行压缩为一维数组,减小空间复杂度
与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了,只用dp[j](一维数组,也可以理解是一个滚动数组。这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层

  1. 确定dp数组的定义
    dp[j] 表示 空间为 j 的背包在当前层(每一个物品为一层)的最大价值

  2. 确定递推公式
    dp [j] = max ( dp[j] , dp[j-weight[i]) + value[i])

  3. dp数组如何初始化
    在物品价值全为正数的情况下,dp数组应全部初始化为0,保证在第一层遍历时,可以将更大的物品成功添加。

  4. 确定遍历顺序
    一维数组的情况下,只能先遍历物品,再遍历空间。因为每一个物品表示一层,更新的dp数组表示经当前物品更新后的最大值,因此必须每一层的遍历,即按物品遍历。

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[j]需按照上一层的dp值进行更新,若从前往后遍历,则后面的dp值会根据前面已被当前层更新的dp值影响。

举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。

  1. 举例推导dp数组

题目链接

#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;

int main() {
	int m, n;// m表示物品种类,n表示背包大小
	cin >> m >> n;
	vector<int> dp(n + 1, 0);// dp[i]表示当前层,容量为i的最大值
	vector<int> weight(m), value(m);
	for (int i = 0; i < m; i++)
		cin >> weight[i];
	for (int i = 0; i < m; i++)
		cin >> value[i];

	for (int i = 0; i < m; i++) // 遍历物品,每一个物品为一层
		// 倒序遍历,保证每层使用的是上一层的数据,即未经当前物品修改后的数据
		for (int j = n; j >= weight[i]; j--) 
			dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
	cout << dp[n];
	return 0;
}

12. 分割等和子集

leetcode链接

思路: 本题可以用回溯算法来做,但会超时,因此选择动态规划。本题每一个元素的空间和价值相等,dp[j]表示当前空间为 j 在当前层的最大价值。target为数组元素总和的一半,即为目标值。当dp[target]的最大价值等于target时,则表示存在子集和为target,返回true,反之,返回fase。

class Solution {
public:
	bool canPartition(vector<int>& nums) {
		sort(nums.begin(), nums.end());
		int target = 0;
		for (int x : nums)
			target += x;
		if (target % 2 != 0)
			return false;
		target /= 2;
		vector<int> dp(target + 1, 0);
		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]);
		return dp[target] == target ? true : false;
	}
};

时间复杂度: O(n^2)
空间复杂度: O(n)

13. 最后一块石头的重量Ⅱ

leetcode链接

思路: 本题主要是要理解清楚什么时候会得到最小石块,即将石堆分为尽可能相等的两部分。因此,本题做法和上题类似。每个石块的空间和价值相等,dp[j]表示在 空间为j的情况下,当层的最大价值。最后返回 另一边价值和-dp【target】。

class Solution {
public:
	int lastStoneWeightII(vector<int>& stones) {
		int sum = 0, target;
		for (int x : stones)
			sum += x;
		target = sum / 2;
		vector<int> dp(target + 1, 0);
		for (int i = 0; i < stones.size(); i++)
			for (int j = target; j >= stones[i]; j--)
				dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
		return sum - 2 * dp[target];
	}
};

时间复杂度: O(n*target)
空间复杂度: O(target)

15. 动态规划周总结3

代码随想录:动规周总结3

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值