每日一题leetcode

每日一题

  • 91.解码方法
  • 1711.大餐计数
  • 79. 单词搜索
  • 897. 递增顺序搜索树
  • 3. 无重复字符的最长子串
  • 633. 平方数之和
  • 2. 两数相加
  • 7. 整数反转
  • 11. 盛最多水的容器
  • 15. 三数之和
  • 16. 最接近的三数之和
  • 17. 电话号码的字母组合
  • 23. 合并K个升序链表
  • 33. 搜索旋转排序数组
  • 62. 不同路径
  • 70. 爬楼梯
  • 78.子集
  • 90. 子集 II
  • 39. 组合总和
  • 46. 全排列
  • 47. 全排列 II
  • 剑指 Offer 38. 字符串的排列
  • 22. 括号生成
  • 842. 将数组拆分成斐波那契序列
  • 93. 复原 IP 地址
  • 面试题 08.12. 八皇后
  • 131. 分割回文串
  • 32. 最长有效括号
  • 416. 分割等和子集
  • 494. 目标和
  • 322. 零钱兑换
  • 518. 零钱兑换 II
  • 377. 组合总和 Ⅳ
  • 139. 单词拆分
  • 309. 最佳买卖股票时机含冷冻期
  • 714. 买卖股票的最佳时机含手续费
  • 123. 买卖股票的最佳时机 III
  • 188. 买卖股票的最佳时机 IV
  • 930和相同的二元子数组
  • 274. H 指数
  • 111. 二叉树的最小深度
  • 110. 平衡二叉树
  • 543. 二叉树的直径
  • 112. 路径总和
  • 113. 路径总和 II
  • 437. 路径总和 III

91.解码方法

每日一题leetcode_第1张图片
每日一题leetcode_第2张图片
题解:
每日一题leetcode_第3张图片

class Solution {
    public int numDecodings(String s) {

        int n = s.length();
        int[] f = new int[n + 1];
        f[0] = 1;
        for (int i = 1; i <= n; ++i) {
            if (s.charAt(i - 1) != '0') {
                f[i] = f[i - 1];
            }
            if (i > 1 && s.charAt(i - 2) != '0' && ((s.charAt(i - 2) - '0') * 10 + (s.charAt(i - 1) - '0') <= 26)) {
                f[i] += f[i - 2];
            }
        }
        return f[n];
    }
}

1711.大餐计数

每日一题leetcode_第4张图片
题解:
使用双指针、递归回溯等方法的时间复杂度为O(n2),存在大量重复计算。
解决办法:
容斥原理,使用map记录所有数组中元素出现的次数。
从1、2、4、8到2^21遍历(i),如果 i 等于map中两个元素之和(包括相同元素)。则满足条件,计数器加1。其中两元素相同:map.get(x) * (map.get(x) - 1)/2 ,两元素不同:map.get(x)*map.get(t)/2(此处除以2是因为,顺序不同而元素相同的组合是冗余的)。将/2提取出来, res>>=1;

class Solution {
    public int countPairs(int[] deliciousness) {
        long res = 0;
        int mod = (int)1e9+7;
        int max = 1 << 22;
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i : deliciousness) {
            map.put(i, map.getOrDefault(i, 0) + 1);
        }
        for (int x : map.keySet()){
            for (int i = 1; i < max; i<<=1) {
                int t = i-x;
                if (map.containsKey(t)){
                    if (t==x)res += (long) map.get(x) * (map.get(x) - 1);
                    else res += (long) map.get(x)*map.get(t);
                }
            }
        }
        res>>=1;
        return (int) (res%mod);
    }
}

79. 单词搜索

每日一题leetcode_第5张图片

class Solution {
    public boolean exist(char[][] board, String word) {
        if (board == null || board[0] == null || word == null || word.length() == 0) return false;
        boolean[][] used = new boolean[board.length][board[0].length];
        boolean res = false;
        //循环找到第一个元素相等的位置,多个相同的位置用 || 连接,有一个返回true即可
        for (int i = 0; i < board.length; i++) {
            for (int j = 0; j < board[0].length; j++) {
                if (board[i][j] == word.charAt(0))
                	//开始递归
                    res = res || dfs(board, word, used, i, j, 0);
            }
        }
        return res;
    }

    private boolean dfs(char[][] board, String word, boolean[][] used, int w, int h, int index) {
    	//	递归结束条件:word到达末尾
        if (index == word.length()) return true;
        //如果当前元素是word index位置的元素,且未使用过,则进入if向四周递归下一个元素
        if (word.charAt(index) == board[w][h] && !used[w][h]) {
        	//条件满足,标记为使用过
            used[w][h] = true;
            //如果已经到达word的末尾,直接返回,不需要再递归
            if (index + 1 == word.length()) return true;
            //向四周递归,用||连接返回结果。有一个成立即可
            boolean res = false;
            if (w + 1 < board.length) res = dfs(board, word, used, w + 1, h, index + 1);
            if (h + 1 < board[0].length) res = res || dfs(board, word, used, w, h + 1, index + 1);
            if (w - 1 >= 0) res = res || dfs(board, word, used, w - 1, h, index + 1);
            if (h - 1 >= 0) res = res || dfs(board, word, used, w, h - 1, index + 1);
            //回溯条件:四周递归都返回false,即是死胡同,则将当前位置重置为false。回溯
            if (!res) {
                used[w][h] = false;
            }
            return res;
        }
      	//递归结束,也没找到,返回false
        return false;
    }
}

897. 递增顺序搜索树

给你一棵二叉搜索树,请你 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的节点成为树的根节点,并且每个节点没有左子节点,只有一个右子节点。
每日一题leetcode_第6张图片
题解:
使用递归中序遍历二叉树,使用集合存储遍历结果。按照题目要求遍历集合连接各节点。

class Solution {
    ArrayList<TreeNode> list = new ArrayList<>();

    public TreeNode increasingBST(TreeNode root) {
        if (root == null) return null;
        dfs(root);
        TreeNode head = new TreeNode(list.get(0).val);
        TreeNode pre = head;
        for (int i = 1; i < list.size(); i++) {
            TreeNode cur = new TreeNode(list.get(i).val);
            pre.right = cur;
            pre = cur;
        }
        return head;
    }
    public void dfs(TreeNode node){
        if (node==null)return;
        dfs(node.left);
        list.add(node);
        dfs(node.right);
    }
}

3. 无重复字符的最长子串

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:
输入: s = "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。
     
示例 4:
输入: s = ""
输出: 0

题解:
滑动窗口。

  1. 使用窗口[i,j)从左向右滑动。
  2. i逐步向右移动;当碰到重复字符时,将j指向重复字符(靠左的这个重复字,而不是i处的重复字符符)的下一个位置;
  3. 同时,用res记录当前无重复字符串的最大长度。
class Solution {
       public int lengthOfLongestSubstring(String s) {
        if (s == null || s.length() == 0) return 0;
        int length = s.length();
        int res = 0;
        int i = 1;
        int j = 0;
        while (i <= length) {
            String substring = s.substring(j, i);
            if (i==length||substring.contains(String.valueOf(s.charAt(i)))) {
                res = Math.max(res, i-j);
                if (i<length)j += substring.indexOf(s.charAt(i))+1;
            }
            i++;
        }

        return res;
    }
}

633. 平方数之和

每日一题leetcode_第7张图片
题解:
满足题意的数只有两种情况:

  1. 本身可以开平方,并且结果为整数。设c的开方得到i,判断i是否为整数。
  2. 本身不是一个整数的平方。但可以由某两个整数的平方相加得到。循环遍历 0i的所有整数 k,当 k为某个值时,c - sqrt(k)如果可以开平方,则返回true;否则返回false
  • 判断一个数的开方是否为整数:Math.sqrt(j) - (int) Math.sqrt(j) == 0
class Solution {
    public boolean judgeSquareSum(int c) {
        int i = (int) Math.sqrt(c);
        if (Math.sqrt(c)-i==0)return true;
        for (int k = i; k > 0; k--) {
            int j = (int) (c - Math.pow(k, 2));
            if (Math.sqrt(j) - (int) Math.sqrt(j) == 0)return true;
        }
        return false;
    }
}

