算法小课堂(六)回溯算法

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

目录

一、概述

1.1概念树形结构

1.2区别

1.3步骤

1.4回溯法模板

1.5应用

1.6回溯三部曲

二、组合问题

2.1组合

回溯算法

优化剪枝操作

2.2组合总和

 2.3组合总和2

2.4组合总和3

2.5电话号码的字母组合

三、切割问题

3.1分割回文串

3.2.复原IP地址

四、子集问题

4.1子集

4.2子集2

4.3递增子序列

五、排列问题

5.1全排列

5.2全排列 II

六、棋盘问题

6.1N皇后

6.2解数独

7.1 0-1背包子集问题

7.2旅行售货员问题

 7.3装载问题(组合问题)

 7.4图的着色问题(组合问题)

7.5最大团问题


一、概述

1.1概念树形结构

回溯算法是一种系统地搜索问题的解的方法。某个问题的所有可能解的称为问题的解空间若解空间是有限的则可将解空间映射成树结构。回溯算法的基本思想是从一条路往前走能进则进不能进则退回来换一条路再试。

回溯是递归的副产品只要有递归就会有回溯

回溯法就是暴力搜索并不是什么高效的算法最多在剪枝一下。

1.2区别

回溯算法和深度优先搜索DFS有很多相似之处但也有不同之处。回溯算法在搜索过程中寻找问题的解当发现已不满足求解条件时就“回溯”返回尝试别的路径。而DFS则是在搜索过程中遍历所有可能的路径直到找到问题的解

1.3步骤

  1. 状态表示需要定义状态表示方式以便在搜索过程中能够正确地扩展状态。
  2. 决策生成需要确定当前状态下的所有可能决策以便进行进一步的搜索。
  3. 解空间剪枝需要根据问题的特点对搜索过程中无用的状态进行剪枝以提高算法效率。
  4. 解的输出需要根据问题的要求确定如何输出符合要求的解。

1.4回溯法模板

result = []
def backtrack(路径, 选择列表):
    if 满足结束条件:
        result.add(路径)
        return
    for 选择 in 选择列表:
        做选择
        backtrack(路径, 选择列表)
        撤销选择

其中result 存储结果backtrack 是回溯函数路径 是已经做出的选择选择列表 是当前可以做出的选择。

1.5应用

  • 组合问题N个数里面按一定规则找出k个数的集合
  • 排列问题N个数按一定规则全排列有几种排列方式
  • 切割问题一个字符串按一定规则有几种切割方式
  • 子集问题一个N个数的集合里有多少符合条件的子集
  • 棋盘问题N皇后解数独等等。

1.6回溯三部曲

回溯算法三部曲是指1. 确定递归函数的参数和返回值2. 确定终止条件3. 确定单层搜索的过程。 回溯算法是一种暴力搜索算法它通过不断地尝试所有可能的结果来解决问题。在回溯算法中我们需要对每个可能的结果进行判断如果符合条件就加入到结果集中否则就回溯到上一步继续尝试其他可能的结果。

二、组合问题

2.1组合

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

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

示例 1

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

#include<stdio.h>
#include <iostream>

using namespace std;

const int N = 100;
int a[N];
int n, k;
void dfs(int cur, int cnt) {
    if (cnt == k) {
        for (int i = 0; i < k; i++) {
            cout << a[i] << " ";
        }
        cout << endl;
        return;
    }
    for (int i = cur; i <= n; i++) {
        a[cnt] = i;
        dfs(i + 1, cnt + 1);
    }
}
int main() {
    cin >> n >> k;
    dfs(1, 0);
    return 0;
}

回溯算法

import java.util.*;

public class Solution {

    // 回溯函数
    private void backtracking(List<List<Integer>> result, List<Integer> path, int n, int k, int startIndex) {
        if (path.size() == k) { // 如果path中的元素个数等于k说明找到了一组符合条件的组合
            result.add(new ArrayList<>(path)); // 将该组合加入结果集中
            return; // 返回上一层递归
        }
        // 枚举所有可能的数
        for (int i = startIndex; i <= n; i++) {
            path.add(i); // 将当前数加入path中进行处理
            backtracking(result, path, n, k, i + 1); // 递归处理下一个数
            path.remove(path.size() - 1); // 撤销处理的数回溯到上一层状态
        }
    }

    // 组合函数
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> result = new ArrayList<>(); // 存储符合条件的结果集
        List<Integer> path = new ArrayList<>(); // 存储符合条件的组合
        backtracking(result, path, n, k, 1); // 开始回溯从数字1开始
        return result; // 返回结果集
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(); // 获取用户输入的n
        int k = scanner.nextInt(); // 获取用户输入的k
        scanner.close(); // 关闭Scanner
        Solution solution = new Solution(); // 创建Solution对象
        List<List<Integer>> result = solution.combine(n, k); // 调用组合函数得到符合条件的结果集
        for (List<Integer> list : result) { // 遍历结果集
            for (int num : list) { // 遍历组合
                System.out.print(num + " "); // 输出组合中的元素
            }
            System.out.println(); // 输出换行用于分隔每个组合
        }
    }
}

优化剪枝操作

