backtracking 算法講解

Backtracking

backtracking

中文稱做「回溯法」,窮舉多維度數據的方法,可以想作是多維度的Exhaustive Search

大意是:把多維度數據看做是是一個多維向量(solution vector),然後運用遞迴依序遞迴窮舉各個維度的值,製作出所有可能的數據(solution space),並且在遞迴途中避免列舉出不正確的數據。

 
  1. backtrack ( [ v1 ,..., vn ] )     // [v1,...,vn]是多維度的向量
  2. {
  3.     /* 製作出了一組數據,並檢驗這組數據正不正確*/
  4.     if  ( [ v1 ,..., vn ]  is  well generated  )
  5.     {
  6.         if  ( [ v1 ,..., vn ]  is   solution  )  process solution ;
  7.         return ;
  8.     }
  9.  
  10.     /* 窮舉這個維度的所有值,並遞迴到下一個維度*/
  11.     for  (   =  possible  values  ​​of  vn  )
  12.         backtrack ( [ v1 ,..., vn ,  ] );
  13. }
  14.  
  15. call  backtrack ( [] );    //從第一個維度開始逐步窮舉

撰寫程式時,可用陣列來實作solution vector的概念。

 
  1. int  solution MAX_DIMENSION ];     // solution vector,多維度的向量
  2.  
  3. void  backtrack int  dimension )
  4. {
  5.     /* 製作出了一組數據,並檢驗這組數據正不正確*/
  6.     if  (  solution []  is  well generated  )
  7.     {
  8.         check  and  record  solution ;
  9.         return ;
  10.     }
  11.  
  12.     /* 窮舉這個維度的所有值,並遞迴到下一個維度*/
  13.     for  (   =  each  value  of  current  dimension  )
  14.     {
  15.         solution dimension ] =  ;
  16.         backtrack (  dimension  +   );
  17.     }
  18. }
  19.  
  20. int  main ()
  21. {
  22.     backtrack );    //從第一個維度開始逐步列舉
  23. }

另外,當我們所需的數據只有唯一一組時,可以讓程式提早結束。

 
  1. int  solution MAX_DIMENSION ];
  2. bool  finished  =  false ;   //如果爲true表示已經找到數據,可以結束。
  3.  
  4. void  backtrack int  dimension )
  5. {
  6.     if  (  solution []  is  well generated  )
  7.     {
  8.         check  and  record  solution ;
  9.         if  (  solution  is  found  )  finished  =  true ;   //找到數據了
  10.         return ;
  11.     }
  12.  
  13.     for  (   =  each  value  of  current  dimension  )
  14.     {
  15.         solution dimension ] =  ;
  16.         backtrack (  dimension  +   );
  17.         if  ( finished )  return ;    //提早結束,跳出這個遞迴
  18.     }
  19. }

附贈一張圖片。畫了很久。

結合pruning

回溯法會在遞迴途中避免列舉出不正確的數據,其意義其實就等同於搜尋樹的pruning技術。

 
  1. int  solution MAX_DIMENSION ];
  2.  
  3. void  backtrack int  dimension )
  4. {
  5.     /* pruning:在遞迴途中避免列舉出不正確的數據*/
  6.     if  (  solution []  will  NOT  be   solution  in  the future  )  return ;
  7.  
  8.     if  (  solution []  is  well generated  )
  9.     {
  10.         check  and  record  solution ;
  11.         return ;
  12.     }
  13.  
  14.     for  (   =  each  value  of  current  dimension  )
  15.     {
  16.         solution dimension ] =  ;
  17.         backtrack (  dimension  +   );
  18.     }
  19. }

結合branch and bound

回溯法可以結合branching

 
  1. int  solution MAX_DIMENSION ];
  2.  
  3. void  backtrack int  dimension )
  4. {
  5.     if  (  solution []  is  well generated  )
  6.     {
  7.         check  and  record  solution ;
  8.         return ;
  9.     }
  10.  
  11.     /* branch:製做適當的分支 */
  12.     
  13.     int  MAX_CANDIDATE ];    // candidates for next dimension
  14.     int  ncandidate ;          // candidate counter
  15.  
  16.     construct_candidates dimension ,  ,  ncandidate );
  17.  
  18.     for  ( int  ;   <  ncandidate ;  ++)
  19.     {
  20.         solution dimension ] =  ];
  21.         backtrack (  dimension  +   );
  22.     }
  23. }

回溯法可以結合bounding

 
  1. int  solution MAX_DIMENSION ];
  2.  
  3. void  backtrack int  dimension ,  int  cost )  //用一數值代表數據好壞
  4. {
  5.     /* bound:數據太糟了,不可能成爲正確數據,不必遞迴下去。*/
  6.     if  (  cost  is  worse  than  best_cost  )  return ;
  7.  
  8.     /* bound:數據夠好了,可以成爲正確數據,不必遞迴下去。*/
  9.     if  (  solution []  is  well generated  )
  10.     {
  11.         check  and  record  solution ;
  12.         if  (  solution  is  found  )  best_cost  =  cost ;  //紀錄cost
  13.         return ;
  14.     }
  15.  
  16.     for  (   =  each  value  of  current  dimension  )
  17.     {
  18.         solution dimension ] =  ;
  19.         backtrack (  dimension  +   ,  cost  + ( cost  of ) );
  20.     }
  21. }

