Algorithm

Algorithm

看五年、想三年、做一年

基础数学问题

长度为 n 的数字的子集的个数

  • 2 n 个 2^n个 2n

组合、排列、子集定义和区别

JAVA 快速语法问题

快速填充:

  1. 填充第一行的数据全部为1
Arrays.fill(nums[0],-1);
Arrays.fill(nums,start,end,value)//填充的时候所表示的是左闭右开区间。
  1. 填充第一列的元素
IntStream.range(0,10).forEach(i->{ // 都是 左闭右开 区间
	nums[i][0]=-1
});

for(int i=0;i<10;i++){
	nums[i][0]=-1
}


数组输出结果

  1. 一维数组输出
Arrays.toString(nums);
  1. 二维数组输出
Arrays.deepToString(nums);

获取子串、子数组

// 无论是Arrays.copyOfRange(nums,startIndex,endIndex),还是String.substring(startIndex,endIndex),都是左闭右开区间
int [] nums=new int[]{1,2,3,4};
System.out.println(Arrays.toString(Arrays.copyOfRange(nums, 0,2)));
[1, 2]
String s="1234";
System.out.println(s.substring(0,2));//12

========================
  根据长度来做切分
  Arrays.copyOf(nums,length);
 	System.out.println(Arrays.toString(Arrays.copyOf(nums, 1)));//1
	

  System.out.println(s.substring(1));//"234"


将字符或者是,数字收集成字符串

char chs[]=new char[]{'a','c'};
new String(chs);
List<Character> path=new ArrayLlist<>();
path.stream().map(String::valueOf).collect(Collectors.joining());

ArrayDeque中常用的方法

1.添加元素
        addFirst(E e)在数组前面添加元素
        addLast(E e)在数组后面添加元素
        offerFirst(E e) 在数组前面添加元素,并返回是否添加成功
        offerLast(E e) 在数组后天添加元素,并返回是否添加成功

2.删除元素
        removeFirst()删除第一个元素,并返回删除元素的值,如果元素为null,将抛出异常
        pollFirst()删除第一个元素,并返回删除元素的值,如果元素为null,将返回null
        removeLast()删除最后一个元素,并返回删除元素的值,如果为null,将抛出异常
        pollLast()删除最后一个元素,并返回删除元素的值,如果为null,将返回null
        removeFirstOccurrence(Object o) 删除第一次出现的指定元素
        removeLastOccurrence(Object o) 删除最后一次出现的指定元素
   

3.获取元素
        getFirst() 获取第一个元素,如果没有将抛出异常
        getLast() 获取最后一个元素,如果没有将抛出异常
   
4.队列操作
        add(E e) 在队列尾部添加一个元素
        offer(E e) 在队列尾部添加一个元素,并返回是否成功
        remove() 删除队列中第一个元素,并返回该元素的值,如果元素为null,将抛出异常(其实底层调用的是removeFirst())
        poll()  删除队列中第一个元素,并返回该元素的值,如果元素为null,将返回null(其实调用的是pollFirst())
        element() 获取第一个元素,如果没有将抛出异常
        peek() 获取第一个元素,如果返回null
      
5.栈操作
        push(E e) 栈顶添加一个元素
        pop(E e) 移除栈顶元素,如果栈顶没有元素将抛出异常
        
6.其他
        size() 获取队列中元素个数
        isEmpty() 判断队列是否为空
        iterator() 迭代器,从前向后迭代
        descendingIterator() 迭代器,从后向前迭代
        contain(Object o) 判断队列中是否存在该元素
        toArray() 转成数组
        clear() 清空队列
        clone() 克隆(复制)一个新的队列

反转 集合

        Collections.reverse(ans);

每日一题

2024/4/22 377. 组合总和 Ⅳ - 力扣(LeetCode)

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

思考

  • 首先我们在看到这个题目的第一眼,就下意识的以湿到这个题目是一个回溯类型的问题,回溯法,旨在枚举出所有符合规范的结果,并且存储在 Path 中,符合规范之后添加到 ans 中。但是我们看这个题目,每一个数字是可以重复的,并且在使用过之后前面的数字都是可以再重复使用的,所以再枚举的时间,可能会有超时。
  • 在此 我们思考回溯法和DP 法的一个区别,DP 一般是求解出有多少种可能,求解的答案是一个数字,而回溯法是求解出来的是一个具体的路径。
class Solution {
    int ans;
    ArrayDeque<Integer> path=new ArrayDeque<>();
    int [] nums;
    int target;
    int n;
    public int combinationSum4(int[] nums, int target) {
        this.nums=nums;
        this.target=target;
        this.n=nums.length;
        
        dfs(0,0);
        return ans;
    }
    public void dfs (int index,int sum ){
        if(sum==target){
            ans++;
            System.out.println(path);
            return ;
        }
        for(int i=0;i<n;i++){
            if(sum+nums[i]>target){
                break;
            }
            path.offer(nums[i]);
            sum+=nums[i];
            dfs(i,sum);
            sum-=nums[i];
            path.pollLast();
        }
    }
}

改进

  • 利用DP 来求解ans 的数量,是不记录答案的路径的;
class Solution {
    public int combinationSum4(int[] nums, int target) {
        //首先第一步,明确dp【j】下标的含义,dp【j】是目标为 j 的时间,从nums中寻找出的 综合为 j 的元素组合的个数
        //dp【j】=dp【j-1】+dp【j-2】+。。。+dp【0】,并且,在这里我们,是不具备所有的 j-1,j-2.j-3 的。只具备所包含的nums,所以这里
        // 初始化,由题意可以知道的是,只有在不取任何元素的时间 target =0 ,只有一种方案
        // 遍历方向,首先应该确定target 等于 1的情况,然后逐步确认target + 1 的情况,直至到达 target,并且每一个 i - 的都是 nums【 j 】 
        int [] dp=new int [target+1];
        dp[0]=1;

        for(int i=1;i<=target;i++){
            for(int j=0;j<nums.length;j++){
                if(nums[j]<=i){
                    dp[i]+=dp[i-nums[j]];
                }
            }
        }
        return dp[target];
    }
}

2024/4/25 2739. 总行驶距离 - 力扣(LeetCode)

注意点:

  • 每一次使用 5 L 才从 副油箱中提取 1 L的油,那么直接 当main 大于等于 5 的时间,main -=5 ,ans+=50,如果副油箱是大于 0 的,将副油箱的1L油添加到 主油箱中。
  • 最后的答案是, ans 加上 不满足 5 L的。
class Solution {
    public int distanceTraveled(int mainTank, int additionalTank) {
        int ans=0;
        while(mainTank>=5){
            mainTank-=5;
            ans+=50;
            if(additionalTank>0){
                additionalTank--;
                mainTank++;
            }
        }
        return ans+mainTank*10;
    }
}

2024/4/27 2639. 查询网格图中每一列的宽度 - 力扣(LeetCode)

思考

  • 首先应该 确定遍历顺序,题目中求解的是每一列的最大数字的长度,所以我们的遍历顺序就是列遍历,这样就可以对比每一列的整数的长度了。
  • 在这里需要注意的是,如果当前数字是 正数 负数 0 对这个长度的处理都是不同的。
class Solution {
    public int[] findColumnWidth(int[][] grid) {
        int ans[]=new int[grid[0].length];
        for(int c=0;c<grid[0].length;c++){
            for(int r=0;r<grid.length;r++){
                int length=0;
                int num=grid[r][c];
                if(num<0){
                    length++;
                    num=num*-1;
                    while(num!=0){
                        num/=10;
                        length++;
                    }
                }else if(num==0){
                    length=1;
                }else{
                    while(num!=0){
                        num/=10;
                        length++;
                    }
                }
                ans[c]=Math.max(ans[c],length);
            }
        }
        return ans;
    }
}

1017. 负二进制转换 - 力扣(LeetCode)

注意点:

  • 首先的思想是 除非字符串是 0 那么就不糊含有前导 0 ,基本的整数转换为 二进制负数,就是 全部按位取反 再加 1
  • 在二进制转换的时候没有太多的区别,只是 在转化为 负数的时候,余数的数值会 变到 0 -1 1 这个取值,所以将余数直接加 x(也就是 x进制)并且将 n +1
class Solution {
    public String baseNeg2(int n) {
        // 首先的思想是 除非字符串是 0  那么就不糊含有前导  0 ,基本的整数转换为 二进制负数,就是 全部按位取反 再加 1 
        // 在二进制转换的时候没有太多的区别,只是 在转化为 负数的时候,余数的数值会 变到 0 -1 1 这个取值,所以将余数直接加  x(也就是  x进制)并且将 n +1
        if(n==0){
            return "0";
        }
        StringBuilder ans=new StringBuilder();
        int yushu=0;
        int shang=0;
        while(n!=0){
            yushu=n%-2;
            n/=-2;
            if(yushu<0){
                yushu+=2;
                n++;
            }
            ans=ans.append(yushu);
        }
        return ans.reverse().toString();
    }
}

994. 腐烂的橘子 - 力扣(LeetCode)

哈希

1. 两数之和 - 力扣(LeetCode)

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 104
  • -109 <= nums[i] <= 109
  • -109 <= target <= 109
  • 只会存在一个有效答案

数组

704. 二分查找 - 力扣(LeetCode)

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1

示例 1:

输入: nums = [-1,0,3,5,9,12], target = 9
输出: 4
解释: 9 出现在 nums 中并且下标为 4

示例 2:

输入: nums = [-1,0,3,5,9,12], target = 2
输出: -1
解释: 2 不存在 nums 中因此返回 -1

代码段:

注意点:

  • 首先记住lo标记的是target 的应该存在的位置,此时的lo和hi的位置是是在hi的右边即lo > hi 的。
  • 第二点注意的是,如果当前的目标元素是最大的,那么我们在判断target == nums【lo】 的时间就会越界。
class Solution {
    public int search(int[] nums, int target) {
        int lo=0;
        int hi=nums.length-1;
        while(lo<=hi){
            int mid=lo+(hi-lo)/2;
            if(target>nums[mid]){
                lo=mid+1;
            }else{
                hi=mid-1;//最后的结果是,lo在hi的右边,即应该插入的位置上
            }
        }
        if(lo==nums.length){
            return  -1;
        }
        return target==nums[lo]?lo:-1;
    }
}

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

给你一个按照非递减顺序排列的整数数组 nums,和一个目标值 target。请你找出给定目标值在数组中的开始位置和结束位置。

如果数组中不存在目标值 target,返回 [-1, -1]

你必须设计并实现时间复杂度为 O(log n) 的算法解决此问题。

示例 1:

输入:nums = [5,7,7,8,8,10], target = 8
输出:[3,4]

示例 2:

输入:nums = [5,7,7,8,8,10], target = 6
输出:[-1,-1]

示例 3:

输入:nums = [], target = 0
输出:[-1,-1]

注意点:

  • 技巧:首先我们查找的是一个元素的起始位置和结束位置,我们二分法找到的是他的起始位置,所以我们可以来寻找比目标值大1 的数字的起始位置,并且将它减一,就得到了target 的
class Solution {
    public int[] searchRange(int[] nums, int target) {
        // 代码思路,因为我们这里寻找到的left 都是 插入的位置,不论是否寻找到指定的target 元素,所以直接查询target目标加一的元素的位置,后来直接去
        int start=binarySearch(nums,target);
        int end=binarySearch(nums,target+1)-1;
        if(start<=end)
        		return new int[]{start,end};
        else{
            return new int[]{-1,-1};
        }
    }

    public int binarySearch(int[] nums, int target) {
        int lo = 0;
        int hi = nums.length - 1;
        while (lo <= hi) {
            int mid = lo + (hi - lo) / 2;
            if (target > nums[mid]) {
                lo = mid + 1;
            } else {
                hi = mid - 1;
            }
        }
        return lo;
    }
}

27. 移除元素 - 力扣(LeetCode)

给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。

不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组

元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。

说明:

为什么返回数值是整数,但输出的答案是数组呢?

请注意,输入数组是以**「引用」**方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。

你可以想象内部操作如下:

// nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);

// 在函数里修改输入数组对于调用者是可见的。
// 根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++) {
    print(nums[i]);
}

示例 1:

输入:nums = [3,2,2,3], val = 3
输出:2, nums = [2,2]
解释:函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。你不需要考虑数组中超出新长度后面的元素。例如,函数返回的新长度为 2 ,而 nums = [2,2,3,3] 或 nums = [2,2,0,0],也会被视作正确答案。

示例 2:

输入:nums = [0,1,2,2,3,0,4,2], val = 2
输出:5, nums = [0,1,3,0,4]
解释:函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。注意这五个元素可为任意顺序。你不需要考虑数组中超出新长度后面的元素。

注意点

  • 首先我门需要来原地删除一个元素,那我们可以换位思考,直接将不等于val 的数值利用双指针进行拷贝。
	class Solution {
    public int removeElement(int[] nums, int val) {
        int lo=0;
        for(int hi=0;hi<nums.length;hi++){
            if(nums[hi]!=val){
                nums[lo++]=nums[hi];
            }
        }
        return lo;
    }
}

977. 有序数组的平方 - 力扣(LeetCode)

给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。

示例 1:

输入:nums = [-4,-1,0,3,10]
输出:[0,1,9,16,100]
解释:平方后,数组变为 [16,1,0,9,100]
排序后,数组变为 [0,1,9,16,100]

示例 2:

输入:nums = [-7,-3,2,3,11]
输出:[4,9,9,49,121]

注意点

  • 首先我们可以明确的是,非递减的,那么最大的数值就在数组的两边,呢么我们可以利用双指针来进行遍历
class Solution {
    public int[] sortedSquares(int[] nums) {
        int lo=0;
        int hi=nums.length-1;
        int [] ans=new int[nums.length];
        int index=nums.length-1;
        while(lo<=hi){
            if(Math.abs(nums[lo])>Math.abs(nums[hi])){
                ans[index--]=nums[lo]*nums[lo];
                lo++;
            }else{
                ans[index--]=nums[hi]*nums[hi];
                hi--;
            }
        }
        return ans;
    }
}

209. 长度最小的子数组 - 力扣(LeetCode)

给定一个含有 n 个正整数的数组和一个正整数 target

找出该数组中满足其总和大于等于 target 的长度最小的 连续

子数组

[numsl, numsl+1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0

示例 1:

输入:target = 7, nums = [2,3,1,2,4,3]
输出:2
解释:子数组 [4,3] 是该条件下的长度最小的子数组。

示例 2:

输入:target = 4, nums = [1,4,4]
输出:1

示例 3:

输入:target = 11, nums = [1,1,1,1,1,1,1,1]
输出:0 
*** 尤其注意当前例子,求和之后的sum=8 但是他的sum 没有达到target(11) 所以ans 是没有进入 代码段的第九行,也就是没有计算hi-lo+1,此时的ans 等于nums.length+1

注意点

  • 首先我们需要明确的是利用华东窗口来判断子数组(连续)的长度,如果当前求和是大于等于target 的时间,这个时候我们就可以查询数值了,知道sum小于target,可以继续让 hi 指针迁移。
  • 还有一点需要注意的是,ans 的最大长度是nums.length,如何当前ans是 小于等于 nums.length 的时间,是答案,否则的话就没有答案。
class Solution {
    public int minSubArrayLen(int target, int[] nums) {
        int lo=0;
        int sum=0;
        int ans=nums.length+1;
        for(int hi=0;hi<nums.length;hi++){
            sum+=nums[hi];
            while(sum>=target){
                ans=Math.min(ans,hi-lo+1);
                sum-=nums[lo++];
            }
        }
        return ans<=nums.length?ans:0;
    }
}

59. 螺旋矩阵 II - 力扣(LeetCode)

给你一个正整数 n ,生成一个包含 1n2 所有元素,且元素按顺时针顺序螺旋排列的 n x n 正方形矩阵 matrix

示例 1:

img

输入:n = 3
输出:[[1,2,3],[8,9,4],[7,6,5]]

示例 2:

输入:n = 1
输出:[[1]]

注意点

  • 首先当前数组是分 奇数 和偶数 的,在奇数的情况之下,我们就需要将最中间的一个元素独立赋值。
  • 第二点,我们在循环的时间,循环次数的变量是需要得知的,为n/2
  • 其次,在每一次的循环过程中,我们需要制定一个循环不变量,就是左闭右开原则,并且每一次填充的元素的长度都会缩减,因此我们需要一个offset 变量,来制定 右 和下 结束的标志。
  • 制定一个startr 和startc 来制定 左,和上 的结束下标。
class Solution {
    public int[][] generateMatrix(int n) {
        int loop=1;
        int r=0;
        int c=0;
        int startr=0;
        int startc=0;
        int [][] ans=new int[n][n];
        if((n&1)==1){
            ans[n/2][n/2]=n*n;
        }
        int offset=1;
        int num=1;
        while(loop<=n/2){
            r=startr;
            c=startc;
            for(;c<n-offset;c++){//右填充,需要变更的是列 c
                ans[r][c]=num++;
            }
            for(;r<n-offset;r++){//  下填充,需要变更的是 r
                ans[r][c]=num++;
            }
            for(;c>startc;c--){// 左填充,需要变更的是 c ,结束标识是 起始点,startc列
                ans[r][c]=num++;
            }
            for(;r>startr;r--){//上填充,需要变更的是 r,结束标志是 起始点 startr
                ans[r][c]=num++;
            }
            startr++;
            startc++;
            loop++;
            offset++;
          //因为我们每一次都在缩减,所以需要拥有 四个变量(实际上是三个) 来控制范围
        }
        return ans;
    }
}

6. Z 字形变换 - 力扣(LeetCode)

class Solution {
    public String convert(String s, int numRows) {
        // 这个题目 ,仔细想一想,这里我们chuachua女鬼剑 每一行的字符串来保存,在遍历字符串的时候,对于需要将这个字符放在哪一行? 这个是需要我们重点考虑的地方,每一行都是挨个去放置,然后到了一个guan'jin'aguanjina 的地方,到达了 最后一行的时候,我们需要想前一行放置遍历的字符,到达的第一行后,我们应该向第后面开始放置字符。
       if(numRows <2)
        return s;
        StringBuilder ans=new StringBuilder();
        List<StringBuilder> rows=new ArrayList<>();
        // IntStream.range(0,numRows).forEach(i->{
        //     rows.add(new StringBuilder());
        // });
           for(int i = 0; i < numRows; i++) rows.add(new StringBuilder());
        int i=0,flag=-1;
        for(char ch: s.toCharArray()){
            rows.get(i).append(ch);
            if(i==0||i==numRows-1) flag=-flag;
            i+=flag;
        }
        for(StringBuilder row:rows){
            ans.append(row);
        }
        return ans.toString();

    }
}

二叉树

递归三要素

  1. 递归函数的参数和返回值:首先确定哪些参数是递归中需要 处理的,那么就在递归函数上添加该参数,并且需要确定每次递归之后的返回值是什么,进而确定递归函数的返回值。
  2. 确定终止条件: 正确的终止条件不会导致栈溢出。
  3. 确定单层递归的逻辑: 确定每一层需要怎样处理信息,在这里往往需要再次调用自己。

144. 二叉树的前序遍历 - 力扣(LeetCode)

递归遍历

思路

  1. 首先明确一下递归的参数和返回值:参数就是在递归中需要处理的。在这里我们需要处理的是当前节点,因为是前序遍历 顺序是 中左右 所以我们需要一个结构来存储前序遍历 节点的值。在这里是不需要返回值的。
  2. 终止条件 就是当前节点为空的时候
  3. 单层的逻辑: 因为是前序遍历算法,所以处理节点的逻辑是 中左右
class Solution {
    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> path=new ArrayList<>();
        dfs(root,path);
        return path;
    }
    void dfs(TreeNode root,List<Integer> path){
        if(root==null){
            return ;
        }
        path.add(root.val);
        dfs(root.left,path);
        dfs(root.right,path);
    }
}
================================================================================================================
class Solution {

