dp
背包九讲问题
- 01 背包问题
- 完全背包问题
- 多重背包问题
- 混合背包问题
- 二维费用的背包问题
- 分组背包问题
- 背包问题求方案数
- 背包问题求方案路径
- 有依赖的背包问题
01背包
问题背景:有 n 件物品,每一件物品 i 对应一个体积 v[i] 和一个价值 w[i],设有一个背包,体积为 V,在这个背包下能装下的价值的最大值是多少?
模板
- 最原始方法:
dp[i][j]定义:表示前 i 个物品,总体积是 j 的情况下,总价值最大是什么?
状态转移方程:dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j -v[i]] + w[i])
- Max = dp[n][V]
- 优化
- dp[i][j] 只跟 上一层的外循环 有关,所以根本没有必要使用二维数组,只需要使用一维数组即可,注意,使用一维数组的时候,内层循环需要从后向前遍历;
- 将 dp[0][j] 全部初始化为 0 ,则 dp[i][j] 就代表前 i 个物品,总体积小于等于 j 的最大总价值,故上文的Max = dp[V],代表总体积小于等于 V 的最大总价值;
- 如果要求的总价值的前提是背包容量恰好是 V,则 只需要将初始化的值改为 dp[0][0] = 0 ,dp[0][j] = - INF 即可。
- 最终模板
1 | int[] dp = new int[V+1]; |
例题
- 416 题代码
1 | public class Solution { |
- 494 题代码
1 | public class Solution { |
完全背包
相比 01 背包问题来说,这里的一件物品可以选不止一次所以,这里的做法跟 01 背包只有一个不同,01背包的一维数组的方法中的内层循环是从后向前遍历,原因是其在循环第 i 个物品时要用到第 i-1个物品的对应的值,且要用到的值都没有被本次循环更新过,而这里恰恰相反,因为一次物品可以用多次,所以完全背包问题允许在循环第 i 个物品时提前更新要用到的值,具体˙证明可以使用 数学归纳法。
模板
f[j] 表示 总体积是 j 的情况下,最大价值是什么
1 | int[] dp = new int[V+1]; |
多重背包问题 I
相比 完全背包问题 相比,就是每个物品多了一个数量的限制,类似于01背包,只不过那里的物品的数量为1,这里的数量是 s。
模板
1 | // 初始化,f[i] = 0,i =0...n |
多重背包问题 II
其实就是对上面这个模板的优化,是对多重背包的二进制优化方法 。最内层循环的意思其实就是把背包的每一次重复都当成一次新的物品,类似于打包的形式,比如第一个物品体积是 v,价值为 w,第二个物品就是 2v,价值为 2w,以此类推,但是这样分解的时间复杂度太高了,可以通过二进制优化方法,为什么呢?因为他们之间是互斥的,每次都会只选出一个来,是后面的分组背包的特殊情况,所以我们只需要找到能表示所有方案的物品就行,无需全部罗列都当成01背包的物品。比如现在的 s 为7,按照上面的方法,我们需要比较0、1、2、3、4、5、6、7这8个方案的最大值,但是其实我们可以用三位数就表示0、1、2、3、4、5、6、7,因为我们只需要从这8个方案中选取一个即可,所以三位数就可以表示所有,然后从中选取自己符合要求的就行,在这里我们可以选择1、2、4,就能表示 0 ~ 7 所有元素,如果 s 为 10,那么我们可以选取 1、2、4、3 ,3是如何来的呢,是
10 - (2^ 3 -1) 得来的,故就将 线性复杂度 变为 log2 复杂度了。
模板
1 | int[] dp = new int[V+1]; |
多重背包问题 III
这是对多重背包的再次优化,上文已经将最内层循环从 O(n) 降为了 O(logn),这里可以运用 单调队列 将最内层循环的 O(n) 降成 O(1)。
完全背包问题转移图和多重背包一样。不过没有了物品个数限制,所以不是滑动窗口情况,不用单调队列。只保存一个最大值就可以。所以完全背包问题可以直接从前向后遍历,因为其只需要保存一个最大值。所以这里引入了单调队列之后,也维护了相同余数的最大值,故 j 也可以从前向后遍历。
思路:https://blog.csdn.net/qq_40679299/article/details/81978770
为何引入单调队列分析:https://www.cnblogs.com/DeepJay/p/12025225.html
代码和思路精简版:https://www.acwing.com/solution/acwing/content/6500/(主推这个)
模板
1 | import java.util.Arrays; |
例题
LeetCode 第 239 题:滑动窗口最大值
该题也是同样采用了单调队列的方式,官方题解的解法二 就是采用双端队列实现单调队列。
1 | class Solution { |
故总结一下,单调队列的模板为:
1 | // 1.存入双端队列的是值的索引,第一步是先把队尾不符合要求的全部弹栈 |
混合背包问题
混合背包,就是01背包,完全背包,多重背包结合在一起,方法就是分类判断,分类处理即可,非常简单,就不做过多解释了。
二维费用的背包问题
其实跟一维费用的背包问题如出一辙,就是多了一层循环,dp 由一维变成了二维,其他的跟一维的都是一样的。
分组背包问题
其实,分组背包前面已经讲过了,就是多重背包的一般化,就是因为多重背包的特殊,所以才会有两个优化的方法——-二进制优化 && 单调队列优化,所以对于分组背包问题,就只能采用 多重背包问题 I 的方法,三层循环。
背包问题求方案数
求方案数的关键就在于确定:在每一次循环中,是不是选了该数。求最优方案的方案数是通过判断该体积对应的价值的最大值有没有改变,从而判断是否选了该数,选了的话就把以前的方案数替换或者累加即可;求固定体积的方案数,就是直接dp[j] = dp[j] + dp[j - v[i]],体积为 j 的就是等于选了和没选的总方案数(这个初始化是关键)。
求最优方案的方案数
这里只有一个比较大的改动,那就是添加了一个 cnt 数组来统计方案数。
这里初始化的时候,有两个方案,一个是,dp[0] = 0,dp[i] = 0; 另外一个是 dp[0] = 0,dp[i] = -INF。
方法一和方法二区别不大,跟 01背包一样,由于初始化的不同,导致最后的结果,一个直接取容量最大值即可,一个需要遍历,这里还是推荐方案一!方法一
- 输入
1 | 总物品数:4 |
- 若初始化的值都为 0
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 2 | 2 | 2 | 2 | 2 |
2 | 0 | 2 | 2 | 6 | 6 | 6 |
3 | 0 | 2 | 2 | 6 | 6 | 8 |
4 | 0 | 2 | 2 | 6 | 6 | 8 |
模板
- 定义两个数组,dp[j] 用来存储背包容积是 j 时的最佳方案的总价值,cnt[j] 用来存储容积为 j 时总价值为最佳的方案数;
- 先初始化 dp[0] = 0,dp[j] = -INF,很容易理解,容积为 0,自然价值就为 0 了,而要使得容积为 1、2…且没有数,则自然价值就不存在了,记为 -INF,再初始化所有的 cnt[j] = 1,因为背包里什么都不装对任何容积来说都是一种方案,在没有数的情况下自然就是最好的情况了;
- 在每次循环新物品时,先求得装新物品的总价值,然后与不装新物品对比,如果装了新物品的价值更大,那么就需要用 dp[j - v[i]] + w 更新 dp[j],同时用 cnt[j - v[i]] 来更新 cnt[j];
- 如果总价值相等,那么 cnt[j] = cnt[j] + cnt[j-v[i]];
- 当然如果装了新物品还不如不装呢,那就啥也不用做了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner scan = new Scanner(System.in);
// 表示物品种类
int n = scan.nextInt();
// 表示 最大的容积
int V = scan.nextInt();
int[] dp = new int[V + 1];
int[] cnt = new int[V + 1];
for (int j = 0; j <= V; j++) {
cnt[j] = 1;
}
for (int i = 0; i < n; i++) {
int v = scan.nextInt();
int w = scan.nextInt();
for (int j = V; j >= v; j--) {
int value = dp[j - v] + w;
if (value > dp[j]) {
dp[j] = value;
cnt[j] = cnt[j - v];
} else if (value == dp[j]) {
cnt[j] = cnt[j] + cnt[j - v];
}
}
}
System.out.println(cnt[V]);
}
}回看表格(括号内为 cnt 的值)
1 | 总物品数:4 |
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0(1) | 0(1) | 0(1) | 0(1) | 0(1) | 0(1) |
1 | 0(1) | 2(1) | 2(1) | 2(1) | 2(1) | 2(1) |
2 | 0(1) | 2(1) | 4(1) | 6(1) | 6(1) | 6(1) |
3 | 0(1) | 2(1) | 4(1) | 6(1) | 6(2) | 8(1) |
4 | 0(1) | 2(1) | 4(1) | 6(1) | 6(3) | 8(2) |
- 再来一个示例
1 | 总物品数:4 |
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0(1) | 0(1) | 0(1) | 0(1) | 0(1) | 0(1) |
1 | 0(1) | 0(1) | 4(1) | 4(1) | 4(1) | 4(1) |
2 | 0(1) | 0(1) | 4(2) | 4(2) | 8(1) | 8(1) |
3 | 0(1) | 0(1) | 4(2) | 4(3) | 8(1) | 8(3) |
4 | 0(1) | 0(1) | 4(2) | 4(3) | 8(1) | 8(3) |
- 套用上述模板,实现 LeetCode 494题
1 | public class Solution { |
方法二
- 若初始化的值不都为 0,用上面的示例
1 | 总物品数:4 |
0 | 1 | 2 | 3 | 4 | 5 | |
---|---|---|---|---|---|---|
0 | 0(1) | -INF(1) | -INF(1) | -INF(1) | -INF(1) | -INF(1) |
1 | 0(1) | -INF(1) | 4(1) | -INF(1) | -INF(1) | -INF(1) |
2 | 0(1) | -INF(1) | 4(2) | -INF(2) | 8(1) | -INF(1) |
3 | 0(1) | -INF(1) | 4(2) | 4(1) | 8(1) | 8(2) |
4 | 0(1) | -INF(1) | 4(2) | 4(1) | 8(1) | 8(2) |
- 模板
1 | import java.util.Scanner; |
求背包装至指定容量的方案数
这个非常简单了,就是上面的特殊化例子,也就是把 循环到的该数的容量 作为价值,然后计算指定价值的方案数,这是一般化的做法,但是既然谈到特殊化,也就是容量和价值相等,那自然可以采用更简单的方法去处理了。
- 初始化 dp[0] = 1,代表 0 容量的背包的方案为1,其他都为0;
- dp[i][j] = dp[i-1][j] + dp[i-1][j-v[i]],很好理解了,就不做解释了。
示例就是 LeetCode 494 题。
背包问题求方案路径
如果没有要求得到 按字典顺序最小的 方案路径的话,就可以直接按01背包的方法去做(由于要记录方案路径,所以不能使用滚动数组,需要用到每一轮的方案,所以需要改为二维数组),然后在遍历的时候,判断 f[i][j]是否和 f[i-1][j-v[i]] + w[i] 相等,如果相等说明用到了该个物品,加入方案路径中即可。
如果要求是得到最小的 方案路径的话,则需要将方案从末尾遍历到开始,得到最佳方案后,再从前向后寻找方案路径。
模板
1 | int[][] dp = new int[n+1][V+1]; |
有依赖的背包问题( 我暂时没复习!)
这应该是背包问题里面最难的一类了,是用 树形 dp + 分组背包问题 的思路去解决。
更多见视频:https://www.bilibili.com/video/av33930433/?p=2、https://www.bilibili.com/video/av34467850/?p=2(这个视频是从第 12 分钟开始讲背包 9 讲后面的 3 讲)
训练地址:https://www.acwing.com/problem/content/2/
更详细的背包九讲问题:已经下载到本地了,崔添翼的 pdf——> 本地文件名字叫 背包问题九讲
链表
热题 100:
剑指 offer:
相关题目
(热题100)两数相加
1 | class Solution { |
- 双端队列
1 | public class addTwoNumbers { |
- 将链表反转,然后计算,最后再反转回来
1 | public ListNode addTwoNumbers2(ListNode l1, ListNode l2){ |
删除链表的倒数第 N 个节点
利用先后指针,常用的双指针技巧之一:快慢指针、先后指针、滑动窗口。
1 | class Solution { |
合并两个有序链表
这个貌似没什么难度,稍微注意一点细节就行了
- 迭代
1 | class Solution { |
- 递归
1 | class Solution { |
合并 K 个排序链表
- 优先级队列
1 | class Solution { |
- 归并
1 | class Solution { |
- 暴力法
1 | class Solution { |
环形链表
- Set集合存储
1 | public class Solution { |
- 快慢指针
1 | public class Solution { |
环形链表 II
典型的快慢指针解决此问题
- 起点是哑巴节点
1 | public class Solution { |
- 起点是头结点
1 | public class Solution { |
排序链表【难点哈!】
- 不符合空间复杂度,直接利用列表
1 | class Solution { |
- 归并排序,并且在合并时采用递归,空间复杂度降为 O(logn),这个就跟合并 K 个有序链表是一样的思路,只不过这里就相当于 K 个 链表长度为 1 的链表进行归并,并且还不能通过数组索引到 mid,只能是通过快慢指针找到 mid。
1 | /** |
- 归并排序,合并时不采用递归,空间复杂度为常数
还未研读!
1 | class Solution { |
相交链表
走相同距离的路,必定在相交点相遇。
1 | public class Solution { |
反转链表
- 递归(这 5 步得牢记于心)
1 | class Solution { |
- 非递归(每一步就只是把下一次该反转的节点记住即可,然后把curr.next换成pre就行了)
1 | class Solution { |
K 个一组翻转链表
给你一个链表,每 k 个节点一组进行翻转,请你返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
示例:
给你这个链表:1->2->3->4->5
当 k = 2 时,应当返回: 2->1->4->3->5
当 k = 3 时,应当返回: 3->2->1->4->5
说明:
- 你的算法只能使用常数的额外空间。
- 你不能只是单纯的改变节点内部的值,而是需要实际进行节点交换。
思路:
步骤分解:
- 链表分区为已翻转部分+待翻转部分+未翻转部分
- 每次翻转前,要确定翻转链表的范围,这个必须通过 k 此循环来确定
- 需记录翻转链表前驱和后继,方便翻转完成后把已翻转部分和未翻转部分连接起来
- 初始需要两个变量 pre 和 end,pre 代表待翻转链表的前驱,end 代表待翻转链表的末尾
- 经过k轮循环,end 到达末尾,记录待翻转链表的后继 next = end.next
- 翻转链表,然后将三部分链表连接起来,然后重置 pre 和 end 指针,然后进入下一次循环
- 特殊情况,当翻转部分长度不足 k 时,在定位 end 完成后,end==null,已经到达末尾,说明题目已完成,直接返回即可
- 时间复杂度为 O(n*K) ,最好的情况为 O(n),最差的情况未 O(n^2)
- 空间复杂度为 O(1),除了几个必须的节点指针外,我们并没有占用其他空间
作者:reals
链接:https://leetcode-cn.com/problems/reverse-nodes-in-k-group/solution/tu-jie-kge-yi-zu-fan-zhuan-lian-biao-by-user7208t/
来源:力扣(LeetCode)
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
1 | /** |
思考:如果最后一段小于 k 的链表也需要翻转呢?如何做?
很简单,只需要加一步,如果最后一段需要翻转,直接处理和前面的链表的连接就可以了,其他的都不需要。
1 | /** |
回文链表
- 先利用快慢指针找到中点,然后反转前半部分,然后比对即可
1 | class Solution { |
- 可以优化一下,直接在快慢指针找中点的同时进行翻转
1 | class Solution { |
- 链表转列表,然后比对
1 | class Solution { |
删除链表中的节点
- 找一个牺牲者
1 | class Solution { |
- 直接判断下一个是不是
1 | class Solution { |
链表中的倒数第K个节点
1 | class Solution { |
复制带随即指针的链表【难点哈】
- HashMap 复制
1 | /* |
- O(1) 空间复杂度
1 | class Solution { |