特色

backtracking的好處,是在遞迴過程中,能有效的避免列舉出不正確的數據,省下很多時間。

另外還可以調整維度的順序、每個維度中列舉值的順序。如果安排得宜,可以更快的找到數據。

這裏是我找到的一些backtracking題目,不過我還沒有驗證它們是否都是backtracking問題。

UVa 140 165 193 222 259 291 301 399435 524 539 565 574 598 628 656 73210624 | 10186 10344 10364 10400 10419 10447 10501 10503 10513 10582 10605 10637

另外還有一些容易被誤認成其他類型,實際上卻可以用backtracking解決的題目。

UVa 193 129

Enumerate all n-tuples

Enumerate all n-tuples

列舉重複排列。這裏示範:列舉出「數字110選擇五次」全部可能的情形。

製作一個陣列,用來存放一組可能的排列(數據)。

 
  1. int  solution ];

例如solution[0] = 4表示第一個抓到的數字是4solution[4] = 9表示第五個抓到的數字是9。陣列中不同的格子,就是solution vector當中不同的維度。

遞迴程式碼設計成這樣:

 
  1. int  solution ];     //用來存放一組可能的數據
  2.  
  3. void  print_solution ()    //印出一組可能的數據
  4. {
  5.     for  ( int  ;  ;  ++)
  6.         cout  <<   <<  ' ' ;
  7.     cout  <<  endl ;
  8. }
  9.  
  10. void  backtrack int  )    // n爲現在正在列舉的維度
  11. {
  12.     // it's a solution
  13.     if  (  ==  )
  14.     {
  15.         print_solution ();
  16.         return ;
  17.     }
  18.     
  19.     // 逐步列舉數字1到10,並且各自遞迴下去,列舉之後的維度
  20.     solution ] =  ;
  21.     backtrack );
  22.  
  23.     solution ] =  ;
  24.     backtrack );
  25.  
  26.     ......
  27.  
  28.     solution ] =  10 ;
  29.     backtrack );
  30. }
  31.  
  32. int  main ()
  33. {
  34.     backtrack );
  35. }

輸出結果會照字典順序排列。附送一張簡圖:

Permutation

Permutation

permutation是「排列」的意思,便是數學課本中「排列組合」的排列。但是這裏並不是要計算排列有多少種,而是實際列舉出所有的排列:

現在有一個集合,裏面有1n的數字,列出所有數字的排列,同樣的排列不能重複列出來。例如{1,2,3}所有的排列就是{1,2,3}{1,3,2}{2,1,3}{2,3,1}{3,1,2 }{3,2,1}

permutation的問題可以使用backtracking的技術來解決!如果不懂backtracking也沒關係,暫且繼續看下去吧。細嚼慢嚥,一定可以融會貫通的!

依序窮舉每個位置,針對每個位置,試着填入各種數字

一般來說,permutation的程式碼都會長成這樣的格式:

 
  1. int  solution MAX ];   //用來存放一組可能的答案
  2. bool  used MAX ];      //紀錄數字是否使用過,用過爲true
  3.  
  4. void  permutation int  ,  int  )
  5. {
  6.     if  (  ==  )  // it's a solution
  7.     {
  8.         for  ( int  ;  ;  ++)
  9.             cout  <<  solution ] <<  " " ;
  10.         cout  <<  endl ;
  11.     }
  12.     else
  13.     {
  14.         for  ( int  ;  ;  ++)  //試着將第k格填入各種數字
  15.             if  (! used ])
  16.             {
  17.                 used ] =  true ;      //紀錄用過的數字
  18.  
  19.                 solution ] =  ;     //將第k格填入數字k
  20.                 permutation ,  );     // iterate next position
  21.  
  22.                 used ] =  false ;     //回收用完的數字
  23.             }
  24.     }
  25. }
  26.  
  27. int  main ()
  28. {
  29.     for  ( int  ;   <  MAX ;  ++)  // initialization
  30.         used ] =  false ;
  31.  
  32.     permutation ,  10 );  //印出0~9,一共10個數字的所有排列
  33. }

permutation的問題都可以使用這段程式碼來解決。而且這支程式,是以字典順序來列舉出所有排列。所以它真的很有用,不妨參考看看。

permutation是一種簡單又容易理解的問題。「Programming Challenges」這本書在教導backtracking的概念時,就用了permutation來當做入門的例子。如果有人想要教導backtracking的程式碼要怎麼撰寫,以permutation當做範例會是個不錯的選擇。

依序窮舉每個數字,針對每個數字,試着填入各個位置