剪枝操作是回溯算法的一种优化方法可以排除不满足解的情况来提高算法的执行效率。在回溯算法中剪枝操作可以有多种方式例如对for循环选择的起始范围的剪枝或者在递归过程中判断是否需要继续递归等等

import java.util.*;

public class Solution {

    // 回溯函数
    private void backtracking(List<List<Integer>> result, List<Integer> path, int n, int k, int startIndex) {
        if (path.size() == k) { // 如果path中的元素个数等于k说明找到了一组符合条件的组合
            result.add(new ArrayList<>(path)); // 将该组合加入结果集中
            return; // 返回上一层递归
        }
        // 枚举所有可能的数
        for (int i = startIndex; i <=  n - (k - path.size()) + 1; i++) {//优化的地方
            path.add(i); // 将当前数加入path中进行处理
            backtracking(result, path, n, k, i + 1); // 递归处理下一个数
            path.remove(path.size() - 1); // 撤销处理的数回溯到上一层状态
        }
    }

    // 组合函数
    public List<List<Integer>> combine(int n, int k) {
        List<List<Integer>> result = new ArrayList<>(); // 存储符合条件的结果集
        List<Integer> path = new ArrayList<>(); // 存储符合条件的组合
        backtracking(result, path, n, k, 1); // 开始回溯从数字1开始
        return result; // 返回结果集
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(); // 获取用户输入的n
        int k = scanner.nextInt(); // 获取用户输入的k
        scanner.close(); // 关闭Scanner
        Solution solution = new Solution(); // 创建Solution对象
        List<List<Integer>> result = solution.combine(n, k); // 调用组合函数得到符合条件的结果集
        for (List<Integer> list : result) { // 遍历结果集
            for (int num : list) { // 遍历组合
                System.out.print(num + " "); // 输出组合中的元素
            }
            System.out.println(); // 输出换行用于分隔每个组合
        }
    }
}

2.2组合总和

找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数并且每种组合中不存在重复的数字。

说明

  • 所有数字都是正整数。

  • 解集不能包含重复的组合。

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

思路

本题就是在[1,2,3,4,5,6,7,8,9]这个集合中找到和为n的k个数的组合。

相对于上题无非就是多了一个限制本题是要找到和为n的k个数的组合而整个集合已经是固定的了[1,...,9]。

例如 k = 2n = 4的话就是在集合[1,2,3,4,5,6,7,8,9]中求 k个数 = 2, n和 = 4的组合。

import java.util.*;

class Solution {
    private List<List<Integer>> result; // 存放结果集
    private List<Integer> path; // 符合条件的结果

    /**
     * backtracking - 回溯算法
     *
     * @param targetSum   目标和也就是题目中的n。
     * @param k           题目中要求k个数的集合。
     * @param sum         已经收集的元素的总和也就是path里元素的总和。
     * @param startIndex 下一层for循环搜索的起始位置。
     */
    private void backtracking(int targetSum, int k, int sum, int startIndex) {
        // 剪枝操作如果path里的元素个数已经等于k且当前元素总和不等于目标和targetSum直接返回。
        if (path.size() == k && sum != targetSum) {
            return;
        }
        // 如果path里的元素个数已经等于k且当前元素总和等于目标和targetSum说明已经找到了符合条件的结果加入到结果集中并返回。
        if (path.size() == k && sum == targetSum) {
            result.add(new ArrayList<>(path));
            return;
        }
        for (int i = startIndex; i <= 9; i++) {
            sum += i; // 处理
            path.add(i); // 处理
            backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex
            sum -= i; // 回溯
            path.remove(path.size() - 1); // 回溯
        }
    }

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

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int k = scanner.nextInt(); // 读取sin
        int n = scanner.nextInt(); // 读取n
        Solution solution = new Solution();
        List<List<Integer>> result = solution.combinationSum3(k, n);
        System.out.println(result);
    }
}

 2.3组合总和2

给定一个无重复元素的数组 candidates 和一个目标数 target 找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的数字可以无限制重复被选取。

说明

  • 所有数字包括 target都是正整数。

  • 解集不能包含重复的组合。

示例 1输入candidates = [2,3,6,7], target = 7, 所求解集为[ [7], [2,2,3] ]

提示

  • 组合没有数量要求

  • 元素可无限重复选

import java.util.*;

class Solution {
    public List<List<Integer>> combinationSum(int[] candidates, int target) {
        List<List<Integer>> res = new ArrayList<>();
        Arrays.sort(candidates); // 先进行排序
        backtracking(res, new ArrayList<>(), candidates, target, 0, 0);
        return res;
    }

    public void backtracking(List<List<Integer>> res, List<Integer> path, int[] candidates, int target, int sum, int idx) {
        // 找到了数字和为 target 的组合
        if (sum == target) {
            res.add(new ArrayList<>(path));
            return;
        }

        for (int i = idx; i < candidates.length; i++) {
            // 如果 sum + candidates[i] > target 就终止遍历
            if (sum + candidates[i] > target) break;
            path.add(candidates[i]);
            backtracking(res, path, candidates, target, sum + candidates[i], i);
            path.remove(path.size() - 1); // 回溯移除路径 path 最后一个元素
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] candidates1 = {2, 3, 6, 7};
        int target1 = 7;
        List<List<Integer>> res1 = s.combinationSum(candidates1, target1);
        System.out.println(res1); // [[7], [2, 2, 3]]

        int[] candidates2 = {2, 3, 5};
        int target2 = 8;
        List<List<Integer>> res2 = s.combinationSum(candidates2, target2);
        System.out.println(res2); // [[2, 2, 2, 2], [2, 3, 3], [3, 5]]
    }
}

