【2022】十二月做题总结(上)

2022-12-01

【知识点】回溯

(1)基本内容

回溯算法是一种纯暴力搜索方式,虽然是纯暴力,但是有些问题能用暴力解出来就很不错了。它能解决组合、分割、子集、排列(与组合问题相比是有序的)和棋盘等问题。回溯与递归共生,有递归就有回溯,在二叉树的一些题目中看似只有递归,但是回溯是存在的,只是没有去用它而已。回溯法可以抽象为树形结构,如下图所示,横向for循环遍历,纵向递归。

将回溯抽象为N叉树

(2)回溯三部曲

// 返回值参数、终止条件、单层搜索逻辑
void backtracking(参数) { //回溯法递归中一般没有返回值,但参数一般较多
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

1.【77】组合(中)

题目链接: 组合问题

题目描述:
给定两个整数n k,返回范围 [1, n] 中所有可能的k 个数的组合。返回顺序任意。

涉及知识点: 回溯【组合问题】、剪枝

思路: 题目比较简单,直接套用回溯法模板就可以解题,是回溯法的简单实践。
另外可以在原生的回溯法上进行剪枝优化。所谓剪枝就是不去遍历不满足情况的分支,比如组合问题中n = 4,k = 4,那么第一层for循环的时候,从元素2开始的遍历都没有意义了,因为以2开头的组合中最多只有三个数,就把这些分支都“剪掉”,它优化的是单层搜索中的逻辑。
剪枝操作

代码:

// 原生版本
class Solution {
    LinkedList<Integer> arr = new LinkedList<>();
    List<List<Integer>> res = new ArrayList<>();
    public List<List<Integer>> combine(int n, int k) {
        backTracing(1,n,k);
        return res;
    }

    public void backTracing(int startIndex,int n, int k){
        if(arr.size()==k){
            // 注意这里要用new ArrayList
            res.add(new ArrayList<>(arr));
            return;
        }
        for(int i = startIndex;i<=n;i++){
            arr.add(i);
            backTracing(i+1,n,k);
            arr.removeLast();
        }
        return;
    }
}
// 剪枝优化 
// 注意:剪枝优化也无法改变其暴力搜索的本质
// 在arr中已经搜到了arr.size()个元素的情况下,
// 至多从n-(k-arr.size())+1的位置开始搜索是满足条件的,即完成剪枝操作
for(int i = startIndex;i<=n-(k-arr.size())+1;i++){ //剪枝操作只在遍历边界上不同,其他一样
	arr.add(i);
	backTracing(i+1,n,k);
	arr.removeLast();
}

2.【216】组合总和 III(中)

题目链接: 组合总和问题

题目描述:
找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。
说明: 所有数字都是正整数。解集不能包含重复的组合。
示例 1: 输入: k = 3, n = 7 输出: [[1,2,4]]
示例 2: 输入: k = 3, n = 9 输出: [[1,2,6], [1,3,5], [2,3,4]

涉及知识点: 回溯【组合问题】、剪枝

思路: 是原生组合问题的衍生,多了一个求和限制的条件,整体思路大差不差,但是这里的剪枝可以在两个地方进行:第一是判断当前总和是否已经超过目标和,超过就return;二是和组合问题一样,限制边界条件。

代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum3(int k, int n) {
        backTracing(1,k,n);
        return res;
    }

    public void backTracing(int startIndex, int k,int n){
        if(path.size()==k){
            if(sum == n){
                res.add(new ArrayList(path));
                return;
            }else{
                return;
            }
        }
        // 剪枝操作1:比较当前和与目前和大小
        if(sum>n){
            return;
        }
        for(int i = startIndex;i<=10-(k-path.size());i++){ //剪枝操作2:限制边界
            path.add(i);
            sum+=i;
            backTracing(i+1,k,n);
            path.removeLast();
            sum-=i;
        }
        return;
    }
}

3.【17】电话号码的字母组合(中)

题目链接: 根据电话号码进行字母组合

题目描述:
给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。答案可以按任意顺序返回。
给出数字到字母的映射如下(与电话按键相同)。注意: 1 不对应任何字母。
在这里插入图片描述

涉及知识点: 回溯【组合问题】

思路: 这个题稍微有一点点绕,要先用数组或map等形式建立起数字和字母的映射关系,记为dict,根据传入的digitis,如"23",要先找到数字2和3在dict中对应的有哪些字母,然后用for循环去遍历2对应的字母,在递归中去遍历3对应的字母。这和前面在一个集合中进行组合的问题不太一样,在同一个组合中需要有一个startIndex来标记当前遍历元素的下一个位置,这里则是需要一个index来标记遍历到了第几个数字,而对于每个数字对应的字母集合则可以从头遍历到尾。

代码:

class Solution {
    List<String> res = new ArrayList<>(); //放结果
    StringBuilder temp = new StringBuilder(); //用来临时存放字母组合
    // 需要用到数组进行映射
    String[] dict = {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};
    public List<String> letterCombinations(String digits) {
        backTracing(digits,0);
        return res;
    }
    
    public void backTracing(String digits,int index){
    	/*注意:这里写成如下形式是错误的,会导致digits=""的测试用例无法通过
    		if(digits == ""){
           		return;
        	}
    	*/
        if(digits == null || digits.length() == 0){
            return;
        }
        if(index==digits.length()){
            res.add(temp.toString());
            return;
        }
        String letters = dict[digits.charAt(index)-'0'];
        for(int i=0;i<letters.length();i++){
            temp.append(letters.charAt(i));
            backTracing(digits,index+1);
            temp.deleteCharAt(temp.length()-1);
        }
    }
}

4.【39】组合总和(中)

题目链接: 组合总和问题

题目描述:
给你一个 无重复元素 的整数数组 candidates 和一个目标整数 target ,找出 candidates 中可以使数字和为目标数 target 的 所有不同组合 ,并以列表形式返回。你可以按任意顺序返回这些组合。candidates 中的同一个数字可以无限制重复被选取 。如果至少一个数字的被选数量不同,则两种组合是不同的。 对于给定的输入,保证和为target的不同组合数少于 150 个。

涉及知识点: 回溯【组合问题】

思路: 本题的解法也是基本套用回溯算法的模板,和组合总和 III相比,本题没有对元素个数进行限制,因此只能靠元素加和sum来控制递归深度。另一点不同在于本题允许元素重复,那么每次递归的时候都是从头递归,这点是靠startIndex来控制的,比如说当前for循环遍历到数组的第i个元素,那么本次循环的递归中传入的startIndex就是i。
树化结构
代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int sum = 0;
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        backTracing(candidates,target,0);
        return res;
    }

    public void backTracing(int[] candidates, int target, int startIndex){
    	/*注意这里判断sum>target一定不能忘写,第一遍就是没写这个条件,会导致栈溢出*/
       if(sum>target){
           return;
       }
       if(sum==target){
           res.add(new ArrayList(path));
           return;
        }
        for(int i=index;i<candidates.length;i++){
            path.add(candidates[i]);
            sum+=candidates[i];
            backTracing(candidates,target,i);
            sum-=candidates[i];
            path.removeLast();
        }
        return;
    }
}

