目录
主要是参考了代码随想录的内容
代码随想录连接
二叉树相关
二叉树的层次遍历
递归实现
需要借助deep来标记处于哪一层
List<List<Integer>> lists = new ArrayList<>();
public List<List<Integer>> levelOrder(TreeNode root) {
if (root == null) {
return lists;
}
backtracking(root, 0);
return lists;
}
public void backtracking(TreeNode root, int deep) {
if (root == null) {
return;
}
deep++;
if (lists.size() < deep) {
lists.add(new ArrayList<Integer>());
}
lists.get(deep - 1).add(root.val);
backtracking(root.left, deep);
backtracking(root.right, deep);
}
迭代实现
需要借助队列先入先出
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> lists = new ArrayList<>();
if (root == null) {
return lists;
}
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
while (!que.isEmpty()) {
List<Integer> items = new ArrayList<>();
int len = que.size();
while (len != 0) {
TreeNode node = que.poll();
items.add(node.val);
if (node.left != null) {
que.offer(node.left);
}
if (node.right != null) {
que.offer(node.right);
}
len--;
}
lists.add(items);
}
// for (int i = 0; i < lists.size(); i++) {
// System.out.println(lists.get(i));
// }
return lists;
}
递归
动态规划
理论基础
概念
动态规划(DP,Dynamic Programming)主要用来解决的问题是,一个大的问题可以划分为许多个小的重叠的问题,分开求解,并且每一个小问题的状态是由上一个问题的状态推到而来的
核心是利用填表法记录上一个子问题的zhuang t
动态规划解决的最常见的问题就是背包问题
例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
常见dp递推公式
- 求背包问题最大值
dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i])
- 求目标值的最大分配方案数量
dp[j] += dp[j - nums[i]]
- 求用最少个物品达到target的分配方案
dp[j] = Math.min(dp[j], dp[j - nums[i]] + 1)
dp常规解题步骤
- 确定dp数组及下标含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
dp的debug方式
- 要清楚自己的dp数组应该是什么样子的
- 把dp数组的情况打印出来看看和自己想的一不一样
2.1 如果打印出来和自己想的一样,那就是想错了(dp公式、初始化or遍历顺序)
2.2 如果打印出来和自己想的不一样,就是代码实现的细节有问题了
灵魂拷问:
- 我举例推导状态转移公式了吗
- 我打印dp数组了吗
- 打印的dp数组和我想的一样吗
就算不会也要清楚自己哪里不会,是状态转移不明白?实现代码不会写?还是不理解遍历dp数组的顺序?
0-1背包问题
由于dp主要是填表法解决,所以背包问题是dp的基础问题
背包问题主要有两种:0-1背包和完全背包
首先是0-1背包,题目:
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
二维dp
- 确定dp数组和下标含义
dp[i][j] : 其中i表示取前i个物品,j表示背包容量为j时,dp[i][j]即表示当前i和j的最优解 - 确定递推公式
不放置物品i
的情况:此时dp[i][j]
的值就是dp[i - 1][j]
的值
放置物品i
的情况:dp[i][j]
的值就是dp[i - 1][j - weight[i]] + value[i]
所以dp[i][j]
的值就是两者取最大,即:
dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
- dp数组初始化
3.1 j为0的时候,不能放置任何物品,所以第0列统一置0
3.2 当选择物品0,背包空间j >= weight[0]
时,dp[0][j]的值置value[0]
其他位置下表都会在遍历过程中被覆盖,所以可以默认为0 - 确定遍历的顺序
dp[i][j]
的值是由dp[i - 1][j]
和dp[i - 1][j - weight[i]] + value[i]
两个位置的值推到出来的,所以dp[i][j]
的值只取决于上一行的dp数组的值,因此先遍历物品或先遍历背包都是可以的 - 代码实现dp数组
完整java代码:
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagsize = 4;
testweightbagproblem(weight, value, bagsize);
}
public static void testweightbagproblem(int[] weight, int[] value, int bagsize){
int wlen = weight.length, value0 = 0;
//定义dp数组:dp[i][j]表示背包容量为j时,前i个物品能获得的最大价值
int[][] dp = new int[wlen + 1][bagsize + 1];
//初始化:背包容量为0时,能获得的价值都为0
for (int i = 0; i <= wlen; i++){
dp[i][0] = value0;
}
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 1; i <= wlen; i++){
for (int j = 1; j <= bagsize; j++){
if (j < weight[i - 1]){
dp[i][j] = dp[i - 1][j];
}else{
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]);
}
}
}
//打印dp数组
for (int i = 0; i <= wlen; i++){
for (int j = 0; j <= bagsize; j++){
System.out.print(dp[i][j] + " ");
}
System.out.print("\n");
}
}
一维dp解法
利用滚动数组可以优化dp数组的空间复杂度
首先,二维dp数组递推公式dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
可以发现每行dp都会复制上一行的数据
如果在行内拷贝数据也是可以的,即dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);
因此,递推公式可以优化为dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
开始步骤:
- 确定dp数组及下标含义
dp[j]
其中j表示背包容量,i表示物品序号 - 确定递推公式
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
- dp数组初始化
j 为0时,dp[0]
数组的值为0,即背包容量为0时背包所能装的最大价值为0 - 确定遍历顺序
一维dp遍历时,先遍历物品再遍历背包,但是需要注意的是遍历背包需要从大往小遍历,避免物品i被同一个背包重复放入
举一个例子:物品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,被放入了两次,所以不能正序遍历。
倒序遍历:
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
- 实现
public static void main(String[] args) {
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWight = 4;
testWeightBagProblem(weight, value, bagWight);
}
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagWeight + 1];
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 0; i < wLen; i++){
for (int j = bagWeight; j >= weight[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
//打印dp数组
for (int j = 0; j <= bagWeight; j++){
System.out.print(dp[j] + " ");
}
}
可以看出,一维dp 的01背包,要比二维简洁的多。 初始化 和 遍历顺序相对简单了。
由一维dp解法引申出来的两个问题
1.二维dp数组为什么不用倒序遍历?
因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
2. 一维dp数组可不可以先遍历背包容量嵌套遍历物品呢?
如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。
完全背包问题
有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品都有无限个(也就是可以放入背包多次),求解将哪些物品装入背包里物品价值总和最大。
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
一维dp求解
- 确定dp数组及下表意义
dp[j]
:j
表示背包容量,i
表示物品,dp[j]
表示背包容量为j
时的最优解 - 确定dp递推公式
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
与0-1背包相同 - dp数组初始化
这里还是dp[0] = 0 - 遍历顺序
从小到大遍历背包和物品,背包和物品没有先后顺序
4.1 先遍历物品再遍历背包
4.2 先遍历背包再遍历物品
- 代码实现
//先遍历物品,再遍历背包
private static void testCompletePack(){
int[] weight = {1, 3, 4};
int[] value = {15, 20, 30};
int bagWeight = 4;
int[] dp = new int[bagWeight + 1];
for (int i = 0; i < weight.length; i++){ // 遍历物品
for (int j = weight[i]; j <= bagWeight; j++){ // 遍历背包容量
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
for (int maxValue : dp){
System.out.println(maxValue + " ");
}
}
//先遍历背包,再遍历物品
public int getCompletePackage(int w[], int[] v, int tar) {
int[] dp = new int[tar + 1];
for (int i = 1; i <= w.length; i++) {
System.out.println("当取第" + (i - 1) +"个物品时:");
for (int j = 1; j <= tar; j++) {
System.out.print("背包容量为"+j+"时:");
if (j < w[i - 1]) {
System.out.println("背包容量小于当前物品 ");
dp[j] = dp[j - 1];
} else {
System.out.print("dp["+j+"]原本的值为:" + dp[j] + " ");
dp[j] = Math.max(dp[j], dp[j - w[i - 1]] + v[i - 1]);
System.out.print("新放入物品"+(i-1)+"时,dp["+j+"]的值为:"+(dp[j - w[i - 1]] + v[i - 1]));
System.out.println("dp["+j+"]更新后的值为:" + dp[j] + " ");
}
}
System.out.println();
}
return dp[dp.length - 1];
}
二维dp求解
- 确定dp数组及下标含义
dp[i][j]:物品i
,背包容量j
,装同一件物品的个数k
- 确定dp递推公式
dp[i - 1][j - k* w[i - 1]] + k* v[i - 1]
- dp数组初始化:j为0时置0,即背包不能装任何物品
- 确定遍历顺序:此时先遍历物品还是背包并不影响
- 代码实现
//二维dp
public int getCompletePackage2DP(int[] w, int[] v, int tar) {
int[][] dp = new int[w.length + 1][tar + 1];
//物品:i,背包容量:j
//初始化:j = 0时,什么都装不了,置0
for (int i = 0; i <= w.length; i++) {
dp[i][0] = 0;
}
// for (int j = w[0]; j <= tar; j++) {
// dp[0][j] += v[0];
// }
//遍历顺序:先物品后背包(在这里没有区别)
for (int i = 1; i <= w.length; i++) {
System.out.println("当取第"+(i - 1)+"个物品时:");
for (int j = 1; j <= tar; j++) {
System.out.println("背包容量为"+j + "时:");
for (int k = 1; k * w[i - 1] <= j; k++) {
dp[i][j] = Math.max(dp[i - 1][j],
dp[i - 1][j - k* w[i - 1]] + k* v[i - 1]);
}
}
}
for (int i = 0; i < dp.length; i++) {
for (int j = 0; j < dp[0].length; j++) {
System.out.print(dp[i][j] + " ");
}
System.out.println();
}
return dp[w.length][tar];
}
背包问题的引申
其实纯粹的背包问题还是比较少见的,更多的是对背包问题的引申
主要有两种类型:
- 排序
- 组合
利用动归求解排列问题
纯粹的背包问题求解的是如何求出当前背包能装的物品价值最大
而排列问题求解的目标是,我要在一个背包中装特定价值的物品(target),要求的是最多有多少种装包的方式,也就是求物品的排列数
排列数和组合数之间的主要区别在于排序不同的序列是否算不同组合
利用动归求解组合问题
组合问题的目标是凑成target的组合数
例如示例一:
5 = 2 + 2 + 1
5 = 2 + 1 + 2
这是一种组合,都是 2 2 1。
如果问的是排列数,那么上面就是两种排列了。
组合不强调元素之间的顺序,排列强调元素之间的顺序。
求解过程
- 确定dp数组及下标含义
dp[j]表示取前j个数时,最多有几种方案
i表示取第几个数 - 确定递推公式
此处和背包是有区别的,主要在于,dp[j - nums[i]]代表的是背包为j - nums[i]时的最大组合数,dp[j]处的最大组合数就是前面选取个数的情况的累加和
即dp[j] += dp[j - nums[i]]
- 初始化
j = 0 时固定只有一种方案就是什么都不装,所以dp[0] = 1 - 确定递推顺序
组合问题每个数只能选取一次,所以是01背包
需要先遍历物品,再遍历背包,背包的遍历要倒序(为什么倒序来着?) - 代码推导dp数组
eg. leetcode 518
/**
* 给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。
*
* 请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0 。
*
* 假设每一种面额的硬币有无限个。
*
* 题目数据保证结果符合 32 位带符号整数。
*/
首先是2维dp实现:
- 确定dp数组及下标
int[][]: 金币:i
; amount:j
dp[i][j] 表示amount为j
时前i
个金币能组合成j
的数量 - 确定递推公式
dp[i][j] += dp[i][j - coins[i - 1]]
从初值开始,dp[i][j]的值=上一行背包可能的情况+本行抛去当前物品的最优解的和 - 确定初值
由递推公式,整个填表法处理过程全仰赖于对初值的迭代
此处dp[0][0]表示背包容量为0时,不放置物品,有几种放置方案,显然是1种 - 确定递推顺序
二维dp时,对数组的遍历顺序不会覆盖上一行数据,因此先遍历物品或是先遍历背包对结果没有影响 - 代码推导
public int change(int amount, int[] coins) {
int[][] dp = new int[coins.length + 1][amount + 1];
dp[0][0] = 1;
//先遍历物品
for (int i = 1; i <= coins.length; i++) {
for (int j = 0; j <= amount; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= coins[i - 1]) {
dp[i][j] += dp[i][j - coins[i - 1]];
}
}
}
// //先遍历背包
// for (int j = 0; j < amount + 1; j++) {
// for (int i = 1; i <= coins.length; i++) {
// dp[i][j] = dp[i - 1][j];
// if (j >= coins[i - 1]) {
// dp[i][j] += dp[i][j - coins[i - 1]];
// }
// }
// }
// dp数组展示
for (int i = 0; i < dp.length; i++) {
for (int j = 0; j < dp[0].length; j++) {
System.out.print(dp[i][j] + " ");
}
System.out.println();
}
return dp[coins.length][amount];
}
}
输出结果:
1 0 0 0 0 0
1 1 1 1 1 1
1 1 2 2 3 3
1 1 2 2 3 4
然后是1维dp的实现:
- 确定dp数组及下标
dp[j]:物品i
;背包容量j
;dp[j]
表示前i
个物品,背包容量为j
时的分配方案数量 - 确定递推公式
dp[j] += dp[j - conins[i]]
每一次循环,都会利用上一次循环中的dp[j]
,因此初值和循环顺序就十分关键了 - 确定初值
dp[0] = 1
当背包容量为0时,我们固定的只有一种分配方案–啥都不放 - 确定循环顺序
这里需要先遍历物品在遍历背包
如果先遍历的是背包,每经过一个背包都会重新放置所有物品,会有相同组合不同排列的情况被重复计算
而先遍历物品,再遍历背包时,虽然也利用了上层的数据,但是物品是有严格顺序的,不存在「1,5」和「5,1」同时被计算的情况 - 代码实现
int[] dp = new int[amount + 1];
dp[0] = 1;
for (int i = 0; i < coins.length; i++) {
for (int j = 1; j <= amount; j++) {
if (j >= coins[i]) {
dp[j] += dp[j - coins[i]];
}
}
System.out.print("遍历第"+i+"个物品时:");
for (int k = 0; k < dp.length; k++) {
System.out.print(dp[k] + " ");
}
System.out.println();
}
return dp[dp.length - 1];