2.4组合总和3

给定一个数组 candidates 和一个目标数 target 找出 candidates 中所有可以使数字和为 target 的组合。

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

说明 所有数字包括目标数都是正整数。解集不能包含重复的组合。

  • 示例 1:
  • 输入: candidates = [10,1,2,7,6,1,5], target = 8,
  • 所求解集为:
    [
      [1, 7],
      [1, 2, 5],
      [2, 6],
      [1, 1, 6]
    ]

思路

  1. 本题candidates 中的每个数字在每个组合中只能使用一次。
  2. 本题数组candidates的元素是有重复的

import java.util.*;

class Solution {
    LinkedList<Integer> path = new LinkedList<>();
    List<List<Integer>> ans = new ArrayList<>();
    boolean[] used;
    int sum = 0;

    public List<List<Integer>> combinationSum2(int[] candidates, int target) {
        used = new boolean[candidates.length];
        // 加标志数组用来辅助判断同层节点是否已经遍历
        Arrays.fill(used, false);
        // 为了将重复的数字都放到一起所以先进行排序
        Arrays.sort(candidates);
        backTracking(candidates, target, 0);
        return ans;
    }

    private void backTracking(int[] candidates, int target, int startIndex) {
        if (sum == target) {
            ans.add(new ArrayList(path));
        }
        for (int i = startIndex; i < candidates.length; i++) {
            if (sum + candidates[i] > target) {
                break;
            }
            // 出现重复节点同层的第一个节点已经被访问过所以直接跳过
            if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) {
                continue;
            }
            used[i] = true;
            sum += candidates[i];
            path.add(candidates[i]);
            // 每个节点仅能选择一次所以从下一位开始
            backTracking(candidates, target, i + 1);
            used[i] = false;
            sum -= candidates[i];
            path.removeLast();
        }
    }

    public static void main(String[] args) {
        Solution s = new Solution();
        int[] candidates = {10, 1, 2, 7, 6, 1, 5};
        int target = 8;
        List<List<Integer>> result = s.combinationSum2(candidates, target);
        System.out.println(result); // expected: [[1, 1, 6], [1, 2, 5], [1, 7], [2, 6]]
    }
}

2.5电话号码的字母组合

给定一个仅包含数字 2-9 的字符串返回所有它能表示的字母组合。

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

示例:

  • 输入"23"
  • 输出["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"].

说明尽管上面的答案是按字典序排列的但是你可以任意选择答案输出的顺序。 

import java.util.*;

class Solution {
    // 字母映射表
    private final String[] letterMap = {
            "",     // 0
            "",     // 1
            "abc",  // 2
            "def",  // 3
            "ghi",  // 4
            "jkl",  // 5
            "mno",  // 6
            "pqrs", // 7
            "tuv",  // 8
            "wxyz"  // 9
    };

    private List<String> result = new ArrayList<>(); // 存储所有结果
    private StringBuilder s = new StringBuilder(); // 存储每个组合

    private void backtracking(String digits, int index) {
        if (index == digits.length()) { // 当处理到最后一个数字时将s加入result中
            result.add(s.toString());
            return;
        }
        int digit = digits.charAt(index) - '0'; // 将当前数字转为int
        String letters = letterMap[digit]; // 找出数字对应的字符集
        for (int i = 0; i < letters.length(); i++) {
            s.append(letters.charAt(i)); // 将当前字符加入s
            backtracking(digits, index + 1); // 递归到下一个数字
            s.deleteCharAt(s.length() - 1); // 回溯将最后一个字符删除
        }
    }

    public List<String> letterCombinations(String digits) {
        result.clear(); // 清空result
        s.setLength(0); // 清空s
        if (digits.length() == 0) { // 边界条件当输入为空时直接返回result
            return result;
        }
        backtracking(digits, 0); // 从第一个数字开始处理
        return result;
    }
    
}
    public class Main {
    public static void main(String[] args) {
        Solution sol = new Solution();
        String digits1 = "23";
        List<String> res1 = sol.letterCombinations(digits1);
        System.out.println(res1);

        String digits2 = "";
        List<String> res2 = sol.letterCombinations(digits2);
        System.out.println(res2);

        String digits3 = "2";
        List<String> res3 = sol.letterCombinations(digits3);
        System.out.println(res3);
    }
}

三、切割问题

3.1分割回文串

给定一个字符串 s将 s 分割成一些子串使每个子串都是回文串。

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

示例: 输入: "aab" 输出: [ ["aa","b"], ["a","a","b"] ]

思路

  1. 切割问题有不同的切割方式
  2. 判断回文

import java.util.*;

