全面解析回溯法:算法框架與問題求解

目錄html

什麼是回溯法?面試

回溯法的通用框架算法

利用回溯法解決問題編程

總結與探討數組

附:《算法設計手冊》第7章其他面試題解答數據結構

 

  摘了一段來自百度百科對回溯法思想的描述:app

在包含問題的全部解的解空間樹中,按照深度優先搜索的策略,從根結點出發深度探索解空間樹。當探索到某一結點時,要先判斷該結點是否包含問題的解,若是包含,就從該結點出發繼續探索下去,若是該結點不包含問題的解,則逐層向其祖先結點回溯。(其實回溯法就是對隱式圖的深度優先搜索算法)。 若用回溯法求問題的全部解時,要回溯到根,且根結點的全部可行的子樹都要已被搜索遍才結束。 而若使用回溯法求任一個解時,只要搜索到問題的一個解就能夠結束。框架

   能夠把回溯法當作是遞歸調用的一種特殊形式。其實對於一個並不是編程新手的人來講,歷來沒使用過回溯法來解決問題的狀況是不多見的,不過每每是「對症下藥」,針對特定的問題進行解答。這些天看了《算法設計手冊》回溯法相關內容,以爲對回溯法抽象的很好。若是說算法是解決問題步驟的抽象,那麼這個回溯法的框架就是對大量回溯法算法的抽象。本文將對這個回溯法框架進行分析,而且用它解決一系列的回溯法問題。文中的回溯法採用遞歸形式。ide

  在進一步的抽象以前,先來回顧一下DFS算法。對於一個無向圖以下圖左,它的從點1開始的DFS過程多是下圖右的狀況,其中實線表示搜索時的路徑,虛線表示返回時的路徑:函數

 

  能夠看出,在回溯法執行時,應當:保存當前步驟,若是是一個解就輸出;維護狀態,使搜索路徑(含子路徑)儘可能不重複。必要時,應該對不可能爲解的部分進行剪枝(pruning)。

  下面介紹回溯法的通常實現框架:

bool finished = FALSE; /* 是否得到所有解? */
backtrack(int a[], int k, data input)
{
    int c[MAXCANDIDATES]; /*此次搜索的候選 */
    int ncandidates; /* 候選數目 */
    int i; /* counter */
    if (is_a_solution(a,k,input))
    process_solution(a,k,input);
    else {
        k = k+1;
        construct_candidates(a,k,input,c,&ncandidates);
        for (i=0; i<ncandidates; i++) {
            a[k] = c[i];
            make_move(a,k,input);
            backtrack(a,k,input);
            unmake_move(a,k,input);
            if (finished) return; /* 若是符合終止條件就提早退出 */
        }
    }
}

  對於其中的函數和變量,解釋以下:

  a[]表示當前得到的部分解;

  k表示搜索深度;

  input表示用於傳遞的更多的參數;

  is_a_solution(a,k,input)判斷當前的部分解向量a[1...k]是不是一個符合條件的解

  construct_candidates(a,k,input,c,ncandidates)根據目前狀態,構造這一步可能的選擇,存入c[]數組,其長度存入ncandidates

  process_solution(a,k,input)對於符合條件的解進行處理,一般是輸出、計數等

  make_move(a,k,input)unmake_move(a,k,input)前者將採起的選擇更新到原始數據結構上,後者把這一行爲撤銷。

 

  其實回溯法框架就是這麼簡單,經過這個框架,足以解決不少回溯法問題了。不信?下面展現一下:

  (因爲後文全部代碼均爲在C中編寫,所以bool類型用int類型代替,其中0爲FALSE,非0爲TRUE。)

 

問題1:求一個集合的全部子集

解答:

  將3個主要的函數實現,這個問題就解決了。因爲每次for循環中a[k]=c[i],這是惟一的改動,而且在下次循環時會被覆蓋,不須要專門編寫make_move()和make_unmove()。

int is_a_solution(int a[],int k, data input)
{
    return k==input;
}

