《代码随想录》刷题笔记——回溯篇【java实现】

news2025/2/21 17:22:17

文章目录

  • 组合
  • 组合总和 III
  • 电话号码的字母组合
  • 组合总和
  • 组合总和II
    • 思路
    • 代码实现
  • 分割回文串※
    • 思路
      • 字符串分割
      • 回文串判断
      • 效率优化※
  • 复原 IP 地址
    • 优化版本
  • 子集
  • 子集 II
    • 使用usedArr辅助去重
    • 不使用usedArr辅助去重
  • 递增子序列※
  • 全排列
  • 全排列 II
  • 重新安排行程
    • 题意
    • 代码
  • N 皇后
  • 解数独
    • 直接遍历
    • 优化

组合

https://leetcode.cn/problems/combinations/description/

在这里插入图片描述

public List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> result = new ArrayList<>();
    // 存储搜索路径
    List<Integer> path = new ArrayList<>();
    // 从1开始递归
    backtrack(n, k, 1, path, result);
    return result;
}

void backtrack(int n, int k, int startNum, List<Integer> path, List<List<Integer>> result) {
    if (path.size() == k) {
        // --if-- 组合的元素个数=k,说明找到了新的解,存储到result中
        List<Integer> clone = new ArrayList<>(k);
        for (Integer num : path) {
            clone.add(num);
        }
        result.add(clone);
        return;
    }
    for (int num = startNum; num <= n; num++) {
        // --for--遍历加下来的元素
        // 将遍历到的元素添加到搜索路径中
        path.add(num);
        // 递归,开始数量+1
        backtrack(n, k, num + 1, path, result);
        // 移除刚刚添加的数量
        path.remove(path.size() - 1);
    }
}

在这里插入图片描述

List<Integer> clone = new ArrayList<>(k);
for (Integer num : path) {
    clone.add(num);
}
result.add(clone);

上面的实现可以简化为

result.add(new ArrayList<>(path));

通过查看源码可以发现,构造方法传入集合的时候,JDK会自动帮我们做克隆操作

/**
 * Constructs a list containing the elements of the specified
 * collection, in the order they are returned by the collection's
 * iterator.
 *
 * @param c the collection whose elements are to be placed into this list
 * @throws NullPointerException if the specified collection is null
 */
public ArrayList(Collection<? extends E> c) {
    Object[] a = c.toArray();
    if ((size = a.length) != 0) {
        if (c.getClass() == ArrayList.class) {
            elementData = a;
        } else {
            elementData = Arrays.copyOf(a, size, Object[].class);
        }
    } else {
        // replace with empty array.
        elementData = EMPTY_ELEMENTDATA;
    }
}

通过提交运行结果可以发现,上面的实现执行用时比较长,这是为什么呢?

假设n=5,k=3,组合可选元素为1,2,3,4,5

  • 当path=[]时,组合的第一个元素为4,后面只有一个5,继续递归遍历,也只能找到两个元素的组合,无法找找到达到长度要求的组合(4、5无需遍历);
  • 当path=[1]时,组合的第二个元素为5,后面没有元素,无法找找到达到长度要求的组合(5无需遍历);

因此通过减少部分遍历来加快代码速度

public List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> result = new ArrayList<>();
    // 存储搜索路径
    List<Integer> path = new ArrayList<>();
    // 从1开始递归
    backtrack(n, k, 1, path, result);
    return result;
}

void backtrack(int n, int k, int startNum, List<Integer> path, List<List<Integer>> result) {
    if (path.size() == k) {
        // --if-- 组合的元素个数=k,说明找到了新的解,存储到result中
        result.add(new ArrayList<>(path));
        return;
    }
    for (int num = startNum; num <= n - (k - path.size()) + 1; num++) {
        // --for--遍历加下来的元素
        // 将遍历到的元素添加到搜索路径中
        path.add(num);
        // 递归,开始数量+1
        backtrack(n, k, num + 1, path, result);
        // 移除刚刚添加的数量
        path.remove(path.size() - 1);
    }
}

在这里插入图片描述

在创建List<Integer> path = new ArrayList<>();,因为path的长度已知,因此可以在初始化集合的时候传入集合长度,避免后面发生集合扩容,List<Integer> path = new ArrayList<>(k);

组合总和 III

https://leetcode.cn/problems/combination-sum-iii/description/

在这里插入图片描述

public List<List<Integer>> combinationSum3(int k, int n) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    backtrack(k, n, 1, result, path, 0);
    return result;
}

private void backtrack(int k, int n, int startNum, List<List<Integer>> result, List<Integer> path, int sum) {
    if (path.size() == k) {
        // --if-- 搜索到的组合元素个数为k
        if (sum == n) {
            // --if-- sum等于n,存储组合
            result.add(new ArrayList<>(path));
        } else {
            // --if-- 元素个数已经为3,无需再往下递归,否则,元素个数超出k了,再搜索也是白费
            return;
        }
    }
    for (int i = startNum; i <= 9; i++) {
        path.add(i);
        backtrack(k, n, i + 1, result, path, sum + i);
        path.remove(path.size() - 1);
    }
}

在这里插入图片描述

【剪枝优化】

  • 剪枝1:path.size()>k
  • 剪枝2:sum>n
  • 剪枝3:无效组合起点,类似上一题,i <= 10- (k - path.size())
public List<List<Integer>> combinationSum3(int k, int n) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    backtrack(k, n, 1, result, path, 0);
    return result;
}

private void backtrack(int k, int n, int startNum, List<List<Integer>> result, List<Integer> path, int sum) {
    if (path.size() == k) {
        // --if-- 搜索到的组合元素个数为k
        if (sum == n) {
            // --if-- sum等于n,存储组合
            result.add(new ArrayList<>(path));
        } else {
            // --if-- 元素个数已经为3,无需再往下递归,否则,元素个数超出k了,再搜索也是白费
            return;
        }
    } else if (sum > n) {
        // --if-- 搜索到的组sum已经大于n,无需再往下搜索
        return;
    }
    for (int i = startNum; i <= 10- (k - path.size()) ; i++) {
        path.add(i);
        backtrack(k, n, i + 1, result, path, sum + i);
        path.remove(path.size() - 1);
    }
}

在这里插入图片描述

电话号码的字母组合

https://leetcode.cn/problems/letter-combinations-of-a-phone-number/description/

在这里插入图片描述

public List<String> letterCombinations(String digits) {
    if (digits.length() == 0) {
        return new ArrayList<>();
    }
    // 先将字符串转化为char数组,方便遍历操作
    char[] charArray = digits.toCharArray();
    // 存储结果
    List<String> result = new ArrayList<>();
    // 存储搜索组合(为什么使用数组呢,因为组合的元素数量是没有变化的,而且后面需要将元素组合转化为字符串,使用char数组更加方便)
    char[] path = new char[digits.length()];
    // 从1开始递归
    backtrack(charArray, 0, path, result);
    return result;
}