class Solution {
    private List<List<String>> result = new ArrayList<>();
    private List<String> path = new ArrayList<>(); // 存放已经回文的子串

    private void backtracking(String s, int startIndex) {
        // 如果起始位置已经大于s的大小说明已经找到了一组分割方案了
        if (startIndex >= s.length()) {
            result.add(new ArrayList<>(path));
            return;
        }

        for (int i = startIndex; i < s.length(); i++) {
            if (isPalindrome(s, startIndex, i)) {   // 是回文子串
                // 获取[startIndex,i]在s中的子串
                String str = s.substring(startIndex, i + 1);
                path.add(str);
            } else {                                // 不是回文跳过
                continue;
            }
            backtracking(s, i + 1); // 寻找i+1为起始位置的子串
            path.remove(path.size() - 1); // 回溯过程弹出本次已经填在的子串
        }
    }

    private boolean isPalindrome(String s, int start, int end) {
        // 判断从start到end的子串是否是回文子串
        while (start < end) {
            if (s.charAt(start++) != s.charAt(end--)) {
                return false;
            }
        }
        return true;
    }

    public List<List<String>> partition(String s) {
        result.clear();
        path.clear();
        backtracking(s, 0);
        return result;
    }
}
public class Main {
    public static void main(String[] args) {
        Solution sol = new Solution();

        List<List<String>> res1 = sol.partition("aab");
        System.out.println(res1);

        List<List<String>> res2 = sol.partition("a");
        System.out.println(res2);

        List<List<String>> res3 = sol.partition("racecar");
        System.out.println(res3);
    }
}

3.2.复原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 地址。

示例 1

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

示例 2

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

提示

切割问题就可以使用回溯搜索法把所有可能性搜出来

import java.util.*;

class Solution {
    private List<String> result = new ArrayList<>(); // 记录结果

    // startIndex: 搜索的起始位置pointNum:添加逗点的数量
    private void backtracking(String s, int startIndex, int pointNum) {
        if (pointNum == 3) { // 逗点数量为3时分隔结束
            // 判断第四段子字符串是否合法如果合法就放进result中
            if (isValid(s, startIndex, s.length() - 1)) {
                result.add(s);
            }
            return;
        }
        for (int i = startIndex; i < s.length(); i++) {
            if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法
                s = s.substring(0, i + 1) + "." + s.substring(i + 1); // 在i的后面插入一个逗点
                pointNum++;
                backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2
                pointNum--; // 回溯
                s = s.substring(0, i + 1) + s.substring(i + 2); // 回溯删掉逗点
            } else {
                break; // 不合法直接结束本层循环
            }
        }
    }

    // 判断字符串s在左闭右闭区间[start, end]所组成的数字是否合法
    private boolean isValid(String s, int start, int end) {
        if (start > end) {
            return false;
        }
        if (s.charAt(start) == '0' && start != end) { // 0开头的数字不合法
            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;
    }

    public List<String> restoreIpAddresses(String s) {
        result.clear();
        if (s.length() < 4 || s.length() > 12) {
            return result; // 算是剪枝了
        }
        backtracking(s, 0, 0);
        return result;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        List<String> result = solution.restoreIpAddresses("25525511135");
        System.out.println(result); // ["255.255.11.135", "255.255.111.35"]
    }
}

四、子集问题

4.1子集

给定一组不含重复元素的整数数组 nums返回该数组所有可能的子集幂集。

说明解集不能包含重复的子集。

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

import java.util.ArrayList;
import java.util.List;

class Solution {
    List<List<Integer>> result = new ArrayList<>(); // 存储所有的子集
    List<Integer> path = new ArrayList<>(); // 存储当前正在生成的子集

    public List<List<Integer>> subsets(int[] nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }

    private void backtracking(int[] nums, int startIndex) {
        result.add(new ArrayList<>(path)); // 加入当前生成的子集
        if (startIndex == nums.length) { // 终止条件
            return;
        }
        for (int i = startIndex; i < nums.length; i++) {
            path.add(nums[i]);
            backtracking(nums, i + 1); // 从i+1开始搜索避免重复
            path.remove(path.size() - 1); // 回溯
        }
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        int[] nums = {1, 2, 3};
        List<List<Integer>> subsets = solution.subsets(nums);
        for (List<Integer> subset : subsets) {
            System.out.println(subset);
        }
    }
}

4.2子集2

给定一个可能包含重复元素的整数数组 nums返回该数组所有可能的子集幂集。

说明解集不能包含重复的子集。

示例:

  • 输入: [1,2,2]
  • 输出: [ [2], [1], [1,2,2], [2,2], [1,2], [] ]

思路

 集合里有重复元素了而且求取的子集要去重。

import java.util.*;

class Solution {
    private List<List<Integer>> result;
    private List<Integer> path;

