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

你可能感兴趣的:(刷题找工作,很好要多看掌握的)