void backtrack(char[] charArray, int charIndex, char[] path, List<String> result) {
    if (charIndex == charArray.length) {
        // --if-- 数字已经遍历完成,存储结果
        result.add(new String(path));
        return;
    }
    char numChar = charArray[charIndex];
    // 根据输入的数字,转化为对应的字符数组
    char[] chooseCharArr = null;
    switch (numChar) {
        case '2':
            chooseCharArr = new char[]{'a', 'b', 'c'};
            break;
        case '3':
            chooseCharArr = new char[]{'d', 'e', 'f'};
            break;
        case '4':
            chooseCharArr = new char[]{'g', 'h', 'i'};
            break;
        case '5':
            chooseCharArr = new char[]{'j', 'k', 'l'};
            break;
        case '6':
            chooseCharArr = new char[]{'m', 'n', 'o'};
            break;
        case '7':
            chooseCharArr = new char[]{'p', 'q', 'r', 's'};
            break;
        case '8':
            chooseCharArr = new char[]{'t', 'u', 'v'};
            break;
        case '9':
            chooseCharArr = new char[]{'w', 'x', 'y', 'z'};
            break;
    }
    // 循环字符数组,挑选一个,并进入下一个数字对应的字符挑选
    for (int i = 0; i < chooseCharArr.length; i++) {
        path[charIndex] = chooseCharArr[i];
        backtrack(charArray, charIndex + 1, path, result);
        // 这里不需要回溯操作,因为下一次循环会主动将 path[charIndex] 替换成另一个元素
    }
}

在这里插入图片描述

【代码随想录实现】

//设置全局列表存储最后的结果
List<String> list = new ArrayList<>();

public List<String> letterCombinations(String digits) {
    if (digits == null || digits.length() == 0) {
        return list;
    }
    //初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串""
    String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"};
    //迭代处理
    backTracking(digits, numString, 0);
    return list;

}

//每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder
StringBuilder temp = new StringBuilder();

//比如digits如果为"23",num 为0,则str表示2对应的 abc
public void backTracking(String digits, String[] numString, int num) {
    //遍历全部一次记录一次得到的字符串
    if (num == digits.length()) {
        list.add(temp.toString());
        return;
    }
    //str 表示当前num对应的字符串
    String str = numString[digits.charAt(num) - '0'];
    for (int i = 0; i < str.length(); i++) {
        temp.append(str.charAt(i));
        //递归,处理下一层
        backTracking(digits, numString, num + 1);
        //剔除末尾的继续尝试
        temp.deleteCharAt(temp.length() - 1);
    }
}

组合总和

https://leetcode.cn/problems/combination-sum/description/

在这里插入图片描述

在这里插入图片描述

public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    backtrack(candidates, target, 0, result, path);
    return result;
}

private void backtrack(int[] candidates, int target, int startIndex,
                       List<List<Integer>> result, List<Integer> path) {
    if (target == 0) {
        result.add(new ArrayList<>(path));
        return;
    }
    for (int i = startIndex; i < candidates.length; i++) {
        // 计算剩下的target最多还可以容纳多少个 candidates[i] 
        int num = target / candidates[i];
        // 循环取不同个数 candidates[i] 的情况
        // 注意j从1开始,j=0的情况其实在i的循环中已经考虑到了,例如当i等于startIndex+1时,实际上就是startIndex对应的元素j=0
        for (int j = 1; j <= num; j++) {
            // 将j个元素添加到搜索路径中
            for (int k = 0; k < j; k++) {
                path.add(candidates[i]);
            }
            backtrack(candidates, target - candidates[i] * j, i + 1, result, path);
            // 将j个元素从搜索路径中移除
            for (int k = 0; k < j; k++) {
                path.remove(path.size() - 1);
            }
        }
    }
}

在这里插入图片描述

从结果可以分析出来,上面的代码还有效率问题

其中的一个原因就是如下代码,每次需要添加j个元素,然后移除j个元素

// 将j个元素添加到搜索路径中
for (int k = 0; k < j; k++) {
    path.add(candidates[i]);
}

// 将j个元素从搜索路径中移除
for (int k = 0; k < j; k++) {
    path.remove(path.size() - 1);
}

改成下面的代码,效率会更好一点

public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    backtrack(candidates, target, 0, result, path);
    return result;
}

private void backtrack(int[] candidates, int target, int startIndex,
                       List<List<Integer>> result, List<Integer> path) {
    if (target == 0) {
        result.add(new ArrayList<>(path));
        return;
    }
    for (int i = startIndex; i < candidates.length; i++) {
        if (target >= candidates[i]) {
            path.add(candidates[i]);
            backtrack(candidates, target - candidates[i], i, result, path);
            path.remove(path.size() - 1);
        }
    }
}

在这里插入图片描述

最后还存在一个剪枝策略,就是先将candidates升序排序,当遇到一个candidates[i]>target时,直接break出循环即可

 public List<List<Integer>> combinationSum(int[] candidates, int target) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    Arrays.sort(candidates);
    backtrack(candidates, target, 0, result, path);
    return result;
}

private void backtrack(int[] candidates, int target, int startIndex,
                       List<List<Integer>> result, List<Integer> path) {
    if (target == 0) {
        result.add(new ArrayList<>(path));
        return;
    }
    for (int i = startIndex; i < candidates.length; i++) {
        if (target >= candidates[i]) {
            path.add(candidates[i]);
            backtrack(candidates, target - candidates[i], i, result, path);
            path.remove(path.size() - 1);
        } else {
            break;
        }
    }
}

不过跑了很多次,还没有前面的效率高,可能是排序也比较耗时

在这里插入图片描述

组合总和II

https://leetcode.cn/problems/combination-sum-ii/description/

在这里插入图片描述

在这里插入图片描述

思路

如下图所示,常规的回溯方法会导致搜索到重复的组合,题目中要求不能出现重复组合。可以做一些去重操作来解决这个问题,但是性能肯定不好。我们应该在搜索的时候进行剪枝。

在这里插入图片描述

如下图所示,分支2 其实已经被 分支1 包含,因此算法在搜索过程,无需搜索分支2

在这里插入图片描述

如何识别出分支2呢,一个明显的特征是,candidates[1] = candidates[0] = 2,那当candidates[i] = candidates[i-1]时,就把i对应分支剪掉?

答案是不允许,因为这样的话,会导致分支1也没有搜索完全。如下图所示,分支a 和 分支b 都会被剪掉。如果target=7 的话,组合[2,2,3]就搜索不到了。因此需要区分 分支a 和 分支b ,分支b才是要被剪枝的。

在这里插入图片描述

那么如何区分 分支a分支b 呢?我使用一个数组useArr来帮助区分,useArr[i]表示candidates[i]在当前搜索组合中是否被使用。如下图所示,搜索过程中useArr的变化如下图所示。从图中可以很容易发现,分支a和分支b的useArr不一样,准确来说,是useArr[i-1]不一样,分支a对应的useArr[i-1]=1,而分支b对应的useArr[i-1]=0。因此当i > 0 && useArr[i - 1] == 0 && candidates[i - 1] == candidates[i]时,进行剪枝。注意,在搜索之前需要先对candidates进行升序排序,才通过判断ii-1对应的元素是否相同。