    private void backtracking(int[] nums, int startIndex) {
        result.add(new ArrayList<>(path)); // 收集子集
        for (int i = startIndex; i < nums.length; i++) {
            // 如果这个元素和上一个元素一样并且上一个元素没有被选中那么这个元素也不用选
            if (i > startIndex && nums[i] == nums[i - 1]) {
                continue;
            }
            path.add(nums[i]);
            backtracking(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }

    public List<List<Integer>> subsetsWithDup(int[] nums) {
        result = new ArrayList<>();
        path = new ArrayList<>();
        Arrays.sort(nums); // 数组排序
        backtracking(nums, 0);
        return result;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        int[] nums = {1, 2, 2};
        List<List<Integer>> subsets = solution.subsetsWithDup(nums);
        for (List<Integer> subset : subsets) {
            System.out.println(subset.toString());
        }
    }
}

4.3递增子序列

给定一个整型数组, 你的任务是找到所有该数组的递增子序列递增子序列的长度至少是2。

示例:

  • 输入: [4, 6, 7, 7]
  • 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]]

说明:

  • 给定数组的长度不会超过15。
  • 数组中的整数范围是 [-100,100]。
  • 给定数组中可能包含重复数字相等的数字应该被视为递增的一种情况。

import java.util.*;

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

    private void backtracking(int[] nums, int startIndex) {
        if (path.size() > 1) {
            result.add(new ArrayList<>(path)); // 注意这里要new一个新的list
            // 注意这里不要加return要取树上的节点
        }
        Set<Integer> set = new HashSet<>(); // 使用set对本层元素进行去重
        for (int i = startIndex; i < nums.length; i++) {
            if ((!path.isEmpty() && nums[i] < path.get(path.size() - 1))
                    || set.contains(nums[i])) {
                continue;
            }
            set.add(nums[i]); // 记录这个元素在本层用过了本层后面不能再用了
            path.add(nums[i]);
            backtracking(nums, i + 1);
            path.remove(path.size() - 1);
        }
    }

    public List<List<Integer>> findSubsequences(int[] nums) {
        result.clear();
        path.clear();
        backtracking(nums, 0);
        return result;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        int[] nums = {4, 6, 7, 7};
        List<List<Integer>> result = solution.findSubsequences(nums);
        System.out.println(result);
    }
}

五、排列问题

5.1全排列

给定一个 没有重复 数字的序列返回其所有可能的全排列。

示例:

  • 输入: [1,2,3]
  • 输出: [ [1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1] ]

import java.util.*;

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

    // 回溯算法
    public void backtracking(List<Integer> nums, boolean[] used) {
        // 如果当前path中的数字数量等于nums中的数字数量则已经找到一组答案
        if (path.size() == nums.size()) {
            result.add(new ArrayList<>(path));
            return;
        }

        // 枚举nums中所有的数字
        for (int i = 0; i < nums.size(); i++) {
            // 如果当前数字已经使用过则跳过
            if (used[i]) {
                continue;
            }

            // 标记当前数字已经使用过
            used[i] = true;
            // 将当前数字加入到path中
            path.add(nums.get(i));
            // 继续递归查找
            backtracking(nums, used);
            // 回溯
            path.remove(path.size() - 1);
            used[i] = false;
        }
    }

    public List<List<Integer>> permute(List<Integer> nums) {
        result.clear();
        path.clear();
        boolean[] used = new boolean[nums.size()];
        backtracking(nums, used);
        return result;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        List<Integer> nums = new ArrayList<>(Arrays.asList(1, 2, 3));
        List<List<Integer>> result = solution.permute(nums);
        System.out.println(result);
    }
}

asList是Java中的一个方法它可以将数组转换为List。返回的List是由指定数组支持的固定大小的列表。 该方法可以作为基于数组和基于集合的API之间的桥梁

5.2全排列 II

给定一个可包含重复数字的序列 nums 按任意顺序 返回所有不重复的全排列。

示例 1

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

import java.util.*;

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

    private void backtracking(int[] nums, boolean[] used) {
        // 如果当前组合中的数字数量等于数组的长度说明找到了一组加入结果集中
        if (path.size() == nums.length) {
            result.add(new ArrayList<>(path)); // 注意需要新建一个ArrayList否则结果会受到后续操作的影响
            return;
        }

        for (int i = 0; i < nums.length; i++) {
            // 如果当前数字已经被使用过跳过
            if (used[i]) {
                continue;
            }
            // 如果当前数字和前一个数字相同并且前一个数字没有被使用过则跳过
            if (i > 0 && nums[i] == nums[i-1] && !used[i-1]) {
                continue;
            }

            // 将当前数字加入组合中
            path.add(nums[i]);
            used[i] = true; // 标记当前数字已经被使用过

            // 继续搜索
            backtracking(nums, used);

            // 回溯将当前数字从组合中删除并标记为未使用过
            path.remove(path.size() - 1);
            used[i] = false;
        }
    }

    public List<List<Integer>> permuteUnique(int[] nums) {
        result.clear(); // 清空结果集
        path.clear(); // 清空当前组合
        Arrays.sort(nums); // 对数组进行排序方便判断重复数字
        boolean[] used = new boolean[nums.length]; // 标记每个数字是否被使用过
        backtracking(nums, used); // 开始搜索
        return result;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        int[] nums = {1, 1, 2};
        List<List<Integer>> result = solution.permuteUnique(nums);
        System.out.println(result);
    }
}

六、棋盘问题

6.1N皇后

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上并且使皇后彼此之间不能相互攻击。

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

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