另外還有一種作法是生做這個樣​​子的:

 
  1. int  solution MAX ];   //用來存放一組可能的答案
  2. bool  filled MAX ];    //紀錄各個位置是否填過數字,填過爲true
  3.  
  4. void  permutation int  ,  int  )
  5. {
  6.     if  (  ==  )  // it's a solution
  7.     {
  8.         for  ( int  ;  ;  ++)
  9.             cout  <<  solution ] <<  " " ;
  10.         cout  <<  endl ;
  11.     }
  12.     else
  13.     {
  14.         for  ( int  ;  ;  ++)  //試着將數字v填入各個位置
  15.             if  (! filled ])
  16.             {
  17.                 filled ] =  true ;    //紀錄填過的位置
  18.  
  19.                 solution ] =  ;     //將數字v填入第i格
  20.                 permutation ,  );     // iterate next position
  21.  
  22.                 filled ] =  false ;   //回收位置
  23.             }
  24.     }
  25. }
  26.  
  27. int  main ()
  28. {
  29.     for  ( int  ;  MAX ;  ++)    // initialization
  30.         filled ] =  false ;
  31.  
  32.     permutation ,  10 );  //印出0~9,一共10個數字的所有排列
  33. }

這也是一個不錯的方法,列出來提供大家參考。多接觸各式各樣的方法,能激發一些創意呢!

爲了講解方便,以下的文章以一開始提到的方法當作基準。

字串排列

有個常見的問題是:列出字串abc的所有排列,要依照字典順序列出。其實這就跟剛纔介紹的東西大同小異,只要稍加修改程式碼即可。

 
  1. char  ] = { 'a', 'b', 'c' };     //字串,需要先由小到大排序過
  2. char  solution ];    //用來存放一組可能的答案
  3. bool  used ];        //紀錄該字母是否使用過,用過爲true
  4.  
  5. void  permutation int  ,  int  )
  6. {
  7.     if  (  ==  )  // it's a solution
  8.     {
  9.         for  ( int  ;  ;  ++)
  10.             cout  <<  solution ];
  11.         cout  <<  endl ;
  12.     }
  13.     else
  14.     {
  15.         // 針對solution[i]這個位置,列舉所有字母,並各自遞迴
  16.         for  ( int  ;  ;  ++)
  17.             if  (! used ])
  18.             {
  19.                 used ] =  true ;
  20.  
  21.                 solution ] =  ];  //填入字母
  22.                 permutation ,  );
  23.  
  24.                 used ] =  false ;
  25.             }
  26.     }
  27. }

程式碼改寫成這樣會更清楚:

 
  1. char  ] = { 'a', 'b', 'c' };     //字串,需要先由小到大排序過
  2. char  solution ];    //用來存放一組可能的答案
  3. bool  used ];        //紀錄該字母是否使用過,用過爲true
  4.  
  5. void  permutation int  ,  int  )
  6. {
  7.     // it's a solution
  8.     if  (  ==  )
  9.     {
  10.         for  ( int  ;  ;  ++)
  11.             cout  <<  solution ];
  12.         cout  <<  endl ;
  13.         return ;                  // if-else改成return
  14.     }
  15.     
  16.     // 針對solution[i]這個位置,列舉所有字母,並各自遞迴
  17.     for  ( int  ;  ;  ++)
  18.         if  (! used ])
  19.         {
  20.             used ] =  true ;
  21.  
  22.             solution ] =  ];  //填入字母
  23.             permutation ,  );
  24.  
  25.             used ] =  false ;
  26.         }
  27. }

避免重複排列

若是字串排列的問題改成:列出abb的所有排列,依照字典順序列出。答案應該爲abbababaa。不過使用剛剛的程式碼的話,答案卻會變成這樣:

abb
abb
bab
bba
bab
bba

這跟預期的不一樣。會有這種結果,是由於之前的程式有個基本假設:字串中的每個字母都不一樣。儘管出現了一樣的字母,但是程式還是把它當作是不一樣的字母,依舊把所有可能的排列都列出,也就是現在的結果──有一些排列重複出現了。

要解決問題,在列舉某一個位置的字母時,就必須避免一直填入一樣的字母。如此就可以避免產生重複的排列。

 
  1. char  ] = { 'a', 'b', 'b' };     //字串,需要先由小到大排序過
  2. char  solution ];
  3. bool  used ];
  4.  
  5. void  permutation int  ,  int  )
  6. {
  7.     if  (  ==  )
  8.     {
  9.         for  ( int  ;  ;  ++)
  10.             cout  <<  solution ];
  11.         cout  <<  endl ;
  12.         return ;
  13.     }
  14.     
  15.     char  last_letter  =  '\0' ;
  16.     for  ( int  ;  ;  ++)
  17.         if  (! used ])
  18.             if  ( ] !=  last_letter )     //避免列舉一樣的字母
  19.             {
  20.                 last_letter  =  ];      //紀錄剛纔使用過的字母
  21.                 used ] =  true ;
  22.  
  23.                 solution ] =  ];
  24.                 permutation ,  );
  25.  
  26.                 used ] =  false ;
  27.             }
  28. }

因爲輸入的字串由小到大排序過,字母會依照順序出現,所以只要檢查上一個使用過的字母,判斷一不一樣之後,就可以避免列舉一樣的字母了。