5.【40】组合总和 II(中)

题目链接: 组合总和问题(组合不能重复)

题目描述:
给定一个候选人编号的集合candidates和一个目标数target,找出candidates 中所有可以使数字和为 target 的组合。candidates 中的每个数字在每个组合中只能使用 一次 。

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

涉及知识点: 回溯【组合问题】、去重

思路: 本题的关键点和难点在于去重操作,总体上有两种方法,在去重操作前要先对原数组进行排序,确保相同的元素是相邻的。
第一种方法是借助used数组,将已经遍历过的点对应的used数组中的值置1,作为标记,那么在树层遍历中(即for循环的横向遍历),如果当前i对应的元素等于i-1对应的元素(n>0情况下)&&used[i-1]==0,这说明将要进行的是树层去重操作,如果满足前述条件,则continue。这里需要used[i-1]==0的原因是防止在树枝上进行去重,因为在树枝上,第i-1个元素一定是used,而在树枝上的元素重复是允许的,所以要加上这个限制条件防止误去重。
第二种方法不再借助used数组,直接利用startIndex进行判断,当i大于startIndex时,说明是在进行树层遍历,这种写法更简洁,但需要好好理解一下。
下图给出了树层遍历和树枝遍历的区别:
借助used数组1
在这里插入图片描述
代码:

// 使用used数组的方法
class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    int[] used;
    int sum = 0;
    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        // 去重操作前要对数组排序
        used = new int[candidates.length];
        Arrays.sort(candidates);
        backTracing(candidates, target, 0);
        return res;
    }
    
    public void backTracing(int[] candidates, int target, int startIndex){
        if(sum>target){
            return;
        }
        if(sum==target){
            res.add(new ArrayList(path));
            return;
        }
        for(int i = startIndex;i<candidates.length;i++){
            // 利用used数组判断该元素是否重复
            if(i>0&&candidates[i]==candidates[i-1]&&used[i-1]==0){
                continue;
            }
            path.add(candidates[i]);
            sum += candidates[i];
            used[i]=1;
            backTracing(candidates,target,i+1);
            path.removeLast();
            sum-=candidates[i];
            used[i] = 0;
        }
    }
}
// 不使用used数组
// 其主要的不同点在于判断元素是否重复那里
if(i>startIndex&&candidates[i]==candidates[i-1]){
	continue;
}

2022-12-02

6.【131】分割回文串(中)

题目链接: 分割回文串问题

题目描述:
给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文串 。返回 s 所有可能的分割方案。回文串是正着读和反着读都一样的字符串。

涉及知识点: 回溯【分割问题】、回文串判断

思路: 分割问题和前面组合问题的遍历过程是类似的,不同的是分割问题中的startIndex要作为分割线,[startIndex,i]是当前子串的区间,想清楚这一点题就能写出来了。另外,判断回文数可以用双指针,也很简单。
切割图示