示例 1

  • 输入n = 4
  • 输出[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
  • 解释如上图所示4 皇后问题存在两个不同的解法。

提示

首先来看一下皇后们的约束条件

  1. 不能同行
  2. 不能同列
  3. 不能同斜线

import java.util.ArrayList;
import java.util.List;

class Solution {
    private List<List<String>> result = new ArrayList<>();
    // n 为输入的棋盘大小
    // row 是当前递归到棋盘的第几行了
    private void backtracking(int n, int row, char[][] chessboard) {
        if (row == n) {
            result.add(charToStringList(chessboard));
            return;
        }
        for (int col = 0; col < n; col++) {
            if (isValid(row, col, chessboard, n)) { // 验证合法就可以放
                chessboard[row][col] = 'Q'; // 放置皇后
                backtracking(n, row + 1, chessboard);
                chessboard[row][col] = '.'; // 回溯撤销皇后
            }
        }
    }

    private boolean isValid(int row, int col, char[][] chessboard, 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 >= 0; i--, j--) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        // 检查 135度角是否有皇后
        for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (chessboard[i][j] == 'Q') {
                return false;
            }
        }
        return true;
    }

    private List<String> charToStringList(char[][] chessboard) {
        List<String> board = new ArrayList<>();
        for (char[] row : chessboard) {
            board.add(new String(row));
        }
        return board;
    }

    public List<List<String>> solveNQueens(int n) {
        result.clear();
        char[][] chessboard = new char[n][n];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                chessboard[i][j] = '.';
            }
        }
        backtracking(n, 0, chessboard);
        return result;
    }

    public static void main(String[] args) {
        Solution solution = new Solution();
        int n = 4;
        List<List<String>> result = solution.solveNQueens(n);
        for (List<String> board : result) {
            for (String row : board) {
                System.out.println(row);
            }
            System.out.println();
        }
    }
}

6.2解数独

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

一个数独的解法需遵循如下规则 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 空白格用 '.' 表示。

一个数独。

答案被标成红色。

提示

  • 给定的数独序列只包含数字 1-9 和字符 '.' 。
  • 你可以假设给定的数独只有唯一解。
  • 给定数独永远是 9x9 形式的。

import java.util.*;

class Solution {
    private boolean backtracking(char[][] board) {
        for (int i = 0; i < board.length; i++) { // 遍历行
            for (int j = 0; j < board[0].length; j++) { // 遍历列
                if (board[i][j] == '.') {
                    for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适
                        if (isValid(i, j, k, board)) {
                            board[i][j] = k; // 放置k
                            if (backtracking(board)) {
                                return true; // 如果找到合适一组立刻返回
                            }
                            board[i][j] = '.'; // 回溯撤销k
                        }
                    }
                    return false; // 9个数都试完了都不行那么就返回false
                }
            }
        }
        return true; // 遍历完没有返回false说明找到了合适棋盘位置了
    }

    private boolean isValid(int row, int col, char val, char[][] board) {
        for (int i = 0; i < 9; i++) { // 判断行里是否重复
            if (board[row][i] == val) {
                return false;
            }
        }
        for (int j = 0; j < 9; j++) { // 判断列里是否重复
            if (board[j][col] == val) {
                return false;
            }
        }
        int startRow = (row / 3) * 3;
        int startCol = (col / 3) * 3;
        for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复
            for (int j = startCol; j < startCol + 3; j++) {
                if (board[i][j] == val) {
                    return false;
                }
            }
        }
        return true;
    }

    public void solveSudoku(char[][] board) {
        backtracking(board);
    }

    public static void main(String[] args) {
        Solution sol = new Solution();
        char[][] board = new char[][]{
                {'5','3','.','.','7','.','.','.','.'},
                {'6','.','.','1','9','5','.','.','.'},
                {'.','9','8','.','.','.','.','6','.'},
                {'8','.','.','.','6','.','.','.','3'},
                {'4','.','.','8','.','3','.','.','1'},
                {'7','.','.','.','2','.','.','.','6'},
                {'.','6','.','.','.','.','2','8','.'},
                {'.','.','.','4','1','9','.','.','5'},
                {'.','.','.','.','8','.','.','7','9'}
        };
        sol.solveSudoku(board);
        for (int i = 0; i < 9; i++) {
            System.out.println(Arrays.toString(board[i]));
        }
    }
}

七、课内实验

7.1 0-1背包子集问题

有n个物品它们有各自的体积和价值现有给定容量的背包如何让背包里装入的物品具有最大的价值总和
与普通背包问题不同0-1背包问题中物品以整体的形式出现只能选择整体放入背包或整体不放入背包。

c语言代码

#include <stdio.h>
#include <stdlib.h>
#define N 100
int n, c, maxValue = 0; // 物品数量背包容量最大价值
int w[N], v[N]; // 物品重量物品价值
void backtrack(int i, int res, int value) {
    if (i == n) { // 所有物品都被考虑过了
        if (value > maxValue) maxValue = value; // 更新最大价值
        return;
    }
    if (res >= w[i]) backtrack(i + 1, res - w[i], value + v[i]); // 考虑第i个物品放入背包
    backtrack(i + 1, res, value); // 不考虑第i个物品放入背包
}
int main() {
    scanf("%d%d", &n, &c);
    for (int i = 0; i < n; i++) scanf("%d%d", &w[i], &v[i]);
    backtrack(0, c, 0);
    printf("%d\n", maxValue);
    return 0;
}

 JAVA