程式碼也可以改寫成這種風格:

 
  1. char  ] = { 'a', 'b', 'b' };     //字串,需要先由小到大排序過
  2. char  solution ];
  3. bool  used ];
  4.  
  5. void  permutation int  ,  int  )
  6. {
  7.     if  (  ==  )
  8.     {
  9.         for  ( int  ;  ;  ++)
  10.             cout  <<  solution ];
  11.         cout  <<  endl ;
  12.         return ;
  13.     }
  14.     
  15.     char  last_letter  =  '\0' ;
  16.     for  ( int  ;  ;  ++)
  17.     {                            // if not改成continue
  18.         if  ( used ])  continue ;
  19.         if  ( ] ==  last_letter )  continue ;   //避免列舉一樣的字母
  20.  
  21.         last_letter  =  ];      //紀錄剛纔使用過的字母
  22.         used ] =  true ;
  23.  
  24.         solution ] =  ];
  25.         permutation ,  );
  26.  
  27.         used ] =  false ;
  28.     }
  29. }

另一種資料結構

如果字母重覆出現次數很多次的話,可以用一個128格的陣列,每一格個別存入128ASCII字元的出現次數。程式碼會簡化成這樣:

 
  1. int  array 128 ];  //個別存入128個ASCII字元的出現次數
  2. char  solution MAX ];
  3.  
  4. void  permutation int  ,  int  )
  5. {
  6.     if  (  ==  )
  7.     {
  8.         for  ( int  ;  ;  ++)
  9.             cout  <<  solution ];
  10.         cout  <<  endl ;
  11.         return ;
  12.     }
  13.     
  14.     for  ( int  ;  128 ;  ++)    //列舉每一個字母
  15.         if  ( array ] >  )        //還有字母剩下來,就要列舉
  16.         {
  17.             array ]--;          //用掉了一個字母
  18.             
  19.             solution ] =  ;     // char變數可以直接存入ascii數值
  20.             permutation ,  );
  21.  
  22.             array ]++;          //回收了一個字母
  23.         }
  24. }

這裏枚舉一些permutation的題目。

UVa 195 441 10098 10063 10776

Next Permutation

Next Permutation

問題:給一個由英文字母組成的字串。現在以這個字串當中的所有字母,依照字典順序列出所有排列,請找出這個字串所在位置的下一個字串是什麼?

有一個很簡單的方法。我們先製作字母卡,一張卡上有一個英文字母。然後用這些字母卡排出字串。要找出下一個排列,依照人類本能,會先將字串最右邊的字母卡,先拿一些起來,看看能不能利用手上的字母卡,重新拼成下一個字串;若是不行的話,就再多拿一點字母卡起來,看看能不能拼成下一個字串。這是很直觀的想法。詳細的辦法就不多說了。【待補程式碼】

若你想出瞭解題的演算法,可以繼續往下看。這裏提供一個不錯的資料結構:令一個 int 陣列 array[] 的第格所存的值,是ASCII 'a'+x 這個字母於字串中出現的個數。用這個資料結構來紀錄手上的字母卡有哪些,是最好不過的了,只要加加減減就可以了!打個簡單的比喻,若是題目給定的字串是aabbc,那麼將所有字母卡都拿在手上時, array[0] 就存入 2array[1] 就存入2array[2] 就存入1。當然,一開始的時候就將所有卡片排成aabbc,所以陣列裏面的值都是 0;隨着卡片越拿越多起來,陣列的值也就越加越多了。用這個資料結構寫起程式來會相當的方便!它可以省去排序的麻煩。

有些比較機車的題目,會提到說有些字母卡可以互相代替着用,例如p可以轉一下變成bw可以轉一下變成m之類的。這個時候就得小心的紀錄可用的字母卡張數了。有個可行的辦法是:若一張字母卡有多種用途,像是pb通用──當多了一張pb的字母卡可用時,那麼就在 array['p'-'a' ]  array['b'-'a'] 的地方同時加一;當少了一張pb的字母卡可用時,那麼就在 array['p'-'a'] array['b '-'a'] 的地方同時減一。仔細想想看爲什麼可行吧!這方法很不錯吧? :p

程式碼就留給大家自行創造吧!這裏是題目。

UVa 146 845

Enumerate all subsets

Enumerate all subsets

列舉子集合。這裏示範:列舉出{0,1,2,3,4}的所有子集合。

該如何列舉呢?先觀察平時我們計算子集合總數的方法。{0,1,2,3,4}所有子集合的個數共有2^5個:0可取可不取,有兩種情形、1可取可不取,有兩種情形、...4可取可不取,有兩種情形。根據乘法原理,總共會有2*2*2*2*2 = 2^5種情形。

backtracking列舉數據的概念等同於乘法原理。首先我們要先建立一個陣列,用來當作是一個集合。

 
  1. bool  solution 10 ];

其中solution[i] = true時表示這個集合擁有第i個元素(此概念等同於本站文件「Set: 另一種資料結構」)。陣列中不同的格子,就是solution vector當中不同的維度。

