回溯问题
回溯算法全面总结
- 回溯算法核心思想
回溯算法是一种系统性搜索问题的算法,它通过深度优先搜索的方式遍历所有可能的解空间,并在搜索过程中通过剪枝来避免无效的搜索。
核心特点:
试错思想:尝试各种可能的选择,发现不可行时回退
系统性遍历:保证找到所有可行解
剪枝优化:提前排除不可能的解,提高效率
- 回溯算法通用框架
''' def backtrack(路径, 选择列表): if 满足结束条件: 结果集.append(路径副本) return
for 选择 in 选择列表:
if 选择不合法: # 剪枝操作
continue
做选择
backtrack(新路径, 新选择列表)
撤销选择
'''
- 三大类回溯问题及解法
3.1 组合问题(Combinations) 特点:不关心顺序,[1,2]和[2,1]是相同的排列 ''' class CombinationSolution {
// 组合总和:可重复使用元素
public List<List<Integer>> combinationSum(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates); // 排序便于剪枝
backtrack(candidates, target, 0, new ArrayList<>(), result, 0);
return result;
}
private void backtrack(int[] candidates, int target, int start,
List<Integer> path, List<List<Integer>> result, int currentSum) {
// 结束条件:当前和等于目标值
if (currentSum == target) {
result.add(new ArrayList<>(path));
return;
}
// 结束条件:当前和超过目标值
if (currentSum > target) {
return;
}
for (int i = start; i < candidates.length; i++) {
// 剪枝:如果当前和加上候选数已经大于目标,跳过
if (currentSum + candidates[i] > target) {
continue;
}
// 做出选择
path.add(candidates[i]);
currentSum += candidates[i];
// 递归:注意这里传入 i 而不是 i+1,允许重复使用元素
backtrack(candidates, target, i, path, result, currentSum);
// 撤销选择
path.remove(path.size() - 1);
currentSum -= candidates[i];
}
}
// 组合总和 II:元素不可重复使用
public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(candidates); // 排序便于去重
backtrack2(candidates, target, 0, new ArrayList<>(), result, 0);
return result;
}
private void backtrack2(int[] candidates, int target, int start,
List<Integer> path, List<List<Integer>> result, int currentSum) {
if (currentSum == target) {
result.add(new ArrayList<>(path));
return;
}
for (int i = start; i < candidates.length; i++) {
// 去重:跳过同一层相同的元素
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
// 剪枝
if (currentSum + candidates[i] > target) {
break;
}
path.add(candidates[i]);
currentSum += candidates[i];
// 递归:传入 i+1,不允许重复使用元素
backtrack2(candidates, target, i + 1, path, result, currentSum);
path.remove(path.size() - 1);
currentSum -= candidates[i];
}
}
}
''' 3.2 排列问题(Permutations)
特点:关心顺序,[1,2]和[2,1]是不同的排列
解法关键:使用used数组标记已用元素 ''' // 全排列问题 class PermutationSolution {
// 全排列(无重复元素)
public List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
boolean[] used = new boolean[nums.length];
backtrack(nums, new ArrayList<>(), result, used);
return result;
}
private void backtrack(int[] nums, List<Integer> path,
List<List<Integer>> result, boolean[] used) {
// 结束条件:路径长度等于数组长度
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
return;
}
for (int i = 0; i < nums.length; i++) {
// 跳过已使用的元素
if (used[i]) {
continue;
}
used[i] = true;
path.add(nums[i]);
backtrack(nums, path, result, used);
path.remove(path.size() - 1);
used[i] = false;
}
}
// 全排列 II(包含重复元素)
public List<List<Integer>> permuteUnique(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums); // 排序便于去重
boolean[] used = new boolean[nums.length];
backtrackUnique(nums, new ArrayList<>(), result, used);
return result;
}
private void backtrackUnique(int[] nums, List<Integer> path,
List<List<Integer>> result, boolean[] used) {
if (path.size() == nums.length) {
result.add(new ArrayList<>(path));
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;
}
used[i] = true;
path.add(nums[i]);
backtrackUnique(nums, path, result, used);
path.remove(path.size() - 1);
used[i] = false;
}
}
}
'''
3.3 子集问题(Subsets)
特点:找出集合的所有子集
解法关键:每个节点都是有效结果
''' // 子集问题 class SubsetSolution {
// 子集(无重复元素)
public List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrack(nums, 0, new ArrayList<>(), result);
return result;
}
private void backtrack(int[] nums, int start, List<Integer> path,
List<List<Integer>> result) {
// 每次递归都记录当前路径
result.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
path.add(nums[i]);
// 从下一个元素开始,避免重复
backtrack(nums, i + 1, path, result);
path.remove(path.size() - 1);
}
}
// 子集 II(包含重复元素)
public List<List<Integer>> subsetsWithDup(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
Arrays.sort(nums); // 排序便于去重
backtrackWithDup(nums, 0, new ArrayList<>(), result);
return result;
}
private void backtrackWithDup(int[] nums, int start, List<Integer> path,
List<List<Integer>> result) {
result.add(new ArrayList<>(path));
for (int i = start; i < nums.length; i++) {
// 去重:跳过同一层相同的元素
if (i > start && nums[i] == nums[i - 1]) {
continue;
}
path.add(nums[i]);
backtrackWithDup(nums, i + 1, path, result);
path.remove(path.size() - 1);
}
}
归并排序
public class MergeSortOptimized {
public static void mergeSort(int[] arr) {
if (arr == null || arr.length < 2) return;
// 1. 只在这里创建一次辅助数组
int[] temp = new int[arr.length];
sort(arr, 0, arr.length - 1, temp);
}
private static void sort(int[] arr, int left, int right, int[] temp) {
if (left >= right) return;
int mid = left + (right - left) / 2;
sort(arr, left, mid, temp);
sort(arr, mid + 1, right, temp);
// 2. 合并时传入复用的 temp 数组
merge(arr, left, mid, right, temp);
}
private static void merge(int[] arr, int left, int mid, int right, int[] temp) {
int i = left; // 左子数组指针
int j = mid + 1; // 右子数组指针
int k = left; // temp 数组写入指针(注意:这里直接从 left 开始,而非 0)
// 3. 归并过程:将结果暂存在 temp 的 [left...right] 区间内
while (i <= mid && j <= right) {
if (arr[i] <= arr[j]) {
temp[k++] = arr[i++];
} else {
temp[k++] = arr[j++];
}
}
// 处理剩余元素
while (i <= mid) {
temp[k++] = arr[i++];
}
while (j <= right) {
temp[k++] = arr[j++];
}
// 4. 将 temp 中 [left...right] 的有序数据拷贝回原数组 arr
// 注意:这里只需要拷贝这一段,不需要像旧版那样拷贝整个 temp
for (int idx = left; idx <= right; idx++) {
arr[idx] = temp[idx];
}
}
}
快速排序
public class QuickSort {
public static void quickSort(int[] arr) {
if (arr == null || arr.length <= 1) return;
quickSort(arr, 0, arr.length - 1);
}
private static void quickSort(int[] arr, int left, int right) {
if (left >= right) return;
int pivot = partition(arr, left, right);
quickSort(arr, left, pivot - 1);
quickSort(arr, pivot + 1, right);
}
private static int partition(int[] arr, int left, int right) {
// 挖坑
int i = left, j = right;
int mid=left+(right-left)/2;
swap(arr, mid, left);
int pivot = arr[left];
while (i < j) {
//一定是先-j,返回i
while (i < j && arr[j] >= pivot) j--;
while (i < j && arr[i] <= pivot) i++;
if (i < j) swap(arr, i, j);
}
//会填
swap(arr, left, i);
return i;
}
private static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
二分查找方法
搜索目标值,如果不存在返回要插入的位置
如果所有元素都大于目标值,返回0
如果所有元素都小于目标值,返回arr.length
public int binarySearchWithInsertPosition(int[] arr, int target) {
if (arr == null || arr.length == 0) {
return 0; // 空数组,插入位置为0
}
int left = 0;
int right = arr.length - 1;
// 先尝试查找目标值是否存在
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == target) {
return mid; // 找到目标值,返回其索引
} else if (arr[mid] < target) {
left = mid + 1;
} else {
right = mid - 1;
}
}
// 未找到目标值,返回应该插入的位置
return left;
}
背包问题
背包问题,n种物品,外面遍历物品,内遍历容量
第i个物品体积为weight[i],价值为values[i]
定义 dp[w] 为背包容量为 w 时能获得的最大价值。所有初始化为0
01背包
for (int i = 0; i < n; i++) {
for (int w = W; w >= weights[i]; w--) {
// 决策:不选当前物品 或 选当前物品
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
完全背包
for (int i = 0; i < n; i++) {
for (int w = weights[i]; w <= W; w++) {
// 决策:不选当前物品 或 新增一个当前物品
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
#恰好装满问题
/**
* 完全背包恰好装满的最大价值
* @param W 背包容量
* @param weights 物品重量数组
* @param values 物品价值数组
* @return 最大价值(若无解返回-1)
*/
public static int knapCompleteExactlyFull(int W, int[] weights, int[] values) {
int[] dp = new int[W + 1];
// 初始化:除了容量0,其他状态都不可达
Arrays.fill(dp, Integer.MIN_VALUE);
dp[0] = 0;
for (int i = 0; i < weights.length; i++) {
for (int j = weights[i]; j <= W; j++) {
// 仅当减去当前物品重量的状态可达时,才能更新
if (dp[j - weights[i]] != Integer.MIN_VALUE) {
dp[j] = Math.max(dp[j], dp[j - weights[i]] + values[i]);
}
}
}
return dp[W] < 0 ? -1 : dp[W]; // -1表示无解
}
/**
* 恰好装满的最小物品数量
* @param W 背包容量
* @param weights 物品重量数组
* @return 最小物品数量(若无解返回-1)
*/
public static int minItemsExactlyFull(int W, int[] weights) {
int[] dp = new int[W + 1];
// 初始化:除了容量0,其他状态都不可达
Arrays.fill(dp, Integer.MAX_VALUE);
dp[0] = 0;
for (int weight : weights) {
for (int j = weight; j <= W; j++) {
if (dp[j - weight] != Integer.MAX_VALUE) {
dp[j] = Math.min(dp[j], dp[j - weight] + 1);
}
}
}
return dp[W] == Integer.MAX_VALUE ? -1 : dp[W];}
}
组合数,不考虑顺序
物品在外,容量在内
和全排列组合数类似
for (int i = 0; i < n; i++) {
for (int w = weights[i]; w <= W; w++) {
// 物品在外层循环 → 组合数
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
排列数,考虑顺序
容量在外,物品在内
for (int w = 0; w <= W; w++) {
for (int i = 0; i < n; i++) {
if(w<weights[i]){continue}
// 容量在外层循环 → 排列数
dp[w] = Math.max(dp[w], dp[w - weights[i]] + values[i]);
}
}
图表示问题
1. 邻接矩阵(Adjacency Matrix)
- 实现方式:二维数组
int[][] matrix,matrix[i][j]表示顶点 i 到 j 的边权重(无权图可用 0/1 表示)。 - 特点:
- 优点:快速判断两顶点是否相邻(O(1))。
- 缺点:空间复杂度高(O(V²)),适合稠密图。
- 示例代码:
int[][] graph = new int[V][V]; graph[0][1] = 1; // 顶点 0 到 1 有一条边
2. 邻接表(Adjacency List)
- 实现方式:数组或
List存储每个顶点的邻居列表,如List<List<Integer>>。 - 特点:
- 优点:空间效率高(O(V+E)),适合稀疏图。
- 缺点:判断两顶点是否相邻需遍历列表(O(V))。
- 示例代码:
List<List<Integer>> graph = new ArrayList<>(); for (int i = 0; i < V; i++) { graph.add(new ArrayList<>()); } graph.get(0).add(1); // 顶点 0 到 1 有一条边
有向图判断是否有环-染色法
public boolean hasCycle() {
// 0: 未访问, 1: 访问中, 2: 已访问完成
int[] color = new int[V];
for (int i = 0; i < V; i++) {
if (color[i] == 0) {
if (dfs(i, color)) {
return true;
}
}
}
return false;
}
private boolean dfs(int v, int[] color) {
color[v] = 1; // 标记为访问中
for (int neighbor : adj.get(v)) {
if (color[neighbor] == 0) {
if (dfs(neighbor, color)) {
return true;
}
} else if (color[neighbor] == 1) {
return true; // 发现环
}
}
color[v] = 2; // 标记为访问完成
return false;
}
无向图判断是否有环-dfs
public boolean hasCycle() {
boolean[] visited = new boolean[V];
for (int i = 0; i < V; i++) {
if (!visited[i]) {
if (dfs(i, visited, -1)) {
return true;
}
}
}
return false;
}
private boolean dfs(int v, boolean[] visited, int parent) {
visited[v] = true;
for (int neighbor : adj.get(v)) {
if (!visited[neighbor]) {
if (dfs(neighbor, visited, v)) {
return true;
}
} else if (neighbor != parent) {
return true; // 发现环
}
}
return false;
}
LinkedList 实现了多个重要接口
作为 List 列表
add(index, element) - 在指定位置插入
get(index) - 获取指定位置元素
remove(index) - 删除指定位置元素
set(index, element) - 修改指定位置元素
Deque<Integer> deque = new LinkedList<>();
Deque<Integer> stack = new LinkedList<>();
Queue<Integer> queue = new LinkedList<>();
PriorityQueue<Integer> priorityQueue = new PriorityQueue<>();
===========================
作为 Queue 队列 (FIFO)
offer(element) / add(element) - 入队
poll() - 出队(队列空返回null)
remove() - 出队(队列空抛异常)
peek() - 查看队首
Queue<Integer> queue= new LinkedList<>();
queue.poll();
queue.offer();
===============================
作为 Deque 双端队列
Deque<Integer> deque= new LinkedList<>();
deque.offerFirst();
deque.offerLast();
deque.pollFirst();
deque.pollLast();
offerFirst() / offerLast() - 两端入队
pollFirst() / pollLast() - 两端出队
peekFirst() / peekLast() - 查看两端
push(element) - 栈式入栈
pop() - 栈式出栈
================================== 作为 Stack 栈 (LIFO)
push(element) - 入栈
pop() - 出栈
Deque<Integer> stack= new LinkedList<>();
stack.pop();
stack.push();
==================================
单独的new priorityQueue(),默认是小根 大根堆写lamda表达式
PriorityQueue<Integer> queue= new PriorityQueue<>();
queue.offer();
queue.poll();
==================================
长度判断:
List 继承 Collection,size(),isEmpty(),contains(), add(), remove(),都可以用
String 继承CharSequence,length().
数组,是关键字,JVM nitive 方法
=======================================================================
StringBuilder 继承,CharSequence,length()。isEmpty(). AbstractStringBuilder。 delete(star,end),deleteCharAt()
======================================================================= string ,subString 左开右闭
=======================================================================
小写字母或者大写字母,new int[26]。 包含小写或者大写new int[128]具体原因如下。
// 1. 判断字符是否为大写字母
public static boolean isUpperCase(char c) {
return c >= 'A' && c <= 'Z'; // 65-90
}
// 2. 判断字符是否为小写字母
public static boolean isLowerCase(char c) {
return c >= 'a' && c <= 'z'; // 97-122
}
// 3. 大写转小写(手动实现)
public static char toLowerCase(char c) {
if (c >= 'A' && c <= 'Z') {
return (char)(c + 32); // 65-90 → 97-122
}
return c;
}
// 4. 小写转大写(手动实现)
public static char toUpperCase(char c) {
if (c >= 'a' && c <= 'z') {
return (char)(c - 32); // 97-122 → 65-90
}
return c;
}