void construct_candidates(int a[],int k, data input, int c[],int *ncandidates) 
{
    c[0] = 1;
    c[1] = 0;
    *ncandidates = 2;
}

void process_solution(int a[],int k,data input)
{
    int i;
    printf("{");
    for(i=1;i<=k;i++)
        if(a[i])
            printf(" %d",i);
    printf(" }\n");
}

  候選構造函數construct_candidates()相對簡單,由於對每一個集合中的元素和一個特定子集,只有出現和不出現這兩種可能。

  調用這個函數只需:

generate_subsets(int n)
{
  int a[NMAX];
  backtrack(a,0,n);
}

擴展:

  Skiena在《算法設計手冊》第14章組合算法部分介紹了生成子集的三種方式:按排序生成、二進制位變換、格雷碼。上面這個算法是二進制變換的一種,格雷碼生成能夠參考後面習題解答的7-18;而按排序生成則比較複雜,它按特定順序生成,如{1,2,3}生成順序爲{} , {1}, {1, 2}, {1, 2, 3}, {1, 3}, {2}, {2, 3},而且建議除非有這種要求,不然不要使用這個方式。 

 

 

問題2:輸出不重複數字的全排列

解答:

  與上1題不一樣的是,因爲不能重複出現,每次選擇的元素都將影響以後的候選元素集合。構造候選時應從以前得到的部分解中獲取信息,哪些元素是能夠後續使用的,哪些是不能夠的:

void construct_candidates(int a[],int k, data input, int c[],int *ncandidates) 
{
    int i;
    int in_perm[NMAX+1];
    for(i=1;i<=NMAX;i++)
        in_perm[i] = 0;
    for(i=1;i<k;i++)
        in_perm[a[i]] = 1;
    *ncandidates = 0;
    for(i=1;i<=input;i++)
        if(!in_perm[i]) {
            c[*ncandidates] = i;
            *ncandidates += 1;
        }
}

  不過這裏能夠看出一個問題,若是每次都是須要選擇分支時構造候選元素,勢必會形成浪費。這裏僅僅是一個展現,若是提升效率,能夠把解空間和原空間優化到一塊兒,這樣沒必要每次都生成解空間。下面的代碼是對這個問題更好的也是更常見的解法,我相信很多人都寫過,並對上一種看似複雜的解法表示不屑一顧:

void permutaion(int *array,int k,int length)
{
    int i;
    if (length==k) {
        for(i=0;i<length;i++)
            printf("%d ",array[i]);
        printf("\n");
        return;
    }
                
    for(i=k;i<length;i++) {
        swap(&array[i],&array[k]);
        permutaion(array,k+1,length);
        swap(&array[i],&array[k]);
    }
}

  但仔細觀察這個解法,能夠發現其實它暗含了is_a_solution()、construct candidates()、process_solution()、make_move()和unmake_move()這些步驟,它實際上是通常的回溯法框架的簡化和優化。

 

問題3:求解數獨——剪枝的示範

解答:

  因爲填入的數字涉及到橫縱兩個座標,單純的解向量a[]不能知足保存解的要求了。僅a[k]表示填入的值,定義一個結構以保存數獨的當前狀態和第k步時填入點的座標:

#define DIMENSION 9
#define NCELLS DIMENSION*DIMENSION
typedef struct {
    int x,y;
} point;

typedef struct {
    int m[DIMENSION+1][DIMENSION+1];
    int freecount;
    point move[NCELLS+1];
} boardtype;

