回溯法的解題步驟與例子解析
回溯法有“通用解題法”之稱。用它可以系統地搜尋問題的所有解。回溯法是一個既帶有系統性又帶有跳躍性的搜尋演算法。
在包含問題的所有解的解空間樹中,按照深度優先搜尋的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,如果包含,就從該結點出發繼續探索下去,如果該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜尋演算法)。若用回溯法求問題的所有解時,要回溯到根,且根結點的所有可行的子樹都要已被搜尋遍才結束。 而若使用回溯法求任一個解時,只要搜尋到問題的一個解就可以結束。
1.回溯法的解題步驟
(1)針對所給問題,定義問題的解空間;
(2)確定易於搜尋的解空間結構;
(3)以深度優先方式搜尋解空間,並在搜尋過程中用剪枝函式避免無效搜尋。
2.子集樹與排列樹
下面的兩棵解空間樹是回溯法解題時常遇到的兩類典型的解空間樹。
(1)當所給問題是從n個元素的集合S中找出S滿足某種性質的子集時,相應的解空間樹稱為子集樹。例如從n個物品的0-1揹包問題(如下圖)所相應的解空間樹是一棵子集樹,這類子集樹通常有2^n個葉結點,其結點總個數為2^(n+1)-1。遍歷子集樹的演算法需Ω(2^n)計算時間。
(2)當所給問題是確定n個元素滿足某種性質的排列時,相應的解空間樹稱為排列樹。例如旅行售貨員問題(如下圖)的解空間樹是一棵排列樹,這類排列樹通常有n!個葉結點。遍歷子集樹的演算法需Ω(n!)計算時間。
用回溯法搜尋子集樹的一般演算法可描述為:
/** * output(x) 記錄或輸出得到的可行解x * constraint(t) 當前結點的約束函式 * bount(t) 當前結點的限界函式 * @param t t為當前解空間的層數 */ void backtrack(int t){ if(t >= n) output(x); else for (int i = 0; i <= 1; i++) { x[t] = i; if(constraint(t) && bount(t)) backtrack(t+1); } }
用回溯法搜尋排列樹的一般演算法可描述為:
/**
* output(x) 記錄或輸出得到的可行解x
* constraint(t) 當前結點的約束函式
* bount(t) 當前結點的限界函式
* @param t t為當前解空間的層數
*/
void backtrack(int t){
if(t >= n)
output(x);
else
for (int i = t; i <= n; i++) {
swap(x[t], x[i]);
if(constraint(t) && bount(t))
backtrack(t+1);
swap(x[t], x[i]);
}
}
3.回溯法的應用例子
(a)子集樹
(為了便於描述演算法,下列方法使用了較多的全域性變數)
I.輸出集合S中所有的子集,即limit為all;
II.輸出集合S中限定元素數量的子集,即limit為num;
III.輸出集合S中元素奇偶性相同的子集,即limit為sp。
public class Subset {
private static int[] s = {1,2,3,4,5,6,7,8};
private static int n = s.length;
private static int[] x = new int[n];
/**
* 輸出集合的子集
* @param limit 決定選出特定條件的子集
* 注:all為所有子集,num為限定元素數量的子集,
* sp為限定元素奇偶性相同,且和小於8。
*/
public static void all_subset(String limit){
switch(limit){
case "all":backtrack(0);break;
case "num":backtrack1(0);break;
case "sp":backtrack2(0);break;
}
}
/**
* 回溯法求集合的所有子集,依次遞迴
* 注:是否回溯的條件為精髓
* @param t
*/
private static void backtrack(int t){
if(t >= n)
output(x);
else
for (int i = 0; i <= 1; i++) {
x[t] = i;
backtrack(t+1);
}
}
/**
* 回溯法求集合的所有(元素個數小於4)的子集,依次遞迴
* @param t
*/
private static void backtrack1(int t){
if(t >= n)
output(x);
else
for (int i = 0; i <= 1; i++) {
x[t] = i;
if(count(x, t) < 4)
backtrack1(t+1);
}
}
/**
* (剪枝)
* 限制條件:子集元素小於4,判斷0~t之間已被選中的元素個數,
* 因為此時t之後的元素還未被遞迴,即決定之後的元素
* 是否應該被遞迴呼叫
* @param x
* @param t
* @return
*/
private static int count(int[] x, int t) {
int num = 0;
for (int i = 0; i <= t; i++) {
if(x[i] == 1){
num++;
}
}
return num;
}
/**
* 回溯法求集合中元素奇偶性相同,且和小於8的子集,依次遞迴
* @param t
*/
private static void backtrack2(int t){
if(t >= n)
output(x);
else
for (int i = 0; i <= 1; i++) {
x[t] = i;
if(legal(x, t))
backtrack2(t+1);
}
}
/**
* 對子集中元素奇偶性進行判斷,還需元素的陣列和小於8
* @param x
* @param t
* @return
*/
private static boolean legal(int[] x, int t) {
boolean bRet = true; //判斷是否需要剪枝
int part = 0; //奇偶性判斷的基準
for (int i = 0; i <= t; i++) { //選擇第一個元素作為奇偶性判斷的基準
if(x[i] == 1){
part = i;
break;
}
}
for (int i = 0; i <= t; i++) {
if(x[i] == 1){
bRet &= ((s[part] - s[i]) % 2 == 0);
}
}
int sum = 0;
for(int i = 0; i <= t; i++){
if(x[i] == 1)
sum += s[i];
}
bRet &= (sum < 8);
return bRet;
}
/**
* 子集輸出函式
* @param x
*/
private static void output(int[] x) {
for (int i = 0; i < x.length; i++) {
if(x[i] == 1){
System.out.print(s[i]);
}
}
System.out.println();
}
}
(b) 排列樹
(為了便於描述演算法,下列方法使用了較多的全域性變數)
I.輸出集合S中所有的排列,即limit為all;
II.輸出集合S中元素奇偶性相間的排列,即limit為sp。
public class Permutation {
private static int[] s = {1,2,3,4,5,6,7,8};
private static int n = s.length;
private static int[] x = new int[n];
/**
* 輸出集合的排列
* @param limit 決定選出特定條件的子集
* 注:all為所有排列,sp為限定元素奇偶性相間。
*/
public static void all_permutation(String limit){
switch(limit){
case "all":backtrack(0);break;
case "sp":backtrack1(0);break;
}
}
/**
* 回溯法求集合的所有排列,依次遞迴
* 注:是否回溯的條件為精髓
* @param t
*/
private static void backtrack(int t){
if(t >= n)
output(s);
else
for (int i = t; i < n; i++) {
swap(i, t, s);
backtrack(t+1);
swap(i, t, s);
}
}
/**
* 回溯法求集合中元素奇偶性相間的排列,依次遞迴
* @param t
*/
private static void backtrack1(int t){
if(t >= n)
output(s);
else
for (int i = t; i < n; i++) {
swap(i, t, s);
if(legal(x, t))
backtrack1(t+1);
swap(i, t, s);
}
}
/**
* 對子集中元素奇偶性進行判斷
* @param x
* @param t
* @return
*/
private static boolean legal(int[] x, int t) {
boolean bRet = true; //判斷是否需要剪枝
//奇偶相間,即每隔一個數判斷奇偶相同
for (int i = 0; i < t - 2; i++) {
bRet &= ((s[i+2] - s[i]) % 2 == 0);
}
return bRet;
}
/**
* 元素交換
* @param i
* @param j
*/
private static void swap(int i, int j,int[] s) {
int tmp = s[i];
s[i] = s[j];
s[j] = tmp;
}
/**
* 子集輸出函式
* @param x
*/
private static void output(int[] s) {
for (int i = 0; i < s.length; i++) {
System.out.print(s[i]);
}
System.out.println();
}
}
參考文獻:
1. 《演算法設計與分析》
相關推薦
回溯法的解題步驟與例子解析
回溯法有“通用解題法”之稱。用它可以系統地搜尋問題的所有解。回溯法是一個既帶有系統性又帶有跳躍性的搜尋演算法。 在包含問題的所有解的解空間樹中,按照深度優先搜尋的策略,從根結點出發深度
[Leetcode] Backtracking回溯法解題思路
一個 quad adr == should store .com 理解 force 碎碎念: 最近終於開始刷middle的題了,對於我這個小渣渣確實有點難度,經常一兩個小時寫出一道題來。在開始寫的幾道題中,發現大神在discuss中用到回溯法(Backtracking)的概
求{1,2,3}的子集————回溯法(遞迴與非遞迴)
求ar[]={1,2,3}的子集序列,小夥伴們可以先自己嘗試解一下~~ #include<iostream> using namespace std; //用回溯法搜尋子集樹 void fun(int *ar,int *br,int n)//非遞迴
分治法的基本思想與例子解析
分治法的設計思想:將一個難以直接解決的大問題,分割成一些規模較小的相同問題,以便各個擊破,分而治之。 凡治眾如治寡,分數是也。——孫子兵法 1.基本思想 (1) 將求解的較大規模
構造 LL(1) 分析表的步驟與例題解析
構造 LL(1) 分析表的步驟與例題解析 易錯點及擴充套件: 1、求每個產生式的 SELECT 集 2、注意區分是對誰 FIRST 集 FOLLOW 集 3、開始符號的 FOLLOW 集包含 # 4、各集合對對應的物件以及含義 集 物件 含義 FIRST 集 是對產生式右部 右部內部的所有終結符
全面解析回溯法:演算法框架與問題求解
#include <stdio.h> #include <stdlib.h> #include <assert.h> typedef int* data; int is_a_solution(int a[],int k, int step); //void
回溯算法 ------回溯算法的幾個例子
tro 回溯 size .cn 背包 例子 技術 小結 cnblogs 1.回溯算法的小結 2.回溯算法的幾個例子 2.1 ------ 4後問題 搜索空間: 2.2 ------01背包問題 01背包問題的算法設計 01背包問題的實
通用解題方法—回溯法
spl arc 工作 簡單 clas eve 一個 尋找 1-1 轉自:http://www.cnblogs.com/steven_oyj/archive/2010/05/22/1741376.html 1、概念 回溯算法實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗
[回溯法] 0 回溯法介紹-回溯與遞迴的區別
【回溯法】有相當一類求一組解、或求全部解或求最優解的問題,例如讀者熟悉的八皇后問題等,不是根據某種確定的計演算法則,而是利用試探的回溯(Backtrcking)的搜尋技術求解 【實質】它的求解過程實質上是一個先序遍歷一棵“狀態樹”的過程,只是這棵樹不是遍歷前預先建立的,而是隱含在遍歷
【資料結構與演算法】回溯法解決裝載問題
回溯法解決裝載問題(約束函式優化) 解題思想 遍歷各元素,若cw+w[t]<=c(即船可以裝下),則進入左子樹,w[t]標記為1,再進行遞迴,若cw+r>bestw(即當前節點的右子樹包含最優解的可能),則進入右子樹,否則,則不遍歷右子樹。 完整程式碼實現如下 p
【資料結構與演算法】回溯法解決N皇后問題,java程式碼實現
N皇后問題 問題描述 在8×8格的國際象棋上擺放八個皇后,使其不能互相攻擊,即任意兩個皇后都不能處於同一行、同一列或同一斜線上,問有多少種擺法,這稱為八皇后問題。 延伸一下,便為N皇后問題。 核心思想 解決N皇后問題有兩個關鍵點。一是如何進行放置棋子,二是如何驗證棋子是否符合
【基礎演算法】回溯法與八皇后問題
#include"iostream" #include"stdlib.h" using namespace std; int x[8],tot=0; bool B(int x[],int k) { int i; for(i=0;i<k;i++) if(x[i]==x[
回溯法與動態規劃例項分析
回溯法與動態規劃 1、回溯法 1.1 適用場景 回溯法很適合解決迷宮及其類似的問題,可以看成是暴力解法的升級版,它從解決問題每一步的所有可能選項裡系統地選擇出一個可行的解決方案。回溯法非常適合由多個步驟組成的問題,並且每個問題都有多個選項。當我們從一步選擇了其中
八皇后問題用棧與回溯法實現
程式的演算法和思想(虛擬碼) (1)建立一個棧stack和一個數組int[8][8]相當於一個8*8的棋盤 (2)把第一行的八個皇后都入棧然後輸出最後一個皇后 (3)while(!stack.isempty)最上面的一個皇后pop出棧,再把皇后這行和下面的行數都清為
資料結構與演算法- 五大常用演算法總結(分治法,回溯法,分治限界法,貪心演算法,動態規劃法)
1.分治法(Recurrence and Divide-Conquer) 對於一個規模為n的問題,若該問題可以容易解決(比如說規模n較小)則直接解決,否則將其分解為k個規模較小的子問題,這些子問題互相獨立且與原問題形式相同,遞迴地解決這些子問
0-1背包問題的動態規劃法與回溯法
-- cstring namespace 動態 com 最優 end 背包 分享圖片 一、動態規劃 狀態轉移方程: 1 從前往後: 2 if(j>=w[i]) 3 m[i][j]=max(m[i-1][j],m[i-1][j-w[i]]+v[i]);
021-回溯法與深搜的關係-《演算法設計技巧與分析》M.H.A學習筆記
關於回溯法與深搜的關係,一直沒有很好的搞明白,其實百度百科已經寫得很好了: 回溯法的基本思想: 在包含問題的所有解的解空間樹中,按照深度優先搜尋的策略,從根結點出發深度探索解空間樹。當探索到某一結點
【LeetCode & 劍指offer刷題】回溯法與暴力列舉法題3:13 機器人的運動範圍
【LeetCode & 劍指offer 刷題筆記】目錄(持續更新中...) 13 機器人的運動範圍 題目描述 地上有一個m行和n列的方格。一個機器人 從座標0,0的格子開始移動,每一次只能向左,右,上,下四個方向移動一格,但是 不能進入行座標和列座
【LeetCode & 劍指offer刷題】回溯法與暴力列舉法題4:Generate Parentheses
【LeetCode & 劍指offer 刷題筆記】目錄(持續更新中...) Generate Parentheses Given n pairs of parentheses, wr
【LeetCode & 劍指offer刷題】回溯法與暴力列舉法題2:12 矩陣中的字串查詢(79. Word Search 系列)
【LeetCode & 劍指offer 刷題筆記】目錄(持續更新中...) 12 矩陣中的字串查詢(79. Word Search 系列) Word Search Given a 2D board and a word, find if