    public List<Integer> preorderTraversal(TreeNode root) {
        List<Integer> ans = new ArrayList<>();
        ArrayDeque<TreeNode> queue = new ArrayDeque<>();
        if (root == null) {
            return ans;
        }
        queue.offer(root);
        while (!queue.isEmpty()) {
            TreeNode node = queue.pollLast();
            ans.add(node.val);
            if (node.right != null) {
                queue.offerLast(node.right);
            }
            if (node.left != null) {
                queue.offerLast(node.left);
            }

        }
        return ans;
    }
}

迭代遍历
前序 中左右

思路

首先前序遍历算法之所以叫前序遍历算法,是因为首先处理根节点 即 中左右 ,同时我们保证处理当前节点之后将她的右孩子和 左孩子先后 放入到栈中,这样就保证了出栈的顺序是, 中 左右。

二叉树前序遍历(迭代法)

class Solution {
    public List<Integer> preorderTraversal(TreeNode root) { 
        
        ArrayDeque<TreeNode> path=new ArrayDeque<>();
        List<Integer> ans=new ArrayList<>();
        if(root==null){
            return ans;
        }
        path.offerLast(root);
        while(!path.isEmpty()){
            TreeNode node =path.pollLast();
            ans.add(node.val);
            if(node.right!=null) path.offerLast(node.right);
            if(node.left!=null) path.offerLast(node.left);
        }
        return ans;
    }
}
中序 左中右

为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:

  1. 处理:将元素放进result数组中
  2. 访问:遍历节点

分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。

那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。

那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。

动画如下:

二叉树中序遍历(迭代法)

class Solution {
    public List<Integer> inorderTraversal(TreeNode root) {
        // 首先当前节点访问的顺序是 左中右,首先访问的是左节点 ,其次处理的是中间节点,这就导致了 访问的节点和处理的节点是不一致的。这个时候我们借助两个 结构来处理。
        List<Integer> ans=new ArrayList<>();
        if(root==null){
            return ans;
        }
        ArrayDeque<TreeNode> path=new ArrayDeque<>();
        TreeNode cur=root;
        while(cur!=null|| !path.isEmpty()){
            if(cur!=null){
                path.offerLast(cur);
                cur=cur.left;

            }else{
                cur =path.pollLast();
                ans.add(cur.val);
                cur=cur.right;
            }
        }
        return ans;
    }
}
后序 左右中

再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:

前序到后序

class Solution {
    public List<Integer> postorderTraversal(TreeNode root) {
        ArrayDeque<TreeNode> path=new ArrayDeque<>();
        List<Integer> ans=new ArrayList<>();
        if(root==null)
            return ans;
        path.offerLast(root);
        while(!path.isEmpty()){
            TreeNode node=path.pollLast();
            ans.add(node.val);
            if(node.left!=null) path.offerLast(node.left);
            if(node.right!=null) path.offerLast(node.right);

        }
        Collections.reverse(ans);
        return ans;
    }
}

链表

203. 移除链表元素 - 力扣(LeetCode)

移除链表元素

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点

示例 1:

img注意点

  • 首先我们需要注意的是,删除的节点可能是头结点,所以需要的一个哨兵节点,这样就可以没有变动的删除头结点。
if(cur.next.val==val){
	cur.next=cur.next.next;
}else{
	cur=cur.next;
}
return ummy.next;
/**
 * 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 removeElements(ListNode head, int val) {
        ListNode dummy = new ListNode(-1, head);
        ListNode cur = dummy;
        while (cur.next != null) {
            if (cur.next.val == val) {
                cur.next = cur.next.next;
            } else {
                cur = cur.next;
            }
        }
        return dummy.next;

    }
}

160. 相交链表 - 力扣(LeetCode)

img

题目数据 保证 整个链式结构中不存在环。

注意,函数返回结果后,链表必须 保持其原始结构

注意点

  • P a 的长度是 a , P b 的长度是 b ,重合的节点的个数是 c Pa的长度是 a,Pb的长度是b,重合的节点的个数是c Pa的长度是aPb的长度是b,重合的节点的个数是c
  • 我们首先让 P a 走 a + ( b − c ) 步,继续让 P b 走 b + ( a − c ) 步 我们首先让Pa走a+(b-c)步,继续让Pb走b+(a-c)步 我们首先让Paa+(bc)步,继续让Pbb+(ac)
  • 此时这两个节点是重合的 a + ( b − c ) = b + ( a − c ) a+(b-c)=b+(a-c) a+(bc)=b+(ac)
  • 如果 C 是大于 0 的 ,那么就是指向的第一个节点了。否则Pa 和Pb 指向的都是 null,直接返回 pa 就可以。
/**
 * Definition for singly-linked list.
 * public class ListNode {
 * int val;
 * ListNode next;
 * ListNode(int x) {
 * val = x;
 * next = null;
 * }
 * }
 */
public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode pa = headA;
        ListNode pb = headB;
        while (pa != pb) {
            pa = pa == null ? headB : pa.next;
            pb = pb == null ? headA : pb.next;
        }
        return pa;
    }
}

206. 反转链表 - 力扣(LeetCode)

img

注意点

  • 首先思考一下,最后的cur 节点是null,并且pre 指向的是最后一个节点。
  • 首先反转链表,刚开始思考,前一个节点肯定指向的是 null 节点,那么第二个节点指向的是第一个节点。
/**
 * 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 reverseList(ListNode head) {
        ListNode cur=head;
        ListNode pre=null;
        while(cur!=null){
            ListNode nxt=cur.next;
            cur.next=pre;
            pre=cur;
            cur=nxt;
        }
        return pre;
    }
}

234. 回文链表 - 力扣(LeetCode)

示例 1:

img

输入:head = [1,2,2,1]
输出:true

注意点找到链表的中间节点是,n/2,向下取整,表示的是链表的中间节点。

  • 首先链表的长度是有奇数和偶数的区别的,那么我们试想一下,首先寻找到中间节点,即 如果是 4 个的话,那么节点就是2,第三个节点,如果是3 ,那么就是1 ,第二个节点
  • 1 -》2-》3-》4 ,对应的中间节点是3,1-》2-》3,对应的中间节点是2,偶数长度的链表在反转的时间是,1-》2-》3-》null,4-》3-》null,在判断回文链表的时间,4 和 1 对比了,3和2 对比了,这个时候反转过的链表已经是null 了,所以已经是结束的状态了,这个时候会直接返回true,所以 1 2 是不会走到 3 的。
  • 1 2 3 ,对应的中间节点是下标为 1 ,数值为2 的,反转之后的 是 3 2 null ,1 2 null,这个时候的链表长度是相同的
/**
 * 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 boolean isPalindrome(ListNode head) {
        //首先找到中间节点
        ListNode midNode=mid(head);
        //将中间节点后面的反转
        ListNode fanhuan=reverse(midNode);

        //开始比较前面和后面的数值是否相等
        while(head!=null&&fanhuan!=null){
            if(head.val!=fanhuan.val){
                return false;
            }
            head=head.next;
            fanhuan=fanhuan.next;
        }
        return true;
    }
    ListNode mid(ListNode head){
        ListNode hi=head;
        ListNode lo=head;
        while(hi!=null&&hi.next!=null){
            hi=hi.next.next;
            lo=lo.next;
        }
        return lo;
    }
    ListNode reverse(ListNode head){
        ListNode pre=null;
        ListNode cur=head;
        while(cur!=null){
            ListNode nxt=cur.next;
            cur.next=pre;
            pre=cur;
            cur=nxt;
        }
        return pre;
    }
}

141. 环形链表 - 力扣(LeetCode)

注意点

  • 首先我们来明确一下什么样子的链表是有环的,举例一个快慢指针,快指针和满指针同时向前走,如果拥有环的话,那么会快指针会首先在环中转圈,在满指针走进环的那一刻起,根据相对速度来判读,慢指针是相对静止的,快指针每次走一步,总会遇到“静止”的慢指针,所以就会产生 hi==lo。
/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode hi=head;
        ListNode lo=head;
        while(hi!=null&&hi.next!=null){
            hi=hi.next.next;
            lo=lo.next;
            if(lo==hi){
                return true;
            }
        }
        return false;
    }
}

142. 环形链表 II - 力扣(LeetCode)

注意点:

  • 就是一个数学问题, 作出如下假设:假设环的入口节点前有a个节点,环有b个节点,快慢指针在环入口节点后的第c个节点处相遇 假设此时慢指针走了n步,则有n=a+ib+c,这里i为整数 则此时快指针走了2n步,则有n=2a+2ib+2c=a+jb+c,这里j为整数 根据2n步的等式可以得出a+c = (j-2i)b = kb,这里k=j-2i,也为整数,易知j>2i,不过不重要 此时慢指针为距离环入口节点c步处,只要再走a步就能够距离环入口节点kb步,即到达环入口节点,证毕
/**
 * Definition for singly-linked list.
 * class ListNode {
 * int val;
 * ListNode next;
 * ListNode(int x) {
 * val = x;
 * next = null;
 * }
 * }
 就是一个数学问题, 作出如下假设:假设环的入口节点前有a个节点,环有b个节点,快慢指针在环入口节点后的第c个节点处相遇 假设此时慢指针走了n步,则有n=a+ib+c,这里i为整数 则此时快指针走了2n步,则有n=2a+2ib+2c=a+jb+c,这里j为整数 根据2n步的等式可以得出a+c = (j-2i)b = kb,这里k=j-2i,也为整数,易知j>2i,不过不重要 此时慢指针为距离环入口节点c步处,只要再走a步就能够距离环入口节点kb步,即到达环入口节点,证毕
 */
public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode hi = head;
        ListNode lo = head;
        while (hi != null && hi.next != null) {
            hi = hi.next.next;
            lo = lo.next;
            if (lo == hi) {
                while(head!=lo){
                    head=head.next;
                    lo=lo.next;
                }
                return lo;
            }
        }
        return null;
    }
}

21. 合并两个有序链表 - 力扣(LeetCode)

img

输入:l1 = [1,2,4], l2 = [1,3,4]
输出:[1,1,2,3,4,4]