2. 两数相加

每日一题leetcode_第8张图片
题解:
遍历两链表,同时新建一个链表保存结果。
见注释。

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode() {}
 *     ListNode(int val) { this.val = val; }
 *     ListNode(int val, ListNode next) { this.val = val; this.next = next; }
 * }
 */
class Solution {
    public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
    //新建一条链表用于存储求和结果
        ListNode head = new ListNode(0);
        //临时节点用于遍历新建链表
        ListNode tmp = head;
        //保存进位值(0 或 1)
        int jinwei = 0;
       	//直到两个链表都遍历结束
        while (l1!=null || l2!=null){
        	//如果有一条链表已经遍历结束,则用 0 代替 null 继续遍历
            int val1 = l1==null?0:l1.val;
            int val2 = l2==null?0:l2.val;

			//对当前两个节点的值求和
            int val = val1+ val2;
            //新建结果节点的值为 val ,注意要加上一次的进位值。
            tmp.next = new ListNode((val + jinwei) % 10);
            tmp = tmp.next;
            //更新下一次的进位,仍要注意加上一次的进位值。
            jinwei = (val + jinwei) / 10;
            
            if (l1!=null)l1 = l1.next;
            if (l2!=null)l2 = l2.next;
        }
        //当两个链表都已遍历结束。此时如果进位值还有1,则新建节点进行保存。
                if (jinwei==1)tmp.next = new ListNode(1);
       //返回新链表的头部。
        return head.next;
    }
}

7. 整数反转

每日一题leetcode_第9张图片

示例 1:

输入:x = 123
输出:321
示例 2:

输入:x = -123
输出:-321
示例 3:

输入:x = 120
输出:21
示例 4:

输入:x = 0
输出:0

题解:
方法1:字符串反转

class Solution {
public int reverse(int x) {
        if (x==0) return 0;
        String s = String.valueOf(x);
        StringBuilder builder = new StringBuilder(s);
        StringBuilder reverse = builder.reverse();
        //反转后,去除开头的 0 
        int index = 0;
        while (reverse.charAt(index)=='0'){
            index++;
        }
        String substring = reverse.substring(index);
        //是负数的情况
        if (substring.charAt(substring.length()-1)=='-'){
            substring = substring.substring(0,substring.length()-1);
            //大数判断,如果转换失败则说明为大数
            try {
                return -Integer.parseInt(substring);
            }catch (Exception e){
                return 0;
            }
        }
        //不是负数的情况
        try {
            return Integer.parseInt(substring);
        }catch (Exception e){
            return 0;
        }
    }
}

方法2:只取最后一位

class Solution {
    public int reverse(int x) {
        int result = 0;
        while (x != 0) {
            int tmp = x % 10;
        	//大数判断
            if (result>214748364||(result==214748364&&tmp>7))return 0;
            if (result<-214748364||(result==-214748364&&tmp<-8))return 0;
            result = result * 10 + tmp;
            x = x / 10;
        }
        return result;
    }
}

11. 盛最多水的容器

每日一题leetcode_第10张图片
每日一题leetcode_第11张图片
每日一题leetcode_第12张图片
题解:
方法1:暴力遍历 – O(n2)
方法2:左右双指针 – O(n)

public class leecode11 {
    //方法1:暴力遍历,时间复杂度:O(n2)
    public int maxArea1(int[] height) {
        int len = height.length;
        if (len < 2) return 0;
        int res = 0;
        for (int left = 0; left < len; left++) {
            for (int right = left + 1; right < len; right++) {
                int tmp = (right - left) * Math.min(height[left], height[right]);
                res = Math.max(res, tmp);
            }
        }
        return res;
    }

    //方法2:左右指针,时间复杂度:O(n)
    public int maxArea(int[] height) {
        int left = 0, right = height.length - 1, res = 0;
        while (right > left) {
            res = Math.max(res, (right - left) * Math.min(height[left], height[right]));
            //左右指针的移动规则:保留大的边界,移动边界小的指针
            if (height[left] > height[right]){
                right--;
            }else {
                left++;
            }
        }
        return res;
    }
}

15. 三数之和

每日一题leetcode_第13张图片
题解:
方法1:暴力法 – O(n3)
方法2:左右指针法 – O(n2)。

  1. 首先对数组进行排序,排序后固定一个数nums[i],再使用左右指针指向nums[i]后面的两端,数字分别为nums[l]nums[r],计算三数之和是否等于0,满足则添加到结果集。
  2. 如果nums[i]>0,则三数之和tmp必然大于0;
  3. 如果nums[i]==nums[i-1],则说明该数字重复,会导致重复的结果,执行continue;同样,如果tmp==0时,nums[l]==nums[l+1]跳过l++,nums[r]==nums[r-1]跳过r–;
  4. tmp>0,则r--;
  5. tmp<0,则l++;
    每日一题leetcode_第14张图片
class Solution {
    public List<List<Integer>> threeSum(int[] nums) {
        ArrayList<List<Integer>> res = new ArrayList<>();
        if (nums.length < 3) return res;
        Arrays.sort(nums);
        for (int i = 0; i < nums.length; i++) {
            if (nums[i]>0)break;
            int l = i + 1, r = nums.length - 1;
            //去重
            if (i > 0 && nums[i] == nums[i - 1]) continue;
            while (r > l) {
                int tmp = nums[i] + nums[l] + nums[r];
                if (tmp == 0) {
                    res.add(Arrays.asList(nums[i], nums[l], nums[r]));
                    while (l < r && nums[l] == nums[l + 1]) l++;
                    while (l < r && nums[r] == nums[r - 1]) r--;
                    r--;
                    l++;
                } else if (tmp > 0) {
                    r--;
                } else {
                    l++;
                }
            }
        }
        return res;
    }
}

16. 最接近的三数之和

每日一题leetcode_第15张图片
题解:
与15题是同一类型。使用左右指针来代替暴力解法。
思路:遍历数组固定一点i,在剩余数组中使用左右指针l,r进行遍历,知道两指针相遇。

  1. 结果返回条件:int tmp = nums[i] + nums[l] + nums[r] - target;当三点之和与target的差最小。
  2. 结果//移动左右指针 if (tmp == 0) { return target; } else if (tmp > 0) { r--; } else { l++; }
class Solution {
    public int threeSumClosest(int[] nums, int target) {
        Arrays.sort(nums);
        int res = 0, l, r, dif = Integer.MAX_VALUE;
        for (int i = 0; i < nums.length - 2; i++) {
            l = i + 1;
            r = nums.length - 1;
            while (l < r) {
                int tmp = nums[i] + nums[l] + nums[r] - target;
                if (dif>Math.abs(tmp)){
                    res = nums[i] + nums[l] + nums[r];
                    dif = Math.abs(tmp);
                }
                //移动左右指针
                if (tmp == 0) {
                    return target;
                } else if (tmp > 0) {
                    r--;
                } else {
                    l++;
                }
            }
        }
        return res;
    }
}

17. 电话号码的字母组合

每日一题leetcode_第16张图片
题解:
深搜+回溯.