在这里插入图片描述

代码实现

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    // 存储组合使用的元素,1说明元素被使用,0说明元素没有被使用
    int[] useArr = new int[candidates.length];
    // 先将数组的值升序排序
    Arrays.sort(candidates);
    backtrack(candidates, useArr, target, 0, result, path);
    return result;
}

private void backtrack(int[] candidates, int[] useArr, int target, int startIndex,
                       List<List<Integer>> result, List<Integer> path) {
    if (target == 0) {
        result.add(new ArrayList<>(path));
        return;
    }
    for (int i = startIndex; i < candidates.length; i++) {
        if (target < candidates[i]) {
            // target很小了,candidates后的数只会更大,所以可以break
            break;
        }
        // 如果 candidates[i - 1] == candidates[i] ,说明同一层遍历到和之前的元素一样。如果useArr[i - 1] == 0,进行剪枝
        if (i > 0 && useArr[i - 1] == 0 && candidates[i - 1] == candidates[i]) {
            continue;
        }
        // 使用了candidates[i]
        useArr[i] = 1;
        path.add(candidates[i]);
        backtrack(candidates, useArr, target - candidates[i], i + 1, result, path);
        path.remove(path.size() - 1);
        // 去除candidates[i]的使用
        useArr[i] = 0;
    }
}

在这里插入图片描述

分割回文串※

https://leetcode.cn/problems/palindrome-partitioning/description/

在这里插入图片描述

思路

字符串分割

在这里插入图片描述

public List<List<String>> partition(String s) {
    List<List<String>> result = new ArrayList<>();
    List<String> path = new ArrayList<>();
    char[] charArray = s.toCharArray();
    backtrack(result, path, charArray, 0);
    return result;
}

private void backtrack(List<List<String>> result, List<String> path, char[] charArray, int startIndex) {
    if (startIndex == charArray.length) {
        // --if-- 字符串切割完成了
        result.add(new ArrayList<>(path));
        return;
    }

    for (int i = startIndex; i < charArray.length; i++) {
        String s = "";
        // 截取[startIndex,i]对应子串
        for (int j = startIndex; j <= i; j++) {
            s += charArray[j];
        }
        // 将子串添加到path中
        path.add(s);
        // 递归处理剩下的子串
        backtrack(result, path, charArray, i + 1);
        // 回溯
        path.remove(path.size() - 1);
    }
}

当然上面的代码是生成所有的切割方案,并没有判断切割出来的子串是否符合回文串要求

在这里插入图片描述

回文串判断

判断一个字符串非常简单,使用双指针法。一个指针从头开始,一个指针从尾开始,判断两个指针对应的字符是否相同即可,如果出现不相同,说明字符串不是回文串。

private boolean isPalindromeSubStr(String s) {
    int start = 0;
    int end = s.length() - 1;
    while (start < end) {
        if (s.charAt(start++) != s.charAt(end--)) {
            return false;
        }
    }
    return true;
}

【总代码】

public List<List<String>> partition(String s) {
    List<List<String>> result = new ArrayList<>();
    List<String> path = new ArrayList<>();
    char[] charArray = s.toCharArray();
    backtrack(result, path, charArray, 0);
    return result;
}

private void backtrack(List<List<String>> result, List<String> path, char[] charArray, int startIndex) {
    if (startIndex == charArray.length) {
        // --if-- 字符串切割完成了
        result.add(new ArrayList<>(path));
        return;
    }

    for (int i = startIndex; i < charArray.length; i++) {
        String s = "";
        for (int j = startIndex; j <= i; j++) {
            s += charArray[j];
        }
        // 判断子串是否为回文串,如果不是的,遍历下一个回文串
        if (isPalindromeSubStr(s) == false) {
            continue;
        }
        path.add(s);
        backtrack(result, path, charArray, i + 1);
        path.remove(path.size() - 1);
    }
}

private boolean isPalindromeSubStr(String s) {
    int start = 0;
    int end = s.length() - 1;
    while (start < end) {
        if (s.charAt(start++) != s.charAt(end--)) {
            return false;
        }
    }
    return true;
}

在这里插入图片描述

通过结果,可以发现计算效率非常低,因为在搜索过程中,不断使用字符串拼接

效率优化※

直接使用substring

public List<List<String>> partition(String s) {
    List<List<String>> result = new ArrayList<>();
    List<String> path = new ArrayList<>();
    backtrack(result, path, s, 0);
    return result;
}

private void backtrack(List<List<String>> result, List<String> path, String s, int startIndex) {
    if (startIndex == s.length()) {
        // --if-- 字符串切割完成了
        result.add(new ArrayList<>(path));
        return;
    }

    for (int i = startIndex; i < s.length(); i++) {
        // 判断子串是否为回文串,如果不是的,遍历下一个回文串
        String str = s.substring(startIndex, i + 1);
        if (isPalindromeSubStr(str) == false) {
            continue;
        }
        path.add(str);
        backtrack(result, path, s, i + 1);
        path.remove(path.size() - 1);
    }
}

private boolean isPalindromeSubStr(String s) {
    int start = 0;
    int end = s.length() - 1;
    while (start < end) {
        if (s.charAt(start++) != s.charAt(end--)) {
            return false;
        }
    }
    return true;
}

效率提高了一点点

在这里插入图片描述

多跑一次,结果完全不同,所以大家不能太相信leetcode的数据统计。同一段代码在计算机中跑,运行时间有波动非常正常

在这里插入图片描述

性能还有优化的空间,还可以使用动态规划来进行优化,等学完动态规划,再回来改进

复原 IP 地址

https://leetcode.cn/problems/restore-ip-addresses/description/

在这里插入图片描述

public List<String> restoreIpAddresses(String s) {
    List<String> result = new ArrayList<>();
    List<String> path = new ArrayList<>();
    char[] charArray = s.toCharArray();
    backtrack(result, path, charArray, 0);
    return result;
}


private void backtrack(List<String> result, List<String> path, char[] charArray, int startIndex) {
    if (startIndex == charArray.length && path.size() == 4) {
        // --if-- 字符串切割完成了,且符合ip形式,就存储ip
        StringBuilder ipSb = new StringBuilder();
        for (int i = 0; i < path.size() - 1; i++) {
            ipSb.append(path.get(i) + ".");
        }
        ipSb.append(path.get(path.size() - 1));
        result.add(ipSb.toString());
        return;
    } else if (path.size() > 4) {
        // --if-- 超过4个整数了,ip不合法
        return;
    }

    StringBuilder sb = new StringBuilder();
    for (int i = startIndex; i < charArray.length; i++) {
        sb.append(charArray[i]);
        // 判断子串是否为回文串,如果不是的,遍历下一个回文串
        if (isReasonable(sb.toString()) == false) {
            break;
        }
        path.add(sb.toString());
        backtrack(result, path, charArray, i + 1);
        path.remove(path.size() - 1);
    }
}