代码:

class Solution {
    List<List<String>> res = new ArrayList<>();
    LinkedList<String> path = new LinkedList<>();
    public List<List<String>> partition(String s) {
        backTracing(s,0);
        return res;
    }

    public void backTracing(String s,int startIndex){
        if(startIndex>=s.length()){
            res.add(new ArrayList(path));
            return;
        }    
        for(int i = startIndex;i<s.length();i++){
        // 子串所在的区间为[startIndex,i]
            if(isPalin(s,startIndex,i)){
                String str = s.substring(startIndex, i + 1);
                path.add(str);
            }else{
                continue;
            }
            backTracing(s,i+1); //这里传的是i+1,不是startIndex+1
            path.removeLast();         
        }
    }
	// 判断是否回文
    public boolean isPalin(String s,int start,int end){
        for(int i = start, j = end;i<j;i++,j--){
            if(s.charAt(i)!=s.charAt(j)){
                return false;
            }
        }
        return true;
    }
}

7.【93】复原 IP 地址(中)

题目链接: 复原 IP 地址问题

题目描述:
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 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 中的任何数字。你可以按任何顺序返回答案。

涉及知识点: 回溯【分割问题】、字符串操作

思路: 本题和上一个分割问题是类似的,但这里不同的是要用加“.”的次数来控制递归结束,因为是四个子串,所以“.”的数量为3,在加“.”的同时用一个pointSum来进行计数,当达到3时就说明到了终止条件,注意:这里还要再判断一下剩下的子串是否满足IP地址的要求。此外,本题里涉及到对字符串的一些拼接操作。
切割图示

代码:

// 这种写法并不是最佳的,代码随想录上有时间复杂度更低的算法,是利用StringBuilder做的。
class Solution {
    List<String> res = new ArrayList<>();
    int pointSum = 0;
    public List<String> restoreIpAddresses(String s) {
        if(s.length()>12){ //相当于剪枝操作
            return res;
        }
        backTracing(s,0);
        return res;
    }

    public void backTracing(String s, int startIndex){
        if(pointSum==3){
            if(isIP(s,startIndex,s.length()-1)){
                res.add(s);
            }
            return;      
        }
        for(int i = startIndex;i<s.length();i++){
            if(isIP(s,startIndex,i)){
                //重构字符串
                s = s.substring(0, i + 1)+"."+s.substring(i+1); 
                pointSum++;
                backTracing(s,i+2);
                s= s.substring(0,i + 1)+ s.substring(i +2);
                pointSum--;
            }else{
                break;
            }     
        }
    }

    public boolean isIP(String s,int start,int end){
        int sum = 0;
        if(start>end){
            return false;
        }
        if(s.charAt(start)=='0'&&start!=end){
            return false;
        }
        for(int i=start;i<=end;i++){
            if(s.charAt(i)>'9'||s.charAt(i)<'0'){
                return false;
            }
            sum =sum*10+(s.charAt(i)-'0');
            if(sum>255){
                return false;
            }
        }        
        return true;
    }
}

2022-12-03

8.【73】子集(中)

题目链接: 子集问题

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

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

涉及知识点: 回溯【子集问题】

思路: 本题前面那些回溯问题的不同在于收获结果是在每一个节点,而不是在叶子节点,明白了这个道理解题就比较容易了。不过要明确收获结果的位置,如果加了终止添加,那么要写在终止条件上面。注: 本题的终止条件也可以不写,因为有for循环控制着,但要在循环后面写return。
子集

代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList sub = new LinkedList<>();
    public List<List<Integer>> subsets(int[] nums) {
        backTracing(nums,0);
        return res;
    }

    public void backTracing(int[] nums, int startIndex){
        res.add(new ArrayList(sub));
        if(startIndex==nums.length){ //这个条件可以不写
            return;
        }
        for(int i=startIndex;i<nums.length;i++){
            sub.add(nums[i]);
            backTracing(nums,i+1);            
            sub.removeLast();
        }
        return;
    }
}

9.【90】子集 II(中)

题目链接: 子集问题进阶

题目描述:
给你一个整数数组nums ,其中可能包含重复元素,请你返回该数组所有可能的子集(幂集)。

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

涉及知识点: 回溯【子集问题】、去重

思路: 本题和上一个子集问题的解题方式类似,只是要考虑树层遍历中不能重,去重思路参考组合总和 II

代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> sub = new LinkedList<>();
    public List<List<Integer>> subsetsWithDup(int[] nums) {
    	// 先排序
        Arrays.sort(nums);
        backTracing(nums,0);
        return res;
    }

    public void backTracing(int[] nums,int startIndex){
        res.add(new ArrayList(sub));
        if(startIndex==nums.length){
            return;
        }
        for(int i = startIndex;i<nums.length;i++){
            if(i>0&&nums[i]==nums[i-1]&&i>startIndex){
                continue;
            }
            sub.add(nums[i]);
            backTracing(nums,i+1);
            sub.removeLast();
        }
    }
}