class Solution {
    private final String[] letter_map = {" ","*","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    private List<String> res = new ArrayList<>();
    public List<String> letterCombinations(String digits) {
    //边界条件
        if (digits==null||digits.length()==0){
            return new ArrayList<>();
        }
        //使用一个StringBuilder可以提高效率
        iterStr(digits,new StringBuilder(),0);
        return res;
    }

    private void iterStr(String str, StringBuilder stringBuilder, int index) {
    //结束条件:到达深搜末尾,即最后一位数字。
        if (index == str.length()){
            res.add(stringBuilder.toString());
            return;
        }
        char c = str.charAt(index);
        int pos = c - '0';
        String map_string = letter_map[pos];
        //每个数字需要一层循环,用递归深搜实现循环嵌套
        for(int i=0;i<map_string.length();i++) {
            stringBuilder.append(map_string.charAt(i));
            iterStr(str, stringBuilder, index+1);
            //回溯:到达末尾后,删除最后一位,继续遍历该位的其他情况
            stringBuilder.deleteCharAt(stringBuilder.length()-1);
        }
    }
}

23. 合并K个升序链表

每日一题leetcode_第17张图片
题解:
优先级队列。时间复杂度:O(n*log(k)),n 是所有链表中元素的总和,k 是链表个数。

class Solution {
    public ListNode mergeKLists(ListNode[] lists) {
        ListNode head = new ListNode(0);
        ListNode tmp = head;
        PriorityQueue<ListNode> queue = new PriorityQueue<ListNode>((x,y)->(x.val-y.val));
        for (ListNode node : lists) {
            if (node!=null)queue.add(node);
        }
        while (!queue.isEmpty()){
            tmp.next = queue.poll();
            tmp = tmp.next;
            if (tmp.next!=null)queue.add(tmp.next);
        }
        return head.next;
    }
}

33. 搜索旋转排序数组

每日一题leetcode_第18张图片
题解:
方法1:暴力法。O(n)

class Solution {
    public int search(int[] nums, int target) {
        List<Integer> list = Arrays.stream(nums).boxed().collect(Collectors.toList());
        return list.indexOf(target);
    }
}

方法2:二分查找。O(log n)
先寻找旋转点。然后再根据target的大小选择在前半段还是后半段进行二分查找。
需要注意的点:旋转点可能是首端,旋转后数组并未变化。此时,后半段=nums,前半段=null

class Solution {
    public int search(int[] nums, int target) {
        int rev = 0;
        for (int i = 0; i < nums.length; i++) {
        //寻找旋转点
        //为了避免旋转点可能是首端而报越界,需添加 i + 1 < nums.length
            if (i + 1 < nums.length && nums[i] > nums[i + 1]) {
                rev = i + 1;
                break;
            }
        }
        
        int l, r;
        //在前半段中二分查找
        //如果旋转点在首端,则前半段为空,所以不能用nums[0]判断。而是选择必然存在的后半段末尾 nums[nums.length-1]
        if (target > nums[nums.length-1]) {
            l = 0;
            r = rev - 1;
        }
        //后半段中二分查找
        else {
            l = rev;
            r = nums.length - 1;
        }
        while (l <= r) {
            int mid = (l + r) / 2;
            if (nums[mid] == target) {
                return mid;
            } else if (nums[mid] < target) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
        return -1;
    }
}

62. 不同路径

每日一题leetcode_第19张图片
*题解:*动态规划

class Solution {
    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 = 1; 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];
    }
}

70. 爬楼梯

每日一题leetcode_第20张图片
题解:
动态规划

class Solution {
    public int climbStairs(int n) {
        int[] dp = new int[n+1];
        dp [0] = 1;
        dp [1] = 1;
        for (int i = 2; i < n+1 ; i++) {
            dp[i] = dp[i-1] + dp[i-2];
        }
        return dp[n];
    }
}

78.子集

每日一题leetcode_第21张图片
题解:
回溯与DFS的区别:DFS 是一个劲的往某一个方向搜索,而回溯算法建立在 DFS 基础之上的,但不同的是在搜索过程中,达到结束条件后,恢复状态,回溯上一层,再次搜索。因此回溯算法与 DFS 的区别就是有无状态重置。

何时需要回溯算法:当问题需要 “回头”,以此来查找出所有的解的时候,使用回溯算法。即满足结束条件或者发现不是正确路径的时候(走不通),要撤销选择,回退到上一个状态,继续尝试,直到找出所有解为止。

回溯算法的步骤
①画出递归树,找到状态变量(回溯函数的参数),这一步非常重要※
②根据题意,确立结束条件
③找准选择列表(与函数参数相关),与第一步紧密关联※
④判断是否需要剪枝
⑤作出选择,递归调用,进入下一层
⑥撤销选择
每日一题leetcode_第22张图片
观察上图可得,选择列表里的数,都是选择路径(红色框)后面的数,比如[1]这条路径,他后面的选择列表只有"2、3",[2]这条路径后面只有"3"这个选择,那么这个时候,就应该使用一个参数start,来标识当前的选择列表的起始位置。也就是标识每一层的状态,因此被形象的称为"状态变量",最终函数签名如下
public void dfs(int[] nums, int start)

②找结束条件
此题非常特殊,所有路径都应该加入结果集,所以不存在结束条件。或者说当 start 参数越过数组边界的时候,程序就自己跳过下一层递归了,因此不需要手写结束条件,直接加入结果集。
③找选择列表
在①中已经提到过了,子集问题的选择列表,是上一条选择路径之后的数,即
for (int i = start; i < nums.length; i++)
④判断是否需要剪枝
从递归树中看到,路径没有重复的,也没有不符合条件的,所以不需要剪枝。
⑤做出选择(即for 循环里面的tmp.add(nums[i]);)
⑥撤销选择

class Solution {
    ArrayList<List<Integer>> res = new ArrayList<>();
    ArrayList<Integer> tmp = new ArrayList<>();

    public List<List<Integer>> subsets(int[] nums) {
        if (nums == null || nums.length == 0) return res;
        dfs(nums, 0);
        return res;
    }

    public void dfs(int[] nums, int start) {
        res.add(new ArrayList<>(tmp));
        for (int i = start; i < nums.length; i++) {//找选择列表
            tmp.add(nums[i]);//做出选择
            dfs(nums, i + 1);//递归进入下一层,注意i+1,标识下一个选择列表的开始位置,最重要的一步
            tmp.remove(tmp.size() - 1);//撤销选择
        }
    }
}

90. 子集 II

每日一题leetcode_第23张图片
题解:
①递归树
每日一题leetcode_第24张图片
可以发现,树中出现了大量重复的集合,②和③和第一个问题一样,不再赘述,我们直接看第四步。
④判断是否需要剪枝,需要先对数组排序,使用排序函数
显然我们需要去除重复的集合,即需要剪枝,把递归树上的某些分支剪掉。那么应去除哪些分支呢?又该如何编码呢?
观察上图不难发现,应该去除当前选择列表中,与上一个数重复的那个数,引出的分支,如 “2,2” 这个选择列表,第二个 “2” 是最后重复的,应该去除这个 “2” 引出的分支
每日一题leetcode_第25张图片
(去除图中红色大框中的分支)

编码呢,刚刚说到是 “去除当前选择列表中,与上一个数重复的那个数,引出的分支”,说明当前列表最少有两个数,当i>start时,做选择的之前,比较一下当前数,与上一个数 (i-1) 是不是相同,相同则 continue,
⑤做出选择
⑥撤销选择

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> tmp = new ArrayList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        if (nums==null||nums.length==0)return res;
        Arrays.sort(nums);
        traceBack(nums,0);
        return res;
    }
    public void traceBack(int[] nums, int start){
        res.add(new ArrayList<>(tmp));
        for (int i = start; i < nums.length; i++) {
            if (i  > start && nums[i]==nums[i-1])continue;//剪枝去重
            tmp.add(nums[i]);
            traceBack(nums, i+1);
            tmp.remove(tmp.size()-1);
        }
    }
}

39. 组合总和