private boolean isReasonable(String string) {
    // 不能含有前导 0
    if (string.length() > 1 && string.charAt(0) == '0') {
        return false;
    }
    // 每个整数位于 0 到 255 之间组成
    int num = Integer.parseInt(string);
    if (num > 255) {
        return false;
    }
    return true;
}

在这里插入图片描述

优化版本

统一使用一个StringBuilder

String temp;

public List<String> restoreIpAddresses(String s) {
    List<String> result = new ArrayList<>();
    StringBuilder sb = new StringBuilder();
    backtrack(result, sb, s, 0, 0);
    return result;
}

/**
 * 回溯方法
 *
 * @param result     记录结果集
 * @param sb         记录当前ip
 * @param s          原字符串
 * @param startIndex 开始索引
 * @param splitNum   逗号分割数量
 */
private void backtrack(List<String> result, StringBuilder sb, String s, int startIndex, int splitNum) {
    if (startIndex == s.length() && splitNum == 4) {
        // --if-- 字符串切割完成了,且符合ip形式,就存储ip
        result.add(sb.toString());
        return;
    } else if (splitNum == 4) {
        // --if-- 超过4个整数了,ip不合法
        return;
    }

    for (int i = startIndex; i < s.length(); i++) {
        if (i - startIndex >= 1 && s.charAt(startIndex) == '0') {
            // --if-- 当前网段长度>1,且含有前导 0,不合法ip,break
            break;
        }
        if (i - startIndex >= 3) {
            // --if-- 当前网段长度>3,不合法ip,break
            break;
        }
        // 截取字符串作为网段,注意是左闭右开,所以i+1
        temp = s.substring(startIndex, i + 1);
        if (Integer.parseInt(temp) > 255) {
            // --if-- 当前网段值>255,不合法ip,break
            break;
        }
        // 将当前网段添加到ip中
        sb.append(temp);
        // 如果逗号数量 < 3(一个ip,网段最多4个,逗号最多3个),拼接一个逗号
        if (splitNum < 3) {
            sb.append(".");
        }
        // 递归
        backtrack(result, sb, s, i + 1, splitNum + 1);
        // 回溯,删除最后一个网段,例如现在是255.255.11.13,回溯之后为 255.255.11.
        sb.delete(startIndex + splitNum, sb.length());
    }
}

在这里插入图片描述

子集

https://leetcode.cn/problems/subsets/description/

在这里插入图片描述

注意:题目说了nums中的所有元素各不相同,所以下面的代码没有必要去重

public List<List<Integer>> subsets(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    backtrack(result, path, nums, 0);
    return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int startIndex) {
    // 任何集合都要存储,没有说需要满足什么条件才可以存储
    result.add(new ArrayList<>(path));
    // 当 startIndex = nums长度 的时候,本来需要return,但是下面的循环不会进行,所以无需把这部分代码写出来了
    for (int i = startIndex; i < nums.length; i++) {
        path.add(nums[i]);
        backtrack(result, path, nums, i + 1);
        path.remove(path.size() - 1);
    }
}

在这里插入图片描述

子集 II

https://leetcode.cn/problems/subsets-ii/description/

在这里插入图片描述

使用usedArr辅助去重

注意,这道题目和上面的不同,数组里面含有重复元素,需要去重操作,其实就和 组合总和II 的思路一样

public List<List<Integer>> subsetsWithDup(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    Arrays.sort(nums);
    int[] useArr = new int[nums.length];
    backtrack(result, path, nums, 0, useArr);
    return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int startIndex, int[] useArr) {
    // 任何集合都要存储,没有说需要满足什么条件才可以存储
    result.add(new ArrayList<>(path));
    // 当 startIndex = nums长度 的时候,本来需要return,但是下面的循环不会进行,所以无需把这部分代码写出来了
    for (int i = startIndex; i < nums.length; i++) {
        // 去重
        if (i > 0 && useArr[i - 1] == 0 && nums[i - 1] == nums[i]) {
            continue;
        }
        path.add(nums[i]);
        useArr[i] = 1;
        backtrack(result, path, nums, i + 1, useArr);
        path.remove(path.size() - 1);
        useArr[i] = 0;
    }
}

在这里插入图片描述

不使用usedArr辅助去重

其实这道题还可以不用use数组

public List<List<Integer>> subsetsWithDup(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    Arrays.sort(nums);
    backtrack(result, path, nums, 0);
    return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int startIndex) {
    // 任何集合都要存储,没有说需要满足什么条件才可以存储
    result.add(new ArrayList<>(path));
    // 当 startIndex = nums长度 的时候,本来需要return,但是下面的循环不会进行,所以无需把这部分代码写出来了
    for (int i = startIndex; i < nums.length; i++) {
        // 去重
        if (i > startIndex && nums[i - 1] == nums[i]) {
            continue;
        }
        path.add(nums[i]);
        backtrack(result, path, nums, i + 1);
        path.remove(path.size() - 1);
    }
}

注意,去重判断变成了如下代码。和上面代码的区别是一个为i > 0,一个为i > startIndex。以[1,2,2]为例子,当startIndex=2时。

  • 如果代码为i > 0,就会进入去重,导致子集[1,2,2]无法找到,只能找到[]、[1]、[1,2]。
  • 但是如果代码为i > startIndex,不会触发去重,因为i = startIndex
// 去重
if (i > startIndex && nums[i - 1] == nums[i]) {
    continue;
}

递增子序列※

在这里插入图片描述

public List<List<Integer>> findSubsequences(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    backtrack(result, path, nums, 0);
    return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, int startIndex) {
    // 任何集合都要存储,没有说需要满足什么条件才可以存储
    if (path.size() >= 2) {
        result.add(new ArrayList<>(path));
    }
    // 用来去重
    HashMap<Integer, Boolean> useMap = new HashMap<>();
    for (int i = startIndex; i < nums.length; i++) {
        // 如果不是递增,continue
        if (path.size() > 0 && nums[i] < path.get(path.size() - 1)) {
            continue;
        }
        // 去重,如果前面有用过相同的元素,直接continue
        if (useMap.getOrDefault(nums[i], false) == true) {
            continue;
        }
        useMap.put(nums[i], true);
        path.add(nums[i]);
        backtrack(result, path, nums, i + 1);
        path.remove(path.size() - 1);
    }
}

在这里插入图片描述

如果说数据为 [1, 2, 3, 4, 1, 1] ,不去重答案就会出问题
在这里插入图片描述

全排列

https://leetcode.cn/problems/permutations/

在这里插入图片描述

这道题只能暴力求解

public List<List<Integer>> permute(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    boolean[] useArr = new boolean[nums.length];
    backtrack(result, path, nums, useArr);
    return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, boolean[] useArr) {
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));
    }
    for (int i = 0; i < nums.length; i++) {
        if (useArr[i] == true) {
            continue;
        }
        useArr[i] = true;
        path.add(nums[i]);
        backtrack(result, path, nums, useArr);
        useArr[i] = false;
        path.remove(path.size() - 1);
    }
}

在这里插入图片描述

不用数组useArr来辅助也可以,直接使用path的contain方法来判断元素是否已经存在即可