注意点

  • 首先我们可以来利用归并排序的思想,来合并两个有序的链表,由此达到为链表排序的目的,主要是为了,让lo mid hi 这三个位置重现。
  • 首先 size 的长度是从 1 开始的,并且 size 的增加幅度是 size ,lists【lo】=merge(lists【lo】,lists【lo+size】,并且所有的排序都是合并在 0 上面的。
/**
 * 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 mergeKLists(ListNode[] lists) {
        if(lists==null||lists.length==0){
            return null ;
        }
        for(int size=1;size<lists.length;size+=size){
            for(int lo=0;lo<lists.length-size;lo+=size+size){
                lists[lo]=merge(lists[lo],lists[lo+size]);
            }
        }
        return lists[0];
    }
    ListNode merge(ListNode heada,ListNode headb){
        // 合并两个升序链表
        ListNode dummy=new ListNode(-1);
        ListNode cur=dummy;
        while(heada!=null&& headb!=null){
            if(heada.val<headb.val){
                cur=cur.next=heada;
                heada=heada.next;
            }else{
                cur=cur.next=headb;
                headb=headb.next;
            }
        }
        while(heada!=null){
            cur=cur.next=heada;
            heada=heada.next;
        }
        while(headb!=null){
            cur=cur.next=headb;
            headb=headb.next;
        }
        return dummy.next;
    }
}

19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

img

输入:head = [1,2,3,4,5], n = 2
输出:[1,2,3,5]

示例 2:

输入:head = [1], n = 1
输出:[]

示例 3:

输入:head = [1,2], n = 1
输出:[1]

注意点

  • 删除倒数第 N 个节点,我们考虑到 可能会删除首节点,所以这个时间需要一个头结点来统一操作。
  • 删除倒数 第 N 个节点,我们可以做的是让快指针首先走 n 个节点,然后两个指针一直走,知道快指针的下一个节点是null ,这个时候慢指针的下一个节点就是我们要删除的倒数第 N 个节点,此时只需要将 lo 的下一个指向 下下一个 节点就已经完成。
/**
 * 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 removeNthFromEnd(ListNode head, int n) {
        ListNode dummy=new ListNode(-1,head);
        ListNode lo=dummy;
        ListNode hi=dummy;
        while(n-->0){
            hi=hi.next;
        }
        while(hi.next!=null){
            hi=hi.next;
            lo=lo.next;
        }
        lo.next=lo.next.next;
        return dummy.next;
    }
}

25. K 个一组翻转链表 - 力扣(LeetCode)

思考

  • 首先我们需要明白的是 K 个一组反转,那就证明在不足 K 的时间是不需要反转的,而且我们在反转的时间是需要通过 链表剩余的长度来判断是否需要删除的,所以 我们是需要链表的长度来判断的, i + = k ;i 《 l ists.length 或者是 length -= k,lenght》=k的时间才是 满足反转的条件的。
/**
 * 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 reverseKGroup(ListNode head, int k) {
        // K 个
        int length = 0;
        ListNode dummy = new ListNode(-1, head);
        ListNode p0 = dummy;
        while (p0.next != null) {
            length++;
            p0 = p0.next;
        }
        if(length<k){
            return head;
        }
        p0=dummy;
        for (; length >= k; length -= k) {
            ListNode cur=p0.next;
            ListNode pre=null;
                
            for (int i = 0; i < k; i++) {
                ListNode nxt=cur.next;
                cur.next=pre;
                pre=cur;
                cur=nxt;    
            }
            // 在链接反转后的 k 个节点 ,我们需要做的 一点是将 p0 继续指向 下一个开始反转的k 个节点的前一个节点,所以这个时间需要来保存 1 号节点。
            ListNode nxt=p0.next;
            p0.next.next=cur;
            p0.next=pre;
            p0=nxt;
        }
        return dummy.next;
    }
}

24. 两两交换链表中的节点 - 力扣(LeetCode)

直接将 前面这个题目的 k 变为 2 就可以了

148. 排序链表 - 力扣(LeetCode)

也是利用归并排序的思想,直接将所有的节点的下一个节点全部指向的是 null,就可以利用归并排序来做

注意点

  • 在这里我们要注意的一点是,因为我们在每次合并之后链表的长度都是会增加的,所以必要的一点是不能够将merge 函数直接变为两个数字的判断
/**
 * 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 sortList(ListNode head) {
        // List<ListNode> lists = new ArrayList<>();
        ListNode[] lists=new ListNode[100000];
        ListNode dummy = new ListNode(-1, head);
        ListNode p0 = dummy.next;
        int length = 0;
        while (p0 != null) {
            ListNode cur = p0;
            p0 = p0.next;
            cur.next = null;
            lists[length++]=cur;
            // lists.add(cur);
            // length++;
        }
        if (length == 0 || head == null) {
            return null;
        }
        System.out.println(length);
        for (int size = 1; size <= length; size += size) {
            for (int lo = 0; lo < length - size; lo += size + size) {
                // lists.set(lo, merge(lists.get(lo), lists.get(lo + size)));
                lists[lo]=merge(lists[lo],lists[lo+size]);
            }
        }
        // return lists.get(0);
        return lists[0];
    }

    ListNode merge(ListNode heada, ListNode headb) {
        // 合并两个升序链表
        ListNode dummy = new ListNode(-1);
        ListNode cur = dummy;
        while (heada != null && headb != null) {
            if (heada.val < headb.val) {
                cur = cur.next = heada;
                heada = heada.next;
            } else {
                cur = cur.next = headb;
                headb = headb.next;
            }
        }
        while (heada != null) {
            cur = cur.next = heada;
            heada = heada.next;
        }
        while (headb != null) {
            cur = cur.next = headb;
            headb = headb.next;
        }
        return dummy.next;
    }
}

146. LRU 缓存 - 力扣(LeetCode)

注意点

  • 首先LRU 的模板是只具有普通的三个方法的,get (int key ,put(key ,value)还由一个初始化 LRU(int capacity)
  • 我们的LRU 中拥有一个链表,用来维护这个数据结构,拥有一个MAP 来判断这个Key 是否存在
  • 而每一个 map 中的 key 是数字,而value 是一个 node的节点;
  • 注意在put 节点的时候,我们是首先获取到当前节点,如果节点存在于map 中,那么我们将这个节点放置到 top 中,并且修改他的数值。
class Node{
    int key;
    int value;
    int capacity;
    Node pre;
    Node next;
    public Node(int key,int value){
        this.key=key;
        this.value=value;
    }
}
class LRUCache {
    int capacity;
    Node dummy;
    HashMap<Integer,Node> map=new HashMap<>();
    public LRUCache(int capacity) {
        this.capacity=capacity;
        dummy=new Node(-1,-1);
        dummy.next=dummy;
        dummy.pre=dummy;
    }
    
    public int get(int key) {
        Node node=getNode(key);
        return node==null?-1:node.value;
    }
    public Node getNode(int key){
        if(!map.containsKey(key)){
            return null;
        }
        Node node=map.get(key);
        //LRU算法的的一个特点就是,在每一次获取节点之后,都需要将当前节点移动到最顶部,表示当前节点刚刚被使用过。
        remove(node);
        top(node);
        return node;
    }
    void remove(Node node){
        node.pre.next=node.next;
        node.next.pre=node.pre;
    }
    void top(Node node){
        node.pre=dummy;
        node.next=dummy.next;
        dummy.next.pre=node;
        dummy.next=node;
    }
    public void put(int key, int value) {
        Node oldNode=getNode(key);
        if(oldNode!=null){
            oldNode.value=value;
            return ;
        }
        Node newNode=new Node(key,value);
        top(newNode);
        map.put(key,newNode);
        if(map.size()>capacity){
            Node lastNode=dummy.pre;
            remove(lastNode);
            map.remove(lastNode.key);

        }

    }
}



回溯

首先明确回溯法可以解决的问题:

  • 组合问题:在N 个数组里面,按照一定的规则来找出 k 个数 的集合,这里的集合可以是不连续的;
  • 切割问题:一组数据,按照一定的规则拥有多少种切割方式;
  • 子集问题:N 个数据中有多少个符合条件的子集,注意这里的子集和 组合问题不同的是它 一般是在树的路径中去寻找答案,而其他的一般都是在叶子节点中寻找数据。
  • 排列问题:N 个数字按照一定的规则排列,有多少种方法,这里注意N 个数字种有重复的,要求排列不能是重复的,所以我们需要在排序之后对已经使用的数据进行去重复操作。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

77. 组合 - 力扣(LeetCode)

给定两个整数 nk,返回范围 [1, n] 中所有可能的 k 个数的组合。

你可以按 任何顺序 返回答案。

示例 1:

输入:n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]

示例 2:

输入:n = 1, k = 1
输出:[[1]]			
	

注意点:

首先我们在使用回溯法进行枚举的时间,注意到递归传递的参数,一般是hi遍历第 i i i​ 个物品,但是需要一定的约束条件的时候,我们可以添加适当的参数,例如在 复原IP 种,可以添加分割出来的 用于判断结束条件,只有在点的个数达到三个的时间,才有可能是答案,这个时候我们就需要判断第四段是否是符合要求的 IP 地址,再根据结果将他加入。

class Solution {
    List<List<Integer>> ans=new ArrayList<>();
    ArrayDeque<Integer> path=new ArrayDeque<>();
    int n;
    int k;
    public List<List<Integer>> combine(int n, int k) {
        this.n=n;
        this.k=k;
        dfs(n);
        return ans;
    }
    public void dfs(int index){
        if(path.size()==k){
            ans.add(new ArrayList<>(path));
            return ;
        }
        if(index< k- path.size()){//如果当前的数字 已经是小于 剩余答案的长度读话,那么下面的所有的答案都是不足以获取的,所以直接剪枝
            return ;
        }
        for(int i=index;i>=1;i--){
            path.offer(i);
            dfs(i-1);
            path.pollLast();
        }
    }
}

216. 组合总和 III - 力扣(LeetCode)

找出所有相加之和为 nk 个数的组合,且满足下列条件:

  • 只使用数字1到9
  • 每个数字 最多使用一次

返回 所有可能的有效组合的列表 。该列表不能包含相同的组合两次,组合可以以任何顺序返回。

示例 1:

输入: k = 3, n = 7
输出: [[1,2,4]]
解释:
1 + 2 + 4 = 7
没有其他符合的组合了。

示例 2:

输入: k = 3, n = 9
输出: [[1,2,6], [1,3,5], [2,3,4]]
解释:
1 + 2 + 6 = 9
1 + 3 + 5 = 9
2 + 3 + 4 = 9
没有其他符合的组合了。

示例 3:

输入: k = 4, n = 1
输出: []
解释: 不存在有效的组合。
在[1,9]范围内使用4个不同的数字,我们可以得到的最小和是1+2+3+4 = 10,因为10 > 1,没有有效的组合。

注意点:

  • 首先注意的是,选择的是 k 个元素,那么我们就需要在path 等于 k 的时间再去判断它是否符合规范;
  • 第二点注意的是,每一个数字只能够使用一次
class Solution {
    List<List<Integer>> ans=new ArrayList<>();
    ArrayDeque<Integer> path =new ArrayDeque<>();
    int target;
    int k;    
    public List<List<Integer>> combinationSum3(int k, int n) {
        this.target=n;
        this.k=k;
        dfs(9,0);
        return ans;
    }
    public void dfs(int index,int sum ){
        if(k==path.size()){
            if(sum==target)
            ans.add(new ArrayList<>(path));
            return ;
        }
        if(index < k- path.size()){
            return ;
        }
        for(int i=index;i>=1;i--){
            if(i>target){
                continue;
            }
            sum+=i;
            path.offer(i);
            dfs(i-1,sum);
            path.pollLast();
            sum-=i;
        }
    }
}

17. 电话号码的字母组合 - 力扣(LeetCode)

给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按 任意顺序 返回。

给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。

img

示例 1:

输入:digits = "23"
输出:["ad","ae","af","bd","be","bf","cd","ce","cf"]

示例 2:

输入:digits = ""
输出:[]

示例 3:

输入:digits = "2"
输出:["a","b","c"]

提示:

  • 0 <= digits.length <= 4
  • digits[i] 是范围 ['2', '9'] 的一个数字。

注意点

  • 这我们遍历的 是 digist 种下标锁对应的字符串的 每一个字符。所以在遍历的时间,开始的节点都是0
  • 胡哪有需要注意的一点是,现在path 种存储的是 digist 的长度,我们在获取a 的时间,index=0的,然后再遍历 abc 的时间,进入到了index=1,也就是再遍历def ,这个时间,直接替换 path 种path【index】 就可以
class Solution {
    List<String> ans=new ArrayList<>();
    char [] path;
    String digits;
    int n;
    int k;
    String [] chars=new String[]{"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    public List<String> letterCombinations(String digits) {
        this.digits=digits;
        this.k=digits.length();
        path=new char[k];
        if(k==0){
            return ans;
        }
        dfs(0);
        return ans;
    }
    public void dfs(int index){
        if(index==k){
            ans.add(new String (path));
            return ;
        }
        String str=chars[digits.charAt(index)-'0'];
        for(int i=0;i< str.length() ;i++){
            path[index]=str.charAt(i);
            dfs(index+1);            
        }
    }
}
class Solution {
    String[] map={"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    List<String> ans=new ArrayList<>();
    StringBuilder path=new StringBuilder();
    int k;
    String digits;
    public List<String> letterCombinations(String digits) {
        this.digits=digits;
        this.k=digits.length();
        if(k==0){
            return ans;
        }
        dfs(0);
        return ans;
    }
    public void dfs(int startIndex){
        if(path.length()==k){
            ans.add(new String(path));
            return ;
        }
        for(char ch: map[digits.charAt(startIndex)-'0'].toCharArray()){
            path.append(ch);
            dfs(startIndex+1);
            path.delete(path.length()-1,path.length());
        }
    }
}

39. 组合总和 - 力扣(LeetCode)

给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有 不同组合 ,并以列表形式返回。你可以按 任意顺序 返回这些组合。

candidates 中的 同一个 数字可以 无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。

对于给定的输入,保证和为 target 的不同组合数少于 150 个。

示例 1:

输入:candidates = [2,3,6,7], target = 7
输出:[[2,2,3],[7]]
解释:
2 和 3 可以形成一组候选,2 + 2 + 3 = 7 。注意 2 可以使用多次。
7 也是一个候选, 7 = 7 。
仅有这两种组合。

示例 2:

输入: candidates = [2,3,5], target = 8
输出: [[2,2,2,2],[2,3,3],[3,5]]

示例 3:

输入: candidates = [2], target = 1
输出: []

注意点:

  • 第一点: 首先我们需要注意的是递归的参数,不仅仅需要的是 index 来判断是否达到了结束的标志,而且我们还需要一个和,来判断是否满足答案的要求。
  • 在剪枝的时候,我们直接判断当前的取值是否是大与target ,如果大于target 那么直接可以剪枝掉一部分。
  • 注意回溯 就可以了
class Solution {
    List<List<Integer>> ans=new ArrayList<>();
    ArrayDeque<Integer> path=new ArrayDeque<>();
    int [] candidates;
    int n;
    int target;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        this.candidates=candidates;
        this.target=target;
        this.n=candidates.length;
        dfs(0,0);
        return ans;
    }
    public void dfs(int index,int sum ){
        if(sum==target){//取到了数值 剪枝
            ans.add(new ArrayList<>(path));
            return ;
        }
        if(index ==n){//越界剪枝
            return ;
        }
        for(int i=index;i<n;i++){
            sum+=candidates[i];
            if(sum>target){
                sum-=candidates[i];
                continue;//剪枝
            }
            path.add(candidates[i]);
            dfs(i,sum);//因为当前数字是可以重复选择的,所以我们可以继续遍历当前数字
            path.pollLast();
            sum-=candidates[i];
        }
    }
}

================================================================================================================
  class Solution {
    ArrayDeque<Integer> path=new ArrayDeque<>();
    List<List<Integer>> ans=new ArrayList<>();
    int []candidates;
    int target;
    int n;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        this.candidates=candidates;
        this.n=candidates.length;
        this.target=target;
        dfs(0);
        return ans;
    }
    public void dfs(int startIndex){
        int sum=0;
        for(int num:path){
            sum+=num;
            if(sum>target){
                return ;
            }
        }
        if(sum==target){
            ans.add(new ArrayList(path));
            return ;
        }
        for(int i=startIndex;i<n;i++){
            path.offerLast(candidates[i]);
            dfs(i);
            path.pollLast();
        }
    }
}

40. 组合总和 II - 力扣(LeetCode)

给定一个候选人编号的集合 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用 一次

**注意:**解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

注意点

  • 最重要的是去重的思想:排序之后去重,指定一个 used 数组,表示当前元素已经被使用了,
class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    ArrayDeque<Integer> path = new ArrayDeque<>();
    int[] candidates;
    boolean used[];
    int target;
    int n;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        Arrays.sort(candidates);

        this.candidates = candidates;
        this.target = target;
        this.n = candidates.length;
        used = new boolean[n];
        dfs(0, 0);
        return ans;
    }

    public void dfs(int index, int sum) {
        if (sum == target) {
            ans.add(new ArrayList<>(path));
            return;
        }
        if(sum>target){
            return;
        }
        
        for (int i = index; i < n; i++) {
            if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
                continue;// 表示当前元素,已经被前面一个树枝使用过了,就是一个组合种已经使用过了,所以我们直接跳过
                //在这里裁剪的是 这一层的当前树枝
            }
            if (sum +candidates[i] > target) {
                break;// 这里的剪枝 优化,因为当前元素是,从小到大的一个顺序,所以前面的已经超过了,target 的时候,那么我们就因该直接将者一层的所有遍历全部裁剪掉
            }
            if (!used[i]) {
                used[i] = true;
                sum += candidates[i];
              
                path.offer(candidates[i]);
                dfs(i + 1, sum);
                sum -= candidates[i];
                used[i] = false;
                path.pollLast();

            }
        }
    }

}

131.分割回文串

给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是

回文串返回 s 所有可能的分割方案。

示例 1:

输入:s = "aab"
输出:[["a","a","b"],["aa","b"]]

示例 2:

输入:s = "a"
输出:[["a"]]

注意点:就是利用dfs 来分割,我们可以想象出遍历每个中间的逗号,比如a,a,b,

class Solution {
    List<List<String>> ans=new ArrayList<>();
    ArrayDeque<String> path=new ArrayDeque<>();
    String s;
    int n;
    public List<List<String>> partition(String s) {
        this.s=s;
        this.n=s.length();
        dfs(0);
        return ans;
    }
    public void dfs(int index){
        if(index==n){
            ans.add(new ArrayList<>(path));
            return ;
        }
        for(int i=index;i<n;i++){
            if(ishuiwen(index,i)){
                path.offer(s.substring(index,i+1));
                dfs(i+1);
                path.pollLast();
            }
        }
    }
    public boolean ishuiwen(int start ,int end){
        while(start<end){
            if(s.charAt(start++)!=s.charAt(end--)){
                return false;
            }
        }
        return true;
    }
}

131.分割回文串

93. 复原IP地址

有效 IP 地址 正好由四个整数(每个整数位于 0255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。

  • 例如:"0.1.2.201" "192.168.1.1"有效 IP 地址,但是 "0.011.255.245""192.168.1.312""192.168@1.1"无效 IP 地址。

给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。

示例 1:

输入:s = "25525511135"
输出:["255.255.11.135","255.255.111.35"]

示例 2:

输入:s = "0000"
输出:["0.0.0.0"]

示例 3:

输入:s = "101023"
输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]

注意点:

  • 首先需要注意的就是我们path 路径直接是对 s 字符串来进行操作的,分割之后添加 point 符号,并且在回溯的时候需要注意跳过 point 符号。

  • 第二点注意的是,在遍历字符串的时间,长度是变更的,因为我们直接操作的是字符串S。

  • 注意边界条件,大于12 的时间直接剪枝。

// 字符串转换为数字的时间
String s="123";
int num=0;
for(int i=0;i<s.length();i++){
	if(s.charAt(i)>'9'||s.charAt(i)<'0'){
		return false;
	}
	num=num*10+s.charAt(i)-'0';
	if(num>255){
		return false;
	}
}
return true;
class Solution {
    String s;
    List<String> ans=new ArrayList<>();
    int n;
    public List<String> restoreIpAddresses(String s) {
        this.s=s;
        n=s.length();
        if(n>12)
            return ans;
        dfs(0,0);
        return ans;
    }
    public void dfs(int index,int poinCount){
        //边界 条件,是切割为三个之后就可以结束
        if(count==3){
            if(isValid(index,s.length()-1)){//判断第四段是否是合法的
                ans.add(s);
            }
            return ;
        }
        for(int i=index;i<s.length();i++){
            if(isValid(index,i)){
                s=s.substring(0,i+1)+"."+s.substring(i+1);
                poinCount++;
                dfs(i+2,poinCount);
                poinCount--;
                s=s.substring(0,i+1)+s.substring(i+2);
            }else{
                break;// 跳出去剪枝
            }
        }
    }
    public boolean isValid(int start,int end){
        if(start>end)
            return false;
        if(s.charAt(start)=='0'&&start!=end){//判断是否拥有前导0 ,start==0 并且 start!=end
            return false;
        }
          int num = 0;
        for (int i = start; i <= end; i++) {
            if (s.charAt(i) > '9' || s.charAt(i) < '0') { // 遇到⾮数字字符不合法
                return false;
            }
            num = num * 10 + (s.charAt(i) - '0');
            if (num > 255) { // 如果⼤于255了不合法
                return false;
            }
        }
        return true;
    }
}

78. 子集 - 力扣(LeetCode)

给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。

解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。

示例 1:

输入:nums = [1,2,3]
输出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

注意点

  • 区分与组合问题和分割问题,子集问题在于枚举的所有结果,所以他的路径中的答案是所有的树结点
class Solution {
    List<List<Integer>> ans=new ArrayList<>();
    ArrayDeque<Integer> path=new ArrayDeque<>();
    int[] nums;
    int n;
    public List<List<Integer>> subsets(int[] nums) {
        this.nums=nums;
        this.n=nums.length;
        dfs(0);
        return ans;
    }
    public void dfs(int index){
        //首先,子集形问题,和组合型,分割形问题不同的是,子集收集的结果是树的所有节点
        ans.add(new ArrayList<>(path));
        if(index>=n){
            return ;
        }
        for(int i=index;i<n;i++){
            path.offer(nums[i]);
            dfs(i+1);
            path.pollLast();
        }
    }
}

90. 子集 II - 力扣(LeetCode)

给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的

子集(幂集)。

解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。

示例 1:

输入:nums = [1,2,2]
输出:[[],[1],[1,2],[1,2,2],[2],[2,2]]

示例 2:

输入:nums = [0]
输出:[[],[0]]

注意点

  • 包含重复元素,并且不能够有相同的子集,这个时间就需要进行树层的 去重,
class Solution {
    //首先不能包含重复的子集,在去重的时间几需要来排序,借助used 数组
    List<List<Integer>> ans=new ArrayList<>();
    ArrayDeque<Integer> path=new ArrayDeque<>();
    int [] nums;
    boolean [] used;
    int n;
    public List<List<Integer>> subsetsWithDup(int[] nums) {
        Arrays.sort(nums);
        this.nums=nums;
        this.n=nums.length;
        used=new boolean[n];
        dfs(0);
        return ans;
    }
    public void dfs(int index){
        ans.add(new ArrayList<>(path));
        if(index>=n){
            return ;
        }
        for(int i=index;i<n;i++){
            if(i>0&&nums[i]==nums[i-1]&&!used[i-1]){//jianzhi 
                continue;
            }
            path.offer(nums[i]);
            used[i]=true;
            dfs(i+1);
            path.pollLast();
            used[i]=false;
        }
    }
}

491. 非递减子序列 - 力扣(LeetCode)

给你一个整数数组 nums ,找出并返回所有该数组中不同的递增子序列,递增子序列中 至少有两个元素 。你可以按 任意顺序 返回答案。

数组中可能含有重复元素,如出现两个整数相等,也可以视作递增序列的一种特殊情况。

示例 1:

输入:nums = [4,6,7,7]
输出:[[4,6],[4,6,7],[4,6,7,7],[4,7],[4,7,7],[6,7],[6,7,7],[7,7]]

示例 2:

输入:nums = [4,4,3,2,1]
输出:[[4,4]]

注意点

  • 首先在处理节点的时间,我们需要将当前节点和path 路径中的最后一个节点对比,看是否符合条件,即当前元素是否大于path中的最后一个元素。

  • 其次需要判断当前元素的是否在当前层被使用过,

    上面两个条件满足的话,就代表不是递增,或者已经使用过了,那我我们直接跳过当前层中的当前元素的遍历。

class Solution {
    List<List<Integer>> ans=new ArrayList<>();
    ArrayDeque<Integer> path=new ArrayDeque<>();
    int [] nums;
    int n;
    public List<List<Integer>> findSubsequences(int[] nums) {
        this.nums=nums;
        this.n=nums.length;
        dfs(0);
        return ans;
    }
    public void dfs(int index){
        if(path.size()>=2){
            ans.add(new ArrayList<>(path));
        }
        HashSet<Integer> hs = new HashSet<>();//这里需要注意的是,每一层中都是开辟了一个去重的Set,所以在使用过之后,不必对Set取消。
        
        for(int i=index;i<n;i++){
            if(!path.isEmpty()&&nums[i]<path.peekLast()||hs.contains(nums[i])){
                continue;//取消一条树枝的搜索
            }
            hs.add(nums[i]);
            path.add(nums[i]);
            dfs(i+1);
            path.pollLast();
            
        }
    }
}

46. 全排列 - 力扣(LeetCode)

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

注意点

  • 全排列和组合类问题,分割子集问题不同的地方在于,他每一次遍历都会使用到所有的数值,1 2 和 2 1是两个不同的数值,所以在遍历的时间下标的开始不能是index ,而是0.

  • 注意如果当前的遍历已经重复了,就应该跳过

  • class Solution {
        List<List<Integer>> ans = new ArrayList<>();
        ArrayDeque<Integer> path = new ArrayDeque<>();
        int[] nums;
        int n;
        boolean[] used;
    
        public List<List<Integer>> permute(int[] nums) {
            this.nums = nums;
            this.n = nums.length;
            used = new boolean[n];
            dfs(0);
            return ans;
        }
    
        public void dfs(int index) {
            if (path.size() == n) {
                ans.add(new ArrayList<>(path));
                return;
            }
            for (int i = 0; i < n; i++) {
                if (used[i]) {
                    continue;
                }
                //全排列在去重的时间,需要添加
    
                    used[i] = true;
                    path.offer(nums[i]);
                    dfs(i + 1);
                    path.pollLast();
                    used[i] = false;
                
            }
        }
    }
    

47. 全排列 II - 力扣(LeetCode)

给定一个不含重复数字的数组 nums ,返回其 所有可能的全排列 。你可以 按任意顺序 返回答案。

示例 1:

输入:nums = [1,2,3]
输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]

示例 2:

输入:nums = [0,1]
输出:[[0,1],[1,0]]

示例 3:

输入:nums = [1]
输出:[[1]]

注意点

  • 注意数字是重复的,所以需要排序之后去重
  • 去重之后,耶需要注意当前元素是否已经被选择过。
class Solution {
    List<List<Integer>> ans = new ArrayList<>();
    ArrayDeque<Integer> path = new ArrayDeque<>();
    int[] nums;
    int n;
    boolean[] used;

    public List<List<Integer>> permuteUnique(int[] nums) {
        Arrays.sort(nums);
        this.nums = nums;
        this.n = nums.length;
        used = new boolean[n];
        dfs(0);
        return ans;
    }

    public void dfs(int index) {
        if (path.size()==n) {
            ans.add(new ArrayList<>(path));
            return;
        }
        for (int i = 0; i < n; i++) {
            if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]) {
                continue;
            }
            if (!used[i]) {//是需要判断当前元素是否已经被使用过的

                used[i] = true;
                path.offer(nums[i]);
                dfs(i + 1);
                used[i] = false;
                path.pollLast();
            }
        }
    }
}

DP

预备知识

首先第一步:

  1. 明确dp[i] i 下标的具体含义;
  2. 确定dp 动态方程
  3. 根据动态方程来确定初始化
  4. 确定遍历的顺序
  5. 举例子推导dp 数组

Debug

  • 首先根据自己写出的动态方程进行模拟出dp 数组
  • 随后打印出数组方程
  • 对比
    • 对比出来的结果没有问题,那就证明是 自己得递推公式,初始化,遍历方式出现了问题
    • 有问题,那就是代码实现的细节有问题

如果求组合数就是外层for循环遍历物品,内层for遍历背包

如果求排列数就是外层for遍历背包,内层for循环遍历物品

509. 斐波那契数 - 力扣(LeetCode)

注意点

  • 首先:我们明确了 i 的含义是项数为 i 的时间,斐波那契数的值
  • 其次:确认了递推公式, f ( n ) = f ( n − 1 ) + f ( n − 2 ) f(n)=f(n-1)+f(n-2) f(n)=f(n1)+f(n2), 即 n 号斐波那契数,是由n-1 和n-2 号求和出来的。
  • 初始化: 0号为 0,1 号为 1
  • 遍历方向:n 只能由 n -1 和 n - 2 推导出来,所以是直接顺讯遍历。
  • 自己举例写出dp 数组。
class Solution {
    public int fib(int n) {
        int d0=0;
        int d1=1;
        int ans=0;
        for(int i=2;i<=n;i++){
            ans=d1+d0;
            d0=d1;
            d1=ans;
        }
        return n==1?d1:ans;
    }
}

70. 爬楼梯 - 力扣(LeetCode)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬 12 个台阶。你有多少种不同的方法可以爬到楼顶呢?

示例 1:

输入:n = 2
输出:2
解释:有两种方法可以爬到楼顶。
1. 1 阶 + 1 阶
2. 2 阶

示例 2:

输入:n = 3
输出:3
解释:有三种方法可以爬到楼顶。
1. 1 阶 + 1 阶 + 1 阶
2. 1 阶 + 2 阶
3. 2 阶 + 1 阶

注意点

  • 楼梯的阶数总是大于等于1 的
  • 到第 i 层楼梯 的方式,有从i-1 层楼梯,一步走上来的;从i- 2层楼梯一步走上来的。
  • 如果当前楼梯的层数小于等于2 的时间,直接返回当前楼梯的层数;
  • 并且我们是从 第三层楼梯开始遍历的。
class Solution {
    public int climbStairs(int n) {
        //首先,爬楼梯的时间一次可以爬1个或者两个,那么如果我们站在 第 n 层楼梯的时间,从第n-1 层楼梯向上爬,有一种方式;从第 n -2 层楼向上爬有一种方式,那么爬到第 n 层楼的方式就是 这两种的和
        
        int d0=0;//
        int d1=1;
        int d2=2;
        int ans=0;
        for(int i=3;i<=n;i++){
            ans=d1+d2;
            d1=d2;
            d2=ans;
        }
        return n<3?n:ans;
        
    }
}

746. 使用最小花费爬楼梯 - 力扣(LeetCode)

给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。

你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。

请你计算并返回达到楼梯顶部的最低花费。

示例 1:

输入:cost = [10,15,20]
输出:15
解释:你将从下标为 1 的台阶开始。
- 支付 15 ,向上爬两个台阶,到达楼梯顶部。
总花费为 15 。

示例 2:

输入:cost = [1,100,1,1,1,100,1,1,100,1]
输出:6
解释:你将从下标为 0 的台阶开始。
- 支付 1 ,向上爬两个台阶,到达下标为 2 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 4 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 6 的台阶。
- 支付 1 ,向上爬一个台阶,到达下标为 7 的台阶。
- 支付 1 ,向上爬两个台阶,到达下标为 9 的台阶。
- 支付 1 ,向上爬一个台阶,到达楼梯顶部。
总花费为 6 

注意点

  1. 首先我们需要明确的是,dp【i】的i 的下标的含义是什么, 在这个题目中,i 表示的是到爬到第i 个台阶所消耗的费用,并且在第 0 个 和 第 1 个台阶式不需要消耗费用的,因为我们直接可以从 0 和 1 开始。

  2. 第二步,我们需要做的就是计算出递推公式,每次可以走一步或者两步,那么dp【i】支付的费用就是,dp【i-1】层的已经消耗的费用 加上 从 i-1 层 走一步到 i 层 所消耗的 费用 就是:cost【i-1】;还有的就是可以一次走两步:那么就是dp【i-2】层已经消耗的费用 加上从 i - 2 层一次走两步 所消耗的费用 就是 cost【i-2】之后就在里面取最小值就可以了

  3. 确定初始值: 首先根据题意我门可以知道,直接可以从第一次层或者是第二层开始爬楼梯,那么就证明了dp【0】 和dp【1】的消费是为0 的

  4. 确定遍历的方向,顺序遍历

class Solution {
    public int minCostClimbingStairs(int[] cost) {
        int dp[]=new int[cost.length+1];
        int n=cost.length;
        dp[0]=0;
        dp[1]=0;
        for(int i=2;i<=cost.length;i++){
            dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        }
        return dp[n];
    }
}
======================================================================================
  class Solution {
    public int minCostClimbingStairs(int[] cost) {
        // 首先明确 i 的含义:dp【i】是爬到第 i 个楼梯需要支付的费用
        // 确定递推公式:dp[i]=Math.min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]);
        // 初始化 dp【0】和dp【1】都是等于0 的,因为我们直接可以从 0 和 1 的楼梯开始
        // 遍历的方向
        int ans=0;
        int d0=0;
        int d1=0;
        int n=cost.length;
        for(int i=2;i<=n;i++){
            ans=Math.min(d0+cost[i-2],d1+cost[i-1]);
            d0=d1;
            d1=ans;
        }
        return ans;
    }
}

2024/4/22 377. 组合总和 Ⅳ - 力扣(LeetCode)

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

思考

  • 首先我们在看到这个题目的第一眼,就下意识的以湿到这个题目是一个回溯类型的问题,回溯法,旨在枚举出所有符合规范的结果,并且存储在 Path 中,符合规范之后添加到 ans 中。但是我们看这个题目,每一个数字是可以重复的,并且在使用过之后前面的数字都是可以再重复使用的,所以再枚举的时间,可能会有超时。
  • 在此 我们思考回溯法和DP 法的一个区别,DP 一般是求解出有多少种可能,求娥姐的答案是一个数字,而回溯法是求解出来的是一个具体的路径。
  • 具体为什么将 背包的体积放在外面会使得重复的数字,可以组合成一个排列,这是因为在遍历背包容量的时间,确定了一个背包的容量,每一个物品都是全部可以再次被选择的,这样就确保了排列。
class Solution {
    int ans;
    ArrayDeque<Integer> path=new ArrayDeque<>();
    int [] nums;
    int target;
    int n;
    public int combinationSum4(int[] nums, int target) {
        this.nums=nums;
        this.target=target;
        this.n=nums.length;
        
        dfs(0,0);
        return ans;
    }
    public void dfs (int index,int sum ){
        if(sum==target){
            ans++;
            System.out.println(path);
            return ;
        }
        for(int i=0;i<n;i++){
            if(sum+nums[i]>target){
                break;
            }
            path.offer(nums[i]);
            sum+=nums[i];
            dfs(i,sum);
            sum-=nums[i];
            path.pollLast();
        }
    }
}

改进

  • 利用DP 来求解ans 的数量,是不记录答案的路径的;
class Solution {
    public int combinationSum4(int[] nums, int target) {
        //首先第一步,明确dp【j】下标的含义,dp【j】是目标为 j 的时间,从nums中寻找出的 综合为 j 的元素组合的个数
        //dp【j】=dp【j-1】+dp【j-2】+。。。+dp【0】,并且,在这里我们,是不具备所有的 j-1,j-2.j-3 的。只具备所包含的nums,所以这里
        // 初始化,由题意可以知道的是,只有在不取任何元素的时间 target =0 ,只有一种方案
        // 遍历方向,首先应该确定target 等于 1的情况,然后逐步确认target + 1 的情况,直至到达 target,并且每一个 i - 的都是 nums【 j 】 
        int [] dp=new int [target+1];
        dp[0]=1;

        for(int i=1;i<=target;i++){
            for(int j=0;j<nums.length;j++){
                if(nums[j]<=i){
                    dp[i]+=dp[i-nums[j]];
                }
            }
        }
        return dp[target];
    }
}

62. 不同路径 - 力扣(LeetCode)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

问总共有多少条不同的路径?

示例 1:

img

输入:m = 3, n = 7
输出:28

示例 2:

输入:m = 3, n = 2
输出:3
解释:
从左上角开始,总共有 3 条路径可以到达右下角。
1. 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右
3. 向下 -> 向右 -> 向下

示例 3:

输入:m = 7, n = 3
输出:28

示例 4:

输入:m = 3, n = 3
输出:6

提示:

  • 1 <= m, n <= 100

  • 题目数据保证答案小于等于 2 * 109

class Solution {
    public int uniquePaths(int m, int n) {
        //首先明确一下 dp【i【j】的含义:因为我们的操作只能向右 ,或者是向下走,那么就是dp【i】【j-1】到dp【i】【j】(向右走)、dp【i-1】【j】(向下走)这两种方式,从中我们可以看出来,走到dp【i】【j】的时候所拥有的方式就是从这两个位置风别向右走一步和向下走一步两种方式走来的。那么我们就可以知道,dp【i】【j】=dp【i】【j-1】+dp【i-1】【j】;
        //初始化:我们知道如果在dp【i】【0】的方向上,只有一种方式到达,那就是一直向下走,所以初始化全部为1;dp【0】【j】的方向上,只有一种方式到达,那就是一直向右走
        int dp[][]=new int[m][n];
        Arrays.fill(dp[0],1);//这里填充的是所有向右走的,只有一种方式
        IntStream.range(0,m).forEach(i->{
            dp[i][0]=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];
    }
}
=====================================================================
  class Solution {
    public int uniquePaths(int m, int n) {
        //首先明确一下 dp【i【j】的含义:因为我们的操作只能想右 ,或者是向下走,那么就是dp【i】【j-1】到dp【i】【j】(向右走)、dp【i-1】【j】(向下走)这两种方式,从中我们可以看出来,走到dp【i】【j】的时候所拥有的方式就是从这两个位置风别向右走一步和向下走一步两种方式走来的。那么我们就可以知道,dp【i】【j】=dp【i】【j-1】+dp【i-1】【j】;
        //初始化:我们知道如果在dp【i】【0】的方向上,只有一种方式到达,那就是一直向下走,所以初始化全部为1;dp【0】【j】的方向上,只有一种方式到达,那就是一直向右走
        int dp[][]=new int[m][n];
        Arrays.fill(dp[0],1);
        // IntStream.range(0,m).forEach(i->{
        //     dp[i][0]=1;
        // });
        for (int i = 0; i < m; i++) {
            dp[i][0] = 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 - 力扣(LeetCode)

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。

机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish”)。

现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径?

网格中的障碍物和空位置分别用 10 来表示。

示例 1:

img

输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]]
输出:2
解释:3x3 网格的正中间有一个障碍物。
从左上角到右下角一共有 2 条不同的路径:
1. 向右 -> 向右 -> 向下 -> 向下
2. 向下 -> 向下 -> 向右 -> 向右

示例 2:

img

输入:obstacleGrid = [[0,1],[0,0]]
输出:1

提示:

  • m == obstacleGrid.length
  • n == obstacleGrid[i].length
  • 1 <= m, n <= 100
  • obstacleGrid[i][j]01

注意点

  1. 首先我们来明确一下,dp【i】【j】的含义是,走到i 行,j 列的时候拥有多少种方法。
  2. 确定dp 方程,只有在obstacleGrid【i】【j】没有障碍物的时间,才需要去计算到达他的方法数量,因为我们不可能去到达障碍物上去,所以障碍物所在的地方的到达数量是 0;
class Solution {
    public int uniquePathsWithObstacles(int[][] obstacleGrid) {
        // 首先我们来明确一下,dp【i】【j】的含义是,走到i 行,j 列的时候拥有多少种方法,
        // 确定dp 方程,只有在obstacleGrid【i】【j】没有障碍物的时间,才需要去计算到达他的方法数量,因为我们不可能去到达障碍物上去,所以障碍物所在的地方的到达数量是 0;
        // 其次就是初始化,首先我们明白的是,当前机器人只能够向右走或者是向下走,这就表明了,在第一行的时间,如果dp【0】【j】拥有一个障碍物,那么dp【0】【j+1】(包含)及其后面的都是不可以到达的,因为我们不能够向上走;同理在第一列的时间,如果dp【i】【0】拥有一个障碍物,那么dp【i+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 j=0;j<n&&obstacleGrid[0][j]==0;j++){
            dp[0][j]=1;//初始化向右走的方式种类,直到遇见了障碍物
        }
        for(int i=1;i<m;i++){
            for(int j=1;j<n;j++){
                if(obstacleGrid[i][j]==1){//如果当前节点是障碍物的时间,dp【i】【j】还是他的初始值 0,表示走到这里的方式有0 种
                    continue;
                }
                dp[i][j]=dp[i][j-1]+dp[i-1][j];
            }
        }
        return dp[m-1][n-1];

    }
}

aaaa343. 整数拆分 - 力扣(LeetCode)

给定一个正整数 n ,将其拆分为 k正整数 的和( k >= 2 ),并使这些整数的乘积最大化。

返回 你可以获得的最大乘积

示例 1:

输入: n = 2
输出: 1
解释: 2 = 1 + 1, 1 × 1 = 1。

示例 2:

输入: n = 10
输出: 36
解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。

提示:

  • 2 <= n <= 58
🎲数学理论

[!IMPORTANT]

我们需要明确一点,在拆分一个数字的时间使之乘积和是最大的,那么他一定是m个相近的数字。

例如 6 拆成 3 * 3, 10 拆成 3 * 3 * 4。 100的话 也是拆成m个近似数组的子数 相乘才是最大的。

只不过我们不知道m究竟是多少而已,但可以明确的是m一定大于等于2,既然m大于等于2,也就是 最差也应该是拆成两个相同的 可能是最大值。

那么 j 遍历,只需要遍历到 n/2 就可以,后面就没有必要遍历了,一定不是最大值。

注意点

  • 首先我们需要明确的是,dp【i】的含义是什么,dp【i】表示的是,数字 i 被拆分所得到的最大数字乘积。

  • 确定递推公式,dp【i】可以是 d p [ i ] = m a x ( d p [ i − j ] ∗ j , ( i − j ) ∗ j dp[i]=max(dp[i-j]*j,(i-j)*j dp[i]=max(dp[ij]j,(ij)j, 并且在我们计算的过程中,他首先被拆分为 ( i − j ) ∗ j (i-j)*j (ij)j d p [ i − j ] ∗ j dp[i-j]*j dp[ij]j ,其中 d p [ i − j ] dp[i-j] dp[ij]是可以继续被拆分的,随后因为我们每一次都是在遍历 j ,dp【i】计算出来的数值是随着 j 来改变的,所以我们也需要将 dp【i】添加进去。

  • 初始化:首先我们明确的是数字 1 和 数字 0 是没有任何拆分的意义的,其次数字 2 可以分为 1 和 1,所以dp【2】=1;

    class Solution {
        public int integerBreak(int n) {
            // 首先我们来明确一下dp【i】的含义:拆分数字 i 的时间,可以获得的最大乘积。
            // 计算递推公式:我们需要一次去计算 i-j 和 j 的乘积,j= 1 2 3... 并且 j < i ,再将 i- j 拆分掉 dp【i-j】*j;随后因为我们每一次都是在遍历 j ,dp【i】计算出来的数值是随着 j 来改变的,所以我们也需要将 dp【i】添加进去。
            // 初始化:首先需要明确的是 n 是大于 2 的,2 可以被拆分为 1 和 1 ,那么dp【2】=1,是毋庸置疑的,至于dp【0】和dp【1】,没有任何意义
            int dp[] =new int[n+1];
            dp[2]=1;
            for(int i=3;i<=n;i++){
                for(int j=1;j<i;j++){//在这里 我们可以做一个简单的优化,就是乘积的最大值都是小于等于 n/2 的
                    dp[i]=Math.max(dp[i],Math.max((i-j)*j,dp[i-j]*j));
                }
            }
            return dp[n];
    
        }
    }
    

aaaaa96. 不同的二叉搜索树 - 力扣(LeetCode)

🎲数学理论

[!IMPORTANT]

卡特兰数: d p ( i ) = d p [ 0 ] ∗ d p [ i − 1 ] + d p [ 1 ] ∗ d p [ i − 2 ] + . . . + d p [ i − 1 ] ∗ d p [ 0 ] dp(i)=dp[0]*dp[i-1]+dp[1]*dp[i-2]+...+dp[i-1]*dp[0] dp(i)=dp[0]dp[i1]+dp[1]dp[i2]+...+dp[i1]dp[0]

class Solution {
    public int numTrees(int n) {
        // 首先我们应该明确的是,dp【i】 i 的含义是,由节点1到n 所组成的 二叉搜索树的个数,就是由n 个 节点所组成的二叉搜索树的个数。

        // 确定递推公式,由图我们可以知道,当根节点是 1 的时间,所组成的二叉搜索树是,左子树的节点个数是0个, 右子树的节点个数是2个;根节点是2 的时间,左子树的节点个数是一个,右子树的节点个数是1 个;根节点是3的时间,左子树的节点个数是2个,右子树的节点个数是0个
        int dp[]=new int[n+1];
        dp[0]=1;
        //卡特蓝数字
        for(int i=1;i<=n;i++){
            for(int j=1;j<=i;j++){
                dp[i]+=dp[j-1]*dp[i-j];
            }
        }
        return dp[n];
    }
}

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

0 1 背包问题

背包问题基础理论

重量价值
物品0115
物品1320
物品2430
  1. 首先我们来明确一下 d p [ i ] [ j ] dp[i][j] dp[i][j]的含义:表示的是在背包容量为 j j j 的情况下,前 i i i 个物品价值的最大值。
  2. 确定递推公式: d p [ i ] [ j ] = M a t h . m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]) dp[i][j]=Math.max(dp[i1][j],dp[i1][jweight[i]]+value[i]):我们来解释一下:首先,如果第i 个物品的体积是大于 j 的容量的时间,那么就不能选择,如果是小于等于的,那么就可以选择,此时的dp 数组,就是在 j- weigth[i] 的背包容量中,选出 i - 1 个物品所能表示的最大的价值。因为此时已经将 i 个物品选择了,我们就需要加上value【i】
  3. 确定初始化:首先我们可以明确的是在背包容量为 0 的时间,所有的dp【i】【0】 都是0,其次我们需要明白的是,对于第一个物品,只有当背包的容量大于weight【0】的时间,dp【0】【j】,才是等于value【0】的,所以我们在初始化的时间,就可以只初始化 j 》weight【0】 的dp【0】 【j】 =value【0】
  4. **遍历的顺序:**首先我们可以明确的是:我们可以遍历物品,让物品根据背包的容量来计算 最大价值。
public back(int [] weigth,int []values,int bagSize ){
  // 这里表示的容量是0 到 bagsize 所以我们在初始化的时候 需要将 bagsize + 1
  int dp[][]=new int[weight.length][bagSize+1];
  for(int j=weight[0];j<=bagSize;j++){
    dp[0][j]=value[0];
  }//初始化第 0 个物品,在背包容量大于 0 号物品的体积的时候,最大价值和是 0 号物品 的价值。
 	
  // 开始遍历物品
  for(int i=0;i<weight.length;i++){
    for(int j=0;j<bagSize;j++){
      	if(j<weight[i]){//表面当前容量是放不下,第 i 号 物品的,所以容量为 j 前 i 个物品的最大价值和,是等于 前 i - 1 个物品 容量为 j 的最大价值和
          dp[i][j]=dp[i-1][j];
          continue;
        }
      dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
    }
  }
	// 打印出来dp 数组 
  System.out.print(Arrays.deepToString(dp));
  
}
	
滚动数组

首先我们需要明白的是,在之前的遍历中 d p [ i ] [ j ] dp[i][j] dp[i][j] j j j 的含义 j j j 表示的是 容量为 j j j 的时间, 在前 i i i 个物品中可以获得的最大价值和.

在这里, 我们需要明确的一点是 d p [ i ] [ j ] = M a t h . m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]) dp[i][j]=Math.max(dp[i1][j],dp[i1][jweight[i]]+value[i]) 其中 d p [ i ] [ j ] dp[i][j] dp[i][j] 是可以由 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i1][j] 推导而来的, 那么我们可以理解到 直接将第 前 i − 1 前i-1 i1 个物品的值拷贝到 $i 个物品中 , 得到 个物品中, 得到 个物品中,得到dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i])$​

如何初始化: 首先我们明白的是, 当背包容量为 0 的时间, D P [ j ] DP[j] DP[j] 的最大价值和是 0, 对于所有的容量 j j j 而言,我们需要做的是遍历每一个物品, 只有在物品的 w i g h t [ i ] wight[i] wight[i] 是小于 b a g S i z e bagSize bagSize 的时间, 我们才去添加当前的物品.

遍历方式: 这里需要注意的是, 我们在遍历物品的时间,仍然是顺序遍历的, 但是在遍历背包容量的时间, 我们需要从背包容量的最大体积开始遍历:

**如果是正序遍历,目前遍历的是(第一个物品,他的weight[0]等于 1, 并且他的价值valuue[0]=15)的话: **

d p [ 1 ] = M a t h . m a x ( d p [ 1 ] , d p [ 1 − w e i g h t [ 0 ] ] + v a l u e [ 0 ] ) = 15 dp[1]=Math.max(dp[1],dp[1-weight[0]]+value[0])=15 dp[1]=Math.max(dp[1],dp[1weight[0]]+value[0])=15

d p [ 2 ] = M a t h . m a x ( d p [ 2 ] , d p [ 2 − w e i g h t [ 0 ] ] + v a l u e [ 0 ] ) = 30 dp[2]=Math.max(dp[2],dp[2-weight[0]]+value[0])=30 dp[2]=Math.max(dp[2],dp[2weight[0]]+value[0])=30

这个时候,我们可以观察到的是,如果是正序遍历的话,那么会带来一个问题,就是在遍历的时间会重复计算,那为什么 d p [ i ] [ j ] dp[i][j] dp[i][j]不会重复计算呢? 因为我们每一次都是重新开了一行去遍历,结果是从上一行来获取到的。下面让我们继续看看倒序遍历的dp 结果

d p [ 2 ] = M a t h . m a x ( d p [ 2 ] , d p [ 2 − w e i g h r [ 0 ] ] + v a l u e [ 0 ] = 15 dp[2]=Math.max(dp[2],dp[2-weighr[0]]+value[0]=15 dp[2]=Math.max(dp[2],dp[2weighr[0]]+value[0]=15 注意到此时的 d p [ 1 ] dp[1] dp[1] 是0 ,所以就有了 [ d p [ 2 − w e i g h t [ 0 ] ] ] + v a l u e [ 0 ] = 15 [dp[2-weight[0]]]+value[0] =15 [dp[2weight[0]]]+value[0]=15

d p [ 1 ] = M a t h . m a x ( d p [ 1 ] , d p [ 1 − w e i g h t [ 0 ] ] + v a l u e [ 0 ] ) = 15 dp[1]=Math.max(dp[1],dp[1-weight[0]]+value[0])=15 dp[1]=Math.max(dp[1],dp[1weight[0]]+value[0])=15

for(int i=0;i<weight.length;i++){
	for(int j=bagSize;j>=weight[i];j--){
		dp[j]=Math.max(dp[j],dp[j-weight[i]]+value[i]);
    
	}
}

416. 分割等和子集 - 力扣(LeetCode)

注意点

  • 类似于组合总和 IV,都是在 nums 中去选择,那些数字可以组成 target

  • 首先我们拆分题意,分割出等和的子集,第一想到的方式就是回溯法:排序之后开始遍历,选择第一个物品。

  • 利用DP 来计算,我们思考一下,想要选出 和为 target 的子集,那么就是在背包容量为 target 的时间,去选择数字,每个数字的权重是 数字的数值,数字的价值是数字的值,最后判断的时候理解出是否拥有一个背包的容量等于这个容量所获得的价值。这个时间就已经表明了,再nums中选择出了,几个数字,这个个数字的和是 target ,并且dp【target】==target 的。如果dp【target】 是 不等于 target 的,那么就表明了 没有选择出 数字的和是 target 的。

  • 初始化:如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了

class Solution {
    ArrayDeque<Integer> path=new ArrayDeque<>();
    boolean exit=false;
    int [] nums;
    int n;
    int target;
    public boolean canPartition(int[] nums) {
        // 首先我们可以思考,分割为两个等和的子集,那么就是其中有有一个子集是全部元素和的一半,这个时候就需要全部和是一个偶数
         int sum=0;
       for(int i=0;i<nums.length;i++){
        sum+=nums[i];
       }
        if((sum&1)==1){
            return false;
        }{
            this.target=sum/2;
        }
    // 首先确定 dp【j】的含义,背包容量为 j 的时间,所能存储的最大价值和,也就是背包容量等于11 的时间是否能够存储到 11
        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]);
            }
             if(dp[target] == target)
                return true;
        }

        return dp[target]==target;
    }
    public void dfs(int index,int sum){
        if(index==n){
            return ;
        }
        if(sum>target){
            return ;
        }
        if(sum==target){
            exit=true;
            return ;
        }
        for(int i=index;i<n;i++){
            if(sum+nums[i]>target){
                break;
            }
            sum+=nums[i];
            dfs(i+1,sum);
            sum-=nums[i];
        }

    }
}
class Solution {
    public boolean canPartition(int[] nums) {
        int sum=0;
        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
        }
        int target=0;
        if((sum&1)==1){
            return false;
        }else{
            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 dp[target]==target;
    }
}

1049. 最后一块石头的重量 II - 力扣(LeetCode)

有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。

每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 xy,且 x <= y。那么粉碎的可能结果如下:

  • 如果 x == y,那么两块石头都会被完全粉碎;
  • 如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x

最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0

示例 1:

输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。

示例 2:

输入:stones = [31,26,33,21,40]
输出:5

题目拆分

  • 每一次都是选择出两个石头,然后进行消除,那我们直接一下,一次性在所有石头中选择出其中的一半 的石头,让这一半的石头和另一半的石头的差值是最小的。那么我们这个时间可以利用Dp 算法,背包问题,在容量为 sum/2 的时候选择出最大的价值和,戒指也是 stones【i】,石头的重量也是stones【i】
class Solution {
    public int lastStoneWeightII(int[] stones) {
        int sum=0;
        for(int i=0;i<stones.length;i++){
            sum+=stones[i];
        }
        int target;
        target=sum/2;
        int [] dp=new int [target+1];
        for(int i=0;i<stones.length;i++){
            for(int j=target;j>=stones[i];j--){
                dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
            }
        }
        return (sum-dp[target]-dp[target]);
    }
}

494. 目标和 - 力扣(LeetCode)

思考

  • 首先我们在遇到这个题目的时间,应该是怎样思考的呢?我们明白的是在nums 中选择出 加和的一组,选择出减法的一组,分别是left 和 right ,left + right =sum 的,那么left - right =target 。结合两个公式 我们可以得到 left=(sum+target)/2 的,这个时候就表明了,如果left 是偶数的话,那么 是有解的,如果是奇数,那么就是无解的。
  • 题目转换后的含义就是在 nums 中寻找能够组合出 target 的数字,就可以利用 回溯法和 DP 法中的背包问题来做,在背包容量为target 的时间,
  1. 首先明确 dp 【j】的含义: 表示的是填满 容量为 j 的背包,总共有 dp【j】 种方法

  2. 确定地推公式:首先我们明确的是,如果我们拥有数字 nums【i】 ,那么凑成dp【j】的方法就是dp【j-nums【i】】,因为我们拥有一个nums【i】了,凑成 dp【j】 的方法的个数就是,dp【j-nums【i】 】的个数。

    1. j = 5 , d p [ 5 ] = d p [ 5 − n u m s [ i = 1 ] ] + d p [ 5 − n u m s [ i = 2 ] . . . . ] + d p [ 5 − n u m s [ i − 5 ] ] j=5,dp[5]=dp[5-nums[i=1]]+dp[5-nums[i=2]....]+dp[5-nums[i-5]] j=5,dp[5]=dp[5nums[i=1]]+dp[5nums[i=2]....]+dp[5nums[i5]]
    2. 此时我们拥有数字, n u m s [ 1 ] , n u m s [ 2 ] . . . n u m s [ 5 ] nums[1],nums[2]...nums[5] nums[1],nums[2]...nums[5]
  3. 初始化:首先容量为0 的时间凑成 dp【0】的数字,都是由 dp【0】=1推到出来的,比如 nums={0,0,0,0,0},是由32 种的,但是他都是由 dp【0】=1 推导出来的。

注意点

  • $left+right=sum;left-right=target;left=(sum+target)/2 $left 表示的是加法的整列,righ 表示的是减法的和,在这里我们需要做的是计算出在容量为 target 的时间,在nums 中可以挑选出加和等于target 的组合的个数
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum=0;
        for(int i=0;i<nums.length;i++){
            sum+=nums[i];
        }
        if(Math.abs(target)>sum){
            return 0;
        }
        //left+right=sum;
        //left-right=target;
        //left=(sum+target)/2
        target=(target+sum);
        if((target&1)==1){
            return 0;
        }
        target/=2;
        int dp[]=new int[target+1];
        dp[0]=1;
        for(int i=0;i<nums.length;i++){
            for(int j=target;j>=nums[i];j--){
                dp[j]+=dp[j-nums[i]];
            }
        }
        return dp[target];
    }
}

474. 一和零 - 力扣(LeetCode)

注意点

  • 首先明确一下,dp【i】【j】 的含义,表示的的由 i 个 0 和 j 个 1 的最大子集的的大小为 dp【i】【j】
  • 确定递推公式,首先明白的是,在 str 中拥有seroNum 个 0,拥有 oneNum 个 1, dp【i】【j】就可以由 dp【i-zeroNum】【j-oneNum】+1得到,表示的就是前一个字符串的0 和 1 的个数的子集的个数,再加上当前字符串的长度。
class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        // 首先来明确dp【i】【j】 的含义,表示的是,背包中0 的个数是 i ,1 的个数是 j 的时间,dp【i】【j】 最大子集的长度
        // 确定地推公式 dp【i】【j】=Math。max(dp[i][j],dp[i-zero][j-one]);

        int[][] dp = new int[m + 1][n + 1];
        for (String str : strs) {
            int zoreNum = 0;
            int oneNum = 0;
            for (int i = 0; i < str.length(); i++) {
                if (str.charAt(i) == '0') {
                    zoreNum++;
                } else {
                    oneNum++;
                }
            }
            for (int i = m; i >= zoreNum; i--) {
                for (int j = n; j >= oneNum; j--) {
                    dp[i][j] = Math.max(dp[i][j], dp[i - zoreNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
}

完全背包问题

  • 如果求组合数就是外层for循环遍历物品,内层for遍历背包。
  • 如果求排列数就是外层for遍历背包,内层for循环遍历物品。

Dp 五部曲

  1. 确定下标和 Dp 数组的含义, d p [ i ] [ j ] dp[i][j] dp[i][j]表示的是在 背包容量为 j 的时间,在前 i 个物品中可以选择出的最大的价值和。
  2. 确定 Dp 数组的递推公式, d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − w e i g h t [ i ] ] + v a l u e [ i ] ) dp[i][j]=max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]) dp[i][j]=max(dp[i1][j],dp[i1][jweight[i]]+value[i]),表示的是容量为 j 的背包,在前 i 个物品中选择的最大价值和 为 d p [ i ] [ j ] dp[i][j] dp[i][j],这里可以使用 滚动数组来代替, d p [ j ] = m a x ( d p [ j ] , d p [ j − w e i g h t [ j ] ] + v a l u e [ i ] ) dp[j]=max(dp[j],dp[j-weight[j]]+value[i]) dp[j]=max(dp[j],dp[jweight[j]]+value[i])​,表示的也是在背包容量为 j 的时间,在前 i 个物品中选择出,最大的价值和。
  3. 初始化:这里dp【0】是0 ,如果商品的价值都是大于 0 的,全部初始化为 0,否则全部初始化为 -MAX,防止被覆盖。
  4. 遍历的顺序:这里和0 1 背包问题最大的区别就是,遍历的顺序是正向遍历的,正序的遍历就可以让每一个商品重复的选择。
  5. 举例子推导

518. 零钱兑换 II - 力扣(LeetCode)

给你一个整数数组 coins 表示不同面额的硬币,另给一个整数 amount 表示总金额。

请你计算并返回可以凑成总金额的硬币组合数。如果任何硬币组合都无法凑出总金额,返回 0

假设每一种面额的硬币有无限个。

题目数据保证结果符合 32 位带符号整数。

示例 1:

输入:amount = 5, coins = [1, 2, 5]
输出:4
解释:有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1

注意点

  1. dp【j】的含义是,在coins 中凑出价值 为 j 的种类的个数是 dp【j】
  2. 当我们拥有数字 coins【i】 的时间,凑出 dp【j】 的个数的其中一个就是 dp【j-nums【i】】,这样一来,我们用有数字coins【i-1】的时间,dp【j】的数量的一个就是,dp【j-coins【j-】】,这样最终的出来的递推公式就是 d p [ j ] + = d p [ j − c o i n s [ i ] ] dp[j]+=dp[j-coins[i]] dp[j]+=dp[jcoins[i]]
  3. 确定初始化,dp【0】等于 1
  4. 确定遍历的顺序,如果遍历的顺序是背包的容量大小是从大往小,那么得到的是 0 1 背包问题,每一个物品只能够选择一次,在这里我们遍历背包的容量大小的时间,由小变大,这样就会重复选择物品。
  5. 举例子推到。
class Solution {
    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];
    }
}

LCR 103. 零钱兑换 - 力扣(LeetCode)

class Solution {
    public int coinChange(int[] coins, int amount) {
        int n=coins.length;
        int dp[]=new int [amount+1];
        for(int i=0;i<=amount;i++){
            dp[i]=amount+1;
        }
        dp[0]=0;

        for(int i=0;i<n;i++){
            for(int j=coins[i];j<=amount;j++){
                dp[j]=Math.min(dp[j],dp[j-coins[i]]+1);
            }
        }
        return dp[amount]<amount+1?dp[amount]:-1;
    }
}

377. 组合总和 Ⅳ - 力扣(LeetCode)

给你一个由 不同 整数组成的数组 nums ,和一个目标整数 target 。请你从 nums 中找出并返回总和为 target 的元素组合的个数。

题目数据保证答案符合 32 位整数范围。

示例 1:

输入:nums = [1,2,3], target = 4
输出:7
解释:
所有可能的组合为:
(1, 1, 1, 1)
(1, 1, 2)
(1, 2, 1)
(1, 3)
(2, 1, 1)
(2, 2)
(3, 1)
请注意,顺序不同的序列被视作不同的组合。

示例 2:

输入:nums = [9], target = 3
输出:0

注意点

  • 这里求的是排列的个数,所以我们拥有一个 nums【i】 的时间 凑出 dp【j】 的个数的其中一个就是 dp【j-nums【i】】,所以凑出 j 的个数 就是 dp【j】+=dp【j-nums【i】】
  • 而且这里是完全背包,并且是求排列的个数,所以遍历的顺序是 先背包的容量 从小到大,保证了元素的可重复选择;内层遍历物品,保证了在对每一个容量 所有的物品都可以再次选择。
class Solution {
    public int combinationSum4(int[] nums, int target) {
        //首先第一步,明确dp【j】下标的含义,dp【j】是目标为 j 的时间,从nums中寻找出的 综合为 j 的元素组合的个数
        //dp【j】=dp【j-1】+dp【j-2】+。。。+dp【0】,并且,在这里我们,是不具备所有的 j-1,j-2.j-3 的。只具备所包含的nums,所以这里
        // 初始化,由题意可以知道的是,只有在不取任何元素的时间 target =0 ,只有一种方案
        // 遍历方向,首先应该确定target 等于 1的情况,然后逐步确认target + 1 的情况,直至到达 target,并且每一个 i - 的都是 nums【 j 】 
        int [] dp=new int [target+1];
        dp[0]=1;

        for(int i=1;i<=target;i++){
            for(int j=0;j<nums.length;j++){
                if(nums[j]<=i){
                    dp[i]+=dp[i-nums[j]];
                }
            }
        }
        return dp[target];
       
    }
}

57. 爬楼梯(第八期模拟笔试) (kamacoder.com)

假设你正在爬楼梯。需要 n 阶你才能到达楼顶。

每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢?

注意:给定 n 是一个正整数。

输入描述:输入共一行,包含两个正整数,分别表示n, m

输出描述:输出一个整数,表示爬到楼顶的方法数。

输入示例:3 2

输出示例:3

提示:

当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。

此时你有三种方法可以爬到楼顶。

  • 1 阶 + 1 阶 + 1 阶段
  • 1 阶 + 2 阶
  • 2 阶 + 1 阶

注意点:

  • 首先我们可以爬楼梯的时间是,一次可以爬 1 2 3 4 5 . 。 。 。步,那么如果我们在第 n 层的时间,爬到 第 n 层的方法有,从 n -1 层 爬一步;从 n - 2 层爬两步,从 n - 3 层爬 三步,。。。这个多的方法加起来。而且 1 2 和 2 1 是两种不同的方法,所以我们需要注意的就是遍历的顺序。

DP 五部曲

  1. dp[j] 的含义是,在 第 j 层楼梯上,爬到 第 j 层楼梯的方法 有 dp【j】 种方法。

  2. 确定递推公式:首先,如果我们每次可以爬一层楼梯,那么爬到 j 层的方法,就是 dp【j】=dp【j-1】,现在我们可以爬两层,那么爬到 j 层的方法 有 dp【j-2】种,由此得知,如果我们每次可以爬的层数是 i 的话,爬到 j 层的时候的方法 dp【j】 =dp【j-i】-》》》dp【j】=dp【j-1】+dp【j-2】+dp【j-3】+。。。。+dp【0】

  3. 确定初始化,由于 dp【1】+=dp【0】,如果dp【0】等于 0 的话,所有的结果都是0 了;或者我们可以假想,到达 0 层的方式只有一种方式,就是直接站在 0 层楼梯上。

  4. 确定遍历的顺序

    1. 确定容量的遍历顺序:首先不论是每一次爬一层还是爬两层,都是可以一只重复去使用的,那么就可以表明的是这是一个完全背包的问题,那么在遍历背包的容量的时间是顺序遍历的,也就是从小到大遍历,这样可以让物品被重复的选择;
    2. 确定首先遍历物品还是容量:这里需要注意的是1 2 和 2 1 是两种不同的爬到3 层的方法,所以每一个物品都是拥有顺序的,也就是排列问题,那么我们直接先遍历容量(从小到大),这样选择 了 1 和 2 ,即可以选择 2 1。如果是首先遍历的物品的话,遍历 了 1 2 ,就不能遍历 2 1了。
  5. 举例子

322. 零钱兑换 - 力扣(LeetCode)

给你一个整数数组 coins ,表示不同面额的硬币;以及一个整数 amount ,表示总金额。

计算并返回可以凑成总金额所需的 最少的硬币个数 。如果没有任何一种硬币组合能组成总金额,返回 -1

你可以认为每种硬币的数量是无限的。

示例 1:

输入:coins = [1, 2, 5], amount = 11
输出:3 
解释:11 = 5 + 5 + 1

示例 2:

输入:coins = [2], amount = 3
输出:-1

示例 3:

输入:coins = [1], amount = 0
输出:0

DP五部曲

  1. dp【j】 表示的是凑成金额 为 j 的金币,所需要的最少的硬币的个数是 dp【j】
  2. 确定递推公式, d p [ j ] = m i n ( d p [ j ] , d p [ j − c o i n s [ i ] ] + 1 ) dp[j]=min(dp[j],dp[j-coins[i]]+1) dp[j]=min(dp[j],dp[jcoins[i]]+1),如果我们拥有一个 coins【i】那么,凑成 j 所需要的硬币的个数就是dp【j-coins【i】】凑成 金额为 j- coins【i】的硬币的个数 加 1.
  3. 确定初始化,dp【0】=0,dp【amount】=amount+1,因为凑成金额为 amount 所需要的硬币的个数最多是 amount ,如果dp【aoumt】是大于 aoumnt 的时间,那么就是没有找到,直接返回 -1;
  4. 遍历的顺序:这里我们采用将物品正向遍历,就可以保证遍历的时间物品可以被选择多次,也就是完全背包的问题。
  5. 举例子写出DP 数组。
  6. 在这里需要注意的是,
class Solution {
    public int coinChange(int[] coins, int amount) {
        // 凑出 金额为 j 的硬币的最少的个数 为 dp【j】
        // 凑出 金额为 j 的硬币的最少的个数。我们拥有一个 coins【i】,那么dp【j】 的个数就是【dp【j-coins【i】】+1
        int dp[]=new int[amount+1];
        //确定初始化,首先明确一点,凑出 金额为 0 的硬币的个数 是 0,然后其他的防止被覆盖,初始化一个比较大的数字。
        Arrays.fill(dp,amount+1);
        dp[0]=0;
        for(int i=0;i<coins.length;i++){
            for(int j=coins[i];j<=amount;j++){
               dp[j]=Math.min(dp[j],dp[j-coins[i]]+1); 
            }
        }
        //在 返回的时间由一个注意点,就是凑成j 的数量dp【j】一定是小于等于 j 的,如果数量是大于的话,那就是错误的 ,直接返回 -1;
        return dp[amount] > amount? -1:dp[amount];
    }
}

279. 完全平方数 - 力扣(LeetCode)

给你一个整数 n ,返回 和为 n 的完全平方数的最少数量

完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,14916 都是完全平方数,而 311 不是。

示例 1:

输入:n = 12
输出:3 
解释:12 = 4 + 4 + 4

示例 2:

输入:n = 13
输出:2
解释:13 = 4 + 9
  • 首先确定,dp【j】的含义,j 表示的是 在小于 n 的数字中,寻找到凑成 j 的所需的元素的 最小的个数是dp【j】
  • d p [ j ] = m i n ( d p [ j ] , d p [ j − n u m s [ i ] ] + 1 ) dp[j]=min(dp[j],dp[j-nums[i]]+1) dp[j]=min(dp[j],dp[jnums[i]]+1), 也就是 d p [ j ] = M a t h . m i n ( d p [ j ] , d p [ j − i ∗ i ] + 1 ) dp[j]=Math.min(dp[j],dp[j-i*i]+1) dp[j]=Math.min(dp[j],dp[jii]+1),凑成数字 j 所需的最小的平方的元素的个数是 dp【j】,如果我们拥有一个数字 i ,那么凑成 j 的元素的个数,就等于 凑成 j - i*i 的元素的个数 + 1。
  • 初始化,dp【0】=1,,没有意义。
  • 遍历顺序,首先我们背包的容量是从小到大遍历的 ,表明了每个 i 可以重复的选择 ,比如 12 -》》 4 4 4,,至于首先遍历 物品,还是首先遍历背包的容量 都是可以的。我们只是求解的是组合成 j 的最小的元素的个数而已
class Solution {
    public int numSquares(int n) {
        int dp[] = new int[n + 1];
        Arrays.fill(dp, Integer.MAX_VALUE);
        dp[0] = 0;
        for (int i = 1; i <= Math.sqrt(n); i++) {

            for (int j = i * i; j <= n; j++) {

                dp[j] = Math.min(dp[j], dp[j - i * i] + 1);
            }
        }
        return dp[n];
    }
}

139. 单词拆分 - 力扣(LeetCode)

给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true

**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。

示例 1:

输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。

示例 2:

输入: s = "applepenapple", wordDict = ["apple", "pen"]
输出: true
解释: 返回 true 因为 "applepenapple" 可以由 "apple" "pen" "apple" 拼接成。
     注意,你可以重复使用字典中的单词。

示例 3:

输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"]
输出: false

注意点:直接不去理解为 完全背包的,利用 dp 去做就可以

  1. 首先 dp【j】 的含义是,在字符串长度为 j 的时间,如果 s 可以被拆分为 字典中的单词的话,那么就是true;
  2. 确定 递推公式,如果 字符串 i 是 存在 字典中,并且 i -j 也是存在于字典中的,那么dp【j】=true;
  3. 初始化,dp【0】=true
  4. 遍历顺序: 我们需要知道 长度 为 j 的字符串是否可以由字典中的单词表示,那么就需要知道 长度为 i 的字符串是否可以被 字典中的单词表示,并且, i 到 j 的字符串是否可以被字典中的单词表示。
class Solution {
    public boolean wordBreak(String s, List<String> wordDict) {
        // queidng dp de hanyi ,长度是 j 的时候 dp【j】表示的是是否可以拆分为字典中的单词
        // 确定地推公式,dp【j】 是由 dp【i】 和 i 到 j 之间的字符串是否存在于 字典中国两个条件来决定的。
        HashSet<String> map = new HashSet<>(wordDict);
        boolean dp[] = new boolean[s.length() + 1];
        dp[0] = true;
        for (int j = 1; j <= s.length(); j++) {
            for (int i = 0; i < j; i++) {
                if (map.contains(s.substring(i, j)) && dp[i]) {
                    dp[j] = true;
                }
            }
        }
        return dp[s.length()];

    }
}

198. 打家劫舍 - 力扣(LeetCode)

你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。

示例 1:

输入:[1,2,3,1]
输出:4
解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

示例 2:

输入:[2,7,9,3,1]
输出:12
解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。
     偷窃到的最高金额 = 2 + 9 + 1 = 12 。
class Solution {
    public int rob(int[] nums) {
        //dp 【j】 表示的是 偷取 前 j 个房间获得的最大的金额
        // dp【j】= max(dp【j-2】+nums【j】,dp【j-1】);
        // dp【0】=nums【0】,dp【1】=max(nums【0】,nums【1】
        // 开始从 2 号房间开始遍历,因为我们已经将 前 0 和 1 的 价值得到了
        // 注意的一点是 我们需要对 长度为 1 0 的特殊处理
        int dp[]=new int[nums.length];
        dp[0]=nums[0];
        if(nums.length==1){
            return nums[0];
        }
        dp[1]=Math.max(nums[0],nums[1]);
        for(int j=2;j<nums.length;j++){
            dp[j]=Math.max(dp[j-2]+nums[j],dp[j-1]);
        }
        return dp[nums.length-1];

    }
}

213. 打家劫舍 II - 力扣(LeetCode)

你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警

给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,今晚能够偷窃到的最高金额。

示例 1:

输入:nums = [2,3,2]
输出:3
解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。

示例 2:

输入:nums = [1,2,3,1]
输出:4
解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。
     偷窃到的最高金额 = 1 + 3 = 4 。

注意点

  • 这里的首位是已经成环的,那么我们思考直接去除 1 或者是 去除 n 号元素,并在 前 n -1 个中计算最大金额,和 后 n -1 个中计算最大 金额,这样就可以避免环的发生,返回两者的最大数值。
class Solution {
    public int rob(int[] nums) {
        // 成环之后 ,那么我们直接考虑计算不包括 1 号房间,和不包括最后一间房,求出的结果求最大值。
        // if(nums.length<=3){
        //     return 
        // }
        if(nums==null||nums.length==0){
            return 0;
        }if(nums.length==1){
            return nums[0];
        }
        int ans2=ans(Arrays.copyOfRange(nums,0,nums.length-1));
        int ans1=ans(Arrays.copyOfRange(nums,1,nums.length));
        
        return Math.max(ans1,ans2);
    }

    int ans (int [] nums){
   
        int dp[] =new int[nums.length];
        dp[0]=nums[0];
        if(nums.length==1){
            return nums[0];
        }
        dp[1]=Math.max(nums[0],nums[1]);
        for(int j=2;j<nums.length;j++){
            dp[j]=Math.max(dp[j-2]+nums[j],dp[j-1]);
        }
        return dp[nums.length-1];

    }
}

337. 打家劫舍 III - 力扣(LeetCode)

除了 root 之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果 两个直接相连的房子在同一天晚上被打劫 ,房屋将自动报警。

给定二叉树的 root 。返回 在不触动警报的情况下 ,小偷能够盗取的最高金额

示例 1:

img

输入: root = [3,2,3,null,3,null,1]
输出: 7 
解释: 小偷一晚能够盗取的最高金额 3 + 3 + 1 = 7
使用传统的递归算法

注意点

  • 首先对于题意,二叉树,我们可以做什么?确定递归的参数 和 返回值;确定终止条件 ; 确定当前节点的逻辑
    • 首先确定递归参数:我们需要计算偷 或者 不偷 所获得的最大的数值,那么只需要这个节点就可以;在返回的时候带上当前节点能够获得的最大的金额。
    • 当前节点为空的时间就会终止
    • 如何处理当前节点呢?在题目中 选择当前节点的时间就不能选择他的两个孩子节点;不选择当前节点 那么就可以选择他的左孩子的左孩子,和左孩子的右孩子这两个节点,右孩子的左孩子和右孩子的右孩子这两个节点。将这两种情况的的最大值返回。
class Solution {
    public int rob(TreeNode root) {
        return dfs(root);
    }
    public int dfs(TreeNode root){
        if(root==null){
            return 0;
        }
        if(root.left==null&&root.right==null){
            return root.val;
        }
        int buxuan=dfs(root.left)+dfs(root.right);
        
        int xuan=root.val;
        int left=0;
        int right=0;
        if(root.left!=null)
             left=dfs(root.left.left)+dfs(root.left.right);
        if(root.right!=null)
            right=dfs(root.right.left)+dfs(root.right.right);
        xuan+=left+right;
        return Math.max(xuan,buxuan);
    }
}
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rob(self, root: Optional[TreeNode]) -> int:
        return self.dfs(root)
    def dfs(self,root : Optional[TreeNode]) -> int:
        if(root==None):
            return 0
        if root.left==None and root.right==None:
            return root.val
        
        buxuan=self.dfs(root.left)+self.dfs(root.right)

        xuan=root.val
        left=0
        right=0
        if(root.left!= None):
            left=self.dfs(root.left.left)+self.dfs(root.left.right)
        if(root.right!=None):
            right=self.dfs(root.right.left)+self.dfs(root.right.right)
        xuan+=left+right

        return max(buxuan,xuan)
树形DP
  • 首先明确 Dp 数组的含义,数字下标表示的是当前节点中 0 是 不偷当前节点表示的最大的价值,下标1 表示的是偷当前节点所获得的最大的价值和。
  • 确定终止条件,当前节点为空的时候,偷或者不偷都是 0 ,所以返回的数值是 0 0
  • 确定遍历顺序:后序遍历
  • 当前层需要做的事情:如果不偷当前节点,那马获得的价值就是,递归左节点和右节点的价值和;如果偷了当前节点,那么获得的价值和就是当前节点的价值加上不偷左节点和不偷右节点的两个价值和
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution:
    def rob(self, root: Optional[TreeNode]) -> int:
        ans=self.dfs(root)
        return max(ans[0],ans[1])
    def dfs(self,root ):
        if(root==None):
                return [0,0]
        left=self.dfs(root.left)
        right=self.dfs(root.right)

        tou=root.val+left[0]+right[0]
        butou=max(left[0],left[1])+max(right[0],right[1])
        return [butou,tou]    
class Solution {
    public int rob(TreeNode root) {
        int []ans=dfs(root);
        return Math.max(ans[0],ans[1]);
    }
    int [] dfs(TreeNode root){
        if(root==null){
            return new int[]{0,0};
        }
        int [] left=dfs(root.left);
        int [] right=dfs(root.right);

        int buxuan=Math.max(left[0],left[1])+Math.max(right[0],right[1]);
        int xuan=root.val+left[0]+right[0];
        return new int[]{buxuan,xuan};
    }
}

121. 买卖股票的最佳时机 - 力扣(LeetCode)

贪心算法

注意点

  • 在最左边选择一个最小的,在右边选择一个最大的,求解两个值的最大差
class Solution {
    public int maxProfit(int[] prices) {
        int min=Integer.MAX_VALUE;
        int ans=0;
        for(int num: prices){
            min=Math.min(min,num);
            ans=Math.max(ans,num-min);
        }
        return ans;
    }
}
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        ans=0;
        min_p=prices[0]
        for p in prices:
            ans=max(ans,p-min_p)
            min_p=min(min_p,p)
        return ans
DP

针对 股票问题,我们可以思考一下,对于每一天的股票而言,我们有持有,和不持有(卖出)两种状态,而且每一天都是有这两种状态的,所以指定一个二位数组,dp【i】【0】,dp【i】【1】分别表示的是第 i 天持有股票可以获得的最大的现金,和第 i 天不持有股票所获得的最大的金额。

dp五部曲

  • dp【i】0表示的是第 i 天 持有股票获得的最大金额,dp【i】1 表示的是第 i 天不持有股票所获得的最大的金额
  • 确定地推公式
    • dp【i】0,在 第 i 天持有股票的时候,我们可以得知在前一天可以持有股票,或者在当天也可以持有股票(那么金额就需要减去今天的股票的价值),所以在第 i 持有股票的最大金额是这两个 金额的 最大值。 d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , − p r i c e [ i ] ) dp[i][0]=max(dp[i-1][0],-price[i]) dp[i][0]=max(dp[i1][0],price[i])
    • dp【i】1,在 第 i 天不持有股票的时候,我们可以得知的是在前一天就可以不持有股票,或者是在当天出售这两种情况,由此可以得知的是 d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] + p r i c e [ i ] ) dp[i][1]=max(dp[i-1][1],dp[i-1][0]+price[i]) dp[i][1]=max(dp[i1][1],dp[i1][0]+price[i])
  • 初始化,在第一天 不持有股票的时候,获得的最大的金额是0 ,如果持有了股票获得的最大的金额是 - price【0】;==值得主义的是,在最后的结果返回的时候,是否是持有股票才能获得最大金额呢? 还是不持有骨牌哦才可以获得最大的金额?因为我们在前面计算出来的都是最大的金额,所以在这个时候卖出股票才可以获得更大的 金额
  • 遍历顺序:第 i 天获得的最大金额 是需要前一天得到的
class Solution {
    public int maxProfit(int[] prices) {
        int dp[][]=new int[prices.length][2];
        dp[0][0]=-prices[0];
        dp[0][1]=0;
        for(int i=1;i<prices.length;i++){
            dp[i][0]=Math.max(dp[i-1][0],-prices[i]);
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]+prices[i]);
        }
        return dp[prices.length-1][1];
    }
}
class Solution:
    def maxProfit(self, prices: List[int]) -> int:
        n=len(prices)
        dp=[[0]*2]*n
        dp[0][0]=-prices[0]
        dp[0][1]=0
        for i in range(1,n):
            dp[i][0]=max(dp[i-1][0],-prices[i])
            dp[i][1]=max(dp[i-1][1],dp[i-1][0]+prices[i])
        return dp[n-1][1]

给你一个整数数组 prices ,其中 prices[i] 表示某支股票第 i 天的价格。

在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。

返回 你能获得的 最大 利润

注意点

121. 买卖股票的最佳时机 (opens new window)中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。

  • 这个和前面不同的是,股票可以多次买卖,但是在同一时刻只能拥有一只股票,所以在计算 某一天 持有股票的金额的时间 dp【i】【0】=max(dp【i-1】【0】,dp【i-1】【1】-price【i】
  • 还有一点就是,在第 i 天 不持有 股票获得的金额,是 前一天不持有股票的金额 和 前一天持有股票并且在今天售出的金额的最大值
class Solution {
    public int maxProfit(int[] prices) {
        int n=prices.length;
        int [][] dp=new int[n][2];
        dp[0][0]=-prices[0];
        dp[0][1]=0;
        for(int i=1;i<n;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[n-1][1];
    }
}

123. 买卖股票的最佳时机 III - 力扣(LeetCode)

给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

class Solution {
    public int maxProfit(int[] price) {
        // 最多可以完成两笔 交易,那么就代表了在 第 i 天 完成第一笔 和在 第 i 天完成 第 2  笔。
        // dp【i】【0】 表示没有操作,dp【i】【1】 表示 第 i 天完成 第 1  笔 不持有股票,dp【i】【2】表示 第 i 天 第 1 笔 不持有股票,dp【i】【3】表示 第 i 天 第二笔不持有股票,dp【i】【4】表示 第 i 天 第二笔 持有股票
        int [][]dp=new int[price.length][5];
        // dp[0][0] :第 0 天没有操作所持有的最大金额
        // dp[0][1] :第 0 天第一次持有股票获得的最大金额     dp[0][1]=-price[0]
        // dp[0][2] : 第 0 天第一次不持有股票所获得的最大金额 dp[0][2]=0
        // dp[0][3] :第 0 天第二次持有股票获得的最大金额 dp[0][3]=-price[0]
        // dp[0][4] : 第 0 天第二次不持有股票获得的最大的金额 0
        dp[0][0]=0;
        dp[0][1]=-price[0];
        dp[0][2]=0;
        dp[0][3]=-price[0];
        dp[0][4]=0;
        // 确定一下地推公式:这四种状态的改变都是有两种操作来形成的,有操作(购入或者是售出)和无操作。
        // 在 第 i 天 第一次持有股票所获得的最大金额表示的是,在前一天就已经持有了,和在前一天没有操作的情况之下在当天买入。
        for(int i=1;i<price.length;i++){
            dp[i][0]=dp[i-1][0];
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][0]-price[i]);
            dp[i][2]=Math.max(dp[i-1][2],dp[i-1][1]+price[i]);
            dp[i][3]=Math.max(dp[i-1][3],dp[i-1][2]-price[i]);
            dp[i][4]=Math.max(dp[i-1][4],dp[i-1][3]+price[i]);
        }
        return dp[price.length-1][4];
    }
}

注意点

  • 首先明确一下 dp 数组的含义
    • dp【i】 【0】 表示的是在 第 i 天没有操作所获得的最大金额
    • dp【i】【1】表示的是在第 i 天第一次持有股票所获得的最大的金额
    • dp【i】【2】表示的是在第 i 天第一次不持有股票所获得的最大的金额
    • dp【i】【3】表示的是在第 i 天第二次持有股票所获得的最大金额
    • dp【i】【4】表示的是在第 i 天第二次不持有股票所获得的最大的金额
  • 确定递推公式
    • 首先 d p [ i ] [ 0 ] = d p [ i − 1 ] [ 0 ] dp[i][0]=dp[i-1][0] dp[i][0]=dp[i1][0]表示的是在 第 i 天没有操作的时候,最大的金额就是 第 i -1 天没有操作所获得的最大的金额
    • d p [ i ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 0 ] − p r i c e s [ i ] ) dp[i][1]=max(dp[i-1][1],dp[i-1][0]-prices[i]) dp[i][1]=max(dp[i1][1],dp[i1][0]prices[i])表示的是在第 i 天中无操作和有操作(购入股票)所获得的最大的金额:第一次购入获得的最大的金额是在前一天无操作的时候第一次购入。
    • d p [ i ] [ 2 ] = m a x ( d p [ i − 1 ] [ 2 ] , d p [ i − 1 ] [ 1 ] + p r i c e s [ i ] ) dp[i][2]=max(dp[i-1][2],dp[i-1][1]+prices[i]) dp[i][2]=max(dp[i1][2],dp[i1][1]+prices[i]) 表示的是在第 i 天中无操作和有操作(第一次卖出股票)所获得的最大的金额:第一次售出股票获得的最大的金额是在前一天持有股票的 基础上第一次售出股票
    • d p [ i ] [ 3 ] = m a x ( d p [ i − 1 ] [ 3 ] , d p [ i − 1 ] [ 2 ] − p r i c e s [ i ] ) dp[i][3]=max(dp[i-1][3],dp[i-1][2]-prices[i]) dp[i][3]=max(dp[i1][3],dp[i1][2]prices[i]) 表示的是在 第 i 天中无操作和有操作(第二次购入股票)所获得的最大的金额:第二次购入获得的最大的金额是在第二次售出之后的基础上的 因为题目中要求了,不能够同时参与两次交易
    • d p [ i ] [ 4 ] = m a x ( d p [ i − 1 ] [ 4 ] , d p [ i − 1 ] [ 3 ] + p r i c e s [ i ] ) dp[i][4]=max(dp[i-1][4],dp[i-1][3]+prices[i]) dp[i][4]=max(dp[i1][4],dp[i1][3]+prices[i])表示的是在第 i 天中无操作和有操作(第二次售出股票)所获得的最大的金额:第二次售出所所得二点最大的金额是在第二次购入的基础之上
  • 确定初始化
    • 首先 无操作: d p [ 0 ] [ 0 ] = 0 dp[0][0]=0 dp[0][0]=0
    • 第一天第一次持有股票: d p [ 0 ] [ 1 ] = − p r i c e s [ 0 ] dp[0][1]=-prices[0] dp[0][1]=prices[0]
    • 第一天第一次不持有股票: d p [ 0 ] [ 2 ] = 0 dp[0][2]=0 dp[0][2]=0
    • 第一天第二次持有股票: d p [ 0 ] [ 3 ] = − p r i c e s [ 0 ] dp[0][3]=-prices[0] dp[0][3]=prices[0] 根据题意不能同时有两笔交易存在
    • 第一天第二次不持有股票: d p [ 0 ] [ 4 ] = 0 dp[0][4]=0 dp[0][4]=0

188. 买卖股票的最佳时机 IV - 力扣(LeetCode)

class Solution {
    public int maxProfit(int k, int[] prices) {
        //首先我们明确一下,最多可以有K 次交易而且不能够同时参与多笔交易,这就表明了我们需要记录每一天中 K 笔交易所获得的最大的金额,记录了 第 i tian 中 无操作获得的最大的金额,持有股票和不持股票所获得的最大的金额
        int length=prices.length;
        int [][]dp=new int[length][2*k+1];
        
        for(int i=1;i<2*k;i+=2){
            dp[0][i]=-prices[0];
        }//表示的是 在 第 0 天 前 k 次交易中 每一次持有股票所获得的最大的金额

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


309. 买卖股票的最佳时机含冷冻期 - 力扣(LeetCode)

  • 首先我们来明确一下题目的内容,题目中包括了一个主要的区别,就是拥有冷冻期,表示了在售出一个股票的时候拥有一天的冷冻期,在这一天之中是不能够存在交易的。

    在普通的股票问题中,我们一般只会有两种状态,就是持有股票和不持有股票这两种状态,现在拥有一个冷冻期,那么就表明了会单独添加一个状态,就是处于冷冻期中。但是在这里不持有股票的状态就可以分为两种,一个是保持不持有股票,一个是当天售出股票(为了给冷冻期)

  • 确定递推公式:

    • 达到持有股票所获得的最大金额: d p [ i ] [ 0 ] = m a x ( d p [ i − 1 ] [ 0 ] , d p [ i − 1 ] [ 3 ] − p r i c e s [ i ] , d p [ i − 1 ] [ 1 ] − p r i c e s [ i ] ) dp[i][0]=max(dp[i-1][0],dp[i-1][3]-prices[i],dp[i-1][1]-prices[i]) dp[i][0]=max(dp[i1][0],dp[i1][3]prices[i],dp[i1][1]prices[i]),这里思考一下,为什么没有 d p [ i − 1 ] [ 2 ] 前一天售出, dp[i-1][2]前一天售出, dp[i1][2]前一天售出,因为前一天售出了,今天就是冷冻期,不能够进行股票交易。
    • 达到保持不持有股票所获得的最大金额: d p [ i − 1 ] [ 1 ] = m a x ( d p [ i − 1 ] [ 1 ] , d p [ i − 1 ] [ 3 ] ) dp[i-1][1]=max(dp[i-1][1],dp[i-1][3]) dp[i1][1]=max(dp[i1][1],dp[i1][3])
    • 达到当天不持有股票所获得的最大金额: d p [ i ] [ 2 ] = d p [ i − 1 ] [ 0 ] + p r i c e s [ i ] dp[i][2]=dp[i-1][0]+prices[i] dp[i][2]=dp[i1][0]+prices[i]
    • 达到冷冻期所获得的最大的金额: d p [ i ] [ 3 ] = d p [ i − 1 ] [ 2 ] dp[i][3]=dp[i-1][2] dp[i][3]=dp[i1][2]
class Solution {
    public int maxProfit(int[] prices) {
        int[][]dp=new int[prices.length][4];
        dp[0][0]=-prices[0];
        for(int i=1;i<prices.length;i++){
            dp[i][0]=Math.max(dp[i-1][0],Math.max(dp[i-1][1]-prices[i],dp[i-1][3]-prices[i]));
            dp[i][1]=Math.max(dp[i-1][1],dp[i-1][3]);
            dp[i][2]=dp[i-1][0]+prices[i];
            dp[i][3]=dp[i-1][2];//如何达到冷冻期?只有在前一天售卖,

        }
        int n=prices.length;
        return Math.max(Math.max(dp[n-1][1],dp[n-1][2]),dp[n-1]3]);
    }
}

714. 买卖股票的最佳时机含手续费 - 力扣(LeetCode)

大致的思路和122. 买卖股票的最佳时机 II - 力扣(LeetCode)是一致的,但是其中重要的一点是在一次交易完成之后需要扣除手续费,在这个时候我们就可以规定扣除手续费的时机:是在购买的时候扣除、还是在售出的时候扣除手续费。

  • 需要注意的一点是,如果规定了在售出股票的时候扣除手续费,那么我们在初始化的时候是不需要对第一天售出股票的金额操作的。
  • 还有一点就是因为这次的股票售出是包含手续费的,这个手续费在最后一天的时候可能会让我们的收益减少,所以我们计算最后一天的最大金额的时间,就需要来对比这两个金额取最大的数值。
class Solution {
    public int maxProfit(int[] prices, int fee) {
        int n=prices.length;
        int [][]dp=new int [n][2];
        dp[0][0]=-prices[0];
        for(int i=1;i<n;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]-fee);
        }
        return Math.max(dp[n-1][0],dp[n-1][1]);
    }
}

300. 最长递增子序列 - 力扣(LeetCode)

  1. 确定dp 数组的含义:dp【i】表示的是数组长度为 i 的时间,最长递增子序列的长度是 dp【i】
  2. 确定递推公式:如果 nums【i】 > nums【j】就表示了,当前长度是需要增加的, d p [ i ] = m a x ( d p [ i ] , d p [ j ] + 1 ) dp[i]=max(dp[i],dp[j]+1) dp[i]=max(dp[i],dp[j]+1)
  3. 初始化:长度为 每一个数字也都表示的是一个递增的子序列,所以也是可以将dp 数组初始化为 1 的。
class Solution {
    public int lengthOfLIS(int[] nums) {
        int n=nums.length;
        int []dp=new int[n];
        Arrays.fill(dp,1);
        int ans=1;
        for(int i=1;i<n;i++){
            for(int j=0;j<i;j++){
                if(nums[i]>nums[j]){
                    dp[i]=Math.max(dp[i],dp[j]+1);

                }
                if(dp[i]>ans){
                    ans=dp[i];
                }
            }
        }
        return ans;
    }
}

674. 最长连续递增序列 - 力扣(LeetCode)

  1. 使用滑动窗口
class Solution {
    public int findLengthOfLCIS(int[] nums) {
        int lo=0;
        int ans=1;
        for(int hi=1;hi<nums.length;hi++){
            if(nums[hi]>nums[hi-1]){
                ans=Math.max(ans,hi-lo+1);
            }else{
              lo=hi;
            }
        }
        return ans;
    }
}
  1. 使用Dp
    1. 明确dp 数组的含义:dp【i】表示的是以下标 i 结尾的连续递增子序列长度为 dp【i】,注意这里的定义:一定是以下标 i 结尾的,但是不一定是从下标 0 开始的,这个是刚开是的理解错误。也就表明了为什么需要一个ans 来记录最大的数值。
    2. 确定递推公式:nums【i】》nums【i-1】,那么以 i 结尾的连续递增子序列的最大长度一定是以 i -1 结尾的连续递增子序列的长度 加 1 。
class Solution {
    public int findLengthOfLCIS(int[] nums) {
       int n=nums.length;
       int [] dp=new int[n];
       Arrays.fill(dp,1);
       int ans=1;
       for(int i=1;i<n;i++){
            if(nums[i]>nums[i-1]){
                dp[i]=dp[i-1]+1;
            }
            if(dp[i]>ans){
                ans=dp[i];
            }
       }
       return ans;
    }
}

718. 最长重复子数组 - 力扣(LeetCode)

  1. 明确dp 数组的含义,dp【i】【j】表示的是:以下标 i-1结尾的 A 和以下标 j -1 结尾的B,所能够表示的最长重复子数组的长度为 dp【i】【j】。
  2. 确定递推公式:如果 A【i-1】和B【j-1】是相等的,那么dp【i】【j】=dp【i-1】【j-1]+1,
  3. 确定初始化:0
  4. 遍历顺序都是可以的。
class Solution {
    public int findLength(int[] nums1, int[] nums2) {
        int ans=0;
        int r=nums1.length;
        int c=nums2.length;
        int [][]dp=new int[r+1][c+1];
        for(int i=1;i<=r;i++){
            for(int j=1;j<=c;j++){
                if(nums1[i-1]==nums2[j-1]){
                    dp[i][j]=dp[i-1][j-1]+1;
                }
                if(dp[i][j]>ans){
                    ans=dp[i][j];
                }
            }
        }
        return ans;
    }
}

1143. 最长公共子序列 - 力扣(LeetCode)

  1. 确定dp 数组的含义:dp【i】【j】表示的是字符串A【0到 i-1】和字符串 B【0到j-1】最长公共子序列的长度是dp【i】【j】
  2. 确定递推公式:if(A【i-1】==B【j-1】)dp【i】【j】=dp【i-1】【j-1】+1,两个不相等的时候就需要表现出dp【i-1】【j】和dp【i】【j-1】最大的两个
  3. 确定初始化:在两者的其中一个为空的时间,他们两个的最长的公共子序列也都是0
class Solution {
    public int longestCommonSubsequence(String text1, String text2) {
        int r=text1.length();
        int c=text2.length();
        int [][]dp=new int [r+1][c+1];
        for(int i=1;i<=r;i++){
            for(int j=1;j<=c;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[r][c];
    }
}

1035. 不相交的线 - 力扣(LeetCode)

class Solution {
    public int maxUncrossedLines(int[] nums1, int[] nums2) {
        int r=nums1.length;
        int c=nums2.length;
        int [][]dp=new int [r+1][c+1];
        for(int i=1;i<=r;i++){
            for(int j=1;j<=c;j++){
                if(nums1[i-1]==nums2[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[r][c];
    }
}

53. 最大子数组和 - 力扣(LeetCode)

  1. 明确dp 数组的含义:dp【i】表示的是以 i 结尾的连续子序列的和
  2. 明确递推公式:dp【i】=max(dp【i-1】+nums【i】,nums【i】)取两个的最大数值
  3. 初始化,ans=nums【0】的
class Solution {
    public int maxSubArray(int[] nums) {
        int n=nums.length;
        int []dp=new int[n];
        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]);
            if(dp[i]>ans){
                ans=dp[i];
            }
        }
        return ans;
    }
}

392. 判断子序列 - 力扣(LeetCode)

  1. 明确递推公式的含义:dp【i】【j】表示是以 i-1结尾的字符串 A和以j-1结尾的字符串B,相同的子序列的长度是dp【i】【j】
  2. 确定递推公式:考虑到一下两种情况;
  • 首先就是 A[i-1]==B[j-1]
    • 在A中找到一个字符是和B 相等的
  • 如果 A[i-1]!=B[j-1]的话,
    • 那么就需要让比较长的哪个字符串删除一个字符,继续进行比较。
class Solution {
    public boolean isSubsequence(String s, String t) {
        int r=s.length();
        int c=t.length();
        int [][]dp=new int[r+1][c+1];
        for(int i=1;i<=r;i++){
            for(int j=1;j<=c;j++){
                if(s.charAt(i-1)==t.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1]+1;
                }else{
                    dp[i][j]=dp[i][j-1];
                }
            }
        }
        return dp[r][c]==r?true:false;
    }   
}

115. 不同的子序列 - 力扣(LeetCode)

  1. 确定dp数组的含义:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp【i】【j】。
  2. 如果 s【i-1】==t【j-1】,dp【i】【j】=dp【i-1】【j-1】+dp【i-1】【j】;如果s【i-1】!=t【j-1】的话,dp【i】【j】=dp【i-1】【j】
  3. 初始化:将 s 的子序列是否是和 t 【j】是相等的,那么如果当 t 只有一个字符的时候,那么dp【i】【0】都是等于 1 的。
class Solution {
    public int numDistinct(String s, String t) {
        int r=s.length();
        int c=t.length();
        int [][] dp=new int[r+1][c+1];
        for(int i=0;i<=r;i++){
            dp[i][0]=1;
        }
        for(int i=1;i<=r;i++){
            for(int j=1;j<=c;j++){
                if(s.charAt(i-1)==t.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1]+dp[i-1][j];
                }else{
                    dp[i][j]=dp[i-1][j];
                }
            }
        }
        return dp[r][c];
    }
}

583. 两个字符串的删除操作 - 力扣(LeetCode)

  1. 确定dp 数组的含义:表示的是以 i -1 结尾的字符串A 和以j -1 结尾的字符串B,两者想要相等删除的操作数是dp【i】【j】
  2. 确定递推公式:
    1. A【i-1】==B【j-1】:就不需要进行删除操作
    2. A【i-1】!=B【j-1】:dp【i】【j】=dp【i-1】【j】+1和dp【i】【j-1】+1两者最小的一个
  3. 初始化:当每一个字符串都为 空 字符的时候,另外一个字符串变为空的时候的操作树就是他们的长度
class Solution {
    public int minDistance(String word1, String word2) {
        int r=word1.length();
        int c=word2.length();
        int [][]dp=new int[r+1][c+1];
        for(int i=0;i<=r;i++){
            dp[i][0]=i;
        }
        for(int j=0;j<=c;j++){
            dp[0][j]=j;
        }
        for(int i=1;i<=r;i++){
            for(int j=1;j<=c;j++){
                if(word1.charAt(i-1)==word2.charAt(j-1)){
                    dp[i][j]=dp[i-1][j-1];
                }else{
                    dp[i][j]=Math.min(dp[i-1][j]+1,dp[i][j-1]+1);
                }
            }
        }
        return dp[r][c];
    }
}

72. 编辑距离 - 力扣(LeetCode)

class Solution {
    public int minDistance(String word1, String word2) {
        int r = word1.length();
        int c = word2.length();
        int[][] dp = new int[r + 1][c + 1];
        for (int i = 0; i <= r; i++) {
            dp[i][0] = i;
        }
        for (int j = 0; j <= c; j++) {
            dp[0][j] = j;
        }
        for (int i = 1; i <= r; i++) {
            for (int j = 1; j <= c; j++) {
                if (word1.charAt(i - 1) == word2.charAt(j - 1)) {
                    dp[i][j] = dp[i - 1][j - 1];
                } else {
                    dp[i][j] =Math.min( dp[i-1][j],Math.min(dp[i][j-1],dp[i-1][j-1]))+1;
                }
            }
        }
        return dp[r][c];
    }
}

647. 回文子串 - 力扣(LeetCode)

  • 明确DP 数组的含义:dp【i】【j】表示的是以 i 开始 和 以 j 结尾的字符串是否是回文子串,并且这个依靠的是 i+1 和 j- 1 之间的字符串。
  • 确定递推公式:如果 s【i】==s【j】
    1. if(i==j)-》》 a,单独的一个字母就是回文字符串
    2. if(j-i<=1) aa,两个相同的字符也是一个回文字符串
    3. aba,这样的三个以上的字符串,就需要 根据dp【i+1】【j-1】是否是回文字符串来判断,dp【i】【j】是否是回文字符串了。
  • 初始化:全部初始化为 false
  • 遍历顺序:这里观察到 的dp【i】【j】是会依赖到,dp【i+1】【j-1】的,所有i 是从大到小来遍历,j 是从小到大来遍历。
class Solution {
    public int countSubstrings(String s) {
        int n = s.length();
        int r = n;
        int c = n;
        boolean[][] dp = new boolean[r + 1][c + 1];
        int ans = 0;
        for (int i = n - 1; i >= 0; i--) {
            for (int j = i; j < n; j++) {
                if (s.charAt(i) == s.charAt(j)) {
                    if (j - i <= 1) {
                        ans++;
                        dp[i][j] = true;
                    } else if (dp[i + 1][j - 1] == true) {
                        ans++;
                        dp[i][j] = true;
                    }
                }
            }
        }
        return ans;

    }
}

516. 最长回文子序列 - 力扣(LeetCode)

  • 确定DP 数组的含义:dp【i】【j】表示的是以 i 开始 j 结尾的字符串中包含的最长回文子序列的长度是dp【i】【j】
  • 确定递推公式:
    • if(s【i】==s【j】):dp【i】【j】=dp【i+1】【j-1】+1,因为已经有两个相同了
    • if(s【i】!=s【j】:dp【i】【j】=max(dp【i+1】【j】,dp【i】【j-1】
  • 确定初始化:每一个单个的字符都是回文子序列,所以在中轴线上的 dp 数组的数值都是 1
  • 确定遍历顺序: 因为这里dp【i】【j】是利用到了dp【i+1】【j-1】的,所以i 是必须从大到小来遍历,并且j 是需要从小到大来遍历的。
class Solution {
    public int longestPalindromeSubseq(String s) {
        int n=s.length();
        int r=n;
        int c=n;
        int [][]dp=new int[r+1][c+1];
        for(int i=0;i<=n;i++){
            dp[i][i]=1;
        }
        for(int i=n-1;i>=0;i--){
            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][c-1];
    }
}

图论

并查集理论

华为笔试:字符串并查集

并查集的两个基本的作用

  1. 将元素归谁为不同的集合中
  2. 判断两个元素是否在同一个集合中
class Solution {
    int [] father;
    int n;
    public boolean validPath(int n, int[][] edges, int source, int destination) {
        this.n=n;
        father=new int [n];
        init();
        for(int i=0;i<edges.length;i++){
            join(edges[i][0],edges[i][1]);
        }
        return isSame(source,destination);
    }
    void init(){
        //初始化 并查集
        IntStream.range(0,n).forEach(i->{
            father[i]=i;
        });
    }
    void join(int u,int v){
        u=find(u);
        v=find(v);
        if(u==v){
            return ;
        }
        father[v]=u;
    }
    int find(int u){
        if(u==father[u]){
            return u;
        }
        father[u]=find(father[u]);
        return father[u];
    }
    boolean isSame(int u,int v){
        u=find(u);
        v=find(v);
        return u==v;
    }
}

适应于所有函数并查集理论

class Solution {
    HashMap<Integer,Integer> father=new HashMap<>();

    public boolean validPath(int n, int[][] edges, int source, int destination) {
        init(n);
        for(int i=0;i<edges.length;i++){
            join(edges[i][0],edges[i][1]);
        }
        return isSame(source,destination);
    }
    boolean isSame(int u,int v){
        u=find(u);
        v=find(v);
        return u==v;
    }
    void init(int n){
        for(int i=0;i<n;i++){
            father.put(i,i);
        }    
    }
   void  join(int u,int v){
        u=find(u);
        v=find(v);
        if(u==v){
            return ;
        }
        father.put(v,u);
    }
    int find(int u){
        if(u==father.get(u)){
            return u;
        }
        father.put(u,find(father.get(u)));
        return father.get(u);
    }

}

Concurrent

交替打印ABC

  • 17
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值