typedef boardtype* data;

  同時把獲取下一步候選的construct_candidates()分解爲兩步:獲取下一步填入點的座標next_square()、獲取該點能夠填入的數值possible_values()。對於這兩步須要進行一些探討:

  next_square()能夠採起任意取一個沒有填入的點的隨機策略(arbitrary);而更有效的策略是最大約束(Most Constrained),即取的點行、列以及所在3*3方陣點數最多的點,這樣它的約束最多,填入的數字的可能性最少。

  possible_values()也能夠採用兩種策略:局部計數(local count),即只要知足行、列、3*3方陣內部都不衝突,就做爲可能填入的數值;預測(look ahead),即對填入的數,預先找下一步時是否全部空均可填入至少一個數字來確認這個數是否能夠被填入。《算法設計手冊》做者認爲,咱們若是採用最大約束和局部計數策略,回溯過程就已經暗含了預測(失敗時會回退),我曾經試過,專門寫一個look ahead函數是得不償失的,它並不比直接回溯開銷小,甚至更大。

  所以,爲了提升效率,next_square()採起最大約束策略,possible_values()採起暗含的預測策略。爲了計算出最大約束的點,我還寫了一個evaluate()函數用來計算某個未填點的得分,得分越大說明約束越強,約束最強的點將成爲候選點。這個evaluate()不是很嚴格,由於它重複計算了一些點,不過影響不大。這兩個策略的採起能夠看做是剪枝的過程。剪枝是回溯法的重要加速途徑,好的剪枝策略可以提升回溯法的運行速度,這是回溯法與暴力算法的一大區別。

void construct_candidates(int a[],int k,boardtype *board, int c[],int *ncandidates)
{
    int x,y;
    int i;
    int possible[DIMENSION+1];
    next_square(&x,&y,board);
    board->move[k].x = x;
    board->move[k].y = y;
    //printf("k:%d left:%d\n",k,board->freecount);
    *ncandidates = 0;
    if(x<0 && y<0)
        return;
    possible_values(x,y,board,possible);
    for(i=1;i<=DIMENSION;i++)
        if(possible[i]) {
            c[*ncandidates] = i;
            *ncandidates += 1;
        }
}

//most constrained square selection
void next_square(int *x,int *y, boardtype *board)
{
    int m_x,m_y,i,j;
    int score,max_score;
    m_x = -1,m_y = -1, max_score = 0;
    for(i=1;i<=DIMENSION;i++)
        for(j=1;j<=DIMENSION;j++) {
            if(board->m[i][j]) //not blank
                continue;
            score = evaluate(i,j,board);
            if(score > max_score) {
                m_x = i;
                m_y = j;
                max_score = score;
            }
        }
    *x = m_x;
    *y = m_y;
}

int evaluate(int x,int y,boardtype* board)
{
    int i,j,i_start,j_start;
    int score = 0;
    
    //row
    i = x;
    for(j=1;j<=DIMENSION;j++)
        score += (board->m[i][j] > 0);

    //column
    j=y;
    for(i=1;i<=DIMENSION;i++)
        score += (board->m[i][j]>0);

    //3*3 square
    i_start = (i-1)/3 *3 +1;
    j_start = (j-1)/3 *3 +1;
    //the most left and up point in the 3*3 square
    for(i=i_start;i<=i_start+2;i++)
        for(j=j_start;j<j_start+2;j++)
            score += (board->m[i][j]>0);
    return score;
}

int possible_values(int x,int y,boardtype* board, int possible[])
{
    int i,j;
    volatile int i_start,j_start;
    for(i=1;i<=DIMENSION;i++)
        possible[i] = 1;
    
    //row
    i = x;
    for(j=1;j<=DIMENSION;j++)
        possible[board->m[i][j]] = 0;

    //column
    j = y;
    for(i=1;i<=DIMENSION;i++)
        possible[board->m[i][j]] = 0;

    //3*3 square
    i_start = (x-1)/3;
    i_start = i_start * 3 + 1;
    j_start = (y-1)/3;
    j_start = j_start * 3 + 1; 
    //printf("i_start:%d j_start:%d\n",i_start,j_start);
    //the most left and up point in the 3*3 square
    for(i=i_start;i<=i_start+2;i++)
        for(j=j_start;j<=j_start+2;j++)
            possible[board->m[i][j]] = 0;
    //printf("(%d,%d):",x,y);
    //for(i=1;i<=DIMENSION;i++)
        //if(possible[i]) {
            //printf("%d ",i);
        //}
    return 0;
}
construct_candidates()、next_square()、possible_values()

  因爲要對定義的數據結構進行修改,make_move()和unmake_move()也須要進行實現了。