100题纪念(2022-12-03)
在这里插入图片描述

10.【491】递增子序列(中)

题目链接: 递增子序列问题

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

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

涉及知识点: 回溯【子集问题】、去重

思路: 本题的去重思路和前面的思路不太一样,因为这里的序列顺序是给定的不能随意更改,所有就不能先排序再去看当前值是否和前一个值相同。所以考虑在每一树层借助HashMap去记录前面出现过的元素,如果当前层已经出现过就跳过本轮循环,进入下一个。

代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> sub = new LinkedList<>();
    public List<List<Integer>> findSubsequences(int[] nums) {
        backTracing(nums,0);
        return res;
    }

    public void backTracing(int[] nums,int startIndex){
        if(sub.size()>1){
            res.add(new ArrayList(sub));
        }
        HashMap<Integer,Integer> map = new HashMap<>();
        for(int i = startIndex;i<nums.length;i++){
            if(!sub.isEmpty() && nums[i]< sub.getLast()){ //不符合递增序列的情况
                continue;
            }
            // getOrDefault(Object,V)方法,如果map中有对应的key,则会返回其value,
            // 否则返回一个“默认值”
            if(map.getOrDefault( nums[i],0 ) >=1){ //元素重复的情况
                continue;
            }
            map.put(nums[i],map.getOrDefault( nums[i],0 )+1);
            sub.add(nums[i]);
            backTracing(nums,i+1);
            sub.removeLast();
        }
    }
}

2022-12-05

11.【46】全排列(中)

题目链接: 全排列问题

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

涉及知识点: 回溯【排列问题】

思路: 排列与组合的不同之处在于:排列是有顺序的,且排列中可以回过头去取前面还没用过的元素,只是在一条树枝路径上不能重复取已经取过的元素。排列问题用一个used数组来辅助比较好,用来标识哪些元素已经用过了。

代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>(); 
    public List<List<Integer>> permute(int[] nums) {
    	// 标识元素是否取过
        int[] used = new int[nums.length];
        backTracing(nums,used);
        return res;
    }

    public void backTracing(int[] nums,int[] used){
        if(path.size()==nums.length){
            res.add(new ArrayList(path));
            return;
        }
        for(int i = 0;i<nums.length;i++){
            if(used[i]!=0){
               continue; 
            }
            path.add(nums[i]);
            used[i]=1;
            backTracing(nums,used);
            used[i]=0;
            path.removeLast();
        }
    }
}

12.【46】全排列 II(中)

题目链接: 全排列问题进阶

题目描述:
给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列

涉及知识点: 回溯【排列问题】、去重

思路: 本题和上题相比多了一个去重的操作,去重的思路和前面组合问题中的一样,也是先排序,然后借助used数组在树层上去重

代码:

class Solution {
    List<List<Integer>> res = new ArrayList<>();
    LinkedList<Integer> path = new LinkedList<>();
    public List<List<Integer>> permuteUnique(int[] nums) {
        int[] used = new int[nums.length];
        Arrays.sort(nums);
        backTracing(nums,used);
        return res;
    }

    public void backTracing(int[] nums,int[] used){
        if(path.size()==nums.length){
            res.add(new ArrayList(path));
            return;
        }
        for(int i = 0;i<nums.length;i++){
            if(used[i]!=0||(i>0&&nums[i]==nums[i-1]&&used[i-1]==0)){
                continue;
            }
            path.add(nums[i]);
            used[i]=1;
            backTracing(nums,used);
            used[i]=0;
            path.removeLast();
        }
    }
}

13.【51】N皇后(难)

题目链接: N皇后问题

题目描述:
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
(即:同一行同一列同一对角线都不能出现两个皇后)

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 ‘Q’ 和 ‘.’ 分别代表了皇后和空位。

涉及知识点: 回溯、N皇后条件判断

思路: N皇后问题的核心还是在于回溯,我觉得比较难的部分在于字符串的相关操作以及判断N皇后条件,前者是因为对字符串和列表的相关方法不熟练,后者则是因为涉及对角线的包括了45度和135度,不细心的情况下很容易漏掉。

代码:

class Solution {
    List<List<String>> res = new ArrayList<>();
    public List<List<String>> solveNQueens(int n) {
        char[][] chessboard = new char[n][n];
        for (char[] c : chessboard) {
            Arrays.fill(c, '.');
        }
        backTracing(n, 0, chessboard);
        return res;
    }

    public void backTracing(int n,int row,char[][] chessboard){
        if(row==n){
            List<String> list = new ArrayList<>();
            for(char[] c:chessboard){
                list.add(String.copyValueOf(c));
            }
            res.add(list);
            return;
        }
        for(int col = 0;col<n;col++){
            if(isValid(chessboard,row,col,n)){
                chessboard[row][col]='Q';
                backTracing(n,row+1,chessboard);
                chessboard[row][col] = '.';
            }
        }
    }