每日一题leetcode_第26张图片
题解:
①递归树
每日一题leetcode_第27张图片
(绿色箭头上面的是路径,红色框[]则为结果,黄色框为选择列表)
从上图看出,组合问题和子集问题一样,1,2 和 2,1 `是同一个组合,因此 **需要引入start参数标识,每个状态中选择列表的起始位置。**另外,每个状态还需要一个 sum 变量,来记录当前路径的和,函数签名如下

private void traceBack(int[] candidates, int target,int start, int sum) {

②找结束条件
由题意可得,当路径总和等于 target 时候,就应该把路径加入结果集,并 return

      if (sum==target){
            res.add(new ArrayList<>(tmp));
        }

③找选择列表
for (int i = start; i < candidates.length; i++) {

④判断是否需要剪枝
从①中的递归树中发现,当前状态的sum大于target的时候就应该剪枝,不用再递归下去了
if (sum>target)continue;
⑤做出选择
题中说数可以无限次被选择,那么 i 就不用 +1 。即下一层的选择列表,从自身开始。并且要更新当前状态的sum。
⑥撤销选择

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> tmp = new ArrayList<>();
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        if (candidates==null||candidates.length==0)return res;
        traceBack(candidates,target,0,0);
        return res;
    }

    private void traceBack(int[] candidates, int target,int start, int sum) {
        if (sum==target){
            res.add(new ArrayList<>(tmp));
        }
        for (int i = start; i < candidates.length; i++) {
            if (sum>target)continue;
            tmp.add(candidates[i]);
            traceBack(candidates, target, i, sum+candidates[i]);
            tmp.remove(tmp.size()-1);
        }
    }
}

46. 全排列

每日一题leetcode_第28张图片
题解:
①递归树
每日一题leetcode_第29张图片
(最下面的叶子节点,红色【】中的就是要求的结果)
然后我们来回想一下,整个问题的思考过程,这棵树是如何画出来的。首先,我们固定1,然后只有2、3可选:如果选2,那就只剩3可选,得出结果[1,2,3];如果选3,那就只剩2可选,得出结果[1,3,2]。再来,如果固定2,那么只有1,3可选:如果选1,那就只剩3,得出结果[2,1,3]…
有没有发现一个规律:如果我们固定了(选择了)某个数,那么他的下一层的选择列表就是——除去这个数以外的其他数!!
traceBack(nums,used);used来标记已经选择了的数。

②找结束条件

        if (tmp.size()==nums.length){
            res.add(new ArrayList<>(tmp));
            return;
        }

③找准选择列表

for (int i = 0; i < nums.length; i++) {
            if (!used[i]){ //从给定的数中除去用过的,就是当前的选择列表
            ...
            }
    }

④判断是否需要剪枝
不需要剪枝,或者你可以认为,!used[i]已经是剪枝

⑤做出选择

        for (int i = 0; i < nums.length; i++) {
            if (!used[i]){
                tmp.add(nums[i]);//做选择
                used[i] = true;设置当前数已用
                traceBack(nums,used);//进入下一层
				...
            }
        }

⑥撤销选择

for (int i = 0; i < nums.length; i++) {
            if (!used[i]){
                tmp.add(nums[i]);
                used[i] = true;
                traceBack(nums,used);
                tmp.remove(tmp.size()-1);//撤销选择
                used[i] = false;//撤销选择
            }
        }
class Solution {
    ArrayList<List<Integer>> res = new ArrayList<>();
    ArrayList<Integer> tmp = new ArrayList<>();
    public List<List<Integer>> permute(int[] nums) {
        if (nums==null||nums.length==0)return res;
        boolean[] used = new boolean[nums.length];
        traceBack(nums,used);
        return res;
    }

    private void traceBack(int[] nums, boolean[] used) {
        if (tmp.size()==nums.length){
            res.add(new ArrayList<>(tmp));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            if (!used[i]){//从给定的数中除去用过的,就是当前的选择列表
                tmp.add(nums[i]);//做选择
                used[i] = true;//设置当前数已用
                traceBack(nums,used);//进入下一层
                tmp.remove(tmp.size()-1);//撤销选择
                used[i] = false;//撤销选择
            }
        }
    }
}

47. 全排列 II

给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。
每日一题leetcode_第30张图片
题解:
①递归树
每日一题leetcode_第31张图片
可以看到,有两组是各自重复的,那么应该剪去哪条分支?首先要弄懂,重复结果是怎么来的,比如最后边的分支,选了第二个2后,,竟然还能选第一个2,从而导致最右边整条分支都是重复的
②③不再赘述,直接看④

④判断是否需要剪枝,如何编码
有了前面“子集、组合”问题的判重经验,同样首先要对题目中给出的nums数组排序,让重复的元素并列排在一起,在if(i>start&&nums[i]==nums[i-1]),基础上修改为if(i>0&&nums[i]==nums[i-1]&&!used[i-1]),语义为:当i可以选第一个元素之后的元素时(因为如果i=0,即只有一个元素,哪来的重复?有重复即说明起码有两个元素或以上,i>0),**然后判断当前元素是否和上一个元素相同?**如果相同,**再判断上一个元素是否能用?**如果三个条件都满足,那么该分支一定是重复的,应该剪去

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> list = new ArrayList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        if (nums==null||nums.length==0)return res;
        boolean[] used = new boolean[nums.length];
        Arrays.sort(nums);
        traceBack(nums,used);
        return res;
    }

    private void traceBack(int[] nums, boolean[] used) {
        if (list.size() == nums.length){
            res.add(new ArrayList<>(list));
            return;
        }
        for (int i = 0; i < nums.length; i++) {
            //剪枝
            if (i>0&&nums[i]==nums[i-1]&& !used[i - 1])continue;//剪枝,三个条件
            if (!used[i]){//从给定的数中除去,用过的数,就是当前的选择列表
                list.add(nums[i]);
                used[i] = true;
                traceBack(nums, used);
                list.remove(list.size()-1);
                used[i] = false;
            }
        }
    }
}

剑指 Offer 38. 字符串的排列

输入一个字符串,打印出该字符串中字符的所有排列。
你可以以任意顺序返回这个字符串数组,但里面不能有重复元素。

示例:

输入:s = “abc”
输出:[“abc”,“acb”,“bac”,“bca”,“cab”,“cba”]
题解:
全排列 II 一摸一样

class Solution {
    List<String> res = new ArrayList<>();
    StringBuilder tmp = new StringBuilder();

    public String[] permutation(String s) {
        if (s == null || s.length() == 0) return new String[0];
        char[] chars = new char[s.length()];
        s.getChars(0, s.length(), chars, 0);
        Arrays.sort(chars);
        boolean[] used = new boolean[chars.length];
        traceBack(chars, used);
        String[] resStr = new String[res.size()];
        res.forEach(e -> {
            resStr[res.indexOf(e)] = e;
        });
        return resStr;
    }

    private void traceBack(char[] chars, boolean[] used) {
        if (tmp.length() == chars.length) {
            res.add(new String(tmp));
            return;
        }
        for (int i = 0; i < chars.length; i++) {
            if (i > 0 && chars[i] == chars[i-1] && used[i-1])continue;
            if (!used[i]){
                tmp.append(chars[i]);
                used[i] = true;
                traceBack(chars, used);
                tmp.deleteCharAt(tmp.length()-1);
                used[i] = false;
            }
        }
    }
}

22. 括号生成

每日一题leetcode_第32张图片
题解:
括号可以重复选,所以选择列表不需要start指定开始位置。
回溯的流程基本不变,重要在两处剪枝check,check的剪枝逻辑:
1.第一个字符不能是)
2.当前情况下,左右括号的数量都不可以超过n,且(的数量>=)的数量

class Solution {
    char[] source = {'(', ')'};
    List<String> res = new ArrayList<>();
    StringBuilder tmp = new StringBuilder();

    public List<String> generateParenthesis(int n) {
        if (n == 0) return res;
        dfs(n);
        return res;
    }

    private void dfs(int n) {
        if (tmp.length() >= n*2) {
        //添加前进行剪枝,主要判断最后一位括号。
            if (check(tmp, n)) res.add(tmp.toString());
            return;
        }
        for (int i = 0; i < source.length; i++) {
            if (check(tmp, n)){//遍历时剪枝
                tmp.append(source[i]);
                dfs(n);
                tmp.deleteCharAt(tmp.length() - 1);
            }
        }
    }

    private boolean check(StringBuilder tmp, int n) {
        if (tmp.length()==0)return true;
        if (tmp.charAt(0) == ')') return false;
        int countLeft = 0, countRight = 0;
        for (int i = 0; i < tmp.length(); i++) {
            if (tmp.charAt(i) == '(') countLeft++;
            else countRight++;
        }
        return countLeft <= n && countRight <= n && countLeft>=countRight;
    }
}

842. 将数组拆分成斐波那契序列

在这里插入图片描述
题解:
递归结束条件:遍历完所有元素,且被分为>=3个数
剪枝情况:
两位以上的数字不能以0开头。
不能超过整数的最大值。
val>res.get(res.size()-1)+res.get(res.size()-2)如果当前数已经超过,则继续下去也不会满足条件。
不剪枝的情况:res中前两个元素直接添加;后面的元素满足斐波那契数列则添加。

class Solution {
    List<Integer> res = new ArrayList<>();
    List<Integer> tmp = new ArrayList<>();

    public List<Integer> splitIntoFibonacci(String num) {
        if (num == null || num.length() == 0) return res;
        dfs(num, 0);
        return tmp;
    }

    private void dfs(String num, int start) {
        if (start>=num.length()&&res.size()>=3){
            tmp = new ArrayList<>(res);
            return;
        }
        for (int i = start; i <num.length() ; i++) {
            long val = Long.parseLong(num.substring(start,i+1));
            if (num.charAt(start)=='0'&& i>start)break;
            if (val>Integer.MAX_VALUE)break;
            if (res.size()>=2&&val>res.get(res.size()-1)+res.get(res.size()-2))break;
            
            if (res.size()<2||val==res.get(res.size()-1)+res.get(res.size()-2)){
                res.add((int)val);
                dfs(num, i+1);
                res.remove(res.size()-1);
            }
        }
    }
}

93. 复原 IP 地址

每日一题leetcode_第33张图片

class Solution {
    List<String> res = new ArrayList<>();
    List<String> tmp = new ArrayList<>();
    public List<String> restoreIpAddresses(String s) {
        dfs(s, 0);
        return res;
    }

    private void dfs(String s, int start) {
    //剪枝,大于四段直接返回
        if (tmp.size()>4)return;
        //等于四段时,后面还有数字
        if (tmp.size()==4&&start!=s.length())return;
        //等于四段,且没数字了
        if (tmp.size()==4){
            res.add(String.join(".", tmp));
            return;
        }
        //遍历选择列表
        for (int i = start; i <s.length() ; i++) {
            String subStr = s.substring(start,i+1);
            //每段不能以0开头、不能小于0、大于255
            if (subStr.length()>1&&subStr.startsWith("0")||subStr.length()>3)break;
            int val = Integer.parseInt(subStr);
            if (val<0||val>255)break;
            tmp.add(subStr);
            dfs(s, i+1);
            tmp.remove(tmp.size()-1);
        }
    }
}

面试题 08.12. 八皇后

每日一题leetcode_第34张图片
题解:

class Solution {
    List<List<String>> res = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        if (n <= 0) return res;
        //初始化棋盘
            char[][] arr = new char[n][n];
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < n; j++) {
                arr[i][j] = '.';
            }
        }
        
        traceBack(arr, 0, n);
        return res;
    }

	//i:行 j:列
    private void traceBack(char[][] arr, int i, int n) {
   		 // ② 找结束条件
        if (i == n) {
            res.add(arrToList(arr));
            return;
        }
        for (int j = 0; j < n; j++) { // ③ 选择列表
            if (checked(arr, i, j, n)) { //④ 做出选择并剪枝
                arr[i][j] = 'Q';
            } else {
                continue;
            }
            traceBack(arr, i + 1, n); // ⑤ 下一行的递归
            arr[i][j] = '.'; // ⑥ 撤销选择
        }
    }

	//判断是否满足条件
    private boolean checked(char[][] arr, int i, int j, int n) {
        //横向
        for (int k = 0; k < n; k++) {
            if (arr[i][k] == 'Q' && k != j) return false;
        }
        //纵向
        for (int k = 0; k < i; k++) {
            if (arr[k][j] == 'Q') return false;
        }
        //左上角
        int ii = i,jj=j;
        while (ii > 0 && jj > 0) {
            if (arr[--ii][--jj] == 'Q') return false;
        }

        //右上角
        while (i > 0 && j < n-1) {
            if (arr[--i][++j] == 'Q') return false;
        }
        return true;
    }

    private List<String> arrToList(char[][] arr) {
        ArrayList<String> list = new ArrayList<>();
        StringBuilder builder = new StringBuilder();
        int length = arr.length;
        for (int i = 0; i < length; i++) {
            for (int j = 0; j < length; j++) {
                builder.append(arr[i][j]);
            }
            list.add(builder.toString());
            builder = new StringBuilder();
        }
        return list;
    }
}

131. 分割回文串

每日一题leetcode_第35张图片
题解:
递归树:
每日一题leetcode_第36张图片

class Solution {

    List<List<String>> res = new ArrayList<>();
    List<String> tmp = new ArrayList<>();

    public List<List<String>> partition(String s) {
        if (s == null || s.length() == 0) return res;
        traceBack(s, 0);
        return res;
    }

    private void traceBack(String s, int start) {
        if (start >= s.length()) { //递归结束条件
            res.add(new ArrayList<>(tmp));
            return;
        }
        for (int i = start; i < s.length(); i++) { //选择列表
            String s1 = s.substring(start, i+1); //选择、剪枝
            if (isPalindrome(s1)) {
                tmp.add(s1);
            } else {
                continue;
            }
            traceBack(s, i + 1); //递归
            tmp.remove(tmp.size() - 1); //撤销选择
        }
    }

    private boolean isPalindrome(String s) {
        StringBuilder builder = new StringBuilder(s);
        return s.equals(builder.reverse().toString());
    }

}

32. 最长有效括号

每日一题leetcode_第37张图片
题解:

像这种配对抵消的问题,一般可以使用辅助栈先进后出的特点进行求解。题目需要求解最长有效括号的长度,需要注意的是,有效括号不仅包括题中示例:()()()这种连续的非嵌套括号,还包括这种套娃型括号((()))都属于有效括号。

  1. 入栈规则:
    • 栈为空时,不管是 ( 还是 )
    • 当前元素是左括号(,因为括号长度都是按右括号来决定。
    • 栈顶元素是右括号)
  2. 出栈规则:
    • 当前元素是左括号( 且栈顶元素是右括号 )
  3. 栈中存储的是元素索引,而非括号本身,使用res记录最长有效括号的长度:每次出栈时说明出现了括号配对,此时需要记录res。每次出栈后,有两种情况:
    • 栈为空:说明当前位置之前的所有括号都成功配对,因此有效括号长度为 i + 1
    • 栈非空:栈中元素索引处的括号未成功配对,但栈底元素之前(可能有多个,但最大长度为res)、栈顶元素之后(长度为i-stack.peek())的括号都成功配对。所以,取二者的较大值:Math.max(res,i-stack.peek()) 。 遍历完 s 的所有元素后,返回res即可。
class Solution {
    public int longestValidParentheses(String s) {
        if (s == null || s.length() < 2) return 0;
        int res = 0;
        Stack<Integer> stack = new Stack<>();
        for (int i = 0; i < s.length(); i++) {
            if (stack.isEmpty()||s.charAt(stack.peek())==')'||s.charAt(i)=='(')stack.push(i);
            else {
                stack.pop();
                res = Math.max(res, stack.isEmpty()?i+1:i-stack.peek());
            }
        }
        return res;
    }
}

416. 分割等和子集

题目:
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
每日一题leetcode_第38张图片
题解:
动态规划(转换为 0-1 背包问题)
在这里插入图片描述
基本思路:
每个物品考虑 不选,一个一个物品考虑,一点一点扩大容量的范围。本题与 0-1 背包问题有一个很大的不同,即:

  • 0-1 背包问题选取的物品的容积总量 不能超过 规定的总量;
  • 本题选取的数字之和需要 恰好等于 规定的和的一半。
  1. 定义状态:
    boolean[][] dp = new boolean[length][target + 1] ; dp[i][j] = true or false
    i:遍历到的物品当前位置;
    j:动态背包容量
    dp[i][j]: [0,i]区间的元素之和是否可以等于j
  2. 状态转移方程:
  • 根据当前元素的大小nums[i]和目标值j关系可分为:
    1. nums[i]方程为: dp[i][j] = dp[i-1][j-nums[i]]||dp[i-1][j];
      其中dp[i-1][j]为true的情况: 只需要[0,i-1]的元素就可以等于 j,不需要再加 nums[i]
      dp[i-1][j-nums[i]]为true的情况: 表示[0,i-1]的元素可以达到 j-nums[i],加上 nums[i],则刚好是[0,i]个元素可以达到 j
    2. nums[i]==j方程为: dp[i][j] = true;
      只要 nums[i],扔掉其他的所有元素,直接为true
    3. nums[i]>j)方程为: dp[i][j] = dp[i-1][j]
      此时,肯定不能考虑把 nums[i]加进来了
class Solution {
    public boolean canPartition(int[] nums) {
        int length = nums.length;
        int sum = Arrays.stream(nums).sum();
        if (length < 2 || sum % 2 == 1) return false;
        int target = sum / 2;
        boolean[][] dp = new boolean[length][target + 1];
        for (int j = 0; j < target; j++) {
            if (nums[0] == j)dp[0][j] = true;
        }
        for (int i = 1; i < length; i++) {
            for (int j = 0; j <= target; j++) {
                if (nums[i]<j){
                    //dp[i-1][j]为true:  只需要[0,i-1]的元素就可以等于 j,不需要再加 nums[i]
                    //dp[i-1][j-nums[i]]为true:  表示[0,i-1]的元素可以达到 j-nums[i],加上 nums[i],则刚好是[0,i]个元素可以达到 j
                    dp[i][j] = dp[i-1][j-nums[i]]||dp[i-1][j]; 
                } 
                if (nums[i]==j){ //那就只要 nums[i],扔掉其他的所有元素,直接为true
                    dp[i][j] = true;
                }
                if (nums[i]>j) dp[i][j] = dp[i-1][j]; //此时,肯定不能考虑把 nums[i]加进来了
            }
        }
        return dp[length-1][target];
    }
}

494. 目标和

每日一题leetcode_第39张图片
题解: 0-1背包问题

  1. 定状态: 一般背包问题,我们都这样定义dp[len][target],len为数组的长度,target为目标值。如果需要返回方案数则定义为整形数组,如果是判断是否可行(例如上题),则定义为布尔数组。但一般背包问题是选与不选:0、num[i],而这道题是必须选,或加或减:+num[i]、-num[i]。因此定义dp数组的大小也发生了变化。int[][] dp = new int[len][2*sum+1];[2*sum+1]这里主要是考虑到数组元素的和>目标值的话,可能会发生数组越界异常。那为什么不设为sum+1的大小呢?这可以用状态转移方程解释。。
  2. 状态转移方程:题中说了。只有加或者减两个选择,因此当前状态也只能是上一次状态加当前元素或减当前元素:dp[i][j] = dp[i-1][j+nums[i]]+dp[i-1][j-nums[i]],注意,一般在写方程的时候都要留意下标越界异常。
    • 对于i-1:遍历从 i=1 开始, i = 0的情况比较简单,我们可以在第三步进行显式初始化。因此不会发生越界。
    • 对于j+nums[i],为了防止越界,我们才把dp[0]的大小设为 2*sum+1。解释了第一步的问题。
    • 对于j-nums[i],此处越界主要是它可能会小于0,而数组的下标是从0开始的正整数。但是,从题意来看,目标值确实也可以为负数。那怎么办呢,我们不可能让数组的下标为负数啊。实际上,目标值的正负并不影响有多少种方法可以得到它。比如:arr={1,1},target=2。那么只有一种方案:+1+1;那target=-2呢,target互为相反数的话,把求取过程全部取反不就行了。因此,得出结论dp[i-1][j-nums[i]]==dp[i-1][Math.abs(j-nums[i])]成立。修改状态转移方程来避免越界。
  3. 初始化:二维数组一般情况下允许我们初始化一行或者一列,这个要根据题意和dp数组的含义来决定的。在本题中,我们可以对首行进行初始化:
    for (int j = 0; j <= sum; j++) { if (nums[0]==j)dp[0][j]=1; }
    如果第一个元素nums[0]和目标值相等,则dp[0][j]=1,有一种方法可以得到。其余都为0.
    这里有一个特殊情况,如果首元素为0,目标值也为0,那么正负号均可。所以:
    if (nums[0]==0)dp[0][0]=2;
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int len  = nums.length;
        target=target<0?-target:target;
        int sum = Arrays.stream(nums).sum();
        if (sum<target)return 0;

        int[][] dp = new int[len][2*sum+1];
        //初始化
        for (int j = 0; j <= sum; j++) {
            if (nums[0]==j)dp[0][j]=1;
        }
        if (nums[0]==0)dp[0][0]=2;
        
        for (int i = 1; i < len; i++) {
            for (int j = 0; j <= sum; j++) {
                dp[i][j] = dp[i-1][j+nums[i]]+dp[i-1][Math.abs(j-nums[i])];
            }
        }
        return dp[len-1][target];
    }
}

322. 零钱兑换

每日一题leetcode_第40张图片

题解:
与0-1背包问题不同,本题每个物品可以拿无限多次,而01背包每个物品只有一个,只需要判断拿或者不拿。因此状态转移方程略有不同。而且一般背包问题要求总值小于目标值,而本题是恰好等于。
定状态
int[][] dp = new int[coins.length][amount + 1]; dp[i][j]表示[0,i]硬币总值为 j 时(恰好等于)的最小硬币数。
状态转移方程:
dp[i][j]=Math.min(dp[i][j],dp[i][j-coins[i]]+1);
因为每个硬币可以重复选,所以dp的状态可以从两种状态转移而来:

  1. dp[i][j]: 有两层含义,第一种含义[0,i]硬币中,coins[i]已经取过不用关心取了几个,且这次不取了,所以状态还是dp[i][j]。第二种含义,coins[i]还没取,这次也同样不取,此时dp[i][j]=dp[i-1][j],所以每次状态转移方程之前都需要把dp[i][j] = dp[i-1][j];先抄过来。
  2. dp[i][j-coins[i]]+1),表示取coins[i]。比较好理解。
    初始化
    由于求的是最小值。所以需要把dp数组初始化为 max,便于Math.min的执行。
    初始化列:dp[i][0] = 0;表示目标值为 0 时,硬币数量也为0;
    初始化行:if(j%coins[0]==0)dp[0][j]=j/coins[0]; 只用第一种硬币。
class Solution {
    public int coinChange(int[] coins, int amount) {
        int max = amount + 1;
        int[][] dp = new int[coins.length][amount + 1];
        for (int i = 0; i < coins.length; i++) {
            for (int j = 0; j <= amount; j++) {
                dp[i][j] = max;
            }
        }
        for (int i = 0; i <coins.length; i++) {
            dp[i][0] = 0;
        }
        for(int j=0;j<=amount;j++){
            if(j%coins[0]==0)dp[0][j]=j/coins[0];
        }
        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])
                    dp[i][j]=Math.min(dp[i][j],dp[i][j-coins[i]]+1);
            }
        }
        return dp[coins.length-1][amount] > amount ? -1 : dp[coins.length-1][amount];
    }
}

518. 零钱兑换 II

每日一题leetcode_第41张图片
题解:
同样是完全背包问题,只是需要求解所有的组合方案。因此,dp状态不变,状态转移方程略有不同。最大的区别在于初始化.
定状态
int[][] dp = new int[coins.length][amount + 1];
写方程
dp[i][j]=dp[i][j]+dp[i][j-coins[i]];
初始化
if (j%coins[0]==0)dp[0][j]=1; 因为是恰好等于目标值,所以需要使用 if 进行限制。不管硬币用了多少种,组合只有 1 个。所以,初始化为1.

class Solution {
    public int change(int amount, int[] coins) {
        if (amount==0)return 1;
        int[][] dp = new int[coins.length][amount + 1];
        for (int j = 0; j <=amount ; j++) {
            if (j%coins[0]==0)dp[0][j]=1;
        }
        for (int j = 0; j <= amount; j++) {
            for (int i = 1; i < coins.length; i++) {
                dp[i][j] = dp[i-1][j];
                if (j>=coins[i])
                    dp[i][j]=dp[i][j]+dp[i][j-coins[i]];
            }
        }
        return dp[coins.length-1][amount];
    }
}

377. 组合总和 Ⅳ

每日一题leetcode_第42张图片
题解:
每日一题leetcode_第43张图片

class Solution {
    public int combinationSum4(int[] nums, int target) {
        int len = target;
        int[][] dp = new int[len+1][target + 1];
        dp[0][0] = 1;
        int res = 0;
        for (int i = 1; i <= len; i++) {
            for (int j = 0; j <= target; j++) {
                for (int num : nums) {
                    if(j>=num)dp[i][j] += dp[i - 1][j - num];
                }
            }
            res += dp[i][target];
        }
        return res;
    }
}

139. 单词拆分

每日一题leetcode_第44张图片
题解:
本题与上题为同一类题目,列表中的元素可以无限取用,属于完全背包问题。同时,字符串自带排列效果。因此和上题一样为排列完全背包问题

对于排列完全背包问题,dp 数组的定义方式一般为:dp[i][j] 从列表中选 i
个元素
(记录的是实际使用到的元素个数),可重复选。使得目标值为 j ; 而对于组合背包问题,dp数组的定义 i
列表中[0,i]的元素(记录的是实际使用到的元素种类数)。

定状态:boolean[][] dp = new boolean[len + 1][s.length() + 1]; 实际使用的元素个数最多为字符串的长度,即每个元素都是单个字符,所以 len = s.length。j 的定义为s的[0,j]部分字符串。
写方程: 所选择的最后一个元素可能是 list 种的任意一个。所以:
dp[i][j] = dp[i-1][j-list[0]] || dp[i-1][j-list[1]] || ... || dp[i-1][j-list[list.size()]]。当然,前提是该元素能和字符串的最后一部分匹配上:s.startsWith(word, j-word.length())为true.
初始化::目标值为0时总为true, 初始化第一列 dp[i][0] = true;

class Solution {
   public boolean wordBreak(String s, List<String> wordDict) {
        int len = s.length();
        //len+1:可以选择最多的 list 中的元素,
        boolean[][] dp = new boolean[len + 1][s.length() + 1];
        for (int i = 0; i <= len; i++) {
            dp[i][0] = true;
        }
        for (int j = 1; j <= s.length(); j++) {
            for (int i = 1; i <= len; i++) {
                for (String word : wordDict) {
                    if (j >= word.length() && s.startsWith(word, j-word.length()))
                        dp[i][j] |= dp[i - 1][j - word.length()];
                }
            }
        }
        return dp[len][s.length()];
    }
}

309. 最佳买卖股票时机含冷冻期

每日一题leetcode_第45张图片
题解:
不要关注冷冻期!不要关注冷冻期!不要关注冷冻期!
只关注卖出的那一天!只关注卖出的那一天!只关注卖出的那一天!
题目中定义的“冷冻期”=卖出的那一天的后一天,题目设置冷冻期的意思是,如果昨天卖出了,今天不可买入,那么关键在于哪一天卖出,只要在今天想买入的时候判断一下前一天是不是刚卖出,即可,所以关键的一天其实是卖出的那一天,而不是卖出的后一天。

定状态::分为持有不持有。不持有又可以分为:未卖出不持有(压根就没买)、卖出不持有。因此,共有三种状态。

  1. 未卖出不持有:dp[i][0]
  2. 持有:dp[i][1]
  3. 卖出不持有:dp[i][2]

写方程::
dp[i][0] = Math.max(dp[i-1][0],dp[i-1][2]):今天未卖出不持有可能是昨天就已经是未卖出不持有,也有可能是昨天刚卖。
dp[i][1] = Math.max(dp[i-1][1],dp[i][0]-prices[i]):今天持有可能是昨天就已经持有,也有可能是由于昨天刚买入。(因为冷冻期的存在,这里不能是昨天卖出不持有dp[i-1][2]后今天再买入)
dp[i][2] = dp[i-1][1]+prices[i]:今天卖出,那昨天一定是持有了

初始化:
dp[0][0] = 0;
dp[0][1] = -prices[0]; 买入是,花钱了收入肯定是负的,卖出时才为正
dp[0][2] = 0;

class Solution {
    public int maxProfit(int[] prices) {
        if (prices.length<1)return 0;
        int[][] dp = new int[prices.length][3];
        dp[0][0] = 0;
        dp[0][1] = -prices[0];
        dp[0][2] = 0;
        for (int i = 1; i < prices.length; i++) {
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][2]);
            dp[i][1] = Math.max(dp[i-1][1],dp[i-1][0]-prices[i]);
            dp[i][2] = dp[i-1][1]+prices[i];
        }
        return Math.max(dp[prices.length-1][0],dp[prices.length-1][2]);
    }
}

714. 买卖股票的最佳时机含手续费

每日一题leetcode_第46张图片
题解:
此题与上题的两个不同点:

  1. 没有冷冻期.
  2. 含手续费.
    因此,买入时的状态转移方程变为:
    dp[i][1] = Math.max(dp[i-1][1],Math.max(dp[i-1][0]-prices[i]-fee,dp[i-1][2]-prices[i]-fee));
    今天持有可能的三种情况:
  • 昨天持有dp[i-1][1]
  • 昨天未卖出未持有并买入:dp[i-1][0]-prices[i]-fee(买入时扣除手续费)
  • 昨天卖出未持有并买入:dp[i-1][2]-prices[i]-fee(买入时扣除手续费)
class Solution {
    public int maxProfit(int[] prices, int fee) {
        if (prices.length<1)return 0;
        int[][] dp = new int[prices.length][3];
        //未卖出未持有
        dp[0][0] = 0;
        //持有
        dp[0][1] = -prices[0]-fee;
        //卖出未持有
        dp[0][2] = 0;
        for (int i = 1; i < prices.length; i++) {
            dp[i][0] = Math.max(dp[i-1][0],dp[i-1][2]);
            dp[i][1] = Math.max(dp[i-1][1],Math.max(dp[i-1][0]-prices[i]-fee,dp[i-1][2]-prices[i]-fee));
            dp[i][2] = dp[i-1][1]+prices[i];
        }
        return Math.max(dp[prices.length-1][0],dp[prices.length-1][2]);
    }
}

123. 买卖股票的最佳时机 III

每日一题leetcode_第47张图片
题解:
此题和上面两题有一个明显的不同:最多完成 2 笔交易.
因此状态需要额外加一个维度来记录当前的交易次数。
定状态dp[i][0][k]:第 i -1天(下标从0开始),持有,且交易次数为 k(k<=2);dp[i][1][k]:第i-1天,未持有,且交易次数为 k (k<=2)
写方程

  • dp[i][0][0] = 0:未持有且交易次数为0,那表明从来没买过,收益一直为0。
  • dp[i][0][1] = Math.max(dp[i-1][0][1],dp[i-1][1][0]+prices[i]):今天未持有且交易过1次,可能昨天也是这样,也可能恰好今天是第1次交易dp[i-1][1][0]+prices[i]
  • dp[i][0][2] = Math.max(dp[i-1][0][2],dp[i-1][1][1]+prices[i]):今天未持有且交易过2次,可能昨天也是这样,也可能恰好今天是第2次交易dp[i-1][1][1]+prices[i]
  • dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][0][0]-prices[i]):今天持有且没有交易过,可能昨天也是这样,也可能恰好今天第1次买入dp[i-1][0][0]-prices[i]
  • dp[i][1][1] = Math.max(dp[i-1][1][1],dp[i-1][0][1]-prices[i]):今天持有且交易过1次,可能昨天也是这样,也可能恰好今天第2次买入dp[i-1][0][1]-prices[i]
  • dp[i][1][2] = MIN_VALUE: 交易次数已经用完,但还持有,这种情况不能发生。
    初始化
    dp[0][0][0] = 0;第0天未买入
    dp[0][1][0] = -prices[0];第0天买入
    不可能发生的几种情况:
    dp[0][0][1] = MIN_VALUE;
    dp[0][0][2] = MIN_VALUE;
    dp[0][1][1] = MIN_VALUE;
    dp[0][1][2] = MIN_VALUE;
class Solution {
    public int maxProfit(int[] prices) {
        if (prices == null || prices.length <= 1) return 0;
        int[][][] dp = new int[prices.length][2][3];
        int MIN_VALUE = Integer.MIN_VALUE / 2;
        dp[0][0][0] = 0;
        dp[0][1][0] = -prices[0];
        dp[0][0][1] = MIN_VALUE;
        dp[0][0][2] = MIN_VALUE;
        dp[0][1][1] = MIN_VALUE;
        dp[0][1][2] = MIN_VALUE;
        for (int i = 1; i < prices.length; i++) {
            dp[i][0][0] = 0;
            dp[i][0][1] = Math.max(dp[i-1][0][1],dp[i-1][1][0]+prices[i]);
            dp[i][0][2] = Math.max(dp[i-1][0][2],dp[i-1][1][1]+prices[i]);
            dp[i][1][0] = Math.max(dp[i-1][1][0],dp[i-1][0][0]-prices[i]);
            dp[i][1][1] = Math.max(dp[i-1][1][1],dp[i-1][0][1]-prices[i]);
            dp[i][1][2] = MIN_VALUE;//不可能
        }
        return Math.max(0,Math.max(dp[prices.length-1][0][1],dp[prices.length-1][0][2]));
    }
}

188. 买卖股票的最佳时机 IV

每日一题leetcode_第48张图片
题解:
上一题目的一摸一样,只不过把2换成了k。思路解法一致。

class Solution {
    public int maxProfit(int k, int[] prices) {
        if (prices.length<1)return 0;
        int[][][] dp = new int[prices.length][2][k+1];
        final int MINVAL = Integer.MIN_VALUE/2;

        for (int i = 0; i <= k; i++) {
            dp[0][0][i] = MINVAL;
            dp[0][1][i] = MINVAL;
            if (i==0){
                dp[0][0][i] = 0;
                dp[0][1][i] = -prices[0];
            }
        }

        for (int i = 1; i < prices.length; i++) {
            for (int j = 0; j <= k; j++) {
                if (j==0) dp[i][0][j] = 0;
                else dp[i][0][j] = Math.max(dp[i-1][0][j],dp[i-1][1][j-1]+prices[i]);
                if (j==k) dp[i][1][j] = MINVAL;
                else dp[i][1][j] = Math.max(dp[i-1][1][j],dp[i-1][0][j]-prices[i]);
            }
        }
        int res = 0;
        for (int i = 0; i <= k; i++) {
            res = Math.max(res, dp[prices.length-1][0][i]);
        }
        return res;
    }
}

930和相同的二元子数组

每日一题leetcode_第49张图片
三指针法,left2 - left1;为开头0的个数,即right每向右移动一位,有多少个子数组。

class Solution {
        //三指针法
    public int numSubarraysWithSum(int[] nums, int goal) {
        int res = 0;
        int sum1 = 0, sum2 = 0;
        int left1 = 0, left2 = 0, right = 0;
        while (right < nums.length) {
            sum1 += nums[right];
            while ((left1 <= right && sum1 > goal)) {
                sum1 -= nums[left1];
                left1++;
            }
            sum2 += nums[right];
            while (left2 <= right && sum2 >= goal) {
                sum2 -= nums[left2];
                left2++;
            }
            res += left2 - left1;
            right++;
        }
        return res;
    }
}

274. H 指数

每日一题leetcode_第50张图片
题解:
使用优先级队列,优先出队最大元素。统计最大元素出队个数,

			if (val == count) {//如果出队列元素和个数相同,则 h = count;结束循环。
                h = count;
                break;
            } else if (val < count) { //如果出列元素值小于count,则 h = count-1;
                h = count - 1;
                break;
            }else {//出列元素大于count,h=count;继续循环。
                h=count;
            }
class Solution {
    public int hIndex(int[] citations) {
        int h = 0;
        int count = 0;
        PriorityQueue<Integer> queue = new PriorityQueue<>(((o1, o2) -> (o2 - o1)));
        for (int i = 0; i < citations.length; i++) {
            queue.add(citations[i]);
        }
        int size = queue.size();
        for (int i = 0; i < size; i++) {
            count++;
            Integer val = queue.poll();
            if (val == count) {
                h = count;
                break;
            } else if (val < count) {
                h = count - 1;
                break;
            }else {
                h=count;
            }
        }
        return h;
    }
}

111. 二叉树的最小深度

每日一题leetcode_第51张图片
题解:
递归结束条件:

  • 叶子节点的定义是左孩子和右孩子都为 null 时叫做叶子节点
  • 当root节点的左右孩子都为空时,返回1
  • 当root节点左右孩子有一个为空时,返回不为空的孩子节点深度
  • 当root节点左右都不为空时,返回左右孩子较小深度的节点值
class Solution {
    public int minDepth(TreeNode root) {
        if (root==null) return 0;
        if (root.left == null && root.right == null)return 1;
        int m1 = minDepth(root.left);
        int m2 = minDepth(root.right);
        if (root.left==null||root.right==null)return m1+m2+1;
        return Math.min(m1, m2)+1;
    }
}

110. 平衡二叉树

每日一题leetcode_第52张图片
题解:
先求左右子树的最大深度,根据最大深度的差值是否大于1判断,然后递归判断左子树和右子树。都成立则返回true。

class Solution {
    public boolean isBalanced(TreeNode root) {
        if (root == null) return true;
        return Math.abs(dfs(root.left) - dfs(root.right)) <= 1 && isBalanced(root.left) && isBalanced(root.right);
    }
	
    public int dfs(TreeNode node) {
        if (node == null) return 0;
        return Math.max(dfs(node.left), dfs(node.right)) + 1;
    }
}

543. 二叉树的直径

每日一题leetcode_第53张图片
题解:

class Solution {
    int res = 0;

    public int diameterOfBinaryTree(TreeNode root) {
        compare(root);
        return res-1;
    }
	    //最大直径就是左右子树的最大深度的和
    public void compare(TreeNode node) {
        if (node != null) {
            res = Math.max(res, dfs(node.left) + dfs(node.right) + 1);
            compare(node.left);
            compare(node.right);
        }
    }

    //树的深度
    public int dfs(TreeNode node) {
        if (node == null) return 0;
        return Math.max(dfs(node.left), dfs(node.right)) + 1;
    }
}

112. 路径总和

每日一题leetcode_第54张图片
题解:

    public boolean hasPathSum(TreeNode root, int targetSum) {
        if (root == null) return false;
        targetSum-=root.val;
        if (targetSum==0&&root.left==null&&root.right==null)return true;
        return hasPathSum(root.left, targetSum)|| hasPathSum(root.right, targetSum);
    }

113. 路径总和 II

每日一题leetcode_第55张图片
题解:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    List<Integer> path = new ArrayList<>();

    public List<List<Integer>> pathSum(TreeNode root, int targetSum) {
        if (root == null) return res;
        dfs(root, targetSum);
        return res;
    }

    private void dfs(TreeNode root, int targetSum) {
        if (root==null)return;
        
        targetSum -= root.val;
        path.add(root.val);
        if (targetSum == 0 && root.left == null && root.right == null) {
            res.add(new ArrayList<>(path));
        }
        
        dfs(root.left, targetSum);
        dfs(root.right, targetSum);
        path.remove(path.size() - 1);
    }
}

437. 路径总和 III

每日一题leetcode_第56张图片
题解:

class Solution {
    int res = 0;
    public int pathSum(TreeNode root, int targetSum) {
        traversal(root, targetSum);
        return res;
    }
    private void traversal(TreeNode root, int targetSum){
        if (root==null)return;
        dfs(root,targetSum);
        if (root.left!=null)traversal(root.left, targetSum);
        if (root.right!=null)traversal(root.right, targetSum);
    }

    private void dfs(TreeNode root, int targetSum) {
        if (root==null)return;
        targetSum-=root.val;
        if (targetSum==0)res++;
        dfs(root.left, targetSum);
        dfs(root.right,targetSum);
    }
}

你可能感兴趣的:(leetcode)