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


目錄

什么是回溯法?

回溯法的通用框架

利用回溯法解決問題

總結與探討

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

 

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

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

   可以把回溯法看成是遞歸調用的一種特殊形式。其實對於一個並非編程新手的人來說,從來沒使用過回溯法來解決問題的情況是很少見的,不過往往是“對症下葯”,針對特定的問題進行解答。這些天看了《算法設計手冊》回溯法相關內容,覺得對回溯法抽象的很好。如果說算法是解決問題步驟的抽象,那么這個回溯法的框架就是對大量回溯法算法的抽象。本文將對這個回溯法框架進行分析,並且用它解決一系列的回溯法問題。文中的回溯法采用遞歸形式。

  在進一步的抽象之前,先來回顧一下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),則必然不是最少的,應該直接返回。

  同時,當烙餅內部幾個部分分別有序時(比如3、4、5已經連在一起、9、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$ 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM