回溯:
回溯法(Backtracking)是一种常用于解决组合、排列、搜索和优化问题的算法技巧。 对于某些计算问题而言,回溯法是一种可以找出所有(或一部分)解的一般性算法,尤其适用于约束性满足问題")(在解决约束满足问题时,我们逐步构造更多的候选解,并且在确定某一部分候选解不可能补全成正确解之后放弃继续搜索这个部分候选解本身及其可以拓展出的子候选解,转而测试其他的部分候选解)
回溯问题解题的思路和技巧
- 明确问题的决策树结构: 首先,要理解问题的决策树结构,确定每一步的选择和决策,以便在搜索中进行遍历。
- 递归搜索: 回溯通常使用递归来实现。每个递归层级代表一个决策点。在每个决策点,你需要考虑选择(尝试一种可能性)和不选择(回退或尝试另一种可能性)两种情况。
- 路径和选择: 在回溯中,通常需要维护一个路径,记录当前已经作出的选择。这通常是一个数组、列表或者其他数据结构。
- 约束条件和剪枝: 在每一步,可以使用约束条件来排除无效的选择,从而避免不必要的搜索。这被称为剪枝,可以显著提高回溯算法的效率。
- 结束条件: 定义何时达到一个有效的解决方案,以及何时回退到上一个决策点。结束条件是决策树的叶子节点。
- 回溯和恢复: 在不满足问题要求时,要回退到上一个决策点,然后继续尝试其他可能性。这通常需要在回退时还原路径状态。
- 遍历所有可能性: 回溯算法会遍历问题空间中的所有可能性,通常是通过递归的方式。这确保了所有潜在解决方案都被探索。
- 时间和空间复杂度: 回溯算法通常有指数级的时间复杂度,因为它要遍历所有可能性。同时,它也可能需要大量的递归调用,占用大量的栈空间。
- 问题变形: 回溯算法可以应用于不同种类的问题,包括排列组合、子集、棋盘类问题(如N皇后问题、数独问题)、字符串匹配等等。
- 适用场景: 回溯算法通常在需要搜索所有可能解决方案的问题中使用,尤其是在解决空间较小且难以用其他算法解决的问题时。
- 剪枝优化: 寻找合适的剪枝条件,可以显著提高回溯算法的效率,避免不必要的搜索。
- 优化数据结构: 选择合适的数据结构来存储路径状态,以降低空间复杂度。例如,使用位运算来表示集合
典型应用-: N 皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。
给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。
每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。
public static void n_queens(int queens){
if(queens <= 4){
System.out.println("No solution exists for N less than 4.");
return;
}
doSearch(0,new int[queens]);
}
/**
* place the queens from first row to the last row
* @param currentRow current row to be placed
* @param cols: the index is the row,value is the column
*/
private static void doSearch(int currentRow, int [] cols){
if(currentRow == cols.length){
printQueens(cols);
return;
}
for (int col = 0; col < cols.length; col++) {
if(isQualified(currentRow,col,cols)){//check each col for current row
cols[currentRow] = col;
doSearch(currentRow+1,cols);
}
}
}
private static boolean isQualified(int currentRow, int currentCol, int[] cols) {
for(int preRow = currentRow-1; preRow >=0; --preRow){
int preCol = cols[preRow];// find the previous rows' col
if(currentCol == preCol){
//in the same col
return false;
}
if(Math.abs(preCol-currentCol) == Math.abs(preRow-currentRow)){
//check the diagonals
return false;
}
}
return true;
}
private static void printQueens(int[] cols) {
int len = cols.length;
for (int i = 0; i < len; i++) {
for (int j = 0; j < len; j++) {
if(cols[i] == j){
System.out.print(" Q");
}else{
System.out.print(" *");
}
}
System.out.println();
}
System.out.println("-----------------------------------");
}
典型应用二: 0-1 背包
我们有一个背包,背包总的承载重量是 Wkg。 现在我们有 n 个物品,每个物品的重量不等,并且不可分割。我们现在期望选择几件物品,装载到背包 中。在不超过背包所能装载重量的前提下,如何让背包中物品的总重量最大?
public static int knapsack_0_1(int weightLimit, int[] goods) {
int[] res = new int[1]; // Initialize a single-element array to store the result.
doSearch(0, 0, res, weightLimit, goods); // Start the search with initial parameters.
return res[0]; // Return the maximum weight that can be carried in the knapsack.
}
private static void doSearch(int currentGoodsIndex, int currentWeight, int[] res, int weightLimit, int[] goods) {
// Check if we've considered all goods or reached the weight limit.
if (currentGoodsIndex == goods.length || currentWeight == weightLimit) {
if (currentWeight > res[0]) {
res[0] = currentWeight; // Update the maximum weight if the current weight is greater.
}
return; // Return to previous recursive call.
}
// Recursive call without adding the current item (not putting it in the knapsack).
doSearch(currentGoodsIndex + 1, currentWeight, res, weightLimit, goods);
// Check if adding the current item to the knapsack is allowed.
if (currentWeight + goods[currentGoodsIndex] <= weightLimit) {
// Recursive call with the current item added to the knapsack.
doSearch(currentGoodsIndex + 1, currentWeight + goods[currentGoodsIndex], res, weightLimit, goods);
}
}
小结
回溯算法本质上就是枚举,优点在于其类似于摸着石头过河的查找策略,且可以通过剪枝少走冤枉路。 它可能适合应用于缺乏规律,或我们还不了解其规律的搜索场景中。另外, 回溯就是暴力枚举的解法:遍历所有情况,当满足情况就停止遍历(剪枝)。