DP类型
1.爬楼梯,爬楼梯最小花费
2.不同路径, 注意问题的初始化,有障碍要将障碍处的dp值置为0,还有循环的时候,要注意continue和break的巧妙使用。
3.整数拆分问题,要注意其递推的时候,要把当前的值拆分成两个数的和或者是大于两个数的和。
4.不同的二叉搜索树问题,要注意其递推公式,考虑的是以当前i节点为根节点,然后将左右子二叉搜索树的种类累加起来。
3.背包问题——01背包 (每种物品只有一件)
3.1:0-1背包的,二维数组dp[i][j],表示容量为j时,从下标为0-i的物品可以取得的最大价值,其遍历顺序是先遍历物品,再遍历背包容量,背包容量要从小到大遍历;
初始化的时候,要注意根据背包的容量去进行初始化,当j == weight[0],。。。
递推公式: dp[i][j] = Math.max(dp[i-1][j], dp[i - 1][j - weight[i]] + value[i]);
3.2:0-1背包的,压缩一维数组dp[j],表示容量为j时候可以获得的最大价值;其遍历顺序依然是先遍历物品再遍历背包容量,但是 背包容量要从大到小遍历, 因为要递推,要保证此时的 j 可以允许装下 nums[i],且从后往前遍历可以保证同一个物品不会被多次重复装入。
初始化的时候,考虑的情况和二维dp一致,
递推公式: dp[j] = Math.max(dp[j], dp[j - weigth[i]] + value[i]);
3.3分割等和子集问题,理解成往 背包中装值,最终看是否可以装满所有数组所有元素和的 1/2。
3.4 求最后一块石头的重量,和 3.3是类似的,都是将数组 问题 拆分为2,在1/2中,以背包问题去解决。
3.5 目标和,其实是和 3.3和 3.4是类似的,只是其构造的背包需要 经过一定的推导才能求出来,比如: 将数组分为二,分为左右两个部分,然后左右相加又和题目所给的target一致,而且通常要让求可能出现的种数题,一般要注意累加的操作。
3.6 1 和 0 的问题,其实也是0 1 背包问题,但是与之前的不同的地方是,其dp[i][j] 包括有两个背包,分别容量是 i 和 j ,而且装不同的 元素,此时需要考虑递推公式是同时
dp[i][j] = Math.max(dp[i][j], dp[i - oneNum][j - zeroNum] + 1);
3.背包问题——完全背包(每种物品有无数件)
循环顺序无所谓,先物品后背包或先背包后物品都是一样的,但是遍历顺序和 01背包(1维数组)不同,其遍历顺序依然是可以从小到大,因为完全背包可以不受物品数量的限制,一个物品可以同时被多次装入。
1)零钱兑换II,希望兑换总数为target的零钱,每种零钱个数都是无限使用的,要求的还是可以实现target的 所有可能的情况数,因此 采用 dp[j] += dp[i - coins[i]];
2)零钱兑换,希望兑换总数amount,且每种面值的金额数量是不限制的,但是本题中会出现一个情况就是,dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1); 因为是要求最小的dp,如果初始化的都是0的话,会在dp更新数值的时候被覆盖掉,因此要给dp的赋值为 Integer.MAX_VALUE;
其次,还要进行递推的条件判断,必须在不加上当前coins[i] 的时候,其零钱个数不是最大值,才有推导的必要。 因为 如果 dp[i - coins[i]] == Integer.MAX_VALUE, 则根据递推公式:
dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1); 就变成了 dp[j] = dp[j],无意义。
3)组合总和(所有组合的个数,因为题目中要求的是不同顺序排列也是一种新的情况,因此是排列问题)
排列问题,必须要先遍历背包,然后再遍历物品,根据背包容量和物品质量进行判断,是否进行dp的推导;
对于组合问题,也就是无关顺序的题,就需要先遍历物品然后再遍历背包容量,然后继续进行该有的判断即可。
4)完全平方数:注意 i 和 i* i 与 j, j 就是背包容量, i * i 就是物品;
5) 单词拆分: j 也是 背包的容量,要判断随着j背包容量的变化,其是否可以被 wordList中的元素所组成; 注意还要判断dp推导的条件;先遍历背包,再遍历物品,可以不需要使用存储临时单词的拼接的容器。
背包问题——注意两点:
1.先遍历背包还是遍历物品
1.1 通常情况先遍历物品,再遍历背包; 如果是对于 排列问题(完全背包,同样的物品可以反复使用,且排列顺序不同也算是一种新的结果),就需要先遍历背包再遍历物品,因为如果先 遍历物品,例如: dp[4],只会出现{1,3},而不会出现{3,1},因为nums——物品放在外层,3只能出现在1的后边。
2.背包是从小到大遍历还是从大到小遍历
2.1 0-1背包情况下,要注意选择一维dp的时候,遍历背包要从大到小,因为如果从小到大遍历的话,会导致同一个物品出现多次的情况。
3.内层遍历的时候要考虑其内存元素和外层物品重量的大小关系,如 背包容量j >= nums[i]时,才能满足递推dp公式
4.注意循环的时候,可以从不同的位置开始,如 i = 2, ....
6 打家劫舍系列:
6.1 打家劫舍:注意打家劫舍系列就是 单纯的要靠题目所暗含的信息去进行dp公式的推导, 和背包问题,关系没有那么的明显。
6.2 打家劫舍II:注意出现了首尾环,此时要分开考虑进行dp推导,就是分成两部分,一种是包含首不包含尾,一种是包含尾不包含首。
6.3 打家劫舍III: 注意出现了二叉树的结构,此时要进行dp推导, 递归 + DP
1.树形DP,递归+DP
2.每一个节点都要对应一个dp数组,长度为2,0和1两种状态,左右子节点也要有一个dp
3.每个节点对应的可能就是偷和不偷
4.注意对于当前节点的左右子节点的偷与不偷也要进行判断
7.买卖股票系列
7.1买卖股票的最佳时机:股票系列dp数组的含义是: 第i天的股票状态是否是持有
8.最长的xxx子序列问题 (注意子序列 和 子数组的区别,子序列强调的是相对顺序-【可间断】,子数组强调的是连续递增的数组)
8.1最长递增子序列——单数组对象Nums
1)假设当前位置是i,要判断i之前一个位置的状态进行递推,为了方便,就写了一个j来代替i-1 ---> j < i
2)注意本题不是返回最后一个dp[]的值,因为该题dp推导是有条件的,并不是说 随着 i 的递增, 最终的结果也是递增的。 有可能最后一个dp的值比之前的还要小。
所以这种情况,就需要在dp数组中,找到最大的元素,作为res返回。
8.2最长连续递增子序列——单数组对象Nums
和8.1中不同的是,其强调连续性——子数组, 因此本题就 只需要比较 nums[i] 和 nums[i - 1] 的大小,然后进行递推就可以了, 而不用和 8.1中 要比较 nums[i] 和 nums[j] 之间的大小, j 是 0 ~ i-1 之间的所有值。
8.3最长重复子数组——双数组对象Nums1,nums2
/**
动态规划+滚动数组:
思路:根据两个数组的内容维护一个dp数组,发现dp数组的当前项总由上一项决定
时间复杂度O(m*n) m,n分别为两个数组的长度
空间O(n) n为其中一个数组的长度
*/
class Solution {
public int findLength(int[] nums1, int[] nums2) {
int len1 = nums1.length;
int len2 = nums2.length;
int dp[] = new int[len2+1];//由于本项由前一项决定,所以要额外加一个空间赋初值为0
int max = 0;
for(int i = 0;i < len1 ;i++){
for(int j = len2; j > 0; j--){
if(nums1[i] == nums2[j-1]){
dp[j] = dp[j-1]+1;//碰见相等了连续继续,根据由前项+1得到此项
}else{
dp[j] = 0;//否则则连续中断,置为0
}
max = Math.max(max, dp[j]);//取一个最大值
}
}
return max;
}
}
注意本题中的dp数组含义,是 从 0 ~ i-1 的数组的最长重复子数组的长度。且内循环要从大到小遍历。
************重要!************
对于DP数组的 倒序和正序的问题,因为我们要使用一维dp的话,且 当前状态dp[i] 需要由 dp[i-1]也就是之前的状态推出来的 话,是依靠的前一位值,如果正序的话,由于一维dp,空间有限,而当dp的时候,外层要多次循环,也就是只能是复用当前的一位数组,因此,如果从前往后的话,会出现多次计算dp结果累加的情况,如果是从后往前的话,则每可以避免累加的情况发生。只能在原来的空间上去修正值,所以当到dp[i]的时候,dp[i-1]已经 累加过了,这样的话,此时的dp[i-1]就不是我们希望得到的上一个状态dp[i-1]了。
8.4最长公共子序列——双字符串对象text1,text2
子序列问题,并不要求字符串中的元素是连续的,1维dp不是很好理解,选用2维dp:
1. i 位置表示从0 ~ i-1 的所有元素的最长公共子序列,注意如果相等的时候该如何做: dp[j] = dp[j - 1] + 1;
2. 如果不相等的时候: dp[j] = Math.max(dp[i - 1][j],dp[i][j - 1]) ;
3. 最终返回的是dp数组的最后一个元素
8.5 不相交的线,其实就是求最长的公共子序列的长度,和8.4一致。
注意,对于子数组或者子序列问题,如果是两个数组的,一般是dp推导公式,不包含当前i,如果是一个数组的,dp推导公式可以包含当前下标i。
8.6最大子序和,让返回的是序列的最大子序列的和的值,本题的推导过程是直接累加每一个遍历到的元素,然后和 当前遍历到的元素进行比较;
8.7判断子序列,是两个序列,因此选择 0 ~ i-1去判断,然后 又已知 s ,和 t 的长度关系,就转化成求 公共子序列的问题,求出两者最长的公共子序列,在递推公式中,有else的判断逻辑,且和最长公共子序列不一样, 最终返回的true和 false也是和 s 序列的长度有关。
8.8 不同的子序列【自己没有理解】,注意初始化的过程,两个序列,因此二维dp,然后注意递推公式,
字符串数组如何 获取到数组中每一个元素(还是字符串) 的 每一个 字符:
例如 String[] strs = {"10", "0001", "111001", "1", "0"};
我们要想统计 字符数组中 1 和 0 的 个数:
1.先遍历strs数组, for(string str : strs) {
//2.对 str 转换成 charArray,可以获取到每一个字符
for (char ch : str.toCharArray()) {
if(ch == '1') {
oneNum++;
} else {
zeroNum++;
}
}
}
单调栈(O(n))
单调栈的使用场景?
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
单调栈的属性:
1.单调栈中存的都是对应压入元素 在数组中的 索引坐标;
2.从栈顶到栈底的对应的数据的大小顺序应该是: 从小到大
3.如果遍历的当前元素小于等于栈顶元素则直接入栈
4.如果遍历的元素大于栈顶元素,则需要 对栈中的所有元素进行判断——if 栈不为空,且 当前元素大于栈顶元素,则先记录结果,再把栈顶元素弹出 ——(while语句去实现),最后把当前元素 入栈。
每日温度;
下一个更大的元素:因为是两个数组,遍历第二个数组,找第一个数组中元素的下个大的元素值,因此需要一个hashMap,建立起,元素值和索引的关系,因为有了这个关系,那么在最终返回res数组的时候,可以直接将索引对应起来(map.get())。
下一个更大的元素II;数组是一个循环数组,因此,就是理解成两个数组拼接起来,i < 2 * len ;
然后继续进行单调栈的逻辑;
接雨水,可以考虑dp或者 左右指针
柱状图中最大的矩形面积
以上两道题,的区别在于 dp时候,左右数组的递推方式不一样,以及 最终的计算公式不一样。
HashSet 和 HashMap常用的方法:
set: 只有值,不可重复;
map: key-value,key不可重复,value被覆盖;
set: contains、 add 、 set.size();
1.一般一个set用来遍历存储元素, 另外一个set用来遍历取
map: containsKey(), getOrDefault(), put(k, v);
其实,map中的 (k, v)比较灵活,可以根据需要去定义对应的含义,从而达到解题的效果。
循环体中的变量也是无法被返回的;
Arrays.sort();
Arrays.asList(a, b, c), 将a,b,c元素变成 List
res.add(new LinkedList<>(path)); 在回溯的时候,往res集合中 添加集合 path
直接将对应的数组变成 list:
String[] dataArray = data.split(",");
List<String> dataList = new LinkedList<String>(Arrays.asList(dataArray));
双指针:
两数之和,map
三数之和: for + left + right + 去重
四数之和: for + for + left + right + 去重(第一层for的剪枝条件)
ACM模式下,输入输出的处理:(具体见代码 finalCoding —— scanner的使用)
1. scanner的 next() 和 nextInt();
nextInt(): 以空格或者回车作为结束;
next() : 以空格或者回车作为结束;
2.序列化和反序列化,二叉树的
序列化与反序列化二叉树 - 序列化与反序列化二叉树 - 力扣(LeetCode)
链表
链表题:
从左往右看理解为箭头指向某个节点;
从右往左看,是将结点覆盖
子串和子序列的问题:
子串是 一个字符串中必须连续出现的;
s = "pwwkew" ----> "wke"
子序列是可以不连续出现的;
s = "pwwkew" ----> "pwke"
以下的写法,较为简单, 如:
s.charAt(r++); 具体的执行顺序是,先将 r 赋值给 c , 然后再进行 r ++ ;
s.charAt(r++) ======》 s.charAt(r); r++;
比较常规的写法:
链表
链表的定义:
public class ListNode {
// 结点的值
int val;
// 下一个结点
ListNode next;
// 节点的构造函数(无参)
public ListNode() {
}
// 节点的构造函数(有一个参数)
public ListNode(int val) {
this.val = val;
}
// 节点的构造函数(有两个参数)
public ListNode(int val, ListNode next) {
this.val = val;
this.next = next;
}
}
链表: 一般定义dummy结点;
对链表赋值的理解:
始终从 = 左往右看——从左往右理解:
node.next = xxx是指向;
pre = cur; 就是直接把 pre 放到cur的位置上;
链表的反转: 主要是要搞懂 几个结点: pre,cur,next 之间的指向关系,以及cur 和 next结点的移动。
1)删除链表中的元素
必须返回的是dummy.nxet;
因为这个dummy一旦确定是不能动的, 实际往下进行操作的是 pre 和 cur。
反转链表系列:
1.全部转
2.两两转
3.指定区间转
2)反转链表
链表定义: ListNode pre = new ListNode(-1, head); //分别定义了值和 指向;
如果 ListNode dummy = null; 意思就是 dummy结点是不会被返回的;
3)两两交换链表中的结点
4)K个一组反转链表
对于字符串题的处理:
1.将字符串转变成 字符数组: 例如:String s, char[] cs = s.toCharArray();
根据这个字符串数组返回string, return String.valueOf(cs);
2.定义区间其实也可以直接在for循环体里边。
判断移除空格的,151. 颠倒字符串中的单词 - 力扣(LeetCode)
3.先设定一个数组,然后将数组转换成字符串
char[] array = new char[3 * len];
最后将这个字符数组转换成字符串:
String s = new String(array, 0 , size); //区间,左闭右开
4.StringBuilder.append();
StringBuilder.setCharAt(index, value);