void make_move(int a[], int k, boardtype *board)
{
    fill_square(board->move[k].x,board->move[k].y,a[k],board);
}

void unmake_move(int a[], int k, boardtype *board)
{
    free_square(board->move[k].x,board->move[k].y,board);
}

void fill_square(int x,int y,int key,boardtype* board){
    board->m[x][y] = key;
    board->freecount--;
}

void free_square(int x,int y,boardtype* board) {
    board->m[x][y] = 0;
    board->freecount++;
}
make_move()和unmake_move()

  is_a_solution()是對freecount是否爲0的判斷,process_solution()能夠用做輸出填好的數獨,這兩個函數的解法略過。而backtrack()函數和基本框架相比,看上去沒多大的區別。

void backtrack(int a[],int k, boardtype* input)
{
    int c[DIMENSION];
    int ncandidates;
    int i;
    if(is_a_solution(a,k,input))
        process_solution(a,k,input);
    else {
        k = k+1;
        construct_candidates(a,k,input,c,&ncandidates);
        for(i=0;i<ncandidates;i++) {
            a[k] = c[i];
            make_move(a,k,input);
            backtrack(a,k,input);
            unmake_move(a,k,input);
            if (finished)
                return;
        }
    }
}
backtrack of sudoku

  經測試,《算法設計手冊》上的Hard級別的數獨,個人這個程序能夠得到和原書一樣的解。

附註:

  這裏是以數獨爲例展現回溯法。而若是須要專門進行數獨求解,能夠試試DancingLinks,有一篇文章對其進行介紹,感興趣的讀者能夠自行查閱。另外有關DancingLinks的性能,能夠參閱:算法實踐——舞蹈鏈(Dancing Links)算法求解數獨

 

問題4:給定一個字符串,生成組成這個字符串的字母的全排列(《算法設計手冊》面試題7-14)

解答:

  若是字符串內字母不重複,顯然和問題2同樣。

  若是字符串中有重複的字母,就比較麻煩了。不過套用回溯法框架仍然能夠解決,爲了簡化候選元素的生成,將全部候選元素排列成數組,造成「元素-值」對,其中值表明這個元素還能出現幾回,把ASCII碼的A~Z、a~z映射爲數組下標0~51。實現以下:

int is_a_solution(char a[],int k, int len) {
    return (k==len);
}

void process_solution(char a[],int k, int len) {
    int i;
    for(i=1;i<=k;i++)
        printf("%c",a[i]);
    printf("\n");
}

void backtrack(char a[],int k, int len,int candidate[])
{
    int i;
    if(is_a_solution(a,k,len))
        process_solution(a,k,len);
    else {
        k = k+1;
        for(i=0;i<MAXCANDIDATES;i++) {
            if(candidate[i]) {
                a[k] = i+ 'A' ;
                candidate[i] --;//make_move
                backtrack(a,k,len,candidate);
                candidate[i]++;//unmake_move
                if (finished)
                    return;
            }
        }
    }
}

void generate_permutations_of_string(char *p) 
{
    //sort
    char a[NMAX];
    int candidate[MAXCANDIDATES];
    int i,len=strlen(p);
    for(i=0;i<MAXCANDIDATES;i++)
        candidate[i] = 0;
    for(i=0;i<len;i++)
        candidate[p[i] - 'A']++;
    backtrack(a,0,len,candidate);
}
問題4解法

顯然,construct_candidates()已經化入了backtrace()內部,並且這也是一個對如何將候選也做爲參數傳遞給下一層遞歸的很好的展現

 

問題5:求一個n元集合的k元子集(n>=k>0)。(《算法設計手冊》面試題7-15)

解答:

  若是想採用問題1的解法,須要稍做修改,使得遍歷至葉結點(也即全部元素都進行標記是否在集合中)時,判斷是否是一個解,即元素數目是否爲k。知足才能輸出。

#include <stdio.h>
#define MAXCANDIDATES 2
#define NMAX 3
typedef int data;

int is_a_solution(int a[],int k, data input);
void construct_candidates(int a[],int k,data input, int c[],int *ncandidates);
void process_solution(int a[],int k, data input);

static int finished = 0;

void construct_candidates(int a[],int k, data input, int c[],int *ncandidates) 
{
    c[0] = 1;
    c[1] = 0;
    *ncandidates = 2;
}

void process_solution(int a[],int k,data input)
{
    int i;
    printf("{");
    for(i=1;i<=k;i++)
        if(a[i])
            printf(" %d",i);
    printf(" }\n");
}

backtrack(int a[],int k, data input,int n,int num)
{
    int c[MAXCANDIDATES];
    int ncandidates;
    int i;
    if(n == num) {//is a solution
        process_solution(a,k,input);
        return;
    }
    else if ((num>n)||(k==input))//not a solution
        return;
    else{
        k=k+1;
        construct_candidates(a,k,input,c,&ncandidates);
        for(i=0;i<ncandidates;i++) {
            a[k] = c[i];
            if(c[i]) {
                num++;
                backtrack(a,k,input,n,num);
                num--;
            }
            else
                backtrack(a,k,input,n,num);

            if (finished)
                return;
        }
    }
}

generate_subsets(int n)
{
    int a[NMAX+1];
    backtrack(a,0,n,2,0);
}

int main()
{
    generate_subsets(NMAX);
}
問題5解法完整示例

 

問題6:電話號碼對應字符串

  電話鍵盤上有9個數字,其中2~9分別表明了幾個字母,如2:ABC,3:DEF......等等。給定一個數字序列,輸出它所對應的全部字母序列。(《算法設計手冊》面試題7-17,以及《編程之美》3.2「電話號碼對應英語單詞」)

解答:

  這個問題在回溯法裏已經很簡單了,由於每一步的選擇都不影響下一步的選擇。稍微要注意的一點是如何把數字與多個字母的對應關係告訴程序。這個存儲結構和相應的construct_candidates()多是這樣的:

static char ch[10][4] =
{
    "",
    "",
    "ABC",
    "DEF",
    "GHI",
    "JKL",
    "MNO",
    "PQRS",
    "TUV",
    "WXYZ",
};

static int total[10] ={0,0,3,3,3,3,3,4,3,4}; 

void construct_candidates(int a[],int k,data input, int *c,int *ncandidates)
{
    *c = input[k-1] - '0';
    *ncandidates = total[*c];
    return;
}

  而backtrack()中填充解空間a[]則是這樣的:

a[k] = ch[c][i];

  你會發現,backtrack()和《編程之美》3.2節解法二的RecurisiveSearch()實質是同樣的:都是回溯法嘛。固然,能簡化仍是應該儘可能簡化的。

//c[i][j] 數字i對應的第j個字母
//total[i] 數字i一共對應幾個字母
//number[] 待轉換的數字序列
//answer[] 解空間
//index 當前處理的數字的位置
//n 電話號碼總長度

void recursive(int * number, int * answer, int index, int n)
{
    if(index == n)
    {
        for(int i=0;i<n;i++)
            printf("%c", c[number[i]][answer[i]]);
        printf("\n");
        return;
    }
    for(answer[index]=0;answer[index]<total[number[index]];answer[index]++)
    {
        recursive(number, answer, index+1, n);
    }
}

 

問題7:一摞烙餅的排序(《編程之美》1.3)

  假設有一堆烙餅,大小不一,須要把它們擺成從下到上由大到小的順序。每次翻轉只能一次翻轉最上面的幾個烙餅,把它們上下顛倒。反覆屢次可使烙餅有序。那麼,最少應該翻轉幾回呢?