import java.util.Scanner;
public class Main {
    static int n, c, maxValue = 0; // 物品数量背包容量最大价值
    static int[] w = new int[100], v = new int[100]; // 物品重量物品价值
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        c = sc.nextInt();
        for (int i = 0; i < n; i++) {
            w[i] = sc.nextInt();
            v[i] = sc.nextInt();
        }
        backtrack(0, c, 0);
        System.out.println(maxValue);
    }
    public static void backtrack(int i, int res, int value) {
        if (i == n) { // 所有物品都被考虑过了
            if (value > maxValue) maxValue = value; // 更新最大价值
            return;
        }
        if (res >= w[i]) backtrack(i + 1, res - w[i], value + v[i]); // 考虑第i个物品放入背包
        backtrack(i + 1, res, value); // 不考虑第i个物品放入背包
    }
}

7.2旅行售货员问题

问题描述某售货员要到若干城市去推销商品一直各城市之间的路程他要选定一条从驻地出发经过每个城市一遍最后回到住地的路线使总的路程最短。 该问题是一个NP完全问题 有(n-1)!条可选路线 最优解(1,3,2,4,1)最优值25

#include <stdio.h>
#include <stdbool.h>
#define MAXN 100 // 最大城市数

int n;             // 城市数
int graph[MAXN][MAXN]; // 图的邻接矩阵
int path[MAXN],bestPath[MAXN];    // 保存当前路径
bool visited[MAXN]; // 标记城市是否访问过
int minDist = 0x7fffffff; // 保存最短路径的长度

void backtracking(int cur, int dist) {
    if (cur == n) {  // 所有城市都已经走过了
        if (dist + graph[path[n - 1]][0] < minDist) {
            minDist = dist + graph[path[n - 1]][0]; // 更新最短路径
            for(int i = 0;i < n;i++){
                bestPath[i] = path[i];
            }
        }
        return;
    }
    for (int i = 1; i < n; i++) { // 枚举下一个城市
        if (!visited[i]) {        // 如果这个城市还没有访问过
            path[cur] = i;         // 选择这个城市
            visited[i] = true;     // 标记这个城市已经访问过
            backtracking(cur + 1, dist + graph[path[cur - 1]][i]); // 递归到下一层
            visited[i] = false;    // 回溯撤销选择
        }
    }
}

int main() {
    scanf("%d", &n); // 输入城市数
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            scanf("%d", &graph[i][j]); // 输入邻接矩阵
        }
    }
    path[0] = 0;       // 起点是城市0
    visited[0] = true; // 标记起点已经访问过
    backtracking(1, 0); // 从第2个城市开始递归
    printf("%d\n", minDist); // 输出最短路径长度
    for(int i = 0;i < n;i++){
        printf("%d ",bestPath[i]+1);
    }
    return 0;
}

 7.3装载问题(组合问题)

给定n个集装箱要装上一艘载重量为c的轮船其中集装箱i的重量为wi。集装箱装载问题要求确定在不超过轮船载重量的前提下将尽可能多的集装箱装上轮船。

由于集装箱问题是从n个集装箱里选择一部分集装箱假设解向量为X(x1, x2, …, xn)其中xi∈{0, 1} xi =1表示集装箱i装上轮船 xi =0表示集装箱i不装上轮船。

输入

每组测试数据第1行有2个整数c和n。C是轮船的载重量0c30000n是集装箱的个数n≤20。第2行有n个整数w1, w2, …, wn分别表示n个集装箱的重量。

输出

对每个测试例输出两行第1行是装载到轮船的最大载重量第2行是集装箱的编号。

输入样例

3 34

21 10 5

输出样例

1 2
31

 

#include <stdio.h>
#define MAX_N 20

int n;                      // 货物数量
int c;                      // 车的载重量
int w[MAX_N],x[MAX_N];               // 每个货物的重量
int best[MAX_N];            // 最优解
int cw;                     // 当前载重量
int bestw;                  // 最优载重量
int r;                      // 剩余物品重量和

// 搜索装载方案
void backtrack(int i)
{
    // 搜索到叶子节点
    if (i > n) {
        // 如果找到更优解则更新最优解
        if (cw > bestw) {
            bestw = cw;
            for (int j = 1; j <= n; j++) {

                best[j] = x[j];
            }
        }
        return;
    }
    // 搜索左子树
    r -= w[i];
    if (cw + w[i] <= c) {
        x[i] = 1;
        cw += w[i];
        backtrack(i + 1);
        cw -= w[i];
    }
    r += w[i];
    // 搜索右子树
    if (cw + r > bestw) {
        x[i] = 0;
        backtrack(i + 1);
    }
}