遞迴程式碼設計成這樣:

 
  1. bool  solution ];    //用來存放一組可能的數據
  2.  
  3. void  print_solution ()    //印出一組可能的數據
  4. {
  5.     for  ( int  ;  ;  ++)
  6.         if  ( solution ])
  7.             cout  <<   <<  ' ' ;
  8.     cout  <<  endl ;
  9. }
  10.  
  11. void  backtrack int  )    // n爲現在正在列舉的數值(也是維度)
  12. {
  13.     // it's a solution
  14.     if  (  ==  )
  15.     {
  16.         print_solution ();
  17.         return ;
  18.     }
  19.     
  20.     // 取數字n,然後繼續列舉之後的位置
  21.     solution ] =  true ;
  22.     backtrack );
  23.  
  24.     // 不取數字n,然後繼續列舉之後的位置
  25.     solution ] =  false ;
  26.     backtrack );
  27. }
  28.  
  29. int  main ()
  30. {
  31.     backtrack );
  32. }

輸出結果會照字典順序排列。附送一張簡圖:

另一種資料結構

這裏改用int陣列來當作set的資料結構(本站文件「Set: 簡單的資料結構」)。儘管solution vector已面目全非、消滅殆盡,但是該遞迴程式碼仍具有backtracking的精神。

 
  1. int  subset ];   //用來存放一組可能的答案
  2.  
  3. void  backtrack int  ,  int  )     // n是現在正在列舉的數值(也是維度)
  4. {                                // N用來記錄子集合的元素個數
  5.     // it's a solution
  6.     if  (  ==  )
  7.     {
  8.         // print solution
  9.         // 集合裏面有N個數字
  10.         for  ( int  ;   <  ;  ++)
  11.             cout  <<  set ] <<  " " ;
  12.         cout  <<  endl ;
  13.         return ;
  14.     }
  15.  
  16.     // 加入n 這個數字,然後繼續列舉後面的數字
  17.     subset ] =  ;
  18.     backtrack ,  );
  19.  
  20.     // 不取n 這個數字,然後繼續列舉後面的數字
  21.     backtrack ,  );
  22. }
  23.  
  24. int  main ()
  25. {
  26.     backtrack ,  );
  27. }

任意集合的所有子集合

 
  1. int  array ] = { ,  ,  13 ,  ,  };     //可自行調整列舉順序
  2. int  subset ];   //用來存放一組可能的數據
  3.  
  4. void  backtrack int  ,  int  )     // n是現在正在列舉的維度
  5. {                                // N用來記錄子集合的元素個數
  6.     // it's a solution
  7.     if  (  ==  )
  8.     {
  9.         print_solution ();
  10.         return ;
  11.     }
  12.  
  13.     // 加入array[n] 這個數字,然後繼續列舉後面的數字
  14.     subset ] =  array ];
  15.     backtrack ,  );
  16.  
  17.     // 不取array[n] 這個數字,然後繼續列舉後面的數字
  18.     backtrack ,  );
  19. }
  20.  
  21. int  main ()
  22. {
  23.     backtrack ,  );
  24. }

另一種窮舉法

這個方法並非backtracking,但也是一種很有特色的窮舉方式。請比照程式碼和附圖,自行揣摩一下。

 
  1. int  array ] = { ,  ,  13 ,  ,  };     //可自行調整列舉順序
  2. int  subset ];   //用來存放一組可能的數據
  3.  
  4. void  recursion int  ,  int  )     // n是現在正在列舉的數值
  5. {                                // N用來記錄子集合的元素個數
  6.     print_solution ();    //目前湊出來的集合
  7.  
  8.     for  ( int  ;  ; ++ )
  9.     {
  10.         // 加入 array[i] 這個數字
  11.         subset ] =  array ];
  12.  
  13.         // 然後繼續列舉後面的數字
  14.         recursion ,  );
  15.     }
  16. }
  17.  
  18. int  main ()
  19. {
  20.     recursion ,  );
  21. }

將陣列先排序好,輸出結果就會照字典順序排列。簡圖:

8 Queen Problem

8 Queen Problem

問題:在8x8的西洋棋棋盤上擺放八隻皇后,讓他們恰好無法互相攻擊對方。

一個非常簡單的想法:每一格都有「放」和「不放」兩種選擇,窮舉所有可能,並避免列舉出皇后互相攻擊的情形。設計solution vector8x8bool陣列,代表一個8x8的棋盤盤面情形。例如solution[0][0] = true表示(0,0)這個位置有放置皇后。

 
  1. bool  solution ][ ];
  2.  
  3. void  backtrack int  ,  int  )
  4. {
  5.     if  (  ==  )  ++,   =  ;  //換到下一排格子
  6.     
  7.     // it's a solution
  8.     if  (  ==  )
  9.     {
  10.         print_solution ();
  11.         return ;
  12.     }
  13.     
  14.     // 放置皇后
  15.     solution ][ ] =  true ;
  16.     backtrack ,  );
  17.     
  18.     // 不放置皇后
  19.     solution ][ ] =  false ;
  20.     backtrack ,  );
  21. }

接着要避免列舉出不可能出現的答案:任一直線、橫線、左右斜線上面只能有一隻皇后。分別建立四個bool陣列,紀錄皇后在各條線上擺放的情形,這個手法很常見,請見程式碼。

 
  1. bool  solution ][ ];
  2. bool  mx ],  my ],  md1 15 ],  md2 15 ];     //初始值都是false
  3.  
  4. void  backtrack int  ,  int  )
  5. {
  6.     if  (  ==  )  ++,   =  ;  //換到下一排格子
  7.     
  8.     // it's a solution
  9.     if  (  ==  )
  10.     {
  11.         print_solution ();
  12.         return ;
  13.     }
  14.     
  15.     // 放置皇后
  16.     int  d1  = ( ) %  15 ,  d2  = ( 15 ) %  15;
  17.     
  18.     if  (! mx ] && ! ​​my ] && ! ​​md1 d1 ] && ! ​​md2 [d2 ])
  19.     {
  20.         // 這隻皇后佔據了四條線,記得標記起來。
  21.         mx ] =  my ] =  md1 d1 ] =  md2 d2 ] = true ;
  22.         
  23.         solution ][ ] =  true ;
  24.         backtrack ,  );
  25.  
  26.         // 遞迴結束,回覆到原本的樣子,要記得取消標記。
  27.         mx ] =  my ] =  md1 d1 ] =  md2 d2 ] = false ;
  28.     }
  29.     
  30.     // 不放置皇后
  31.     solution ][ ] =  false ;
  32.     backtrack ,  );
  33. }

改進

由於一條線必須剛好擺放一隻皇后,故可以以線爲單位來遞迴窮舉。重新設計solution vector爲一條一維int陣列,solution[0] = 5表示第零個直行上的皇后,擺在第五個位置。

 
  1. int  solution ];
  2.  
  3. void  backtrack int  )  //每次都換一排格子
  4. {
  5.     // it's a solution
  6.     if  (  ==  )
  7.     {
  8.         print_solution ();
  9.         return ;
  10.     }
  11.     
  12.     // 分別放置皇后在每一格,並各自遞迴下去。
  13.     solution ] =  ;
  14.     backtrack );
  15.     
  16.     solution ] =  ;
  17.     backtrack );
  18.     
  19.     ......
  20.     
  21.     solution ] =  ;
  22.     backtrack );
  23. }

縮成迴圈是一定要的啦!

 
  1. int  solution ];
  2.  
  3. void  backtrack int  )    //每次都換一排格子
  4. {
  5.     // it's a solution
  6.     if  (  ==  )
  7.     {
  8.         print_solution ();
  9.         return ;
  10.     }
  11.     
  12.     // 分別放置皇后在每一格,並各自遞迴下去。
  13.     for  ( int  ;  ; ++ )
  14.     {
  15.         solution ] =  ;
  16.         backtrack );
  17.     }
  18. }

接着要避免列舉出不可能出現的答案。

 
  1. int  solution ];
  2. bool  my ],  md1 15 ],  md2 15 ];    //初始值都是false
  3.                                 // x這條線可以不用檢查了
  4.  
  5. void  backtrack int  )    //每次都換一排格子
  6. {
  7.     // it's a solution
  8.     if  (  ==  )
  9.     {
  10.         print_solution ();
  11.         return ;
  12.     }
  13.     
  14.     // 分別放置皇后在每一格,並各自遞迴下去。
  15.     for  ( int  ;  ; ++ )
  16.     {
  17.         int  d1  = ( ) %  15 ,  d2  = ( 15 ) % 15 ;
  18.         
  19.         if  (! my ] && ! ​​md1 d1 ] && ! ​​md2 d2 ])
  20.         {
  21.             // 這隻皇后佔據了四條線,記得標記起來。
  22.             my ] =  md1 d1 ] =  md2 d2 ] =  true ;
  23.             
  24.             solution ] =  ;
  25.             backtrack );
  26.             
  27.             // 遞迴結束,回覆到原本的樣子,要記得取消標記。
  28.             my ] =  md1 d1 ] =  md2 d2 ] =  false ;
  29.         }
  30.     }
  31. }

改進

8 Queen Problem的答案是上下、左右、對角線對稱的。排除對稱的情形,可以節省列舉的時間。這裏不加贅述。

另一種左右斜線判斷方式

比用陣列紀錄還麻煩。自行斟酌。

 
  1. void  backtrack int  )    //每次都換一排格子
  2. {
  3.     for  ( int  ;  ; ++ )
  4.         if  ( abs  -  ) ==  abs solution ] - solution ]))
  5.             return ;
  6.  
  7.     ......
  8. }

這裏是練習題。

UVa 167 750 10513 639

Sudoku

數獨

解決方法和8 Queen Problem十分相似。設計solution vector爲二維的int陣列,solution[0][0] = 2表示(0,0)的位置填了數字2

 
  1. int  solution ][ ];
  2.  
  3. void  backtrack int  ,  int  )
  4. {
  5.     if  (  ==  )  ++,   =  ;  //換到下一排格子
  6.     
  7.     // it's a solution
  8.     if  (  ==  )
  9.     {
  10.         print_solution ();
  11.         return ;
  12.     }
  13.  
  14.     // 分別填入一到九的數字,並各自遞迴下去。
  15.     solution ][ ] =  ;
  16.     backtrack ,  );
  17.     
  18.     solution ][ ] =  ;
  19.     backtrack ,  );
  20.     
  21.     ......
  22.     
  23.     solution ][ ] =  ;
  24.     backtrack ,  );
  25. }

縮成迴圈是一定要的啦!

 
  1. int  solution ][ ];
  2.  
  3. void  backtrack int  ,  int  )
  4. {
  5.     if  (  ==  )  ++,   =  ;  //換到下一排格子
  6.     
  7.     // it's a solution
  8.     if  (  ==  )
  9.     {
  10.         print_solution ();
  11.         return ;
  12.     }
  13.     
  14.     // 分別填入一到九的數字,並各自遞迴下去。
  15.     for  ( int  ;  <= ; ++ )
  16.     {
  17.         solution ][ ] =  ;
  18.         backtrack ,  );
  19.     }
  20. }

接着要避免列舉出不可能出現的答案:直線、橫線、3x3方格內不能有重複的數字。分別建立三個bool陣列,紀錄數字在各地方使用的情形,這個手法很常見,請見程式碼。

 
  1. int  solution ][ ];
  2. bool  mx ][ 10 ],  my ][ 10 ],  mg ][ ][ 10];     //初始值爲false
  3.  
  4. void  backtrack int  ,  int  )
  5. {
  6.     if  (  ==  )  ++,   =  ;  //換到下一排格子
  7.     
  8.     // it's a solution
  9.     if  (  ==  )
  10.     {
  11.         print_solution ();
  12.         return ;
  13.     }
  14.     
  15.     // 分別填入一到九的數字,並各自遞迴下去。
  16.     for  ( int  ;  <= ; ++ )
  17.         if  (! mx ][ ] && ! ​​my ][ ] && ! ​​mg /][ ][ ])
  18.         {
  19.             mx ][ ] =  my ][ ] =  mg ][ y][ ] =  true ;
  20.  
  21.             solution ][ ] =  ;
  22.             backtrack ,  );
  23.             
  24.             mx ][ ] =  my ][ ] =  mg ][ y][ ] =  false ;
  25.         }
  26. }

再加上原本格子裏就有數字的判斷。

 
  1. int  board ][ ];     //沒有值時爲0
  2.  
  3. int  solution ][ ];
  4. bool  mx ][ 10 ],  my ][ 10 ],  mg ][ ][ 10];     //初始值爲false
  5.  
  6. void  initialize ()
  7. {
  8.     for  ( int  ;  ; ++ )
  9.         for  ( int  ;  ; ++ )
  10.             if  ( board ][ ])
  11.             {
  12.                 int   =  board ][ ];
  13.                 mx ][ ] =  my ][ ] =  mg 3][ ][ ] =  true ;
  14.                 solution ][ ] =  board ][ ];
  15.             }
  16. }
  17.  
  18. void  backtrack int  ,  int  )
  19. {
  20.     if  (  ==  )  ++,   =  ;  //換到下一排格子
  21.     
  22.     // it's a solution
  23.     if  (  ==  )
  24.     {
  25.         print_solution ();
  26.         return ;
  27.     }
  28.     
  29.     // 判斷格子裏有沒有先填入值
  30.     if  ( board ][ ])
  31.     {
  32.         // solution vector和bool陣列已經在initialize()填寫過了
  33.         backtrack ,  );
  34.         return ;
  35.     }
  36.     
  37.     // 分別填入一到九的數字,並各自遞迴下去。
  38.     for  ( int  ;  <= ; ++ )
  39.         if  (! mx ][ ] && ! ​​my ][ ] && ! ​​mg /][ ][ ])
  40.         {
  41.             mx ][ ] =  my ][ ] =  mg ][ y][ ] =  true ;
  42.  
  43.             solution ][ ] =  ;
  44.             backtrack ,  );
  45.             
  46.             mx ][ ] =  my ][ ] =  mg ][ y][ ] =  false ;
  47.         }
  48. }

這裏是練習題。

UVa 989 10893 10957

0/1 Knapsack Problem

0/1揹包問題

問題:將一羣各式各樣的物品儘量塞進揹包裏,令揹包裏物品總價值最高。

這個問題當數值範圍不大時,可用Dynamic Programming快速的解決掉。可以參考上面幾篇文章。