解答:

  根據《編程之美》的分析可知,對於n個烙餅,若是每次都把最大的先翻到最上面,而後再把它翻到最下面,這樣就只用處理最上面的(n-1)個。而翻完n-1個時,最小的必然已經在上面,所以,翻轉的上界是2(n-1)。

  爲了在搜索解的時候剪枝,若是當前翻轉次數多於上界2(n-1),則必然不是最少的,應該直接返回。

  同時,當烙餅內部幾個部分分別有序時(好比三、四、5已經連在一塊兒、九、10已經連在一塊兒),不該該拆散它們,而是應該視爲一個總體進行翻轉。這樣,每次把最大的和次大的翻在一塊兒,確定要優於上界。把這個不怎麼緊密的下界記爲LowerBound,值爲順序相鄰兩個烙餅大小不相鄰順序的對數(pairs,不是log)。

  這樣,有了粗略的上界和下界,就能夠進行剪枝了。爲了更有效的剪枝,能夠把當前翻轉步數大於已記錄解的翻轉步數的全部解也給剪掉。

  套用回溯法的框架,如下是求解代碼。雖然和《編程之美》上的C++的面向對象版本看上去不太同樣,但實質是同樣的:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
typedef int* data;

int is_a_solution(int a[],int k, int step);
//void construct_candidates(int a[],int k,data input, int c[],int *ncandidates);
void process_solution(int a[],int n,int step, data input,data output);
//void make_move(int a[],int k, data input);
//void unmake_move(int a[],int k,data inupt);

int LowerBound(int *CakeArray,int n);
int UpperBound(int n);
void Reverse(int CakeArray[],int begin,int end);
void generate_sort(int a[],int n);
void show(data output);
static int maxStep;

//is_sorted()
int is_a_solution(int *CakeArray,int n,int step)
{
    int i;
    for(i=1;i<n;i++)
        if(CakeArray[i-1]>CakeArray[i])
            return 0;
    if(step<maxStep)
        return 1;
    return 0;
}

void process_solution(int a[],int n,int step,data input,data output)
{
    int i;
    maxStep = step;
    printf("new maxStep:%d\n",maxStep);
    for(i=0;i<step;i++)
        output[i] = input[i];
    return;
}

int UpperBound(int n)
{
    return 2*(n-1);
}

int LowerBound(int *CakeArray,int n)
{
    int i,t,ret = 0;
    for(i=1;i<n;i++)
    {
        t = CakeArray[i-1] - CakeArray[i];
        if((t==1)||(t==-1))
            continue;
        else
            ret++;
    }
    return ret;
}

void Reverse(int CakeArray[],int begin,int end) {
    assert(end>begin);
    int i,j,t;
    for(i=begin,j=end;i<j;i++,j--)
    {
        t = CakeArray[i];
        CakeArray[i] = CakeArray[j];
        CakeArray[j] = t;
    }
}

backtrack(int a[],int n,int step, data input,data output)
{
    int i;
    if(step+LowerBound(a,n)>maxStep)
        return;
    if(is_a_solution(a,n,step))
        process_solution(a,n,step,input,output);
    else {
        //construct_candidates(a,k,input,c,&ncandidates);
        for(i=1;i<n;i++) {
            Reverse(a,0,i);//make_move(a,k,input);
            input[step] = i;
            backtrack(a,n,step+1,input,output);
            Reverse(a,0,i);//unmake_move(a,k,input);
        }
    }
}

void generate_sort(int a[],int n)
{
    maxStep = UpperBound(n);
    int *SwapArray = malloc((UpperBound(n)+1)*sizeof(int));
    int *minSwapArray = malloc((UpperBound(n)+1)*sizeof(int));
    backtrack(a,n,0,SwapArray,minSwapArray);
    show(minSwapArray);
}

void show(data output)
{
    int i;
    for(i=0;i<maxStep;i++)
        printf("%d",output[i]);
    printf("\n");
    return ;
}


