1.回溯算法的概念
回溯算法顧名思義就是有回溯的算法
回溯算法實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當發現已不滿足求解條件時,就“回溯”返回,嘗試別的路徑。回溯法是一種選優搜索法,按選優條件向前搜索,以達到目標。但當探索到某一步時,發現原先選擇并不優或達不到目標,就退回一步重新選擇,這種走不通就退回再走的技術為回溯法,而滿足回溯條件的某個狀態的點稱為“回溯點”。許多復雜的,規模較大的問題都可以使用回溯法,有“通用解題方法”的美稱。
回溯算法的核心思想是:
- 問題分解:將原問題分解成若干個規模較小的子問題。
- 候選解的生成:從根節點開始,逐層生成候選解。
- 活節點:當前正在擴展的節點稱為活節點。
- 死節點:當前擴展的節點如果已經確定不能得到問題的解,則該節點稱為死節點。
- 剪枝:根據問題的限制條件,對已經確定的死節點進行剪枝。
2.回溯算法解空間樹的問題
?典型案例:
集合求冪集的解空間樹
意思就是一個集合s所有的子集的集合
問題:求集合S={1,2,3}的所有子集,依次選擇每個元素的過程就是一棵樹,如圖所示
在樹中,由從根節點到葉子結點的每條路徑上的權值組成三元組(0,0,0)~(1,1,1)都是解,如(0,0,1)表示子集{B,C},共=8個解;因此,該樹被稱為解空間樹,高度為4。
為什么幫助理解,我們先看普通的遞歸方法是什么實現的
下面是迭代方法代碼實現:
import java.util.ArrayList;
import java.util.List;public class PowerSet {public static void main(String[] args) {List<String> originalSet =new ArrayList<>();originalSet.add("A");originalSet.add("B");originalSet.add("C");//這里又嵌套了一個list是為了使每一個元素都變成list//適用于多種算法和數據處理任務List<List<String>> powerSet =getPowerSet(originalSet);for (List<String> subset:powerSet){System.out.println(subset);}}private static List<List<String>> getPowerSet(List<String> originalSet) {List<List<String>> powerSet =new ArrayList<>();if (originalSet==null||originalSet.isEmpty()){return powerSet;}//添加空集powerSet.add(new ArrayList<>());for (String element:originalSet){int size =powerSet.size();for (int i =0;i<size;i++){List<String> subset =new ArrayList<>(powerSet.get(i));subset.add(element);powerSet.add(subset);}}return powerSet;}
現在讓我們來分析一下這個過程:
假設第一次迭代中,我們處理元素“A”:
- 初始時,powerSet只包含一個空集[ ]。
- 我們復制這個空集,然后添加“A”,得到["A"]。
- 將["A"]添加到powerSet,現在powerSet是[ ],["A"]。
在二次迭代中處理“B”:
- 現在powerSet有兩個子集:[ ]和["A"]。
- 我們首先復制空集,然后添加“B”,得到["B"]。
- 然后復制[“A”],然后添加“B”,得到["A","B"]。
- 然后將這兩個新的子集添加到powerSet,現在powerSet有[ ],["A"],["B"],["A","B"]
然后三次迭代重復這個過程就可以了。
下面是回溯方法的代碼:
public class PowerSetWithBacktracking {public static void main(String[] args) {List<String> originalSet = new ArrayList<>();originalSet.add("A");originalSet.add("B");originalSet.add("C");List<List<String>> powerSet = new ArrayList<>();backtrack(0, originalSet, new ArrayList<>(), powerSet);for (List<String> subset : powerSet) {System.out.println(subset);}}private static void backtrack(int start, List<String> originalSet, List<String> currentSubset, List<List<String>> powerSet) {//將當前子集的副本添加到冪集powerSet中,這里使用new ArrayList<>()來復制當前的子集。powerSet.add(new ArrayList<>(currentSubset));//遍歷集合中的每個元素,從start開始for (int i =start;i<originalSet.size();i++){//做出選擇:包含當前元素currentSubset.add(originalSet.get(i));//遞歸調用:移動到下一個元素backtrack(i+1,originalSet,currentSubset,powerSet);//撤銷選擇:回溯currentSubset.remove(currentSubset.size()-1);}
}
}
分析過程:
步驟 | start | currentSubset | powerSet |
第一次 | 0 | [ ] | [[ ]] |
第二次 | 0 | ["a"] | [[ ]] |
第三次 | 1 | ["a","b"] | [[ ]] |
第四次 | 2 | ["a","b","c"] | [[ ]] |
第五次 | 3 | ["a","b","c"] | [[" "],["a","b","c"],["a","b"],["a"]] |
第六次 | 2 | ["a","b"] | [[" "],["a","b","c"],["a","b"],["a"]] |
第七次 | 1 | ["a"] | [[" "],["a","b","c"],["a","b"],["a"]] |
第八次 | 2 | ["a","c"] | [[" "],["a","b","c"],["a","b"],["a"]] |
第九次 | 2 | ["a","c"] | [[" "],["a","b","c"],["a","b"],["a"],["a","c"]] |
第十次 | 1 | ["a"] | [[" "],["a","b","c"],["a","b"],["a"],["a","c"]] |
第十一次 | 1 | [ ] | [[" "],["a","b","c"],["a","b"],["a"],["a","c"]] |
第十二次 | 1 | ["b"] | [[" "],["a","b","c"],["a","b"],["a"],["a","c"],["b"]] |
?接下來依次推導就行了。整體的思路就是不斷遞歸下一個元素,然后從currentSubset中移除最后添加的元素,進行回溯,準備處理下一個元素。
3.八皇后問題
八皇后問題是一個以國際象棋為背景的問題:如何能夠在8×8的國際象棋棋盤上放置八個皇后,使得任何一個皇后都無法直接吃掉其他的皇后?為了達到此目的,任兩個皇后都不能處于同一條橫行、縱行或斜線上嗎,問有多少種擺法。八皇后問題可以推廣為更一般的n皇后擺放問題:這時棋盤的大小變為n×n,而皇后個數也變成n。當且僅當n?= 1或n?≥ 4時問題有解
public class EightQueens {private static final int n =8;private static int[] queens;//存儲皇后的位置private static int count =0;//解的計數public static void main(String[] args){queens=new int[n];for (int i =0;i<n;i++){queens[i]=-1;//初始化都為-1,表示都沒有放皇后}placeQueens(0);//從第0行開始放置皇后System.out.println("");}private static void placeQueens(int row) {if (row==n){count++;printBoard();return;}for (int col =0;col<n;col++){if (isSafe(row,col)){queens[row]=col;//在當前位置放置皇后placeQueens((row+1));//進入下一行queens[row]=-1;//回溯,撤銷當前行的皇后放置的位置}}}private static boolean isSafe(int row, int col) {//檢查列是否有沖突for (int i =0;i<row;i++ ){if (queens[i]==col){return false;}}//檢查左上對角線是否有沖突for (int i =row-1,j=col+1;i>=0&&j>=0;i--,j--){if (queens[i]==j){return false;}}//檢查右下對角線是否有沖突for (int i =row-1,j=col+1;i>=0&&j<n;i--,j++){if (queens[i]==j){return false;}}return true;}private static void printBoard() {System.out.println("第"+count+"種解法");for(int i =0;i<n;i++){for (int j=0;j<n;j++){if (queens[i]==j){System.out.println("Q");}else{System.out.println(". ");}}System.out.println();}System.out.println();}
}
過程分析:
回溯發生在兩種情況下:
- 當前行是最后一行(row==n-1),并且已經嘗試了所有可能的列,并且已經找到了一個解決方案,遞歸將終止,開始回溯
- 在遞歸過程中,當前行不是最后一行,但在下一行無法找到任何安全位置放置皇后。
?4.騎士游歷問題
騎士游歷問題是指,在國際象棋的棋盤(8行*8列)上,一個馬要遍歷棋盤,即走到棋盤上的每一格,并且每隔只到達一次。 設碼在棋盤的某一位置(x,y)上,按照“走馬日”的規則,下一步有8個方向走,如圖所示。 若給定起始位置(x0,y0),使用站和隊列探索出一條馬遍歷棋盤的路徑。
?算法步驟:
- 定義棋盤大小和騎士的可能移動的方式。
- 創建一個二維數組來表示棋盤,初始值設為-1表示未訪問。
- ?編寫一個輔助函數來檢查當前位置是否合法。
- 編寫一個遞歸函數來嘗試所有可能的移動,使用回溯法來尋找解。
- 在主函數中調用遞歸函數,并從指定的起始點開始。
public class KnightTour{private static final int N =8;private static final int[][] moves={{2,1},{1,2},{-1,2},{-2,1},{-2,-1},{-1,-2},{1,-2},{2,-1}};public static void main(String[] args) {int[][] board =new int[N][N];for (int i =0;i<N;i++){for (int j =0;j<N;j++){board[i][j]=-1;}}//起始點int x =0,y =0;board[x][y]=0;if (!solveKTUtil(board,x,y,1)){System.out.println("此解決方案不存在");}else{printSolution(board);}}private static boolean solveKTUtil(int[][] board, int x, int y, int moveCount) {if (moveCount==N*N){return true;}for (int i =0;i<8;i++){int nextX =x+moves[i][0];int nextY =y+moves[i][1];if (isSafe(board,nextX,nextY)){board[nextX][nextY]=moveCount;if (solveKTUtil(board,nextX,nextY,moveCount+1)){return true;}else{board[nextX][nextY]=-1;//回溯}}}return false;}private static boolean isSafe(int[][] board, int X, int Y) {return X>=0&&Y>=0&&X<N&&Y<N&&board[X][Y]==-1;}private static void printSolution(int[][] board){for (int i =0;i<N;i++){for(int j =0;j<N;j++){System.out.print(board[i][j]+" ");}System.out.println();}}
}