    public boolean isValid(char[][] chessboard,int row,int col,int n){
        for(int i = 0;i<row;i++){
            if(chessboard[i][col]=='Q'){
                return false;
            }
        }
        // 检查45度对角
        for(int i = row-1 ,j = col+1; i>=0&&j<n;i--,j++){
            if(chessboard[i][j]=='Q'){
                return false;
            }
        }
        // 检查135度对角
        for(int i = row-1,j=col-1;i>=0&&j>=0;i--,j--){
            if(chessboard[i][j]=='Q'){
                return false;
            }
        }
        return true;
    }
}

14.【37】解数独(难)

题目链接: 解数独

题目描述:
编写一个程序,通过填充空格来解决数独问题。

数独的解法需 遵循如下规则:
数字 1-9 在每一行只能出现一次。
数字 1-9 在每一列只能出现一次。
数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。(请参考示例图)
数独部分空格内已填入了数字,空白格用 ‘.’ 表示。
数独示例

涉及知识点: 回溯、二维递归

思路: 这道题和N皇后隐隐约约有点像,但实际上,N皇后只需要每行确定一个位置,而数独问题需要在每一个位置都找到能填入的数,所以要用到一个二维循环来遍历每个位置[row,col],再用一个循环确定当前位置的值能填几,确定之后进行递归,这里的下一层实际上是“确定了[row,col]位置的数值后对应的棋盘”。另外,本题中只要找到一种数独填法就可以,所以递归中要有一个boolean类型返回值,一旦找到解就立刻向上返回。

代码:

class Solution {
    public void solveSudoku(char[][] board) {
        backTracing(board);
    }
    
    public boolean backTracing(char[][] board){
        // 二维循环用来遍历每个位置位置
        for(int row = 0;row<9;row++){
            for(int col = 0;col<9;col++){
                if(board[row][col]!='.'){
                    continue;
                }
                // 用来确定该位置的数
                for(char k = '1'; k<='9';k++){
                    if(isValid(k,row,col,board)){
                        board[row][col]=k;
                        if(backTracing(board)){
                            return true;
                        }  
                        board[row][col]='.';
                    }
                }
                return false;     
            }
        }
        return true;
    }
    
    public boolean isValid(char ch,int row,int col,char[][] board){
        // 验证列是否出现过
        for(int i = 0;i<9;i++){
            if(board[i][col]==ch){
                return false;
            }
        }
        // 验证行是否出现过
        for(int j = 0;j<9;j++){
            if(board[row][j]==ch){
                return false;
            }
        }
        // 验证3*3宫内
        int a = (row/3)*3;
        int b = (col/3)*3;
        for(int i = a;i<a+3;i++){
            for(int j = b;j<b+3;j++)
            if(board[i][j]==ch){
                return false;
            }
        }
        return true;
    }
}

2022-12-06

【知识点】贪心理论

基本思想

贪心算法的本质是选择每一阶段的局部最优,从而达到全局最优。贪心算法并没有固定的套路,需要靠自己手动模拟,如果模拟可行,并且举不出反例,那就就可以试一试贪心策略,如果不可行,可能需要动态规划。

15.【455】分发饼干(易)

题目链接: 分发饼干问题

题目描述:
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干j分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

涉及知识点: 贪心算法

思路: 本题比较简单,适合用来入门贪心算法,这里的局部最优就是大饼干喂给胃口大的(或是小饼干喂给小胃口的),全局最优就是喂饱尽可能多的小孩。利用排序+双指针就能很方便解出。

代码:

class Solution {
    public int findContentChildren(int[] g, int[] s) {
        int sum = 0;
        int sPoint = 0;
        Arrays.sort(g);
        Arrays.sort(s);
        for(int i = 0;i<s.length;i++){
            // 小饼干喂饱小胃口
            if(s[i]>=g[sPoint]){
                sum+=1;
                sPoint+=1;
                if(sPoint==g.length){
                    break;
                }
            }
        }
        return sum;
    }
}

16.【1005】K 次取反后最大化的数组和(易)

题目链接: K次反转

题目描述:
给你一个整数数组nums和一个整数k,按以下方法修改该数组:

选择某个下标 i 并将 nums[i] 替换为 -nums[i]
重复这个过程恰好 k 次。可以多次选择同一个下标 i

以这种方式修改数组后,返回数组 可能的最大和 。

涉及知识点: 贪心算法、IntStream

思路: 本题也不难,本题的局部最优是让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。最开始想的有点绕:先将数组从小到大排列,然后统计负数个数negLen,与k比较,如果k-negLen>0且为偶数,则将所有负值转为正值相加;若k-negLen>0且为奇数,则选出绝对值最小的值,将其置为负数;若k-negLen<0,则对前k个绝对值最大的负值变正。这种方法虽然能写,但是要考虑很多细节,提交了好几次才勉强通过后面看了代码随想录的代码,里面是按数值的绝对值大小从大到小排序的,这样在从头遍历的时候,在k>0的情况下,遇到负数就将其变正,这样就能实现局部最优,遍历完成后再看剩余的k是否为奇数,如果是则选排序后数值的最后一个元素(即绝对值最小的数字)将其置为负。代码随想录中用到了IntStream来进行排序,是之前没有接触过的东西。