int main() {
    int i,n;
    printf("number of pancake:");
    scanf("%d",&n);
    int *CakeArray = malloc(n*sizeof(int));
    printf("pancakes' order(continuously):\n");
    for(i=0;i<n;i++)
        scanf("%d",&CakeArray[i]);
    printf("searching...\n");
    generate_sort(CakeArray,n);
    return 0;
}
烙餅排序

  其實理解這個算法的關鍵是如何把「翻轉烙餅」的過程抽象成數據結構的改變,回溯法倒不是那麼重要。

 

問題8:8皇后問題

  國際象棋棋盤上有8*8個格子。如今有8枚皇后棋子,一個格子只能放一個棋子,求解全部放法,使得這些棋子不一樣行、不一樣列、且不在對角線上((4,5)和(5,6)就是在對角線上的狀況,不合法)。

解答:

  上面練習了那麼多回溯法的問題,我相信能看到這裏的人水平已經足以解決這個問題了。按行放置能夠保證棋子不一樣行,對於每種放置可能,檢查是否與上面各行的棋子是否同列、同對角線。都不知足的才能選做這次的決策便可。

#include <stdio.h>
#define DIM 8

int is_a_solution(int a[DIM][DIM],int row);
//void construct_candidates(int a[],int k,data input, int c[],int *ncandidates);
void process_solution(int a[DIM][DIM]);
//void make_move(int a[],int k, data input);
//void unmake_move(int a[],int k,data inupt);

static int finished = 0;
static count = 0;

int is_a_solution(int chess[DIM][DIM],int row)
{
    return (row == DIM);
}

void process_solution(int chess[DIM][DIM])
{
    int i,j;
    count++;
    for(i=0;i<DIM;i++) {
        for(j=0;j<DIM;j++)
            printf("%d ",chess[i][j]);
        printf("\n");
    }
    printf("\n");
}

int is_collision(int chess[DIM][DIM],int x,int y)
{
    int i,j;
    for(i=x-1,j=y-1;i>=0 && j>=0;i--,j--)
        if(chess[i][j] == 1)
            return 1;

    for(i=x-1,j=y+1;i>=0 && j<DIM;i--,j++)
        if(chess[i][j] == 1)
            return 1;

    return 0;
}

backtrack(int chess[DIM][DIM],int row, int* candidates)
{
    if(is_a_solution(chess,row))
        process_solution(chess);
    else {
        int i;
        //construct_candidates(a,k,input,c,&ncandidates);
        for(i=0;i<DIM;i++) {
            if(candidates[i] || is_collision(chess,row,i))
                continue;
            //make_move(a,k,input);
            chess[row][i] = 1;
            candidates[i] = 1;

            backtrack(chess,row+1,candidates);

            //unmake_move(a,k,input);
            chess[row][i] = 0;
            candidates[i] = 0;
        }
    }
}

void generate_8queen(int chess[8][8])
{
    int candidates[DIM] = {0,0,0,0,0,0,0,0};
    backtrack(chess,0,candidates);
    printf("total:%d\n",count);
    return;
}

int main()
{
    int chess[8][8] = {
        {0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0},
        {0,0,0,0,0,0,0,0}
    };
    generate_8queen(chess);
}
8皇后問題

 

總結與探討

  經過以上的實例,能夠發現回溯法框架確實可以解決許多形態萬千的問題,這也得歸功於這個框架足夠抽象而不限於具體問題的求解,其通用性毋庸置疑。

  然而若是一個問題看到以後就有了思路,並能直接寫出相似於問題2的精簡版的狀況又如何呢?這種狀況下固然就不必再去套用回溯法框架了,由於你已經把這個框架的步驟內化到本身的思考中並能在這個問題上運用自如了,這一點是值得高興的。這時回溯法框架對於你來講只是用於檢查代碼正確性的一種額外驗證方式罷了,不必退而求其次。

  當你思路比較混亂,不知如何下手時我才建議搬出回溯法框架進行分析和套用。不過從問題7烙餅排序中能夠看到,有時思路的不清晰每每是對實際問題的抽象不夠,而不是編寫回溯法解決自己的問題。

  編寫回溯法時應該注意儘量剪枝,同時維護好構造候選時所用的數據結構。 

 