一個簡單的想法:每個物品都有「要」和「不要」兩種選擇,窮舉所有可能,並避免列舉出揹包超載的情形。設計solution vector爲一個一維bool陣列,solution[0] = true表示第零個物品有放進揹包,即是set的概念(本站文件「Set: 另一種資料結構」)。

 
  1. bool  solution 10 ];   //十個物品
  2.  
  3. int  weight 10 ] = { ,  54 ,  , ...,  32 };    //十個物品分別的重量
  4. int  cost 10 ] = { ,  ,  11 , ...,  23 };      //十個物品分別的價值
  5.  
  6. const  int  maxW  =  100 ;    //揹包承載上限
  7. int  maxC  =  ;            //出現過的最高總值
  8.  
  9. void  backtrack int  ,  int  ,  int  )
  10. {
  11.     // it's a solution
  12.     if  (  ==  10 )
  13.     {
  14.         if  (  >  maxC )    //紀錄總值 ​​最高的
  15.         {
  16.             maxC  =  ;
  17.             store_solution ();
  18.         }
  19.         return ;
  20.     }
  21.  
  22.     // 放進揹包
  23.     if  (  +  weight ] <  maxW )    //檢查揹包超載
  24.     {
  25.         solution ] =  true ;
  26.         backtrack ,   +  weight ],   +  cost]);
  27.     }
  28.  
  29.     // 不放進揹包
  30.     solution ] =  false ;
  31.     backtrack ,  ,  );
  32. }
  33.  
  34.  
  35. bool  answer 10 ];     //正確答案
  36.  
  37. void  store_solution ()
  38. {
  39.     for  ( int  ;  10 ; ++ )
  40.         answer ] =  solution ];
  41. }

檢查揹包超載的部分可以修改成更美觀的樣子。

 
  1. void  backtrack int  ,  int  ,  int  )
  2. {
  3.     if  (  >  maxW )  return ;    //揹包超載
  4.  
  5.     // it's a solution
  6.     if  (  ==  10 )
  7.     {
  8.         if  (  >  maxC )    //紀錄總值 ​​最高的
  9.         {
  10.             maxC  =  ;
  11.             store_solution ();
  12.         }
  13.         return ;
  14.     }
  15.  
  16.     // 放進揹包
  17.     solution ] =  true ;
  18.     backtrack ,   +  weight ],   +  cost n]);
  19.  
  20.     // 不放進揹包
  21.     solution ] =  false ;
  22.     backtrack ,  ,  );
  23. }

Pruning

各位可嘗試將物品重量排序,再執行backtracking程式碼,看看效率有何不同。

Inclusion-Exclusion Principle

排容原理

類似於列舉所有子集合(本站文件「Backtracking ─Enumerate All Subsets」),但是每個子集合有正負號之別──奇數個集合的交集爲正號、偶數個集合的交集爲負號。

舉例:求出1100當中可被358整除的整數,且除數均兩兩互質。

 
  1. int  array ] = { ,  ,  };
  2.  
  3. // 排容,weight爲正負號,divisor爲各種可能的除數
  4. int  backtrack int  ,  int  weight ,  int  divisor )
  5. {
  6.     // it's a solution
  7.     if  (  ==  )  return  weight  * ( 100  /  divisor );
  8.     
  9.     int  value  =  ;
  10.     
  11.     /* 不選。正負號維持不變,除數維持不變。*/
  12.     
  13.     // solution[n] = false;
  14.     value  +=  backtrack ,  weight ,  divisor );
  15.  
  16.     /* 選。須變號,並逐步累計除數 */
  17.     
  18.     // solution[n] = true;
  19.     // 因逐步累計除數,故不需要具體的solution vector記錄選到的數字
  20.     value  +=  backtrack , - weight ,  divisor *array ]);
  21.     
  22.     return  value ;
  23. }
  24.  
  25. int  main ()
  26. {
  27.     cout  <<  "answer: "  <<  backtrack , + ,  ) << endl ;
  28.     return  ;
  29. }

考慮數字之間不互質的一般情形:

 
  1. int  array ] = { ,  ,  ,  ,  };
  2.  
  3. // 最大公因數
  4. int  gcd int  ,  int  ) {
  5.     return   ?  gcd ,  ) :  ;
  6. }
  7.  
  8. // 最小公倍數
  9. int  lcm int  ,  int  ) {
  10.     return   /  gcd ,  ) *  ;
  11. }
  12.  
  13. // 精簡過後的排容程式碼,w爲正負號,d爲各種可能的除數
  14. int  backtrack int  ,  int  ,  int  )
  15. {
  16.     if  (  ==  )  return   * ( 100  /  );
  17.     return  backtrack ,  ,  ) +  backtrack +, - ,  lcm array ]));
  18. }

另一種實作方法

列舉所有子集合有兩種窮舉方法,排容原理亦有兩種對應的實作方法。此方法並非backtracking,故不贅述。

 
  1. int  array ] = { ,  ,  ,  ,  };
  2.  
  3. int  recursion int  ,  int  )  // d爲各種可能的除數
  4. {
  5.     int  value  =  ;
  6.     value  +=  100  /  ;    //目前湊出來的集合
  7.     
  8.     // 繼續列舉之後的數字,記得變號
  9.     for  ( int  ;  ; ++ )
  10.     {
  11.         int  next_divisor  =  lcm ,  array ]);
  12.         value  -=  recursion ,  next_divisor );
  13.     }
  14.  
  15.     return  value ;
  16. }
  17.  
  18. int  main ()
  19. {
  20.     cout  <<  "answer: "  <<  recursion ,  ) <<  endl ;
  21.     return  ;
  22. }

UVa 10325

發佈了160 篇原創文章 · 獲贊 10 · 訪問量 16萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章