1.穷举法简介
穷举法(Exhaustive Search)也称为暴力搜索或暴力穷举,是一种简单直观的算法思想,通过穷举所有可能的解来解决问题。
穷举法的基本思想是将问题的解空间中的每一个可能的解都遍历一遍,然后判断每个解是否满足问题的条件。具体步骤如下:
-
确定问题的解空间:确定问题的解的结构和限制条件,明确解的取值范围或解的组成方式。
-
遍历解空间:根据解空间的定义,遍历解空间中的每一个可能的解。可以使用循环、递归或迭代等方式进行遍历。
-
判断解的有效性:对于每一个遍历得到的解,在问题的约束条件下判断其是否有效。如果解有效,则继续执行下一步操作;如果解无效,则舍去。
-
判断是否满足问题的要求:如果满足问题的要求,则得到了问题的解;如果不满足,则继续遍历解空间。
穷举法的优点是简单直观,能够保证找到问题的解(如果存在),适用于问题规模较小、解空间可遍历的情况。然而,穷举法在问题规模较大时,需要遍历大量的可能解,时间复杂度往往较高,效率较低。因此,在实际应用中,需要根据问题的规模和复杂程度选择合适的解决方法。
2.回溯简介
回溯算法是一种穷举搜索的算法,通常用于解决组合优化问题、排列问题和搜索问题等。其本质是通过不断尝试和回溯来找到问题的解。
回溯算法的基本思想是逐步构建解决方案,并在每一步尝试后进行检查,如果发现当前方案不可行(不符合问题的约束条件或无法得到最优解),就回溯到上一步进行调整,然后再次尝试其他可能的选择。这样不断地迭代尝试,直至找到问题的解或穷尽所有可能。
回溯算法的一般步骤如下:
- 定义问题的解空间:确定问题的解的结构和限制条件。
- 确定选择列表:确定每一步可以进行的选择。
- 递归穷举:使用递归函数实现对解空间的穷举搜索,每一步选择一个合适的选择,再深入下一层进行递归调用。
- 回溯和剪枝:在递归过程中,如果发现当前选择不满足问题的约束条件,就进行回溯,撤销当前选择,回到上一层继续尝试其他选择。
- 判断终止条件:当满足问题的终止条件时,即找到了问题的解或穷尽了所有可能,停止递归。
回溯算法通常使用深度优先搜索(DFS)的思想,通过递归实现搜索过程。这种算法的时间复杂度取决于问题的规模和解的个数,往往是指数级的,因此在应用时需要考虑问题的规模和算法的效率。
程序在运行过程中分成了多个阶段,通过某些手段,将数据恢复到之前某一阶段,这就称之为回溯,手段包括:方法栈,自定义栈
一个方法的局部变量,是不会被其他方法所修改【干涉】的
基本数据类型和引用数据类型的回溯不一样
基本数据类型可以直接恢复,但是引用数据类型用的是同一个对象,不能自己恢复,得手动恢复,递归前后进行相加和相减
减枝:由于一些条件没有满足,就不用继续递归了,所以分枝变少了
总结:如果用的是可变的集合数据,数组数据... 在递归的前后,自己手动恢复数据的状态
3.应用
Leetcode46
public static List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
dfs(nums, new boolean[nums.length], new LinkedList<>(), result);
return result;
}
static void dfs(int[] nums, boolean[] visited, LinkedList<Integer> stack, List<List<Integer>> result) {
//说明找到一种排列
if (stack. Size() == nums.length) {
//stack是引用数据类型,result.add加的是stack指向的值,stack会变化,所以new ArrayList(stack) 把当前值保存下来
result. Add(new ArrayList<>(stack));
return;
}
//遍历nums数组,发现没有被使用的元素,就把它标记为已使用,然后压入栈
for (int i = 0; i < nums.length; i++) {
if (!visited[i]) {
stack.push(nums[i]);
visited[i] = true;
dfs(nums, visited, stack, result);
//处理下一个时,把状态恢复成未使用,并且把压入栈的元素弹出
visited[i] = false;
stack.pop();
}
}
}
Leetcode47
和46的区别是,去除重复的排列
解法:对传入的数组进行排序,目的是让有重复值的元素排到相邻的位置
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
dfs(nums, new boolean[nums.length], new LinkedList<>(), result);
return result;
}
void dfs(int[] nums, boolean[] visited, LinkedList<Integer> stack, List<List<Integer>> result) {
if (stack.size() == nums.length) {
result.add(new ArrayList<>(stack));
return;
}
for (int i = 0; i < nums.length; i++) {
//减枝操作
if (i > 0 && nums[i - 1] == nums[i] && !visited[i-1]) {
continue;
}
if (!visited[i]) {
stack.push(nums[i]);
visited[i] = true;
dfs(nums, visited, stack, result);
visited[i] = false;
stack.pop();
}
}
}
Leetcode77
和排列的题目不一样,不用考虑顺序,即12 21是一样的
public List<List<Integer>> combine(int n, int k) {
List<List<Integer>> result = new ArrayList<>();
dfs(1, n, k, new LinkedList<>(), result);
return result;
}
void dfs(int start, int n, int k, LinkedList<Integer> stack, List<List<Integer>> result) {
if (stack.size() == k) {
result.add(new ArrayList<>(stack));
return;
}
for (int i = start; i <= n; i++) {
//缺的数字>备用数字,就要减枝
if(k - stack. Size() > n - i + 1) {
continue;
}
stack. Push(i);
//从i+1开始,是为了避免重复,如果从1开始,那么求的就是排列,不是组合
dfs(i + 1, n, k, stack, result);
stack.pop();
}
}
Leetcode39
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
dfs(0, candidates, target, new LinkedList<>(), result);
return result;
}
void dfs(int start, int[] candidates, int target, LinkedList<Integer> stack, List<List<Integer>> result) {
if (target == 0) {
result.add(new ArrayList<>(stack));
return;
}
for (int i = start; i < candidates. Length; i++) {
int candidate = candidates[i];
//剩的数 < 待组合的数 那么没有必要组合,减枝操作
if (target < candidate) {
continue;
}
stack.push(candidate);
dfs(i, candidates, target - candidate, stack, result);
stack.pop();
}
}
零钱兑换问题:求最值求汇总信息的可以用动态规划
Leetcode40
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates);
dfs(0, candidates, new boolean[candidates.length], target, new LinkedList<>(), result);
return result;
}
static void dfs(int start, int[] candidates, boolean[] visited, int target, LinkedList<Integer> stack, List<List<Integer>> result) {
if (target == 0) {
result.add(new ArrayList<>(stack));
return;
}
for (int i = start; i < candidates.length; i++) {
int candidate = candidates[i];
if (target < candidate) {
continue;
}
if (i > 0 && candidate == candidates[i - 1] && !visited[i - 1]) {
continue;
}
visited[i] = true;
stack. Push(candidate);
//避免重复,让我们的数字只出现一次
dfs(i + 1, candidates, visited, target - candidate, stack, result);
stack.pop();
visited[i] = false;
}
}
Leetcode216
和Leetcode77非常像
public List<List<Integer>> combinationSum3(int k, int target) {
List<List<Integer>> result = new ArrayList<>();
dfs(1, target, k, new LinkedList<>(), result);
return result;
}
static void dfs(int start, int target, int k, LinkedList<Integer> stack, List<List<Integer>> result) {
if (target == 0 && stack.size() == k) {
result.add(new ArrayList<>(stack));
return;
}
for (int i = start; i <= 9; i++) {
if (target < i) {
continue;
}
if (stack.size() == k) {
continue;
}
stack.push(i);
dfs(i + 1, target - i, k, stack, result);
stack.pop();
}
}
Leetcode51
public List<List<String>> solveNQueens(int n) {
boolean[] ca = new boolean[n];//记录列冲突
boolean[] cb = new boolean[2 * n - 1];//记录左斜线冲突
boolean[] cc = new boolean[2 * n - 1];//记录右斜线冲突
char[][] table = new char[n][n];//皇后的状态
List<List<String>> result = new ArrayList<>();
for (char[] t : table) {
Arrays.fill(t, '.');
}
dfs(0, n, table, ca, cb, cc, result);
return result;
}
static void dfs(int i, int n, char[][] table, boolean[] ca, boolean[] cb, boolean[] cc, List<List<String>> result) {
//结束条件
if (i == n) {//找到解
List<String> temp = new ArrayList<>();
for (char[] t : table) {
temp.add(new String(t));
}
result.add(temp);
return;
}
//列的循环
for (int j = 0; j < n; j++) {
if (ca[j] || cb[i + j] || cc[n - 1 - (i - j)]) {
continue;
}
table[i][j] = 'Q';
ca[j] = cb[i + j] = cc[n - 1 - (i - j)] = true;
dfs(i + 1, n, table, ca, cb, cc, result);
//回溯--递归没成功,退出递归的时候应该由下面的代码回复成递归之前的状态
table[i][j] = '.';
ca[j] = cb[i + j] = cc[n - 1 - (i - j)] = false;
}
}
Leetcode37
public void solveSudoku(char[][] table) {
boolean[][] ca = new boolean[9][9];//行冲突状态
boolean[][] cb = new boolean[9][9];//列冲突状态
//(i/3)*3+j/3--找到九宫格索引,每个九宫格也有九个格子,九种状态
boolean[][] cc = new boolean[9][9];//九宫格冲突状态
//记录初始冲突
for (int i = 0; i < 9; i++) {
for (int j = 0; j < 9; j++) {
char ch = table[i][j];
if (ch != '.') {//初始化冲突状态
ca[i][ch - '1'] = true;
cb[j][ch - '1'] = true;
cc[i / 3 * 3 + j / 3][ch - '1'] = true;
}
}
}
dfs(0, 0, table, ca, cb, cc);
}
//针对某个空格进行填充
static boolean dfs(int i, int j, char[][] table, boolean[][] ca, boolean[][] cb, boolean[][] cc) {
while (table[i][j] != '.') {//查找下一个空格
if (++j >= 9) {
j = 0;
i++;
}
if (i >= 9) {
return true;//找到解
}
}
//填空
for (int x = 1; x <= 9; x++) {
//检查冲突
if (ca[i][x - 1] || cb[j][x - 1] || cc[i / 3 * 3 + j / 3][x - 1]) {
continue;
}
table[i][j] = (char) (x + '0');
//更新记录冲突的数组
ca[i][x - 1] = cb[j][x - 1] = cc[i / 3 * 3 + j / 3][x - 1] = true;
if (dfs(i, j, table, ca, cb, cc)) {//这里不用再做处理,因为已经被填充,下一次递归的时候,while循环会进行处理【查找下一个空格】
return true;//找到解,不需要再尝试
}
//回溯,尝试下一个数字
table[i][j] = '.';
ca[i][x - 1] = cb[j][x - 1] = cc[i / 3 * 3 + j / 3][x - 1] = false;
}
//当前这个位置 9个数字都尝试了不行,那么失败,就要回溯,填错了
return false;
}
Leetcode167
public int[] twoSum(int[] numbers, int target) {
int i = 0;
int j = numbers.length - 1;
while (numbers[i] + numbers[j] != target) {
int num = numbers[i] + numbers[j];
if (num < target) {
i++;
}else if (num > target) {
j--;
}else{
break;
}
}
return new int[]{i + 1, j + 1};
}
Leetcode15
思路:固定其中一个数字,就可以用两数之和来求解 ,四数之和的解法也类似
public List<List<Integer>> threeSum(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
dfs(3, 0, nums.length - 1, 0, nums, new LinkedList<>(), result);
return result;
}
static void dfs(int n, int i, int j, int target, int[] nums, LinkedList<Integer> stack, List<List<Integer>> result) {
// stack够了 就算解--不能这样优化,效率低
// if (stack.size() == 3) {
// if (target == 0) {
// result.add(new ArrayList<>(stack));
// }
// return;
// }
if (n == 2) {
//套用两数之和,不用递归
twoSum(i, j, nums, target, stack, result);
return;
}
//开始固定数字
for (int k = i; k < j; k++) {
//检查重复
if (k > i && nums[k] == nums[k - 1]) {
continue;
}
//固定一个数字,再尝试 n-1 数字之和
stack.push(nums[k]);
dfs(n - 1, k + 1, j, target - nums[k], nums, stack, result);
stack.pop();
}
}
public static void twoSum(int i, int j, int[] numbers, int target, LinkedList<Integer> stack, List<List<Integer>> result) {
while (i < j) {
int num = numbers[i] + numbers[j];
if (num < target) {
i++;
} else if (num > target) {
j--;
} else {
//一部分解在stack,并一部分解在numbers数组中
ArrayList<Integer> list = new ArrayList<>(stack);
list.add(numbers[i]);
list.add(numbers[j]);
result.add(list);
//继续查找其他的解 -- 缩小范围
i++;
j--;
//处理重复解
while (i < j && numbers[i] == numbers[i - 1]) {
i++;
}
while (i < j && numbers[j] == numbers[j + 1]) {
j--;
}
}
}
}
Leetcode18
解题思路
先对数组进行排序,固定一个变为三数之和,固定两个变为两数之和
public class SumLeetcode18 {
public List<List<Integer>> fourSum(int[] nums, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums);
dfs(4, 0, nums.length - 1, target, nums, new LinkedList<>(), result);
return result;
}
static void dfs(int n, int i, int j, int target, int[] nums, LinkedList<Integer> stack, List<List<Integer>> result) {
if (n == 2) {
//套用两数之和,不用递归
twoSum(i, j, nums, target, stack, result);
return;
}
for (int k = i; k < j - (n - 2); k++) {//四树之和 i < j - 2 三数之和 i < j - 1
//检查重复
if (k > i && nums[k] == nums[k - 1]) {
continue;
}
//固定一个数字,再尝试 n-1 数字之和
stack.push(nums[k]);
dfs(n - 1, k + 1, j, target - nums[k], nums, stack, result);
stack.pop();
}
}
public static void twoSum(int i, int j, int[] numbers, int target, LinkedList<Integer> stack, List<List<Integer>> result) {
while (i < j) {
int num = numbers[i] + numbers[j];
if (num < target) {
i++;
} else if (num > target) {
j--;
} else {
ArrayList<Integer> list = new ArrayList<>(stack);
list.add(numbers[i]);
list.add(numbers[j]);
result.add(list);
//继续查找其他的解 -- 缩小范围
i++;
j--;
while (i < j && numbers[i] == numbers[i - 1]) {
i++;
}
while (i < j && numbers[j] == numbers[j + 1]) {
j--;
}
}
}
}
}