这道题和前面比较大的一个区别是不再需要startIndex这个变量,而是遍历整个数组,因为需要的所有排序

全排列 II

https://leetcode.cn/problems/permutations-ii/description/

在这里插入图片描述

public List<List<Integer>> permuteUnique(int[] nums) {
    List<List<Integer>> result = new ArrayList<>();
    List<Integer> path = new ArrayList<>();
    boolean[] useArr = new boolean[nums.length];
    // 升序排序
    Arrays.sort(nums);
    backtrack(result, path, nums, useArr);
    return result;
}

private void backtrack(List<List<Integer>> result, List<Integer> path, int[] nums, boolean[] useArr) {
    if (path.size() == nums.length) {
        result.add(new ArrayList<>(path));
    }
    for (int i = 0; i < nums.length; i++) {
        if (useArr[i] == true) {
            continue;
        }
        // 去重,如果当前元素和前一个元素相同,而且useArr[i - 1] == false,前面一个元素肯定已经被同样的方式使用过,当前组合不需要再往下遍历了
        if (i > 0 && nums[i - 1] == nums[i] && useArr[i - 1] == false) {
            continue;
        }
        useArr[i] = true;
        path.add(nums[i]);
        backtrack(result, path, nums, useArr);
        useArr[i] = false;
        path.remove(path.size() - 1);
    }
}

在这里插入图片描述

重新安排行程

https://leetcode.cn/problems/reconstruct-itinerary/description/

在这里插入图片描述

在这里插入图片描述

题意

List<List<String>> tickets 中存储了多张票的起点和终点,我们要做的是:从起点JFK开始旅行,然后把每张票使用一遍,注意每张票只能使用一次

字典排序的意思如下,例如有两个字符串 ACD 和 ABE

  • 首先对比两个字符串的第一个字母,两者都是A打平手
  • 然后对比两个字符串的第二个字母,第一个字符串的字母为C,第二个字符串的字母为B,因为B在字母表中的顺序更加靠前,因此排序时ABE要排在ACD前面

代码

public List<String> findItinerary(List<List<String>> tickets) {
    List<String> result = new ArrayList<>();
    List<String> path = new ArrayList<>();
    // 存储开始站点 及 开始站点所对应的票 索引
    Map<String, List<Integer>> startStationAndIndexListMap = new HashMap<>();
    // 填充 startStationAndIndexListMap 的数据
    for (int i = 0; i < tickets.size(); i++) {
        List<String> ticket = tickets.get(i);
        if (!startStationAndIndexListMap.containsKey(ticket.get(0))) {
            ArrayList<Integer> indexList = new ArrayList<>();
            indexList.add(i);
            startStationAndIndexListMap.put(ticket.get(0), indexList);
        } else {
            startStationAndIndexListMap.get(ticket.get(0)).add(i);
        }
    }
    // 将indexList按照字典序升序排序
    for (Map.Entry<String, List<Integer>> entry : startStationAndIndexListMap.entrySet()) {
        List<Integer> indexList = entry.getValue();
        Collections.sort(indexList, ((o1, o2) -> {
            return compare(tickets.get(o1).get(1), tickets.get(o2).get(1));
        }));
    }
    // 用来标识哪张机票已经被使用过
    boolean[] useArr = new boolean[tickets.size()];
    // 起点是固定的,添加起点
    path.add("JFK");
    backtrack(tickets, result, path, startStationAndIndexListMap, useArr, 0, "JFK");
    return result;
}

private void backtrack(List<List<String>> tickets, List<String> result, List<String> path, Map<String, List<Integer>> startStationAndIndexListMap, boolean[] useArr, int useTicketNum, String startStation) {
    if (useTicketNum == tickets.size() && result.size() == 0) {
        // --if-- 所有机票已经用完了,存储当前路径
        result.addAll(path);
        return;
    }
    List<Integer> indexList = startStationAndIndexListMap.get(startStation);
    if (indexList == null) {
        // 当前起点站没有机票了,直接退出
        return;
    }
    // 遍历从当前起始站出发的机票
    for (int i = 0; i < indexList.size(); i++) {
        if (result.size() > 0) {
            // --if-- 因为题目要求只要字典序最小的一条路径,因此找到一个结果就可以结束整个搜索了,这里直接return
            return;
        }
        // 机票索引
        Integer ticketIndex = indexList.get(i);
        if (useArr[ticketIndex] == true) {
            // --if-- 该机票已经被使用过
            continue;
        }
        // 去重,当前机票首站-尾站和上一张机票的相同,直接跳过即可
        if (i > 0 && tickets.get(indexList.get(i - 1)).get(1).equals(tickets.get(indexList.get(i)).get(1)) && useArr[indexList.get(i - 1)] == false) {
            continue;
        }
        // 标识当前机票已经被使用
        useArr[ticketIndex] = true;
        path.add(tickets.get(ticketIndex).get(1));
        backtrack(tickets, result, path, startStationAndIndexListMap, useArr, useTicketNum + 1, tickets.get(ticketIndex).get(1));
        path.remove(path.size() - 1);
        useArr[ticketIndex] = false;
    }
}

/**
 * 字典排序
 *
 * @param str1
 * @param str2
 * @return
 */
private int compare(String str1, String str2) {
    for (int i = 0; i < str1.length(); i++) {
        int compare = Integer.compare(str1.charAt(i), str2.charAt(i));
        if (compare != 0) {
            // --if-- 当前字母没有分出胜负,继续比较下一个字母
            return compare;
        }
    }
    return 0;
}

当然**return **compare(tickets.get(o1).get(1), tickets.get(o2).get(1));可以使用

return tickets.get(o1).get(1).compareTo(tickets.get(o2).get(1));来代替
在这里插入图片描述

小小优化了一下,去掉path,只使用result

public List<String> findItinerary(List<List<String>> tickets) {
    List<String> result = new ArrayList<>();
    // 存储开始站点 及 开始站点所对应的票 索引
    Map<String, List<Integer>> startStationAndIndexListMap = new HashMap<>();
    // 填充 startStationAndIndexListMap 的数据
    for (int i = 0; i < tickets.size(); i++) {
        List<String> ticket = tickets.get(i);
        if (!startStationAndIndexListMap.containsKey(ticket.get(0))) {
            ArrayList<Integer> indexList = new ArrayList<>();
            indexList.add(i);
            startStationAndIndexListMap.put(ticket.get(0), indexList);
        } else {
            startStationAndIndexListMap.get(ticket.get(0)).add(i);
        }
    }
    // 将indexList按照字典序升序排序
    for (Map.Entry<String, List<Integer>> entry : startStationAndIndexListMap.entrySet()) {
        List<Integer> indexList = entry.getValue();
        Collections.sort(indexList, ((o1, o2) -> {
            return tickets.get(o1).get(1).compareTo(tickets.get(o2).get(1));
        }));
    }
    // 用来标识哪张机票已经被使用过
    boolean[] useArr = new boolean[tickets.size()];
    // 起点是固定的,添加起点
    result.add("JFK");
    backtrack(tickets, result, startStationAndIndexListMap, useArr, "JFK");
    return result;
}