int main()
{
    //printf("请输入货物数量和车的载重量用空格分隔\n");
    scanf("%d%d", &n, &c);
    //printf("请输入每个货物的重量\n");
    for (int i = 1; i <= n; i++) {
        scanf("%d", &w[i]);
        r += w[i];
    }
    backtrack(1);
    printf("最优装载方案为\n");
    for (int i = 1; i <= n; i++) {
        if (best[i]) {
            printf("%d ", i);
        }
    }
    printf("\n最优载重量为%d\n", bestw);
    return 0;
}

 7.4图的着色问题(组合问题)

给定图G=(V,E)和m重颜色若这个图不是m可着色给出否定回答若这个图是m可着色的找出所有不同的着色法。

#include <stdio.h>
#include <stdlib.h>

#define MAXN 100

int n, m;               // 顶点数和着色数
int color[MAXN];        // 存储每个顶点的颜色
int graph[MAXN][MAXN];  // 存储无向图的邻接矩阵
int count = 0;          // 记录着色方案数

// 判断第i个顶点是否可以涂上color
int isOk(int i, int color[]) {
    for (int j = 1; j <= n; j++) {
        if (graph[i][j] == 1 && color[j] == color[i]) {
            // 如果i和j相邻且颜色相同返回0
            return 0;
        }
    }
    return 1;
}

// 递归求解m着色问题
void backtrack(int i) {
    if (i > n) {
        // 如果所有顶点都涂色了输出方案
        count++;
        printf("方案%d: ", count);
        for (int j = 1; j <= n; j++) {
            printf("%d ", color[j]);
        }
        printf("\n");
    } else {
        for (int j = 1; j <= m; j++) {
            color[i] = j;  // 尝试给第i个顶点涂上第j种颜色
            if (isOk(i, color)) {
                // 如果第i个顶点可以涂上第j种颜色继续涂色
                backtrack(i + 1);
            }
            color[i] = 0;  // 回溯撤销第i个顶点的颜色
        }
    }
}

int main() {
    printf("请输入顶点数和着色数n和m:");
    scanf("%d%d", &n, &m);

    // 读入无向图的邻接矩阵
    printf("请输入无向图的邻接矩阵:\n");
    for (int i = 1; i <= n; i++) {
        for (int j = 1; j <= n; j++) {
            scanf("%d", &graph[i][j]);
        }
    }

    // 求解m着色问题
    printf("着色所有可能的解:\n");
    backtrack(1);

    // 输出着色可能解的总数
    printf("着色可能解的总数为%d\n", count);
    return 0;
}
请输入顶点数和着色数n和m:4 3
请输入无向图的邻接矩阵:
0 1 1 1
1 0 1 0
1 1 0 1
1 0 1 0
着色所有可能的解:
方案1: 1 2 3 2
方案2: 1 3 2 3
方案3: 2 1 3 1
方案4: 2 3 1 3
方案5: 3 1 2 1
方案6: 3 2 1 2
着色可能解的总数为6

7.5最大团问题

它是指在一个无向图中找到一个最大的完全子图其中任意两个顶点之间都有边相连。

 

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#define MAX_N 100

int n, m;  // n: 图中节点数, m: 最大团中节点数
bool graph[MAX_N][MAX_N];  // 邻接矩阵表示图
bool visited[MAX_N];  // 标记节点是否在当前团中

// 判断节点 v 是否可以加入当前团
bool is_clique(int v, bool selected[MAX_N]) {
    for (int i = 0; i < n; i++) {
        if (selected[i] && !graph[v][i]) {
            return false;
        }
    }
    return true;
}

// 计算最大团
void max_clique(bool selected[MAX_N], int size, int index) {
    if (index == n) {  // 所有节点都判断过了
        if (size > m) {  // 更新最大团
            m = size;
            for (int i = 0; i < n; i++) {
                visited[i] = selected[i];
            }
        }
        return;
    }
    // 剪枝如果还有未被选择的节点且这些节点连向的节点数已经比最大团的大小小则不可能成为最大团的一部分
    if (size + (n - index) < m) {
        return;
    }
    // 当前节点不在团中的情况
    selected[index] = false;
    max_clique(selected, size, index + 1);
    // 当前节点在团中的情况
    if (is_clique(index, selected)) {
        selected[index] = true;
        max_clique(selected, size + 1, index + 1);
        selected[index] = false;
    }
}

int main() {
    printf("请输入图中节点数 n: ");
    scanf("%d", &n);
    printf("请输入邻接矩阵表示的图\n");
    for (int i = 0; i < n; i++) {
        for (int j = 0; j < n; j++) {
            scanf("%d", &graph[i][j]);
        }
    }
    bool selected[MAX_N] = {false};  // 标记节点是否在当前团中
    max_clique(selected, 0, 0);
    printf("最大团包含的节点为");
    for (int i = 0; i < n; i++) {
        if (visited[i]) {
            printf("%d ", i + 1);  // 输出节点编号
        }
    }
    printf("\n最大团大小为%d\n", m);
    return 0;
}

输入
请输入图的顶点个数: 5
请输入邻接矩阵(0表示不连通1表示连通)
0 1 1 0 0
1 0 1 1 1
1 1 0 1 0
0 1 1 0 1
0 1 0 1 0

输出
最大团大小为3
最大团包含的顶点编号为2 3 4

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6