代码:

// 笨方法
class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        int sum = 0;
        Arrays.sort(nums);
        int negLen = 0;
        for(int i = 0;i<nums.length;i++){
            if(nums[i]>0){
                break;
            }
            negLen++;
        }
        
        int diff = k-negLen;
        if(diff>0){
            if(diff%2==0){
                for(int i = 0;i<nums.length;i++){
                    sum += Math.abs(nums[i]);
                }
            }else{
                int min = 0;
                if(negLen==0){
                    min = nums[0];
                }else{
                    if(negLen<nums.length){
                        min = Math.abs(nums[negLen-1])<nums[negLen]?Math.abs(nums[negLen-1]):nums[negLen];
                    }else{
                        min = Math.abs(nums[negLen-1]);
                    }   
                }
                for(int i = 0;i<nums.length;i++){
                    sum += Math.abs(nums[i]);
                }
                sum-=2*min;    
            }
        }else{
            for(int i = 0;i<k;i++){
                sum +=Math.abs(nums[i]);
            }
            for(int i = k;i<nums.length;i++){
                sum+=nums[i];
            }
        }        
        return sum;
    }
}
// 代码随想录方法
class Solution {
    public int largestSumAfterKNegations(int[] nums, int k) {
        nums = IntStream.of(nums)
		     .boxed()
		     .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1))
		     .mapToInt(Integer::intValue).toArray();
        for(int i = 0;i<nums.length;i++){
            if(nums[i]<0&&k>0){
                nums[i] = -nums[i];
                k--;
            }
        }
        if(k%2==1){
            nums[nums.length-1] = -nums[nums.length-1];
        }
        return Arrays.stream(nums).sum();
    }
}

2022-12-07

17.【322】重新安排行程 (难)

题目链接: 飞机航班问题

题目描述:
给你一份航线列表tickets ,其中 tickets[i] = [fromi, toi] 表示飞机出发和降落的机场地点。请你对该行程进行重新规划排序。

所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。如果存在多种有效的行程,请你按字典排序返回最小的行程组合。

例如,行程 [“JFK”, “LGA”] 与 [“JFK”, “LGB”] 相比就更小,排序更靠前。
假定所有机票至少存在一种合理的行程。且所有的机票 必须都用一次 且 只能用一次。

涉及知识点: 回溯、深搜

思路: 这道题据说是图论的深度优先搜索中使用回溯的例子,但是现在对图论还不太了解,所以还是暂时以回溯会重点。这道题重点在于对容器的使用上,要合理建立航程起点和终点的映射关系,代码中,Map<String,Map<String,Integer>> map记录航程起点和终点及终点出现次数,其中的Map<String,Integer>即对应着代码中用Map<String,Integer> temp来记录航程终点及其在当前起点下的出现次数。整体思路基本上还是回溯的思想。

代码:

class Solution {
    Map<String,Map<String,Integer>> map;
    Deque<String> res;
    public List<String> findItinerary(List<List<String>> tickets) {
        map = new HashMap<String, Map<String, Integer>>();
        res = new LinkedList<>();
        for(List<String> ticket:tickets){
            Map<String,Integer> temp;
            if(map.containsKey(ticket.get(0))){
                temp = map.get(ticket.get(0));
                temp.put(ticket.get(1),temp.getOrDefault(ticket.get(1),0)+1);
            }else{
                temp = new TreeMap<>(); //要注意需要排序
                temp.put(ticket.get(1),1);
            }
            map.put(ticket.get(0),temp);         
        }
        res.add("JFK");
        backTracing(tickets.size());
        return new ArrayList(res);
    }
    public boolean backTracing(int ticketNum){
        // 当结果长度等于航班数+1时,说明达到终止条件
        if(res.size()==ticketNum+1){
            return true;
        }
        String last = res.getLast();
        if(map.containsKey(last)){
            for(Map.Entry<String,Integer> target: map.get(last).entrySet()){
                int count = target.getValue();
                if(count>0){
                    res.add(target.getKey());
                    target.setValue(count-1);
                    if(backTracing(ticketNum)){
                        return true;
                    }
                    target.setValue(count);
                    res.removeLast();
                }
            }
        }
        return false;          
    }  
}

18.【806】柠檬水找零(易)

题目链接: 柠檬水找零问题

题目描述:
在柠檬水摊上,每一杯柠檬水的售价为 5 美元。顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。

每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。

注意,一开始你手头没有任何零钱。

给你一个整数数组 bills ,其中 bills[i] 是第 i 位顾客付的账。如果你能给每位顾客正确找零,返回 true ,否则返回 false
涉及知识点: 贪心

思路: 这道题的策略比较简单,所以看起来只要把这个过程模拟出来就行,但是这里也体现着贪心的思想,因为5美元适用的情况多,所以要优先消耗10美元而不是5美元,则局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零