private void backtrack(List<List<String>> tickets, List<String> result,  Map<String, List<Integer>> startStationAndIndexListMap, boolean[] useArr, String startStation) {
    if (result.size() == tickets.size() + 1) {
        // --if-- 所有机票已经用完了,存储当前路径
        return;
    }
    List<Integer> indexList = startStationAndIndexListMap.get(startStation);
    if (indexList == null) {
        // 当前起点站没有机票了,直接退出
        return;
    }
    // 遍历从当前起始站出发的机票
    for (int i = 0; i < indexList.size(); i++) {
        // 机票索引
        Integer ticketIndex = indexList.get(i);
        if (useArr[ticketIndex] == true) {
            // --if-- 该机票已经被使用过
            continue;
        }
        // 去重,当前机票首站-尾站和上一张机票的相同,直接跳过即可
        if (i > 0 && tickets.get(indexList.get(i - 1)).get(1).equals(tickets.get(ticketIndex).get(1)) && useArr[indexList.get(i - 1)] == false) {
            continue;
        }
        // 标识当前机票已经被使用
        useArr[ticketIndex] = true;
        result.add(tickets.get(ticketIndex).get(1));
        backtrack(tickets, result, startStationAndIndexListMap, useArr, tickets.get(ticketIndex).get(1));
        if (result.size() == tickets.size() + 1) {
            // --if-- 因为题目要求只要字典序最小的一条路径,因此找到一个结果就可以结束整个搜索了,这里直接return
            return;
        }
        result.remove(result.size() - 1);
        useArr[ticketIndex] = false;
    }
}

在这里插入图片描述

字典直接存储一个站可以去往哪些站点,如果选择了该站点,将其从集合中删除,回溯的时候,再将该站点加回来

int totalStationNum;

public List<String> findItinerary(List<List<String>> tickets) {
    totalStationNum = tickets.size() + 1;
    List<String> result = new ArrayList<>();
    // 存储开始站点 及 开始站点所对应的票 索引
    Map<String, List<String>> startStationAndEndStationListMap = new HashMap<>();
    // 填充 startStationAndIndexListMap 的数据
    for (int i = 0; i < tickets.size(); i++) {
        List<String> ticket = tickets.get(i);
        if (!startStationAndEndStationListMap.containsKey(ticket.get(0))) {
            List<String> endStationList = new ArrayList<>();
            endStationList.add(ticket.get(1));
            startStationAndEndStationListMap.put(ticket.get(0), endStationList);
        } else {
            startStationAndEndStationListMap.get(ticket.get(0)).add(ticket.get(1));
        }
    }
    // 将indexList按照字典序升序排序
    for (Map.Entry<String, List<String>> entry : startStationAndEndStationListMap.entrySet()) {
        Collections.sort(entry.getValue(), ((o1, o2) -> {
            return o1.compareTo(o2);
        }));
    }
    // 起点是固定的,添加起点
    result.add("JFK");
    backtrack(result, startStationAndEndStationListMap, "JFK");
    return result;
}

private boolean backtrack(List<String> result, Map<String, List<String>> startStationAndEndStationListMap, String startStation) {
    if (result.size() == totalStationNum) {
        // --if-- 所有机票已经用完了,存储当前路径
        return true;
    }
    List<String> endStationList = startStationAndEndStationListMap.get(startStation);
    if (endStationList == null) {
        // 当前起点站没有机票了,直接退出
        return false;
    }
    // 遍历从当前起始站出发的机票
    int i = 0;
    while (i < endStationList.size()) {
        // 目标站点
        String endStation = endStationList.get(i);
        // 去重,当前机票首站-尾站和上一张机票的相同,直接跳过即可
        if (i > 0 && endStation.equals(endStationList.get(i - 1))) {
            i++;
            continue;
        }
        // 去往该站点,将其从集合中删除
        endStationList.remove(i);
        result.add(endStation);
        if (backtrack(result, startStationAndEndStationListMap, endStation) == true) {
            // --if-- 因为题目要求只要字典序最小的一条路径,因此找到一个结果就可以结束整个搜索了,这里直接return
            return true;
        }
        result.remove(result.size() - 1);
        endStationList.add(i, endStation);
        i++;
    }
    return false;
}

在这里插入图片描述

代码随想录的代码效率更高

N 皇后

https://leetcode.cn/problems/n-queens/description/

在这里插入图片描述

思路:遍历每一行,然后看当行的每列,再判断当前位置是否可行

public List<List<String>> solveNQueens(int n) {
    List<List<String>> result = new ArrayList<>();
    // 初始化棋盘
    char[][] chessboard = new char[n][n];
    for (int i = 0; i < n; i++) {
        Arrays.fill(chessboard[i], '.');
    }
    backtrack(result, chessboard, 0, n);
    return result;
}

private void backtrack(List<List<String>> result, char[][] chessboard, int layer, int n) {
    if (layer == n) {
        // --for-- 遍历到最后一行了,保存结果就结束
        List<String> path = new ArrayList<>();
        for (int i = 0; i < chessboard.length; i++) {
            path.add(new String(chessboard[i]));
        }
        result.add(path);
    }
    for (int j = 0; j < n; j++) {
        // --for-- 遍历棋盘中当前行的每一列
        boolean isCanPlace = true;
        if (layer > 0) {
            // --if-- 如果是第二行以上,需要判断上面行有没有皇后排斥。第一行不需要看
            for (int i = 0; i < layer; i++) {
                // 判断同列是否有皇后
                if (chessboard[i][j] == 'Q') {
                    isCanPlace = false;
                    break;
                }
                // 判断斜对角是否有皇后
                if (((j - (layer - i)) >= 0 && chessboard[i][j - (layer - i)] == 'Q') ||
                        (j + (layer - i) < n && chessboard[i][j + (layer - i)] == 'Q')) {
                    isCanPlace = false;
                    break;
                }
            }
        }
        if (isCanPlace == true) {
            // --if-- 当前位置可以放皇后
            chessboard[layer][j] = 'Q';
            backtrack(result, chessboard, layer + 1, n);
            chessboard[layer][j] = '.';
        }
    }
}

在这里插入图片描述

代码随想录中有更快的方法

解数独

https://leetcode.cn/problems/sudoku-solver/description/

在这里插入图片描述

在这里插入图片描述

直接遍历

一个单元格一个单元格的遍历,然后遍历[1,9]的每个数字,判断该数字是否可设置。如果所有数字都不可以设置,回溯

char[] selectNumCharArr = new char[]{'1', '2', '3', '4', '5', '6', '7', '8', '9'};
int cellRowIndex;
int cellColumnIndex;
int cellRowIndexStart;
int cellColumnIndexStart;
int cellRowIndexEnd;
int cellColumnIndexEnd;
boolean reachEnd;
boolean isFindResult;
boolean isExit;

public void solveSudoku(char[][] board) {
    backtrack(0, 0, board);
}

/**
 * 思路:依次遍历每一个格子来填充数字
 *
 * @param rowIndex
 * @param columnIndex
 * @param board
 * @return
 */
