滑动窗口
首先要考虑
- 窗口内是什么?
- 如何移动窗口的起始位置?
- 如何移动窗口的结束位置?
209. 长度最小的子数组
原题链接:https://leetcode.cn/problems/minimum-size-subarray-sum/description/
public int minSubArrayLen(int target, int[] nums) {
int l = 0;
int sum = 0;
int result = Integer.MAX_VALUE;
for(int r = 0;r < nums.length;r++) {
sum += nums[r];
while(sum >= target) {
result = Math.min(result,r - l + 1);
sum -= nums[l++];
}
}
return result == Integer.MAX_VALUE ? 0 : result;
}
904. 水果成篮
原题链接:https://leetcode.cn/problems/fruit-into-baskets/
public int totalFruit(int[] fruits) {
Map<Integer,Integer> map = new HashMap<>();
int ans = 0;
int l = 0;
for(int r = 0;r < fruits.length;r++) {
if(map.containsKey(fruits[r])) {
map.put(fruits[r],map.get(fruits[r]) + 1);
} else {
map.put(fruits[r],1);
}
while(map.size() > 2) {
map.put(fruits[l],map.get(fruits[l]) - 1);
if(map.get(fruits[l]) == 0) {
map.remove(fruits[l]);
}
l++;
}
ans = Math.max(ans,r - l + 1);
}
return ans;
}
哈希表
242.有效的字母异位词
方法一:排序
思路:先判断长度是否相等,如果相等再把两个字符串转化为字符数组,对字符数组进行排序,最后用equals方法比较
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) {
return false;
}
char[] ch1 = s.toCharArray();
char[] ch2 = t.toCharArray();
Arrays.sort(ch1);
Arrays.sort(ch2);
return Arrays.equals(ch1,ch2);
}
方法二:哈希表
思路:先判断两个字符串的长度,新建一个数组做为哈希表,数组下标表示26个字母遍历s,在每个字母的位置计数+1,遍历t每个字母的位置计数-1,最后看是否都为0,如果有不为0的直接返回false,都为0返回true
public boolean isAnagram(String s, String t) {
if(s.length() != t.length()) {
return false;
}
int[] records = new int[26];
for(int i = 0;i < s.length();i++) {
records[s.charAt(i) - 'a']++;
}
for(int i = 0;i < t.length();i++) {
records[t.charAt(i) - 'a']--;
}
for(int count: records) {
if(count != 0) {
return false;
}
}
return true;
}
349.两个数组的交集
方法一:两个哈希表
思路:题干说可以不考虑顺序第一时间想到hashset,新建两个hashset,遍历num1并把元素装入到set1中,然后遍历num2,比较set1中是否有,如果有装入答ans中,最后把ans转化成数组(java8新特性),返回数组
public int[] intersection(int[] nums1, int[] nums2) {
HashSet<Integer> set1 = new HashSet<>();
HashSet<Integer> ans = new HashSet<>();
for(int i = 0;i < nums1.length;i++) {
set1.add(nums1[i]);
}
for(int i = 0;i < nums2.length;i++) {
if(set1.contains(nums2[i])) {
ans.add(nums2[i]);
}
}
return ans.stream().mapToInt(x -> x).toArray();
}
202. 快乐数
方法一:哈希表
思路:变为1或无限循环,其中如果出现无限循环我们需要用哈希表看看是不是循环了从而判断是否为快乐数,所以我们每次求完平方和先判断是否为1,如果为1返回true,再判断哈希表中是否有该平方和,如果有返回false。
public boolean isHappy(int n) {
Set<Integer> set = new HashSet<>();
set.add(n);
int temp;
int sum;
while(true) {
sum = 0;
while(n != 0) {
temp = n % 10;
n = n / 10;
sum += temp * temp;
}
if(sum == 1) {
return true;
}
if(set.contains(sum)) {
return false;
}
n = sum;
set.add(sum);
}
}
1. 两数之和
直接上代码不解释
public int[] twoSum(int[] nums, int target) {
Map<Integer,Integer> map = new HashMap<>();
for(int i = 0;i < nums.length;i++) {
if(map.containsKey(target - nums[i])) {
return new int[]{map.get(target - nums[i]),i};
}
map.put(nums[i],i);
}
return new int[]{0,0};
}
454.四数相加
方法一:哈希表
思路:将四个数组两两分组,两两组合相加(挺暴力的),然后把第一组的两个数之和添加到哈希表中并记录出现的次数,遍历另一组数组,两两组合相加,比较哈希表中是否存在与两数相加的值相加等于0的数,如果有,直接用计数器加出现的次数。
public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) {
Map<Integer,Integer> map = new HashMap<>();
int ans = 0;
int temp;
for(int i:nums1) {
for(int j:nums2) {
temp = i + j;
if(map.containsKey(temp)) {
map.put(temp,map.get(temp) + 1);
} else {
map.put(temp,1);
}
}
}
for(int i:nums3) {
for(int j:nums4) {
temp = i + j;
if(map.containsKey(0 - temp)) {
ans += map.get(0 - temp);
}
}
}
return ans;
}
字符串
151. 反转字符串中的单词
没什么难度,但是细节在于单词中间可能包含多个空格或者单词前后都有多余的空格如何处理这些空格
public String reverseWords(String s) {
s = s.trim(); //去除首位和末位多余的空格
String[] strs = s.split("\\s+"); // \\s+ 表示一个或多个空格
int l = 0;
int r = strs.length - 1;
String temp;
while(l < r) {
temp = strs[l];
strs[l] = strs[r];
strs[r] = temp;
l++;
r--;
}
StringBuilder sb = new StringBuilder();
sb.append(strs[0]);
for(int i = 1;i < strs.length;i++) {
sb.append(" ").append(strs[i]);
}
return sb.toString();
}
kmp算法
kmp算法解决的是字符匹配的问题
如果用暴力算法解决字符串问题会浪费很多时间去做一些没有意义的判断
例如:文本串为:aabaabaafa
模式串为:aabaaf
当使用双重循环对文本串和模式串进行匹配时aabaa都能匹配上,到f发生了冲突文本串会从**[1]开始和模式串[0]位置开始匹配,其实从我们的角度看文本串应该从[5](也是出现冲突的位置)与模式串的[2]**开始匹配才比较合理,kmp算法正是让字符匹配做到自适应的去找开始匹配的位置
a a b a a b a a f a
a a b a a f
0 1 2 3 4 5 6 7 8 9
从上面的解释可以看出用kmp算法之后我们可以减少很多不必要的匹配来提高匹配的效率,而且文本串不需要回退直接走到底就行,至于模式串从哪开始匹配则需要借助前缀表
而前缀表是基于模式串记录的是最长公共前后缀的长度拿上面的模式串aabaaf为例
a没有前后缀所以最长公共前后缀的长度为0
aa 1
aab 0
aaba 1
aabaa 2
aabbaaf 0
由此我们得到了一个前缀表
a a b a a f
0 1 0 1 2 0
当我们进行字符匹配发生冲突时会找到模式串中发生冲突位置的前一个位置并查看该位置前缀表的值为多少,在一些kmp算法的代码中前缀表会有不同的表现形式不同的称呼(next数组或prefix数组)
a a b a a f
0 0 1 0 1 2
如这种形式是将前缀表整体右移第一位补0,当字符匹配发生冲突时直接从模式串发生冲突的位置去查前缀表
获取next数组的代码如下(没有右移)
public void getNext(int[] next,String s) {
int j = 0; //j表示前缀的最后一位 i表示后缀的最后一位
next[0] = 0; //后面代码不太好理解实在不行就当模板记住
for(int i = 1;i < s.length();i++) {
while(j > 0 && s.charAt(j) != s.charAt(i)) {
j = next[j - 1];
}
if (s.charAt(j) == s.charAt(i)) {
j++;
}
next[i] = j;
}
}
28. 找出字符串中第一个匹配项的下标
public int strStr(String haystack, String needle) {
int m = needle.length();
int n = haystack.length();
if(m == 0) {
return 0;
}
int[] next = new int[m];
getNext(next,needle);
int j = 0;
for(int i = 0;i < n;i++) {
while(j > 0 && haystack.charAt(i) != needle.charAt(j)) {
j = next[j - 1];
}
if(haystack.charAt(i) == needle.charAt(j)) {
j++;
}
if(j == m) {
return i - m + 1; //前面都可以当成模板,这个行代码是收获结果的代码
} //根据题的要求拿自己的结果
}
return -1;
}
public void getNext(int[] next,String s) {
int j = 0;
next[0] = 0;
for(int i = 1;i < s.length();i++) {
while(j > 0 && s.charAt(j) != s.charAt(i)) {
j = next[j - 1];
}
if (s.charAt(j) == s.charAt(i)) {
j++;
}
next[i] = j;
}
}
双指针
15. 三数之和
方法一:双指针
思路:先排序,再遍历数组,并且使用left和right指针找三个数具体思路看下面注释
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> ans = new ArrayList<>(); //答案集合
Arrays.sort(nums); //排序
int left;
int right;
for(int i = 0;i < nums.length;i++) {
if(nums[i] > 0) { //排序之后如果第一个数大于0后面数都大于0直接不用看了
return ans;
}
if(i > 0 && nums[i] == nums[i - 1]) {//如果前一个数和当前数一样,直接continue去重
continue;
}
left = i + 1;
right = nums.length - 1;
while(right > left) {
int sum = nums[i] + nums[left] + nums[right]; //求和
if(sum == 0) { //如果和等于0,则获得一组答案
List<Integer> result = new ArrayList<>();
result.add(nums[i]);
result.add(nums[left]);
result.add(nums[right]);
ans.add(result); //收集答案并把答案添加到集合中
/*如果left的下一个数跟left的数相等继续右移动直到不相等,right同理*/
while (right > left && nums[right] == nums[right - 1]) right--;
while (right > left && nums[left] == nums[left + 1]) left++;
left++;
right--;
} else if(sum < 0) {
left++; //如果sum小于0说明我们要增大三数之和,那么让left右移即可
} else if(sum > 0) {
right--; //right同理
}
}
}
return ans;
}
栈与队列
栈与队列的题都特别简单,但是比较恶心的点在于API的用法容易混淆
创建栈对象
Stack<Integer> stack = new Stack<Integer>();
栈的常用方法
stack.push(x) //使x入栈
stack.pop() //返回并且删除栈顶
stack.peek() //返回但不删除栈顶
queue.isEmpty() //判断栈是否为空
创建队列对象
Queue<Integer> queue = new LinkedList<Integer>();
队列的常用方法
queue.offer(x) //将x添加到队列中
queue.peek() //返回但不删除队列的头
queue.remove() //删除队列的头
queue.poll() //返回并且删除队列的头
queue.isEmpty() //判断队列是否为空
创建双端队列对象
Deque<Integer> queue = new LinkedList<Integer>();
双端队列常用方法
queue.offerFirst(x) //将x添加到队列头
queue.offerLast(x) //将x添加到队列尾
queue.peekFirst() //返回但不删除队列的头
queue.peekLast() //返回但不删除队列的尾
queue.removeFirst() //删除队列的头
queue.removeLast() //删除队列的尾
queue.pollFirst() //返回并且删除队列的头
queue.pollLast() //返回并且删除队列的尾
queue.isEmpty() //判断队列是否为空
创建优先队列(堆)对象
PriorityQueue<Integer> queue = new PriorityQueue<>((a1,a2) -> a1 - a2);//小顶堆
PriorityQueue<Integer> queue = new PriorityQueue<>((a1,a2) -> a2 - a1);//大顶堆
优先队列常用方法
queue.offer(x) //将x添加到堆中
queue.peek() //返回但不删除堆的顶
queue.remove(x) //删除队列中的元素x,如果元素存在
queue.poll() //返回并且删除堆的顶
queue.isEmpty() //判断队列是否为空
queue.size() //查看堆中元素的个数
232. 用栈实现队列
思路其实很简单用两个栈就能实现队列,但是有很多细节
class MyQueue {
Stack<Integer> inStack; //inStack用于push数据
Stack<Integer> outStack; //outStack用于pop数据
public MyQueue() {
inStack = new Stack<Integer>();
outStack = new Stack<Integer>();
}
public void push(int x) {
inStack.push(x); //直接添加到inStack
}
public int pop() {
if (outStack.isEmpty()) { //如果outStack没有元素就让inStack的元素进来
in2out();
}
return outStack.pop(); //出栈
}
public int peek() {
if (outStack.isEmpty()) {
in2out();
}
return outStack.peek(); //查看栈顶
}
public boolean empty() {
return outStack.isEmpty() && inStack.isEmpty(); //两个栈都为空,队列才为空
}
public void in2out() { //当outStack没有元素了会让inStack的所有元素压入outStack中
while(!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}
}
225.用队列实现栈
用队列实现栈,只需要一个队列就可以很简单,在入栈的时候让入栈元素到队头就可以了其他都不用管
class MyStack {
Queue<Integer> queue;
public MyStack() {
queue = new LinkedList<Integer>();
}
public void push(int x) {
int n = queue.size(); //添加元素前队列的大小
queue.offer(x); //添加元素
for(int i = 0;i < n;i++) {
queue.offer(queue.poll()); //依次把之前的元素全部重新入队
}
}
public int pop() {
return queue.poll();
}
public int top() {
return queue.peek();
}
public boolean empty() {
return queue.isEmpty();
}
}
1047. 删除字符串中的所有相邻重复项
这道题一上来第一反应就是用栈,但是我们不需要new一个栈,只需要用栈的top指针的思想就可以
public String removeDuplicates(String s) {
StringBuilder sb = new StringBuilder();
int top = -1; //先置top指针为-1
for(int i = 0;i < s.length();i++) {
char ch = s.charAt(i);
//top>=0 是为了不报错通过,因为charAt里不能有负数
if(top >= 0 && ch == sb.charAt(top)) { //如果栈顶元素与我们遍历到的元素相等
sb.deleteCharAt(top); //删除栈顶位置的元素
top--; //移动top指针到上一个元素
} else {
sb.append(ch); //将没有重复的元素添加到结尾
top++; //top指针移动到该元素,为了下次比较
}
}
return sb.toString();
}
150. 逆波兰表达式求值
这道题使用栈,遍历tokens数组,如果是数就加入到栈中,如果是符号就从栈中取出两个数**(注意:先弹出的是右操作数,后弹出的是左操作数)**将两个数进行该符号的运算并把运算结果再次加入到栈中
class Solution {
public int evalRPN(String[] tokens) {
Stack<Integer> stack = new Stack<>();
for(int i = 0;i < tokens.length;i++) {
String token = tokens[i];
if(isNumber(token)) {
stack.push(Integer.parseInt(token));
} else {
int num2 = stack.pop();
int num1 = stack.pop();
switch(token) {
case "+":
stack.push(num1 + num2);
break;
case "-":
stack.push(num1 - num2);
break;
case "*":
stack.push(num1 * num2);
break;
case "/":
stack.push(num1 / num2);
break;
default:
}
}
}
return stack.pop();
}
public boolean isNumber(String token) {
return !("+".equals(token) || "-".equals(token) ||
"*".equals(token) || "/".equals(token));
}
}
239. 滑动窗口最大值
这道题需要用特殊的队列解决。
特殊队列的特点:
1. 队列中存入的是数组下标
2. 队列中每次加入新的元素会从队尾开始比较大小把比自己小的删除掉再将自己插入进去
3. 队列会淘汰过期的数
4. 由以上特点可知每次最大值都在队头
public int[] maxSlidingWindow(int[] nums, int k) {
int n = nums.length;
Deque<Integer> queue = new LinkedList<Integer>(); //双端队列
for(int i = 0;i < k;i++) { //删除比当前遍历的数小的的数,如果都小,会把队列清空
while(!queue.isEmpty() && nums[i] >= nums[queue.peekLast()]) {
queue.pollLast();
}
queue.offerLast(i); //删除后将当前遍历的下标存入队列
}
int[] ans = new int[n - k + 1];
ans[0] = nums[queue.peekFirst()]; //第一个最大值就是刚刚插入k个数后队列的头
for(int i = k;i < n;i++) { //删除比当前遍历的数小的的数,如果都小,会把队列清空
while(!queue.isEmpty() && nums[i] >= nums[queue.peekLast()]) {
queue.pollLast();
}
queue.offerLast(i); //删除后将当前遍历的下标存入队列
if(queue.peekFirst() <= i - k) { //判断队头是否过期,如果过期就删除掉
queue.pollFirst();
}
ans[i - k + 1] = nums[queue.peekFirst()]; //队头就是当前窗口的最大值
}
return ans;
}
347. 前 K 个高频元素
看到题目第一反应是使用哈希表统计每个数出现的次数。想求出k个高频元素要使用最大长度为k的堆,因为我们需要不断向队列中进频率数,满了每次需要一进一出,所以我们需要使用小顶堆来淘汰频率小的数。
public int[] topKFrequent(int[] nums, int k) {
/*统计每种数出现的频次*/
Map<Integer,Integer> map = new HashMap<Integer,Integer>();
for(int i : nums) {
if(map.containsKey(i)) {
map.put(i,map.get(i) + 1);
} else {
map.put(i,1);
}
}
//创建小顶堆(小顶堆中的元素是数组用来存放key和value)
PriorityQueue<int[]> queue = new PriorityQueue<>
((pair1,pair2) -> pair1[1] - pair2[1]);
for(int key: map.keySet()) {
int value = map.get(key);
if(queue.size() < k) {
queue.add(new int[]{key,value}); //如果堆没满,直接将key和value加入到堆中
} else {
if(value > queue.peek()[1]) { //如果堆满了,判断堆顶和当前键值对的value谁大
queue.poll(); //如果当前的value大,移除堆顶,把当前的键值对加到堆中
queue.add(new int[]{key,value});
}
}
}
int[] ans = new int[k];
for(int i = k - 1;i >= 0;i--) { //由于要按顺序输出,我们用的又是小顶堆,所以从数组末尾
ans[i] = queue.poll()[0]; //把key装到ans中
}
return ans;
}
单调栈
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
739. 每日温度
这道题很明显是要找数组任意一个位置右边比自己大的元素的位置用单调递增栈(从上到下),栈中存储的是数组的下标,当我们的元素要如栈时,先查看栈顶元素是否小于要入栈的元素,如果小于就要弹出栈,并且可以计算出i - stack.peek就是我们要求的天数,再次查看反复上述过程知道栈空或者栈顶元素大于要入栈的元素。最后在栈中没有被弹出的元素都是最大的元素和最后一个元素,用数组默认的0就行。
public int[] dailyTemperatures(int[] temperatures) {
int[] ans = new int[temperatures.length];
Stack<Integer> stack = new Stack<Integer>();
for(int i = 0;i < temperatures.length;i++) {
while(!stack.isEmpty() && temperatures[i] > temperatures[stack.peek()]) {
ans[stack.peek()] = i - stack.pop();
}
stack.push(i);
}
return ans;
}
496.下一个更大的数1
这道题跟每日温度的区别是多了一个数组num1,并且最后查看的不是位置,而是值,由于多了一个数组,我们需要用哈希表从num1映射到num2,哈希表的key存储num2中各个数的值,value存储key的下一个更大元素的值,num1通过哈希表就可以找到自己的数对应num2中元素的下一个最大值,我们从后向前遍历,每次把遍历到的值放入栈中,当遇到比自己小或者等于自己的元素,直接弹出栈。最后看栈中是否为空,如果为空说明右面没有比自己大的数,如果不为空那么栈顶的数就是第一个比自己大的元素
public int[] nextGreaterElement(int[] nums1, int[] nums2) {
Map<Integer,Integer> map = new HashMap<Integer,Integer>();
Stack<Integer> stack = new Stack<>();
for(int i = nums2.length - 1;i >= 0;i--) {
while(!stack.isEmpty() && nums2[i] >= stack.peek()) {
stack.pop();
}
map.put(nums2[i],stack.isEmpty() ? -1 : stack.peek());
stack.push(nums2[i]);
}
int[] ans = new int[nums1.length];
for(int i = 0;i < ans.length;i++) {
ans[i] = map.get(nums1[i]);
}
return ans;
}
503. 下一个更大元素 II
跟每日温度如出一辙,只是涉及到了循环队列并且返回的答案不是距离是数值,我们让数组遍历两圈收集数值即可
public int[] nextGreaterElements(int[] nums) {
int size = nums.length;
Stack<Integer> stack = new Stack<>();
int[] ans = new int[size];
Arrays.fill(ans,-1);
for(int i = 0;i < 2 * size;i++) {
while(!stack.isEmpty() && nums[i % size] > nums[stack.peek()]) {
ans[stack.pop()] = nums[i % size];
}
stack.push(i % size);
}
return ans;
}
42. 接雨水
很重要的一道高频面试题,有三种做法
方法一:动态规划
思路:如果我们按列计算,每列能接住的雨水=左边和右边最大高度中较小的高度 - 该列高度,所以我们先求出从左往右看的最大高度和从右往左看的最大高度,求出两个数组,然后套上面的公式即可
public int trap(int[] height) {
int n = height.length;
if(n == 0) {
return 0;
}
int[] leftMax = new int[n];
leftMax[0] = height[0];
for(int i = 1;i < n;i++) {
leftMax[i] = Math.max(leftMax[i - 1],height[i]);
}
int[] rightMax = new int[n];
rightMax[n - 1] = height[n - 1];
for(int i = n - 2;i >= 0;i--) {
rightMax[i] = Math.max(rightMax[i + 1],height[i]);
}
int sum = 0;
for(int i = 0;i < n;i++) {
int x = Math.min(leftMax[i],rightMax[i]) - height[i];
sum += x;
}
return sum;
}
方法二:单调栈
思路:利用单调递减栈(从上到下),遍历数组每次把当前的下标放入到栈中,当遇到当前高度大于栈顶高度时,让栈顶弹出再次判断是否为空如果为空说明是边界(像下图0的位置一样)直接break退出循环,如果不为空,查看弹出元素后栈顶元素并计算宽度和高度再相乘最后加到答案中。
如图中7的位置,我们弹出了cur = 6此时再查看栈顶元素应该是left = 3(4在上计算已经弹出了)
宽度= i - left - 1,高度 = min(height[i],height[left]) - height[cur], ans += 宽度 * 高度
public int trap(int[] height) {
int ans = 0;
Stack<Integer> stack = new Stack<Integer>();
for(int i = 0;i < height.length;i++) {
while(!stack.isEmpty() && height[i] > height[stack.peek()]) {
int cur = stack.pop();
if(stack.isEmpty()) {
break;
}
int left = stack.peek();
int w = i - left - 1;
int h = Math.min(height[i],height[left]) - height[cur];
ans += w * h;
}
stack.push(i);
}
return ans;
}
方法三:双指针(最优方法)
思路:如果左指针位置的高度比右指针小看第一个图,把左侧的最大高度 - 当前高度加到答案中,让左指针右移
如果右指针位置的比左指针小看第二个图,把右侧的对打高度 - 当前高度加到答案中,让右指针左移
当两个指针相遇时退出循环
public int trap(int[] height) {
int max_left = 0;
int max_right = 0;
int left = 0;
int right = height.length - 1;
int ans = 0;
while(left < right) {
max_left = Math.max(height[left],max_left);
max_right = Math.max(height[right],max_right);
if(height[left] < height[right]) {
ans += max_left - height[left];
left++;
} else {
ans += max_right - height[right];
right--;
}
}
return ans;
}
回溯
回溯算法模板
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
77. 组合
题目特点:所有元素没有重复的,而且不能重复使用,没有限制数的总和,但是限制了数的数量
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
public List<List<Integer>> combine(int n, int k) {
back(n,k,1);
return ans;
}
public void back(int n,int k,int start) {
if(cur.size() == k) {
ans.add(new ArrayList<Integer>(cur));
return;
}
for(int i = start;i <= n - (k - cur.size()) + 1;i++) { //剪支
cur.add(i);
back(n,k, i + 1);
cur.remove(cur.size() - 1);
}
}
当循环剩余的数和集合中的数+1(当前数)的数量和小于k的时候无论我们怎么遍历都无法的到答案所以可以剪支
216.组合总和 III
题目特点:元素没有重复的,元素不能重复使用,限制了元素的总和,限制了元素个数
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
public List<List<Integer>> combinationSum3(int k, int n) {
back(1,0,k,n);
return ans;
}
public void back(int start,int sum,int k,int n) {
if(sum > n) { //当sum大于n再怎么循环都得不到解,所以剪支
return;
}
if(cur.size() == k) {
if(sum == n) {
ans.add(new ArrayList<Integer>(cur));
}
return;
}
for(int i = start;i <= 9;i++) {
cur.add(i);
sum += i;
back(i + 1,sum,k,n);
sum -= i;
cur.remove(cur.size() - 1);
}
}
39. 组合总和
元素没有重复的,元素可以无限次数使用,限制了数的总和,但是没有限制数的数量
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
public List<List<Integer>> combinationSum(int[] candidates, int target) {
Arrays.sort(candidates);
back(0,0,candidates,target);
return ans;
}
public void back(int start,int sum,int[] candidates, int target) {
if(sum == target) {
ans.add(new ArrayList<>(cur));
return;
}
if(sum > target) {
return;
}
for(int i = start;i < candidates.length && sum + candidates[i] <= target;i++) {
sum += candidates[i];
cur.add(candidates[i]);
back(i,sum,candidates,target);
cur.remove(cur.size() - 1);
sum -= candidates[i];
}
}
40. 组合总和 II
有重复元素,元素不能多次使用,限制了数的总和,没限制数的个数
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
boolean[] used;
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
used = new boolean[candidates.length];
Arrays.fill(used,false);
Arrays.sort(candidates);
back(0,0,candidates,target);
return ans;
}
public void back(int start,int sum,int[] candidates, int target) {
if(sum == target) {
ans.add(new ArrayList<>(cur));
return;
}
for(int i = start;i < candidates.length && sum + candidates[i] <= target;i++) {
if(i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
continue;
}
used[i] = true;
sum += candidates[i];
cur.add(candidates[i]);
back(i + 1,sum,candidates,target);
cur.remove(cur.size() - 1);
sum -= candidates[i];
used[i] = false;
}
}
17. 电话号码的字母组合
没有重复元素,元素不能重复使用,间接限制了元素的个数,跟总和没关系
List<String> ans = new ArrayList<>();
StringBuilder cur = new StringBuilder();
String[] stringNum =
{"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
public List<String> letterCombinations(String digits) {
if(digits == null || digits.length() == 0) {
return ans;
}
back(digits,0);
return ans;
}
public void back(String digits,int num) {
if(num == digits.length()) {
ans.add(cur.toString());
return;
}
String str = stringNum[digits.charAt(num) - '0'];
for(int i = 0;i < str.length();i++) {
cur.append(str.charAt(i));
back(digits,num + 1);
cur.deleteCharAt(cur.length() - 1);
}
}
131. 分割回文串
List<List<String>> ans = new ArrayList<>();
List<String> cur = new ArrayList<>();
public List<List<String>> partition(String s) {
back(0,s);
return ans;
}
public void back(int start,String s) {
if(start == s.length()) {
ans.add(new ArrayList<>(cur));
return;
}
for(int i = start;i < s.length();i++) {
if(cheak(s,start,i)) {
cur.add(s.substring(start,i + 1));
} else {
continue;
}
back(i + 1,s);
cur.remove(cur.size() - 1);
}
}
public boolean cheak(String s,int start,int end) {
while(start < end) {
if(s.charAt(start) != s.charAt(end)) {
return false;
}
start++;
end--;
}
return true;
}
93. 复原 IP 地址
List<String> ans = new ArrayList<>();
public List<String> restoreIpAddresses(String s) {
back(0,0,s);
return ans;
}
public void back(int start,int count,String s) {
if(count == 3) {
if(cheak(s,start,s.length() - 1)) {
ans.add(s);
}
return;
}
for(int i = start;i < s.length();i++) {
if(cheak(s,start,i)) {
s = s.substring(0,i + 1) + "." + s.substring(i + 1);
count++;
back(i + 2,count,s);
count--;
s = s.substring(0,i + 1) + s.substring(i + 2);
} else {
break;
}
}
}
public boolean cheak(String s,int start,int end) {
if(start > end) {
return false;
}
if(s.charAt(start) == '0' && start != end) {
return false;
}
int num = 0;
for(int i = start;i <= end;i++) {
num = num * 10 + (s.charAt(i) - '0');
if(num > 255) {
return false;
}
}
return true;
}
78. 子集
没有重复元素
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
public List<List<Integer>> subsets(int[] nums) {
back(0,nums);
return ans;
}
public void back(int start,int[] nums) {
ans.add(new ArrayList<>(cur));
for(int i = start;i < nums.length;i++) {
cur.add(nums[i]);
back(i + 1,nums);
cur.remove(cur.size() - 1);
}
}
90. 子集 II
可能有重复元素
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
boolean[] used;
public List<List<Integer>> subsetsWithDup(int[] nums) {
used = new boolean[nums.length];
Arrays.sort(nums);
back(0,nums);
return ans;
}
public void back(int start,int[] nums) {
ans.add(new ArrayList<>(cur));
for(int i = start;i < nums.length;i++) {
if(i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
used[i] = true;
cur.add(nums[i]);
back(i + 1,nums);
used[i] = false;
cur.remove(cur.size() - 1);
}
}
491. 递增子序列
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
public List<List<Integer>> findSubsequences(int[] nums) {
back(0,nums);
return ans;
}
public void back(int start,int[] nums) {
if(cur.size() >= 2) {
ans.add(new ArrayList<>(cur));
}
int[] used = new int[201];
for(int i = start;i < nums.length;i++) {
if((!cur.isEmpty() && nums[i] < cur.get(cur.size() - 1))
|| used[nums[i] + 100] == 1) {
continue;
}
used[nums[i] + 100] = 1;
cur.add(nums[i]);
back(i + 1,nums);
cur.remove(cur.size() - 1);
}
}
46. 全排列
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
boolean[] used;
public List<List<Integer>> permute(int[] nums) {
used = new boolean[nums.length];
back(nums);
return ans;
}
public void back(int[] nums) {
if(cur.size() == nums.length) {
ans.add(new ArrayList<>(cur));
return;
}
for(int i = 0;i < nums.length;i++) {
if(used[i]) {
continue;
}
used[i] = true;
cur.add(nums[i]);
back(nums);
cur.remove(cur.size() - 1);
used[i] = false;
}
}
47. 全排列 II
List<List<Integer>> ans = new ArrayList<>();
List<Integer> cur = new ArrayList<>();
boolean[] used;
public List<List<Integer>> permuteUnique(int[] nums) {
used = new boolean[nums.length];
Arrays.sort(nums);
back(nums);
return ans;
}
public void back(int[] nums) {
if(nums.length == cur.size()) {
ans.add(new ArrayList<>(cur));
return;
}
for(int i = 0;i < nums.length;i++) {
if(i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
continue;
}
if(used[i]) {
continue;
}
used[i] = true;
cur.add(nums[i]);
back(nums);
cur.remove(cur.size() - 1);
used[i] = false;
}
}
动态规划
动态规划解题步骤
- 确定dp数组(dp table)以及下标的含义
- 确定递推公式
- dp数组如何初始化
- 确定遍历顺序
- 举例推导dp数组
入门级问题
746. 使用最小花费爬楼梯
- dp[i]中i为台阶的阶数,dp[i]为达到该阶数需要花费的费用
-
dp[i] = min(dp[i - 1] + cost[i - 1],dp[i - 2] + cost[i - 2])
- dp[0] = 0 dp[i] = 0;
- 从dp[2] 开始遍历
- dp[2] = min(dp[0] + cost[0],dp[1] + cost[1])
public int minCostClimbingStairs(int[] cost) {
int n = cost.length;
if(n < 1) {
return 0;
}
int[] f = new int[n + 1];
for(int i = 2;i <= n;i++) {
f[i] = Math.min(f[i - 1] + cost[i - 1],f[i - 2] + cost[i - 2]);
}
return f[n];
}
62. 不同路径1
- dp[i] [j]中 i 表示行号 j表示列号 dp[i] [j]表示到达(i,j)的坐标点有多少条路径
-
dp[i] [j] = dp[i - 1] [j] + dp[i] [j - 1] 机器人只能向下或右走,所以只能从上面和左面两个位置走到[i] [j]点
- dp[i] [0] = 1,dp[0] [i] = 1(第一行和第一列只有一种路径就是直下走或直右走)
- i :1 ~ m j:1 ~ n
- dp[1] [1] = dp[0] [1] + dp[1] [0] = 1 + 1 = 2
public int uniquePaths(int m, int n) {
int[][] dp = new int[m][n];
for(int i = 0;i < m;i++) {
dp[i][0] = 1;
}
for(int i = 0;i < n;i++) {
dp[0][i] = 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];
}
63. 不同路径 II
这道题其实和不同路径1差不多,只是多了一个障碍物,我们在初始化和递推公式上要注意
- dp[i] [j]中 i 表示行号 j表示列号 dp[i] [j]表示到达(i,j)的坐标点有多少条路径
-
使用递归公式之前要先判断obstacleGrid[i] [j]是否为0,如果为1说明有障碍物直接用数组默认的0就可以,dp[i] [j] = dp[i - 1] [j] + dp[i] [j - 1] 机器人只能向下或右走,所以只能从上面和左面两个位置走到[i] [j]点
- 初始化之前也要判断是否有障碍物,如果有也直接用数组默认的0,dp[i] [0] = 1,dp[0] [i] = 1(第一行和第一列只有一种路径就是直下走或直右走)
- i :1 ~ m j:1 ~ n
- dp[1] [1] = dp[0] [1] + dp[1] [0]
int m = obstacleGrid.length;
int n = obstacleGrid[0].length;
int[][] dp = new int[m][n];
for(int i = 0;i < m && obstacleGrid[i][0] == 0;i++) {
dp[i][0] = 1;
}
for(int i = 0;i < n && obstacleGrid[0][i] == 0;i++) {
dp[0][i] = 1;
}
for(int i = 1;i < m;i++) {
for(int j = 1;j < n;j++){
if(obstacleGrid[i][j] == 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
}
343. 整数拆分
-
dp[i] 中 i为数字(1 ~ n) dp[i]为i拆分之后的最大乘积
-
数字拆分有2种可能,一种是把数字拆成2个数 j 和 i - j,一种是把数字拆成多个数 j 和 dp[i - j] 所以递推公式为dp[i] = max(dp[i],max(j * (i - j) ,j * dp[i - j]))
-
dp[2] = 1 * 1 = 1 dp[0]和dp[1] 没有意义 所以不初始化
-
i :3 ~ n j:1 ~ i - j
public int integerBreak(int n) {
int[] dp = new int[n + 1];
dp[2] = 1;
for(int i = 3;i <= n;i++) {
for(int j = 1;j <= i - j;j++) {
dp[i] = Math.max(dp[i],Math.max(j * (i - j),j * dp[i - j]));
}
}
return dp[n];
}
96. 不同的二叉搜索树
-
dp[i]中 i代表有值为(1 ~ i)的节点 i 个节点 dp[i]为i个节点有多少种二叉树
-
dp[i] += dp[j - 1] * dp[i - j] 左树的种类数*右树的种类数 其中j - 1是为了去掉root节点所占用的具体看下面的列举
-
dp[0] = 1,dp[1] = 1
-
从2个节点开始求有多少种二叉树直到n
-
dp[3] = dp[0] * dp[2] + dp[1] * dp[1] + dp[2] * dp[0]
public int numTrees(int n) {
int[] dp = new int[n + 1];
dp[0] = 1;
dp[1] = 1;
for(int i = 2;i <= n;i++) {
for(int j = 1; j <= i;j++) {
dp[i] += dp[j - 1] * dp[i - j];
}
}
return dp[n];
}
背包问题及其应用
01背包
背包问题是一个经典的dp问题我们用dp[i] [j]这个二维数组进行动态规划
dp[i] [j] 其中i表示遍历到哪个物品(如果i = 1 代表遍历过物品 0 和物品 1)j代表背包的容量 dp[i] [j] 则代表总价值
如果容量 j >= 物品 i 的重量
dp[i] [j] = max(dp[i - 1] [j], dp[i - 1] [j - 物品i的重量] +物品i的容积)
如果容量 j < 物品 i的重量 dp[i][j] = dp[i - 1][j] //复制上一层的结果
如下图所示我们每一个结果都是由它上方(dp[i - 1] [j] )或者左上方(dp[i - 1] [j - 物品i的重量])推出来的,所以我们要初始化第一行和第一列,其中第一列代表背包的容量为0,全部初始化为默认的0即可,而第一行代表遍历物品0之后背包中物品的价值,所以初始化时先判断当前容量是否大于等于物品0的重量如果大于等于则初始化为物品0的质量,反之则用默认的0
遍历时我们先遍历物品(从物品1开始)再遍历背包容量(从0开始)
演例代码如下
for (int j = weight[0]; j <= bagweight; j++) {
dp[0][j] = value[0];
}
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]);
}
}
用一维数组解决01背包问题
用二维数组的时候我们每次需要用上一层的正上方或者左上方的数据推出当前层的数据,这个过程中浪费很多空间,其实没必要用二维数组,直接用一维数组并且每次更新数据即可,但是要注意的一点是遍历顺序,我们这次只有一层数据,我们需要左上方或者上方的数据来推到当前层的数据,所以我们要反向遍历背包的容量,因为如果正向遍历背包的容量会破坏左上方的数据。对于初始化我们直接用默认的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]);
}
}
完全背包
完全背包和01背包问题唯一不同的地方就是,每种物品有无限件。
用一维数组解决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]);
}
}
完全背包问题常常会让求有多少种方法
递推公式:dp[j] += dp[j - num[i]] //这个递推公式很重要背包问题求有多少种方式基本都是用这个公式
而求有多少种方法装满背包还有两种情况,一种考虑顺序,一种不考虑顺序
考虑顺序时:先遍历背包后遍历物品
不考虑顺序时:先遍历物品后遍历背包
求背包最少装多少东西能装满
初始化dp数组为max,dp[0] = 0
递推公式:
if(dp[j - num[i]] != max)
dp[j] = Math.min(dp[j],dp[j - num[i]] + 1);
416. 分割等和子集
这道题的解题思路是如果我们要找两个子集元素和相等,首先数组的各数和要为偶数,其次我们要在数组中找到一个sum / 2的子集那么另一个子集的和一定也为sum / 2。很明显这是一个有规则的选择问题而且每个数只能选一次跟01背包如出一辙,这道题的最大容量是sum / 2,物品为数组中的每个数,物品的重量和价值都为数组中数的值,直接套用01背包的解题过程即可
public boolean canPartition(int[] nums) {
int sum = 0;
for(int i = 0;i < nums.length;i++) {
sum += nums[i]; //求和
}
if(sum % 2 == 1) {
return false; //不为偶数直接返回false
}
int target = sum / 2; //我们要寻找的和
int[] dp = new int[target + 1]; //背包最大容量为target
for(int i = 0;i < nums.length;i++) {
for(int j = target;j >= nums[i];j--) {
dp[j] = Math.max(dp[j],dp[j - nums[i]] + nums[i]);
}
}
return target == dp[target];
}
1049. 最后一块石头的重量 II
这道题跟分割等和数其实是差不多的,我们可以把石头分成两堆,两堆的重量和尽量都接近于 sum / 2,所以我们用01背包的思路尽量凑出一组 sum / 2 如果凑不出也没关系用能凑出的最接近sum / 2的数,最后让两个石头堆相撞就行,我们通过动规找出的石头堆重量为sum - dp[target],令一堆石头的重量为sum - dp[target],因为sum/2是向下取整的所以 sum - dp[target] >= dp[target],最后的答案为(sum - dp[target]) - dp[target],简化为sum - 2 * target
public int lastStoneWeightII(int[] nums) {
int sum = 0;
for(int i = 0;i < nums.length;i++) {
sum += nums[i];
}
int target = sum / 2;
int[] dp = new int[target + 1];
for(int i = 0;i < nums.length;i++) {
for(int j = target;j >= nums[i];j--) {
dp[j] = Math.max(dp[j],dp[j - nums[i]] + nums[i]);
}
}
return sum - 2 * dp[target];
}
494. 目标和
这道题也可以变为背包问题我们可以把数组中的数分为正数和负数两组,然后正数+负数=target,其实从给出的已知条件可以求出正数或负数是多少推导步骤如下
设正数为left 负数为right 数组上所有数的和为sum
sum = left + right
target = left - right
其中sum和target都是已知量求这个方程组
left = (sum + target) / 2
所以我们接下来只要找到有多少种方式能在数组中凑出left即可。
当sum + target % 2 ==1, 因为从方程组的角度考虑让两个方程相加得到sum + target == 2 * left,left又是一个正整数,那么这时如果sum + target如果是奇数。那么left这个正整数是不存在的,所以return 0
当(sum + target )/ 2 < 0时我们的left也是不存在的因为left是一个正整数,我们推出的又是一个负数,显然是不存在的
递推公式:dp[j] += dp[j - num[i]] //这个递推公式很重要背包问题求有多少种方式基本都是用这个公式
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for(int num :nums)
sum += num;
if (sum + target < 0 || (sum + target) % 2 == 1) {
return 0;
}
int x = (sum + target) / 2;
int[] dp = new int[x + 1];
dp[0] = 1;
for(int i = 0;i < nums.length;i++) {
for(int j = x;j >= nums[i];j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[x];
}
518. 零钱兑换 II
这道题是完全背包问题并且是一个组合的问题即不考虑零钱的顺序,所以在遍历的时候先遍历物品再遍历背包
public int change(int amount, int[] coins) {
int[] dp = new int[amount + 1];
dp[0] = 1;
for(int i = 0;i < coins.length;i++) {
for(int j = coins[i];j <= amount;j++) {
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
377. 组合总和 Ⅳ
这道题还是完全背包问题,但是这道题是排列问题即考虑数的顺序,所以先遍历背包再遍历物品
public int combinationSum4(int[] nums, int target) {
int[] dp = new int[target + 1];
dp[0] = 1;
for(int j = 0;j <= target;j++) {
for(int i = 0;i < nums.length;i++) {
if(j >= nums[i])
dp[j] += dp[j - nums[i]];
}
}
return dp[target];
}
322. 零钱兑换
这道题还是完全背包但是要求出最少硬币个数所以递推公式要用到min函数,所以初始化dp数组为max,
dp[0] = 0,递推公式为dp[j] = Math.min(dp[j],dp[j - coins[i]] + 1);
public int coinChange(int[] coins, int amount) {
int max = Integer.MAX_VALUE;
int[] dp = new int[amount + 1];
Arrays.fill(dp,max);
dp[0] = 0;
for(int i = 0;i < coins.length;i++) {
for(int j = coins[i];j <= amount;j++) {
if(dp[j - coins[i]] != max)
dp[j] = Math.min(dp[j],dp[j - coins[i]] + 1);
}
}
return dp[amount] == max ? -1 : dp[amount];
}
279. 完全平方数
还是完全背包问题,这次完全平方数是物品,n是背包跟零钱兑换差不多
public int numSquares(int n) {
int max = Integer.MAX_VALUE;
int[] dp = new int[n + 1];
Arrays.fill(dp,max);
dp[0] = 0;
for(int i = 1;i * i <= n;i++) {
for(int j = i * i;j <= n;j++) {
if(dp[j -i * i] != max)
dp[j] = Math.min(dp[j],dp[j - i * i] + 1);
}
}
return dp[n];
}
139. 单词拆分
还是完全背包问题,但是这道题比较特殊,首先我们把字典装进哈希表里,然后定义一个boolean类型的dp数组,由于这道题需要考虑顺序,因此要先遍历背包,再遍历物品。这道题的背包就是字符串的大小,但是物品很特殊,当我们找对应的单词时要保证拼接的位置是ture,所以j从0开始找是ture的位置最大不超过i,然后我们判断哈希表种是否有从字符串的 j到i这样的单词并判断起始位置j是否为true,如果满足条件dp[i]就是ture
public boolean wordBreak(String s, List<String> wordDict) {
Set<String> set = new HashSet<>(wordDict);
boolean[] dp = new boolean[s.length() + 1];
dp[0] = true;
for(int i = 1;i <= s.length();i++) {
for(int j = 0;j < i && !dp[i];j++) {
if(set.contains(s.substring(j,i)) && dp[j])
dp[i] = true;
}
}
return dp[s.length()];
}
打家劫舍问题
198. 打家劫舍
dp[i] 中 i代表房间编号, dp[i]代表到第i个房间能取得最高金额
dp[i] = max(dp[i - 1],d[i - 2] + num[i]) //有两种情况一种是取i号房间,一种是不取
dp[0] = nums[0] dp[1] = max(nums[0], nums[1])
直接遍历数组即可
public int rob(int[] nums) {
if (nums == null || nums.length == 0) return 0;
if (nums.length == 1) return nums[0];
int n = nums.length;
int[] dp = new int[n];
dp[0] = nums[0];
dp[1] = Math.max(nums[0],nums[1]);
for(int i = 2;i < n;i++) {
dp[i] = Math.max(dp[i - 1],dp[i - 2] + nums[i]);
}
return dp[n - 1];
}
213. 打家劫舍 II
这道题跟打家劫舍1得区别在于首位是相连的,我们可以做两次dp一次不考虑最后一个元素,一次不考虑第一个元素,然后让两次dp的结果取最大值
class Solution {
public int rob(int[] nums) {
int n = nums.length;
if(n == 0) {
return 0;
}
if(n == 1) {
return nums[0];
}
if(n == 2) {
return nums[0] > nums[1] ? nums[0] : nums[1];
}
int[] dp1 = new int[n];
dp1[0] = nums[0];
dp1[1] = Math.max(nums[0],nums[1]);
for(int i = 2;i < n - 1;i++) {
dp1[i] = Math.max(dp1[i - 1],dp1[i - 2] + nums[i]);
}
int[] dp2 = new int[n];
dp2[1] = nums[1];
dp2[2] = Math.max(nums[1],nums[2]);
for(int i = 3;i < n;i++) {
dp2[i] = Math.max(dp2[i - 1],dp2[i - 2] + nums[i]);
}
return dp1[n - 2] > dp2[n - 1] ? dp1[n - 2] : dp2[n - 1];
}
}
337. 打家劫舍 III
这道题是一道树形dp要使用递归,每个二叉树的节点都用一个dp数组,dp[0]表示不选择该节点,dp[1]表示选择该节点,用后序遍历,先查看左边的dp数组,再查看右边的dp数组,最后分两种情况,一种是选择当前节点,一种是不选择当前节点,然后将该节点的dp[0]和dp[1]存储起来,如果当前节点为null直接返回数组{0,0}(相当于初始化dp数组了)
public int rob(TreeNode root) {
int[] ans = recursion(root);
return Math.max(ans[0],ans[1]);
}
public int[] recursion(TreeNode root) {
int[] ans = new int[2];
if(root == null) {
return ans;
}
int[] left = recursion(root.left);
int[] right = recursion(root.right);
ans[0] = Math.max(left[0],left[1]) + Math.max(right[0],right[1]);
ans[1] = root.val + left[0] + right[0];
return ans;
}
买卖股票问题
121. 买卖股票的最佳时机
dp[i] [j] i 表示第i天 j 为 0 表示当天不持有股票 j 为 1 表示当天持有股票 dp[i] [j] 表示第 i 天买或不买持有的钱
递推公式分两种
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1] + prices[i])
//当天不持有股票时,可能前一天也不持有,可能前一天持有但是今天卖掉了
dp[i][1] = Math.max(dp[i - 1][1], - prices[i]);
//当天持有股票时,可能前一天也持有,可能前一天不持有但是今天买了
初始化时只需要把第0天不持有股票设为0,持有股票设为 - prices[0]即可
直接遍历prices数组即可
最后输出最后一天不持有股票的状态即可
public int maxProfit(int[] prices) {
if (prices == null || prices.length == 0) return 0;
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1;i < prices.length;i++) {
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1], - prices[i]);
}
return dp[prices.length - 1][0];
}
122. 买卖股票的最佳时机 II
跟上一题略有变化,这次可以多次买卖股票了 递推公式有所变化,当第 i 天持有股票时,可能时昨天就持有,也可能是昨天不持有,今天买的上一题是(0 - preice[i])因为只能买一次,这次是(dp[i - 1] [0] - prices[i])因为这次前一天拥有的钱不一定是0了,可能已经买卖过股票了,所以要把上一次的利润加上
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1] + prices[i])
dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0]- prices[i]);
public int maxProfit(int[] prices) {
int[][] dp = new int[prices.length][2];
dp[0][0] = 0;
dp[0][1] = -prices[0];
for(int i = 1;i < prices.length;i++) {
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][1] + prices[i]);
dp[i][1] = Math.max(dp[i - 1][1],dp[i - 1][0] - prices[i]);
}
return dp[prices.length - 1][0];
}
123. 买卖股票的最佳时机 III
这次的状态比前两次要多,一天有可能的五种状态
- 不操作:手里的钱永远为0
- 第一次持有:可能前一天就第一次持有,可能前一天不操作,今天第一次买入
- 第一次不持有:可能前一天就第一次不持有,可能前一天第一次持有,今天卖出
- 第二次持有:可能前一天就第二次持有,可能前一天第一次不持有,今天买入
- 第二次不持有:可嫩前一天就第二次不持有,可能前一天第二次持有,今天卖出
所以二维dp数组的为
int[][] dp = new int[prices.length][5] //5代表上面五种状态
递推公式也有五种
dp[i][1] = Math.max(dp[i - 1][1],dp[i - 1][0] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][2],dp[i - 1][1] + prices[i]);
dp[i][3] = Math.max(dp[i - 1][3],dp[i - 1][2] - prices[i]);
dp[i][4] = Math.max(dp[i - 1][4],dp[i - 1][3] + prices[i]);
//还有一种就是一直不操作手里的钱一直为0,所以可以不写了
我们可以把五种状态都初始化,因为可以当天买当天卖
所以初始化为
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
dp[0][0] = dp[0][2] = dp[0][4] = 0 //可以直接不用写了因为默认就是0
遍历顺序还是遍历整个数组除了已经初始化的第0天
打印最后一天的最后一种状态
public int maxProfit(int[] prices) {
int[][] dp = new int[prices.length][5];
dp[0][1] = -prices[0];
dp[0][3] = -prices[0];
for(int i = 1;i < prices.length;i++) {
dp[i][1] = Math.max(dp[i - 1][1],dp[i - 1][0] - prices[i]);
dp[i][2] = Math.max(dp[i - 1][2],dp[i - 1][1] + prices[i]);
dp[i][3] = Math.max(dp[i - 1][3],dp[i - 1][2] - prices[i]);
dp[i][4] = Math.max(dp[i - 1][4],dp[i - 1][3] + prices[i]);
}
return dp[prices.length - 1][4];
}
188. 买卖股票的最佳时机 IV
其实这道题就是上一道题的总结版可以有多种写法主要思路还是源于上一题
首先定义dp数组方面每多一次交易,我们就要多两个状态,所以数组长度为 2 * k + 1
递推公式我们需要再加一层循环来把所有从 1 到 2k的状态都列出来
通过观察按照上一题状态的表示,第0天奇数状态都要初始化为 - prices[0]
遍历顺序和最终结果跟前面所有题一样不必多说
public int maxProfit(int k, int[] prices) {
int[][] dp = new int[prices.length][2 * k + 1];
for(int i = 1;i <= 2 * k;i += 2) {
dp[0][i] = -prices[0];
}
int temp;
for(int i = 1;i < prices.length;i++) {
for(int j = 1;j <= 2 * k;j++) {
if(j % 2 == 1) {
temp = -prices[i];
} else {
temp = prices[i];
}
dp[i][j] = Math.max(dp[i - 1][j],dp[i - 1][j - 1] + temp);
}
}
return dp[prices.length - 1][2 * k];
}
309. 最佳买卖股票时机含冷冻期
int n = prices.length;
int[][] dp = new int[n][3];
dp[0][0] = -prices[0];
for(int i = 1;i < prices.length;i++) {
// 0 持有 1 冷冻 2未持有
dp[i][0] = Math.max(dp[i - 1][0],dp[i - 1][2] - prices[i]);
dp[i][1] = dp[i - 1][0] + prices[i]; //这里有逻辑漏洞 但是只能这样写才能ac
dp[i][2] = Math.max(dp[i - 1][1],dp[i - 1][2]);
}
return Math.max(dp[n - 1][1],dp[n - 1][2]);
}
子序列问题
300. 最长递增子序列
dp[i] 中 i 表示数组中的元素下标,dp[i]表示以num[i]为结尾的最长递增子序列
用两重循环进行遍历 第一重循环遍历数组中的数,第二重循环遍历前面的数
递推公式:先判断num[i]>num[j] dp[i] = max(dp[i],dp[j] + 1);
初始化dp数组中所有值为1
最后结果是整个dp数组的最大值
public int lengthOfLIS(int[] nums) {
int[] dp = new int[nums.length];
Arrays.fill(dp,1);
for(int i = 1;i < nums.length;i++) {
for(int j = 0;j < i;j++) {
if(nums[i] > nums[j])
dp[i] = Math.max(dp[i],dp[j] + 1);
}
}
int ans = 0;
for(int i = 0;i < dp.length;i++) {
ans = Math.max(ans,dp[i]);
}
return ans;
}
674. 最长连续递增序列
这道题不用动态规划很简单,设置一个计数器变量和一个答案变量,初始值都为1当后面的数大于前面的数就让计数器+1,然后让答案变量跟计数器变量比较大小去较大的值,如果后面的数比前面的数小,就置计数器为1
public int findLengthOfLCIS(int[] nums) {
int ans = 1;
int count = 1;
for(int i = 1;i < nums.length;i++) {
if(nums[i] > nums[i - 1]) {
count++;
ans = Math.max(ans,count);
} else {
count = 1;
}
}
return ans;
}
718. 最长重复子数组
dp[i][j] 表示nums1[i - 1]为结尾和num2[j - 1]为结尾的最长重复子数组长度为dp[i][j]
递推公式:dp[i][j] = dp[i - 1][j - 1] + 1;
public int findLength(int[] nums1, int[] nums2) {
int[][] dp = new int[nums1.length + 1][nums2.length + 1];
int ans = 0;
for(int i = 1;i <= nums1.length;i++) {
for(int j = 1;j <= nums2.length;j++) {
if(nums1[i - 1] == nums2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1;
ans = Math.max(ans,dp[i][j]);
}
}
}
return ans;
}
空间优化后
public int findLength(int[] nums1, int[] nums2) {
int[] dp = new int[nums2.length + 1];
int ans = 0;
for(int i = 1;i <= nums1.length;i++) {
for(int j = nums2.length;j > 0;j--) {
if(nums1[i - 1] == nums2[j - 1]) {
dp[j] = dp[j - 1] + 1;
ans = Math.max(dp[j],ans);
} else {
dp[j] = 0;
}
}
}
return ans;
}
递推公式推导图示
由于必须是连续的所以只能从斜上方推出斜下方
1143. 最长公共子序列
这道题跟上一题的区别是可以不连续了
假设我们比较 “abcde” 和 “ace"
当我们比较到 abc 和 ace的时候发现 c 和 e不相等,如果要求连续后面就不用看了,但是这道题可以不连续,当元素不相等的时候我们无法直接放弃匹配 而是继续看 abc 和 ac有多少匹配的或者 ab 和ace有多少匹配的所以在递推公式上有所改变。
if(text1.charAt(i - 1) == text2.charAt(j - 1)) { //当前匹配的元素如果相等就从左上角的位置+1
dp[i][j] = dp[i - 1][j - 1] + 1;
} else { //如果不等就从左面或者右面推出
dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
}
如下图所示
由于这次不管怎么推都有结果不会中途放弃,所以最终答案在二维数组的右下角
public int longestCommonSubsequence(String text1, String text2) {
int[][] dp = new int[text1.length() + 1][text2.length() + 1];
for(int i = 1;i <= text1.length();i++) {
for(int j = 1;j <= text2.length();j++) {
if(text1.charAt(i - 1) == text2.charAt(j - 1)) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j],dp[i][j - 1]);
}
}
}
return dp[text1.length()][text2.length()];
}
53. 最大子数组和
非常简单的dp问题,每次比较前面的和加当前遍历的数大还是当前的数大,然后用ans记录最大值
public int maxSubArray(int[] nums) {
int[] dp = new int[nums.length];
int ans = nums[0];
dp[0] = nums[0];
for(int i = 1;i < nums.length;i++) {
dp[i] = Math.max(dp[i - 1] + nums[i],nums[i]);
ans = Math.max(ans,dp[i]);
}
return ans;
}
647. 回文子串
思路:dp是一个Boolean型的二维数组 dp[ i ] [ j ]表示在 i 到 j 的范围是否是回文子串
递推公式: if(s.charAt(i) == s.charAt(j) && (j - i <= 1 || dp[i + 1][j - 1]) ) {
dp[i][j] = true;
ans++;
}
如果i位置和j位置的字符相同,有两种情况i到j是回文子串
1. i到j的距离小于等于1 即 字符串为 a aa 这种类型
2. i到j的字符串长度为3以上,我们需要判断dp[i + 1][j - 1]是否为回文子串 即中间是不是回文子串
遍历顺序:先让右指针 j 向右扩展 每次 i 跟着 j 走到 j 的位置就结束这样可以确保递推公式中可以推出结果
public int countSubstrings(String s) {
int n = s.length();
boolean [][] dp = new boolean[n][n];
int ans = 0;
for(int j = 0;j < n;j++) {
for(int i = 0;i <= j;i++) {
if(s.charAt(i) == s.charAt(j) && (j - i <= 1 || dp[i + 1][j - 1]) ) {
dp[i][j] = true;
ans++;
}
}
}
return ans;
}
5. 最长回文子串
这道题其实和回文子串一样,只不过问的内容不同这次我们需要一个begin变量和一个len变量,来记录最长子串的开始位置和长度
public String longestPalindrome(String s) {
int n = s.length();
boolean [][] dp = new boolean[n][n];
int begin = 0;
int len = 0;
for(int j = 0;j < n;j++) {
for(int i = 0;i <= j;i++) {
if(s.charAt(i) == s.charAt(j) && (j - i <= 1 || dp[i + 1][j - 1]) ) {
dp[i][j] = true;
if(dp[i][j] && j - i + 1 > len) {
len = j - i + 1;
begin = i;
}
}
}
}
return s.substring(begin,len + begin);
}
516. 最长回文子序列
dp [ i ] [ j ]表示从 i 到 j 最长回文子序列长度
递推公式: if(s.charAt(i) == s.charAt(j)) { //如果i和j位置的字符相同就把长度+2
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i][j - 1],dp[i + 1][j]);
//如果不相同看看哪个加进来可以使长度更长
}
遍历顺序:从下往上从左往右
public int longestPalindromeSubseq(String s) {
int n = s.length();
int[][] dp = new int[n][n];
for(int i = n - 1;i >= 0;i--) {
dp[i][i] = 1;
for(int j = i + 1;j < n;j++) {
if(s.charAt(i) == s.charAt(j)) {
dp[i][j] = dp[i + 1][j - 1] + 2;
} else {
dp[i][j] = Math.max(dp[i][j - 1],dp[i + 1][j]);
}
}
}
return dp[0][n - 1];
}