附:《算法設計手冊》第7章其他面試題解答

7-16.

  請用給定字符串中的字母從新組合成在字典中的單詞。好比Steven Skiena能夠重組爲Vainest Knees。

解答:

  雖然經過回溯法能夠把全部狀況列出並與字典對照,但這未免太沒有效率了。

  更快的方法是把給定字符串和全部字典單詞排序成字母序,好比apple變成aelpp,再對排序後的字符串在排序後的字典進行搜索。這是個變位詞的變形,變位詞的處理能夠參考:http://www.cnblogs.com/wuyuegb2312/p/3139926.html#title21

 

7-18.

  一間能容納n我的的空房,房外有n我的。你站在門口,能夠選擇讓門外的一我的進屋,也能夠選擇讓屋內的人出來一個。請輸出全部的2n種屋中人的出現狀況的可能,而且這些狀況是相鄰的(上一種狀況經過一次操做能變成下一種狀況)

解答:

  一開始不是很理解,參考答案上也提到是用格雷碼來解決。不過若是知道格雷碼的生成方式,就好解決了:

  1. 1位格雷碼有兩個碼字
  2. (n+1)位格雷碼中的前2 n個碼字等於n位格雷碼的碼字,按順序書寫,加前綴0
  3. (n+1)位格雷碼中的前2 n個碼字等於n位格雷碼的碼字,按逆序書寫,加前綴1

 

  不過爲了輸出美觀,因爲C語言不提供printf直接輸出2進制數,須要把10進制數轉化成2進制數輸出,並且首端的0要補上,這須要花點心思。下面是一個生成4位格雷碼的程序,並非回溯法。(爲了省事直接在回溯法框架上改的)

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

void process_solution(int a[],int k, int n);
int print_bin(int num);
int e2(int n);

void gray_code(int a[],int k, int n)
{
    int i,j;
    if(k==n+1)
        process_solution(a,k,n);
    else {
        for(i=e2(k)-1,j=e2(k);i>=0;i--,j++) 
            a[j] = (1<<k)+a[i];
        gray_code(a,k+1,n);
    }
}

void process_solution(int a[],int k,int n)
{
    int i;
    int j = e2(n);
    for(i=0;i<j;i++) {
        print_bin(a[i]);
        printf("\n");
    }
    return;
}

int print_bin(int num)
{
    int pes_num=0;
    int i=1,bits=4;
    if(num==0)
        bits--;
    while(num>0) {
        if(num%2)
            pes_num += i;
        i *= 10;
        num /= 2;
        bits--;
    }
    for(;bits>0;bits--)
        printf("0");
    printf("%d",pes_num);
    return 0;
}

int e2(int n)
{
    int res = 1;
    assert(n<16 && n>= 0);
    while(n>0) {
        res *= 2;
        n--;
    }
    return res;
}

int generate_gray_code(int n)
{
    int *a = malloc(e2(n)*sizeof(int));
    a[0] = 0;
    gray_code(a,0,4);
}

int main()
{
    generate_gray_code(4);
    //int i;
    //for(i=0;i<16;i++)
    //{
    //    print_bin(i);
    //    printf("\n");
    //}
}
格雷碼生成

 

7-19.

  使用能生成隨機數{0,1,2,3,4}的函數rng04來生成rng07。每次運行rng07平均要調用幾回rng04?

解答:

  隨機數生成函數之前已經分析過了:http://www.cnblogs.com/wuyuegb2312/p/3141292.html。對於調用次數的指望,

  若是將rng07寫做rng03+4*rng01,那麼rng04調用的次數爲它在rng03和rng01之和,都是 $1\cdot \frac{4}{5} +2\cdot \frac{4}{5}\cdot \frac{1}{5} +3\cdot \frac{4}{5}\cdot (\frac{1}{5})^{2}+ ... = 1.25$ 

相關文章
相關標籤/搜索