private boolean backtrack(int rowIndex, int columnIndex, char[][] board) {
    if (rowIndex == board.length) {
        // --if-- 所有行都已经遍历完成,说明全部数字填充完成
        return true;
    }
    if (board[rowIndex][columnIndex] != '.') {
        // --if-- 当前格子已经有数字
        // 判断当前行的每个格子是否遍历完成
        reachEnd = (columnIndex + 1 == board.length);
        // 如果当前行遍历完成,就要换行,columnIndex从0开始
        isFindResult = backtrack(reachEnd ? rowIndex + 1 : rowIndex, reachEnd ? 0 : columnIndex + 1, board);
        if (isFindResult) {
            // --if-- 找到结果,直接返回
            return true;
        }
    } else {
        for (int k = 0; k < selectNumCharArr.length; k++) {
            char numChar = selectNumCharArr[k];
            // 检测同一行是否有重复值
            isExit = false;
            for (int j = 0; j < board.length; j++) {
                if (board[rowIndex][j] == numChar) {
                    isExit = true;
                    break;
                }
            }
            if (isExit) {
                continue;
            }

            // 检测同一列是否有重复值
            for (int i = 0; i < board.length; i++) {
                if (board[i][columnIndex] == numChar) {
                    isExit = true;
                    break;
                }
            }
            if (isExit) {
                continue;
            }

            // 检测框里面是否有重复值
            cellRowIndex = rowIndex / 3;
            cellColumnIndex = columnIndex / 3;
            cellRowIndexStart = cellRowIndex * 3;
            cellColumnIndexStart = cellColumnIndex * 3;
            cellRowIndexEnd = (cellRowIndex + 1) * 3;
            cellColumnIndexEnd = (cellColumnIndex + 1) * 3;
            for (int i = cellRowIndexStart; i < cellRowIndexEnd; i++) {
                for (int j = cellColumnIndexStart; j < cellColumnIndexEnd; j++) {
                    if (board[i][j] == numChar) {
                        isExit = true;
                        break;
                    }
                }
                if (isExit) {
                    break;
                }
            }
            if (isExit) {
                continue;
            }

            // 通过上述校验,可以放下当前数字
            board[rowIndex][columnIndex] = numChar;
            // 递归到下一个格子
            reachEnd = (columnIndex + 1 == board.length);
            isFindResult = backtrack(reachEnd ? rowIndex + 1 : rowIndex, reachEnd ? 0 : columnIndex + 1, board);
            if (isFindResult) {
                return true;
            }
            // 回溯
            board[rowIndex][columnIndex] = '.';
        }
    }
    return false;
}

在这里插入图片描述

优化

使用boolean数组去重,减少行、列、单元格的循环判断

char[] selectNumCharArr = new char[]{'1', '2', '3', '4', '5', '6', '7', '8', '9'};
boolean reachEnd;
boolean isFindResult;
// 行去重
boolean[][] rowUsedArr = new boolean[9][9];
// 列去重
boolean[][] columnUsedArr = new boolean[9][9];
// 单元去重
boolean[][] cellUsedArr = new boolean[9][9];

public void solveSudoku(char[][] board) {
    int num;
    for (int i = 0; i < board.length; i++) {
        for (int j = 0; j < board.length; j++) {
            if (board[i][j] != '.') {
                num = board[i][j] - '1';
                rowUsedArr[i][num] = true;
                columnUsedArr[j][num] = true;
                cellUsedArr[(i / 3) * 3 + (j / 3)][num] = true;
            }
        }
    }
    backtrack(0, 0, board);
}

/**
 * 思路:依次遍历每一个格子来填充数字
 *
 * @param rowIndex
 * @param columnIndex
 * @param board
 * @return
 */