代码:

class Solution {
    public boolean lemonadeChange(int[] bills) {
        int five = 0;
        int ten = 0;
        for(int i = 0;i<bills.length;i++){
            if(i==0&&bills[i]!=5){
                return false;
            }
            if(bills[i]==5){
                five++;
            }else if(bills[i]==10){
                five--;
                ten++;
                if(five<0){
                    return false;
                }
            }else{
                if(ten-1<0){
                    five -=3;
                }else{
                    ten--;
                    five--;
                }
                if(five<0){
                    return false;
                }
            }
        }
        return true;
    }
}

19.【376】摆动序列(中)

题目链接: 摆动序列问题

题目描述:
如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列 。第一个差(如果存在的话)可能是正数或负数。仅有一个元素或者含两个不等元素的序列也视作摆动序列。

  • 例如,[1, 7, 4, 9, 2, 5] 是一个 摆动序列 ,因为差值(6, -3, 5, -7, 3) 是正负交替出现的。
  • 相反,[1, 4, 7, 2, 5][1, 7, 4, 5, 5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。

子序列 可以通过从原始序列中删除一些(也可以不删)元素来获得,剩下的元素保持其原始顺序。

给你一个整数数组 nums ,返回 nums中作为 摆动序列 的 最长子序列的长度
涉及知识点: 贪心

思路: 本题的局部最优是删除单调坡度上的节点(不包括单调坡度两端的节点);整体最优是整个序列有最多的局部峰值,从而达到最长摆动序列。本题的贪心思路是比较符合直觉的,主要是自己实现的代码写的比较复杂且结果不对,还是要多锻炼。另外,本题还可以用动态规划来解,但是现在还不太会动态规划,等到动态规划章节再看。
子序列
代码:

// 代码随想录的贪心代码
class Solution {
    public int wiggleMaxLength(int[] nums) {
        int count = 1;
        int preDiff = 0;
        int curDiff = 0;
        for(int i = 1; i < nums.length; i++){
            curDiff  = nums[i]-nums[i-1];
            if(curDiff>0&&preDiff<=0||curDiff<0&&preDiff>=0){
                count++;
                preDiff = curDiff;
            }
        }
        return count;
    }
}

20.【738】单调递增的数字(中)

题目链接: 单调递增数字

题目描述:
当且仅当每个相邻位数上的数字 x y 满足 x <= y 时,我们称这个整数是单调递增的。
给定一个整数 n ,返回 小于或等于 n 的最大数字,且数字呈 单调递增

涉及知识点: 贪心

思路: 本题的局部最优:遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9,可以保证这两位变成最大单调递增整数。全局最优:得到小于等于N的最大单调递增的整数。把整数的每位拆开成字符可以方便操作,从后向前遍历,依次比较与前一位数的大小,如果小于前一位,那么前一位的值要减1,当前位的值要在后面变为9,记下当前下标作为“变9”的起始下标,重复上述过程。

代码:

// 代码随想录的贪心代码
class Solution {
    public int monotoneIncreasingDigits(int n) {
        String s = String.valueOf(n);
        char[] chars = s.toCharArray();
        int start = s.length();
        for(int i = s.length()-1; i > 0; i--){
            if(chars[i]<chars[i-1]){
                chars[i-1] -= 1;
                start = i;
            }
        }
        for(int i = start; i < s.length(); i++){
            chars[i] = '9';
        }
        return Integer.parseInt(String.valueOf(chars));
    }
}

2022-12-08

21.【122】买卖股票的最佳时机 II (中)

题目链接: 买卖股票问题

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

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

涉及知识点: 贪心

思路: 这道题只需要返回能获得的最大利润,其实可以将从第i天第j天获得的利润分解,即prices[j]-prices[i]=prices[j]-prices[j-1]+prices[j-1]-prices[j-2]+...+prices[i+1]-prices[i],因此只要保证每次交易都是正利润即可,局部最优:收集每天的正利润,全局最优:求得最大利润。也可以用动态规划。

代码:

// 贪心
class Solution {
    public int maxProfit(int[] prices) {
        // 只要收集正利润即可
        List<Integer> income = new ArrayList<>();
        int sum = 0;
        for(int i = 1;i<prices.length;i++){
            income.add(prices[i]-prices[i-1]);
        }
        for(int i = 0;i<income.size();i++){
            if(income.get(i)>0){
                sum+=income.get(i);
            }
        }
        return sum;
    }
}

22.【714】 买卖股票的最佳时机含手续费 (中)

题目链接: 买卖股票含手续费问题

题目描述:
给定一个整数数组 prices,其中 prices[i]表示第 i天的股票价格;整数 fee 代表了交易股票的手续费用。

你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。返回获得利润的最大值。

注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。

涉及知识点: 贪心

**思路:**这道题其实用动态规划更好理解,用贪心的话会有一点点绕。用prices[i]+fee作为第i天的买入价格buy,如果遇到prices[j]+fee<buy,则令buy =prices[j]+fee,即选择在第j天买比在第i天买更能获得大的收益。若prices[j]>buy,说明在第j天卖出是可以获得正利润的,但是不是最大的还不知道,所以这里有一个“反悔操作”,即可以假定在第j天卖出,然后以prices[j]作为新的buy(这里不用加fee是因为只是假设要卖,不是真卖,所以相当于fee在前面已经出过了),然后观望后面的价格,如果prices[k]比buy大,则说明后面卖利润更高,不用加fee代表着可以撤销在第j天的卖出操作,放到第k天卖,重复上述过程。
反悔操作:prices[k]-(prices[i]+fee) = prices[k]-prices[j]+prices[j]-(prices[i]+fee)
代码:

class Solution {
    public int maxProfit(int[] prices, int fee) {
        int buy = prices[0]+fee;
        int sum = 0;
        for(int i = 0;i<prices.length;i++){
            if(prices[i]+fee<buy){
                buy = prices[i]+fee;
            }else if(prices[i]>buy){
                sum += prices[i]-buy;
                buy = prices[i]; //反悔操作
            }
        }
        return sum;
    }
}

23.【135】分发糖果 (难)

题目链接: 分发糖果问题

题目描述:
n 个孩子站成一排。给你一个整数数组 ratings 表示每个孩子的评分。

你需要按照以下要求,给这些孩子分发糖果:

  • 每个孩子至少分配到1个糖果。
  • 相邻两个孩子评分更高的孩子会获得更多的糖果。

请你给每个孩子分发糖果,计算并返回需要准备的 最少糖果数目 。

涉及知识点: 贪心

思路: 这道题不能想着一次遍历就同时兼顾左右相邻的孩子,最开始就是想兼顾所以钻进牛角尖。应该从前向后遍历来比较每个孩子的评分是否大于其左边孩子,然后再从后向前遍历比较每个孩子的评分是否大于其右边孩子,这样就可以全面考虑到左右的情况。

代码:

class Solution {
    // 要确定一个方向的糖果数量后再确定另一边,不然会顾不过来
    public int candy(int[] ratings) {
        int sum = 0;
        int[] candys = new int[ratings.length];
        candys[0] = 1;
        // 前向遍历,看右边孩子评分是否比当前孩子高
        for(int i = 0;i<ratings.length-1;i++){
            if(ratings[i]<ratings[i+1]){
                candys[i+1] = candys[i]+1;
            }else{
                candys[i+1] = 1; 
            }
        }
        // 后向遍历,看左边孩子评分是否比当前孩子高
        for(int i = ratings.length-1;i>0;i--){
            if(ratings[i]<ratings[i-1]){
                if(candys[i-1]<candys[i]+1){
                    candys[i-1]=candys[i]+1;
                }
            }
        }
        for(int candy:candys){
            sum += candy;
        }
        return sum;
    }
}

24.【406】根据身高重建队列 (中)

题目链接: 根据身高重建队列

题目描述:
假设有打乱顺序的一群人站成一个队列,数组people 表示队列中一些人的属性(不一定按顺序)。每个people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面正好有 ki 个身高大于或等于hi 的人。

请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。

涉及知识点: 贪心

思路: 这道题理解起来稍微有点绕,和分糖果题类似,也是涉及到两个维度hi, ki,如果同时兼顾就会顾此失彼,所以还是要先从一个维度出发。这里要先从身高hi出发从高到底排序(身高相同时ki小的站前面),这样排序后,后面的人就算站到前面人前面,也不会对前面人的属性产生影响,于是就可以按他们的ki进行插空,这样可以保证前面有ki个身高大于等于他的人。

代码:

class Solution {
    public int[][] reconstructQueue(int[][] people) {
        // 按身高从大到小排,若身高相同k小的站前面
        Arrays.sort(people, (a, b) -> {
            if (a[0] == b[0]) return a[1] - b[1];
            return b[0] - a[0];
        });
        LinkedList<int[]> que = new LinkedList<>();
        for(int[] p:people){
            que.add(p[1],p);
        }
        return que.toArray(new int[people.length][]);
    }
}

2022-12-09

25.【55】跳跃游戏 (中)

题目链接: 跳跃游戏

题目描述:
给定一个非负整数数组 nums ,你最初位于数组的第一个下标 数组中的每个元素代表你在该位置可以跳跃的最大长度。
判断你是否能够到达最后一个下标。

涉及知识点: 贪心

思路: 这道题如果拘泥于每次跳几步的问题就想偏了,其实不需要具体看走几步,只需要看每次能走的最大范围,最后看最大范围是否包括终点位置即可。局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点。

代码:

class Solution {
    // 不用拘泥于跳几步,只用看能跳的范围有没有覆盖到终点
    public boolean canJump(int[] nums) {
        if (nums.length == 1) {
            return true;
        }
        int end = 0;
        for(int i = 0;i<=end;i++){
            if(nums[i]+i>end){
                end = nums[i]+i;
            }
            if(end>=nums.length-1){
                return true;
            }
        }
        return false;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值