private boolean backtrack(int rowIndex, int columnIndex, char[][] board) {
    if (rowIndex == board.length) {
        // --if-- 所有行都已经遍历完成,说明全部数字填充完成
        return true;
    }
    if (board[rowIndex][columnIndex] != '.') {
        // --if-- 当前格子已经有数字
        // 判断当前行的每个格子是否遍历完成
        reachEnd = (columnIndex + 1 == board.length);
        // 如果当前行遍历完成,就要换行,columnIndex从0开始
        isFindResult = backtrack(reachEnd ? rowIndex + 1 : rowIndex, reachEnd ? 0 : columnIndex + 1, board);
        if (isFindResult) {
            // --if-- 找到结果,直接返回
            return true;
        }
    } else {
        for (int k = 0; k < selectNumCharArr.length; k++) {
            int num = selectNumCharArr[k] - '1';
            // 检测同一行是否有重复值
            if (rowUsedArr[rowIndex][num]) {
                continue;
            }

            // 检测同一列是否有重复值
            if (columnUsedArr[columnIndex][num] == true) {
                continue;
            }

            // 检测框里面是否有重复值
            int cellIndex = (rowIndex / 3) * 3 + (columnIndex / 3);
            if (cellUsedArr[cellIndex][num]) {
                continue;
            }

            // 通过上述校验,可以放下当前数字
            board[rowIndex][columnIndex] = selectNumCharArr[k];
            rowUsedArr[rowIndex][num] = true;
            columnUsedArr[columnIndex][num] = true;
            cellUsedArr[cellIndex][num] = true;
            // 递归到下一个格子
            reachEnd = (columnIndex + 1 == board.length);
            isFindResult = backtrack(reachEnd ? rowIndex + 1 : rowIndex, reachEnd ? 0 : columnIndex + 1, board);
            if (isFindResult) {
                return true;
            }
            // 回溯
            board[rowIndex][columnIndex] = '.';
            rowUsedArr[rowIndex][num] = false;
            columnUsedArr[columnIndex][num] = false;
            cellUsedArr[cellIndex][num] = false;
        }
    }
    return false;
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2299360.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【JavaEE进阶】验证码案例

目 &#x1f332;实现说明 &#x1f384;Hutool介绍 &#x1f333;准备工作 &#x1f334;约定前后端交互接口 &#x1f6a9;接口定义 &#x1f6a9;实现服务器后端代码 &#x1f6a9;前端代码 &#x1f6a9;整体测试 &#x1f332;实现说明 随着安全性的要求越来越⾼…

Qt Creator 5.0.2 (Community)用久了突然变得很卡

目录 1.现象 2.解决方案 1.现象 很久没有用Qt Creator开发项目了&#xff0c;刚刚结束的项目又是用VS2019开发的&#xff1b;这两天刚好有时间去学习一下Qt&#xff0c;刚好要用Qt Creator&#xff0c;结果一打开就没反应&#xff0c;主界面显示出来要好几分钟&#xff0c;最…

阅读论文笔记《Efficient Estimation of Word Representations in Vector Space》

这篇文章写于2013年&#xff0c;对理解 word2vec 的发展历程挺有帮助。 本文仅适用于 Word2Vect 的复盘 引言 这篇论文致力于探索从海量数据中学习高质量单词向量的技术。当时已发现词向量能保留语义特征&#xff0c;例如 “国王 - 男人 女人≈女王”。论文打算借助该特性&am…

初学PADS使用技巧笔记(也许会继续更新)

操作意图&#xff1a;网上找某个芯片封装又不想自己画&#xff0c;再加上没经验&#xff0c;怎么办&#xff1f; 就以AC-DC芯片PN8036为例&#xff0c;打开嘉立创的的DFM&#xff0c;打开立创商城&#xff0c;输入PN8036&#xff0c;点击数据手册&#xff0c;然后点击直接打开…

从无序到有序:上北智信通过深度数据分析改善会议室资源配置

当前企业普遍面临会议室资源管理难题&#xff0c;预约机制不完善和临时会议多导致资源调度不合理&#xff0c;既有空置又有过度拥挤现象。 针对上述问题&#xff0c;上北智信采用了专业数据分析手段&#xff0c;巧妙融合楼层平面图、环形图、折线图和柱形图等多种可视化工具&a…

CAS单点登录(第7版)4.管理

如有疑问&#xff0c;请看视频&#xff1a;CAS单点登录&#xff08;第7版&#xff09; 管理 概述 Admin Console & 仪表板 CAS 提供了许多可用于管理 CAS 服务器部署的工具和控制板。此类选项通常不是互斥的&#xff0c;旨在协同工作并呈现 CAS 配置和构建的各个方面&am…

Baklib一站式云平台:全场景赋能企业知识资产激活

内容概要 在数字化浪潮推动下&#xff0c;企业知识资产的高效管理与价值释放成为核心议题。Baklib作为一站式云平台&#xff0c;以全场景赋能为核心定位&#xff0c;通过构建知识中台架构&#xff0c;为企业提供从资源整合到应用落地的闭环解决方案。该平台不仅支持文本、图像…

登录弹窗效果

1&#xff0c;要求 点击登录按钮&#xff0c;弹出登录窗口 提示1&#xff1a;登录窗口 display:none 隐藏状态&#xff1b; 提示2&#xff1a;登录按钮点击后&#xff0c;触发事件&#xff0c;修改 display:block 显示状态 提示3&#xff1a;登录窗口中点击关闭按钮&#xff0…

小小小病毒(3)(~_~|)

一分耕耘一分收获 声明&#xff1a; 仅供损害电脑&#xff0c;不得用于非法。损坏电脑&#xff0c;作者一律不负责。此作为作者原创&#xff0c;转载请经过同意。 欢迎来到小小小病毒&#xff08;3&#xff09; 感谢大家的支持 还是那句话&#xff1a;上代码&#xff01; …

【Java 面试 八股文】Spring Cloud 篇

Spring Cloud 篇 1. Spring Cloud 5大组件有哪些&#xff1f;2. 服务注册和发现是什么意思&#xff1f;Spring Cloud 如何实现服务注册发现&#xff1f;3. 我看你之前也用过nacos&#xff0c;你能说下nacos与eureka的区别&#xff1f;4. 你们项目负载均衡如何实现的&#xff1f…

Esxi8.0设置nvidia显卡直通安装最新驱动

ESXI8.0设置显卡直通 在某些情况下&#xff0c;我们需要多次切换操作系统&#xff0c;以测试软件是否适用于特定系统和环境&#xff0c;减少多次重装系统的麻烦 ESXI8.0安装包 通过网盘分享的文件&#xff1a;ESXi-8.0U2-22380479-USB-NVME-集成网卡镜像.iso 链接: https://…

LabVIEW袜品压力测试系统

开发了一种基于LabVIEW开发的袜品压力测试系统。该系统利用LabVIEW并结合灵敏的传感器和高精度的处理模块&#xff0c;实现了对袜品压力的精确测量和分析。系统不同于传统的服装压力测试方法&#xff0c;为研究和评价袜子的舒适性提供了新的测试手段。 ​ 项目背景 该系统的…

TestHubo基础教程-创建项目

TestHubo是一款国产开源一站式测试工具&#xff0c;涵盖功能测试、接口测试、性能测试&#xff0c;以及 Web 和 App 测试&#xff0c;可以满足不同类型项目的测试需求。本文将介绍如何快速创建第一个项目&#xff0c;以快速入门上手。 1、创建项目 在 TestHubo 中&#xff0c;…

深度求索—DeepSeek API的简单调用(Java)

DeepSeek简介 DeepSeek&#xff08;深度求索&#xff09;是由中国人工智能公司深度求索&#xff08;DeepSeek Inc.&#xff09;研发的大规模语言模型&#xff08;LLM&#xff09;&#xff0c;专注于提供高效、智能的自然语言处理能力&#xff0c;支持多种场景下的文本生成、对…

企业使用统一终端管理(UEM)工具提高端点安全性

什么是统一终端管理(UEM) 统一终端管理(UEM)是一种从单个控制台管理和保护企业中所有端点的方法&#xff0c;包括智能手机、平板电脑、笔记本电脑、台式机和 IoT设备。UEM 解决方案为 IT 管理员提供了一个集中式平台&#xff0c;用于跨所有作系统和设备类型部署、配置、管理和…

爱彼(Audemars Piguet):瑞士制表艺术的巅峰之作(中英双语)

爱彼&#xff08;Audemars Piguet&#xff09;&#xff1a;瑞士制表艺术的巅峰之作 在瑞士高级制表界&#xff0c;Audemars Piguet&#xff08;爱彼&#xff09; 以其大胆创新、卓越工艺和独立家族经营的传统&#xff0c;成为世界顶级腕表品牌之一。作为瑞士“三大制表品牌”之…

1134. 国王的魔镜

题目 AC代码 #include<bits/stdc.h> using namespace std; bool hw(char s[]) {bool rtrue;int i;for(i0;i<strlen(s)/2;i){if(s[i]!s[strlen(s)-i-1]){rfalse;break;}}return r; } int main() {char s[1005];gets(s);while(hw(s)true&&strlen(s)%20){s[strl…

浅说树形dp

文章目录 前言树形dp的转移方式树形dp的使用的场景小结 初步感知——简单的树形dp例题1例题2 深入分析——树形dp的经典模型最大独立集最小点覆盖最小支配集树上直径 前言 因为树的形式非常适合递归&#xff0c;他所带来的访问顺序也是非常符合拓扑排序的&#xff0c;故而在处…

使用Charles进行mock请求

在 Charles 中&#xff0c;“Compose”&#xff08;构建请求&#xff09;功能允许你手动创建和发送 HTTP 或 HTTPS 请求&#xff0c;方便进行 API 测试、调试等操作。以下是 Compose 功能的详细使用方式&#xff1a; 1. 打开 Compose 界面 启动 Charles 后&#xff0c;在菜单…

RAMinit 程序编译运行考古记录

本科的时候浅浅研究了DOSBox&#xff0c;今天看到网上挺多关于雷军代码的新闻&#xff0c;我把雷军代码在web上编译出来了&#xff0c;但是我是业余选手&#xff0c;所以做得比较差&#xff0c;不过大家感兴趣可以关注我的dosplay项目&#xff0c;旨在利用js-dos实现汇编语言在…