Backtracking
backtracking
中文称做「回溯法」,穷举多维度数据的方法,可以想作是多维度的Exhaustive Search。
大意是:把多维度数据看做是是一个多维向量(solution vector),然后运用递回依序递回穷举各个维度的值,制作出所有可能的数据(solution space),并且在递回途中避免列举出不正确的数据。
- backtrack ( [ v1 ,..., vn ] ) // [v1,...,vn]是多维度的向量
- {
- /* 制作出了一组数据,并检验这组数据正不正确*/
- if ( [ v1 ,..., vn ] is well - generated )
- {
- if ( [ v1 ,..., vn ] is a solution ) process solution ;
- return ;
- }
- /* 穷举这个维度的所有值,并递回到下一个维度*/
- for ( x = possible values of vn + 1 )
- backtrack ( [ v1 ,..., vn , x ] );
- }
- call backtrack ( [] ); //从第一个维度开始逐步穷举
撰写程式时,可用阵列来实作solution vector的概念。
- int solution [ MAX_DIMENSION ]; // solution vector,多维度的向量
- void backtrack ( int dimension )
- {
- /* 制作出了一组数据,并检验这组数据正不正确*/
- if ( solution [] is well - generated )
- {
- check and record solution ;
- return ;
- }
- /* 穷举这个维度的所有值,并递回到下一个维度*/
- for ( x = each value of current dimension )
- {
- solution [ dimension ] = x ;
- backtrack ( dimension + 1 );
- }
- }
- int main ()
- {
- backtrack ( 0 ); //从第一个维度开始逐步列举
- }
另外,当我们所需的数据只有唯一一组时,可以让程式提早结束。
- int solution [ MAX_DIMENSION ];
- bool finished = false ; //如果为true表示已经找到数据,可以结束。
- void backtrack ( int dimension )
- {
- if ( solution [] is well - generated )
- {
- check and record solution ;
- if ( solution is found ) finished = true ; //找到数据了
- return ;
- }
- for ( x = each value of current dimension )
- {
- solution [ dimension ] = x ;
- backtrack ( dimension + 1 );
- if ( finished ) return ; //提早结束,跳出这个递回
- }
- }
附赠一张图片。画了很久。
![](http://acm.nudt.edu.cn/~twcourse/Backtracking.png)
结合pruning
回溯法会在递回途中避免列举出不正确的数据,其意义其实就等同于搜寻树的pruning技术。
- int solution [ MAX_DIMENSION ];
- void backtrack ( int dimension )
- {
- /* pruning:在递回途中避免列举出不正确的数据*/
- if ( solution [] will NOT be a solution in the future ) return ;
- if ( solution [] is well - generated )
- {
- check and record solution ;
- return ;
- }
- for ( x = each value of current dimension )
- {
- solution [ dimension ] = x ;
- backtrack ( dimension + 1 );
- }
- }
结合branch and bound
回溯法可以结合branching。
- int solution [ MAX_DIMENSION ];
- void backtrack ( int dimension )
- {
- if ( solution [] is well - generated )
- {
- check and record solution ;
- return ;
- }
- /* branch:制做适当的分支 */
- int c [ MAX_CANDIDATE ]; // candidates for next dimension
- int ncandidate ; // candidate counter
- construct_candidates ( dimension , c , ncandidate );
- for ( int i = 0 ; i < ncandidate ; i ++)
- {
- solution [ dimension ] = c [ i ];
- backtrack ( dimension + 1 );
- }
- }
回溯法可以结合bounding。
- int solution [ MAX_DIMENSION ];
- void backtrack ( int dimension , int cost ) //用一数值代表数据好坏
- {
- /* bound:数据太糟了,不可能成为正确数据,不必递回下去。*/
- if ( cost is worse than best_cost ) return ;
- /* bound:数据够好了,可以成为正确数据,不必递回下去。*/
- if ( solution [] is well - generated )
- {
- check and record solution ;
- if ( solution is found ) best_cost = cost ; //纪录cost
- return ;
- }
- for ( x = each value of current dimension )
- {
- solution [ dimension ] = x ;
- backtrack ( dimension + 1 , cost + ( cost of x ) );
- }
- }
特色
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解决的题目。
Enumerate all n-tuples
Enumerate all n-tuples
列举重复排列。这里示范:列举出「数字1到10选择五次」全部可能的情形。
制作一个阵列,用来存放一组可能的排列(数据)。
- int solution [ 5 ];
例如solution[0] = 4表示第一个抓到的数字是4,solution[4] = 9表示第五个抓到的数字是9。阵列中不同的格子,就是solution vector当中不同的维度。
递回程式码设计成这样:
- int solution [ 5 ]; //用来存放一组可能的数据
- void print_solution () //印出一组可能的数据
- {
- for ( int i = 0 ; i < 5 ; i ++)
- cout << i << ' ' ;
- cout << endl ;
- }
- void backtrack ( int n ) // n为现在正在列举的维度
- {
- // it's a solution
- if ( n == 5 )
- {
- print_solution ();
- return ;
- }
- // 逐步列举数字1到10,并且各自递回下去,列举之后的维度
- solution [ n ] = 1 ;
- backtrack ( n + 1 );
- solution [ n ] = 2 ;
- backtrack ( n + 1 );
- ......
- solution [ n ] = 10 ;
- backtrack ( n + 1 );
- }
- int main ()
- {
- backtrack ( 0 );
- }
输出结果会照字典顺序排列。附送一张简图:
![](http://acm.nudt.edu.cn/~twcourse/EnumerateAllN-tuples.png)
Permutation
Permutation
permutation是「排列」的意思,便是数学课本中「排列组合」的排列。但是这里并不是要计算排列有多少种,而是实际列举出所有的排列:
现在有一个集合,里面有1到n的数字,列出所有数字的排列,同样的排列不能重复列出来。例如{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的程式码都会长成这样的格式:
- int solution [ MAX ]; //用来存放一组可能的答案
- bool used [ MAX ]; //纪录数字是否使用过,用过为true
- void permutation ( int k , int n )
- {
- if ( k == n ) // it's a solution
- {
- for ( int i = 0 ; i < n ; i ++)
- cout << solution [ i ] << " " ;
- cout << endl ;
- }
- else
- {
- for ( int i = 0 ; i < n ; i ++) //试着将第k格填入各种数字
- if (! used [ i ])
- {
- used [ i ] = true ; //纪录用过的数字
- solution [ k ] = i ; //将第k格填入数字k
- permutation ( k + 1 , n ); // iterate next position
- used [ i ] = false ; //回收用完的数字
- }
- }
- }
- int main ()
- {
- for ( int i = 0 ; i < MAX ; i ++) // initialization
- used [ i ] = false ;
- permutation ( 0 , 10 ); //印出0~9,一共10个数字的所有排列
- }
permutation的问题都可以使用这段程式码来解决。而且这支程式,是以字典顺序来列举出所有排列。所以它真的很有用,不妨参考看看。
permutation是一种简单又容易理解的问题。「Programming Challenges」这本书在教导backtracking的概念时,就用了permutation来当做入门的例子。如果有人想要教导backtracking的程式码要怎么撰写,以permutation当做范例会是个不错的选择。
依序穷举每个数字,针对每个数字,试着填入各个位置
另外还有一种作法是生做这个样子的:
- int solution [ MAX ]; //用来存放一组可能的答案
- bool filled [ MAX ]; //纪录各个位置是否填过数字,填过为true
- void permutation ( int v , int n )
- {
- if ( v == n ) // it's a solution
- {
- for ( int i = 0 ; i < n ; i ++)
- cout << solution [ i ] << " " ;
- cout << endl ;
- }
- else
- {
- for ( int i = 0 ; i < n ; i ++) //试着将数字v填入各个位置
- if (! filled [ i ])
- {
- filled [ i ] = true ; //纪录填过的位置
- solution [ i ] = v ; //将数字v填入第i格
- permutation ( v + 1 , n ); // iterate next position
- filled [ i ] = false ; //回收位置
- }
- }
- }
- int main ()
- {
- for ( int i = 0 ; i < MAX ; i ++) // initialization
- filled [ i ] = false ;
- permutation ( 0 , 10 ); //印出0~9,一共10个数字的所有排列
- }
这也是一个不错的方法,列出来提供大家参考。多接触各式各样的方法,能激发一些创意呢!
为了讲解方便,以下的文章以一开始提到的方法当作基准。
字串排列
有个常见的问题是:列出字串abc的所有排列,要依照字典顺序列出。其实这就跟刚才介绍的东西大同小异,只要稍加修改程式码即可。
- char s [ 3 ] = { 'a', 'b', 'c' }; //字串,需要先由小到大排序过
- char solution [ 3 ]; //用来存放一组可能的答案
- bool used [ 3 ]; //纪录该字母是否使用过,用过为true
- void permutation ( int k , int n )
- {
- if ( k == n ) // it's a solution
- {
- for ( int i = 0 ; i < n ; i ++)
- cout << solution [ i ];
- cout << endl ;
- }
- else
- {
- // 针对solution[i]这个位置,列举所有字母,并各自递回
- for ( int i = 0 ; i < n ; i ++)
- if (! used [ i ])
- {
- used [ i ] = true ;
- solution [ k ] = s [ i ]; //填入字母
- permutation ( k + 1 , n );
- used [ i ] = false ;
- }
- }
- }
程式码改写成这样会更清楚:
- char s [ 3 ] = { 'a', 'b', 'c' }; //字串,需要先由小到大排序过
- char solution [ 3 ]; //用来存放一组可能的答案
- bool used [ 3 ]; //纪录该字母是否使用过,用过为true
- void permutation ( int k , int n )
- {
- // it's a solution
- if ( k == n )
- {
- for ( int i = 0 ; i < n ; i ++)
- cout << solution [ i ];
- cout << endl ;
- return ; // if-else改成return
- }
- // 针对solution[i]这个位置,列举所有字母,并各自递回
- for ( int i = 0 ; i < n ; i ++)
- if (! used [ i ])
- {
- used [ i ] = true ;
- solution [ k ] = s [ i ]; //填入字母
- permutation ( k + 1 , n );
- used [ i ] = false ;
- }
- }
避免重复排列
若是字串排列的问题改成:列出abb的所有排列,依照字典顺序列出。答案应该为abb、aba、baa。不过使用刚刚的程式码的话,答案却会变成这样:
abb abb bab bba bab bba
这跟预期的不一样。会有这种结果,是由于之前的程式有个基本假设:字串中的每个字母都不一样。尽管出现了一样的字母,但是程式还是把它当作是不一样的字母,依旧把所有可能的排列都列出,也就是现在的结果──有一些排列重复出现了。
要解决问题,在列举某一个位置的字母时,就必须避免一直填入一样的字母。如此就可以避免产生重复的排列。
- char s [ 3 ] = { 'a', 'b', 'b' }; //字串,需要先由小到大排序过
- char solution [ 3 ];
- bool used [ 3 ];
- void permutation ( int k , int n )
- {
- if ( k == n )
- {
- for ( int i = 0 ; i < n ; i ++)
- cout << solution [ i ];
- cout << endl ;
- return ;
- }
- char last_letter = '\0' ;
- for ( int i = 0 ; i < n ; i ++)
- if (! used [ i ])
- if ( s [ i ] != last_letter ) //避免列举一样的字母
- {
- last_letter = s [ i ]; //纪录刚才使用过的字母
- used [ i ] = true ;
- solution [ k ] = s [ i ];
- permutation ( k + 1 , n );
- used [ i ] = false ;
- }
- }
因为输入的字串由小到大排序过,字母会依照顺序出现,所以只要检查上一个使用过的字母,判断一不一样之后,就可以避免列举一样的字母了。
程式码也可以改写成这种风格:
- char s [ 3 ] = { 'a', 'b', 'b' }; //字串,需要先由小到大排序过
- char solution [ 3 ];
- bool used [ 3 ];
- void permutation ( int k , int n )
- {
- if ( k == n )
- {
- for ( int i = 0 ; i < n ; i ++)
- cout << solution [ i ];
- cout << endl ;
- return ;
- }
- char last_letter = '\0' ;
- for ( int i = 0 ; i < n ; i ++)
- { // if not改成continue
- if ( used [ i ]) continue ;
- if ( s [ i ] == last_letter ) continue ; //避免列举一样的字母
- last_letter = s [ i ]; //纪录刚才使用过的字母
- used [ i ] = true ;
- solution [ k ] = s [ i ];
- permutation ( k + 1 , n );
- used [ i ] = false ;
- }
- }
另一种资料结构
如果字母重覆出现次数很多次的话,可以用一个128格的阵列,每一格个别存入128个ASCII字元的出现次数。程式码会简化成这样:
- int array [ 128 ]; //个别存入128个ASCII字元的出现次数
- char solution [ MAX ];
- void permutation ( int k , int n )
- {
- if ( k == n )
- {
- for ( int i = 0 ; i < n ; i ++)
- cout << solution [ i ];
- cout << endl ;
- return ;
- }
- for ( int i = 0 ; i < 128 ; i ++) //列举每一个字母
- if ( array [ i ] > 0 ) //还有字母剩下来,就要列举
- {
- array [ i ]--; //用掉了一个字母
- solution [ k ] = i ; // char变数可以直接存入ascii数值
- permutation ( k + 1 , n );
- array [ i ]++; //回收了一个字母
- }
- }
这里枚举一些permutation的题目。
Next Permutation
Next Permutation
问题:给一个由英文字母组成的字串。现在以这个字串当中的所有字母,依照字典顺序列出所有排列,请找出这个字串所在位置的下一个字串是什么?
有一个很简单的方法。我们先制作字母卡,一张卡上有一个英文字母。然后用这些字母卡排出字串。要找出下一个排列,依照人类本能,会先将字串最右边的字母卡,先拿一些起来,看看能不能利用手上的字母卡,重新拼成下一个字串;若是不行的话,就再多拿一点字母卡起来,看看能不能拼成下一个字串。这是很直观的想法。详细的办法就不多说了。【待补程式码】
若你想出了解题的演算法,可以继续往下看。这里提供一个不错的资料结构:令一个 int 阵列 array[] 的第x 格所存的值,是ASCII码 'a'+x 这个字母于字串中出现的个数。用这个资料结构来纪录手上的字母卡有哪些,是最好不过的了,只要加加减减就可以了!打个简单的比喻,若是题目给定的字串是aabbc,那么将所有字母卡都拿在手上时, array[0] 就存入 2、array[1] 就存入2、array[2] 就存入1。当然,一开始的时候就将所有卡片排成aabbc,所以阵列里面的值都是 0;随着卡片越拿越多起来,阵列的值也就越加越多了。用这个资料结构写起程式来会相当的方便!它可以省去排序的麻烦。
有些比较机车的题目,会提到说有些字母卡可以互相代替着用,例如p可以转一下变成b,w可以转一下变成m之类的。这个时候就得小心的纪录可用的字母卡张数了。有个可行的办法是:若一张字母卡有多种用途,像是p和b通用──当多了一张p或b的字母卡可用时,那么就在 array['p'-'a' ] 和 array['b'-'a'] 的地方同时加一;当少了一张p或b的字母卡可用时,那么就在 array['p'-'a'] 和array['b '-'a'] 的地方同时减一。仔细想想看为什么可行吧!这方法很不错吧? :p
程式码就留给大家自行创造吧!这里是题目。
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列举数据的概念等同于乘法原理。首先我们要先建立一个阵列,用来当作是一个集合。
- bool solution [ 10 ];
其中solution[i] = true时表示这个集合拥有第i个元素(此概念等同于本站文件「Set: 另一种资料结构」)。阵列中不同的格子,就是solution vector当中不同的维度。
递回程式码设计成这样:
- bool solution [ 5 ]; //用来存放一组可能的数据
- void print_solution () //印出一组可能的数据
- {
- for ( int i = 0 ; i < 5 ; i ++)
- if ( solution [ i ])
- cout << i << ' ' ;
- cout << endl ;
- }
- void backtrack ( int n ) // n为现在正在列举的数值(也是维度)
- {
- // it's a solution
- if ( n == 5 )
- {
- print_solution ();
- return ;
- }
- // 取数字n,然后继续列举之后的位置
- solution [ n ] = true ;
- backtrack ( n + 1 );
- // 不取数字n,然后继续列举之后的位置
- solution [ n ] = false ;
- backtrack ( n + 1 );
- }
- int main ()
- {
- backtrack ( 0 );
- }
输出结果会照字典顺序排列。附送一张简图:
![](http://acm.nudt.edu.cn/~twcourse/EnumerateAllSubsets1.png)
另一种资料结构
这里改用int阵列来当作set的资料结构(本站文件「Set: 简单的资料结构」)。尽管solution vector已面目全非、消灭殆尽,但是该递回程式码仍具有backtracking的精神。
- int subset [ 5 ]; //用来存放一组可能的答案
- void backtrack ( int n , int N ) // n是现在正在列举的数值(也是维度)
- { // N用来记录子集合的元素个数
- // it's a solution
- if ( n == 5 )
- {
- // print solution
- // 集合里面有N个数字
- for ( int i = 0 ; i < N ; i ++)
- cout << set [ i ] << " " ;
- cout << endl ;
- return ;
- }
- // 加入n 这个数字,然后继续列举后面的数字
- subset [ N ] = n ;
- backtrack ( n + 1 , N + 1 );
- // 不取n 这个数字,然后继续列举后面的数字
- backtrack ( n + 1 , N );
- }
- int main ()
- {
- backtrack ( 0 , 0 );
- }
任意集合的所有子集合
- int array [ 5 ] = { 6 , 7 , 13 , 4 , 2 }; //可自行调整列举顺序
- int subset [ 5 ]; //用来存放一组可能的数据
- void backtrack ( int n , int N ) // n是现在正在列举的维度
- { // N用来记录子集合的元素个数
- // it's a solution
- if ( n == 5 )
- {
- print_solution ();
- return ;
- }
- // 加入array[n] 这个数字,然后继续列举后面的数字
- subset [ N ] = array [ n ];
- backtrack ( n + 1 , N + 1 );
- // 不取array[n] 这个数字,然后继续列举后面的数字
- backtrack ( n + 1 , N );
- }
- int main ()
- {
- backtrack ( 0 , 0 );
- }
另一种穷举法
这个方法并非backtracking,但也是一种很有特色的穷举方式。请比照程式码和附图,自行揣摩一下。
- int array [ 5 ] = { 6 , 7 , 13 , 4 , 2 }; //可自行调整列举顺序
- int subset [ 5 ]; //用来存放一组可能的数据
- void recursion ( int n , int N ) // n是现在正在列举的数值
- { // N用来记录子集合的元素个数
- print_solution (); //目前凑出来的集合
- for ( int i = n ; i < 5 ; ++ i )
- {
- // 加入 array[i] 这个数字
- subset [ N ] = array [ i ];
- // 然后继续列举后面的数字
- recursion ( i + 1 , N + 1 );
- }
- }
- int main ()
- {
- recursion ( 0 , 0 );
- }
将阵列先排序好,输出结果就会照字典顺序排列。简图:
![](http://acm.nudt.edu.cn/~twcourse/EnumerateAllSubsets2.png)
8 Queen Problem
8 Queen Problem
问题:在8x8的西洋棋棋盘上摆放八只皇后,让他们恰好无法互相攻击对方。
一个非常简单的想法:每一格都有「放」和「不放」两种选择,穷举所有可能,并避免列举出皇后互相攻击的情形。设计solution vector为8x8的bool阵列,代表一个8x8的棋盘盘面情形。例如solution[0][0] = true表示(0,0)这个位置有放置皇后。
- bool solution [ 8 ][ 8 ];
- void backtrack ( int x , int y )
- {
- if ( y == 8 ) x ++, y = 0 ; //换到下一排格子
- // it's a solution
- if ( x == 8 )
- {
- print_solution ();
- return ;
- }
- // 放置皇后
- solution [ x ][ y ] = true ;
- backtrack ( x , y + 1 );
- // 不放置皇后
- solution [ x ][ y ] = false ;
- backtrack ( x , y + 1 );
- }
接着要避免列举出不可能出现的答案:任一直线、横线、左右斜线上面只能有一只皇后。分别建立四个bool阵列,纪录皇后在各条线上摆放的情形,这个手法很常见,请见程式码。
- bool solution [ 8 ][ 8 ];
- bool mx [ 8 ], my [ 8 ], md1 [ 15 ], md2 [ 15 ]; //初始值都是false
- void backtrack ( int x , int y )
- {
- if ( y == 8 ) x ++, y = 0 ; //换到下一排格子
- // it's a solution
- if ( x == 8 )
- {
- print_solution ();
- return ;
- }
- // 放置皇后
- int d1 = ( x + y ) % 15 , d2 = ( x - y + 15 ) % 15;
- if (! mx [ x ] && ! my [ y ] && ! md1 [ d1 ] && ! md2 [d2 ])
- {
- // 这只皇后占据了四条线,记得标记起来。
- mx [ x ] = my [ y ] = md1 [ d1 ] = md2 [ d2 ] = true ;
- solution [ x ][ y ] = true ;
- backtrack ( x , y + 1 );
- // 递回结束,回复到原本的样子,要记得取消标记。
- mx [ x ] = my [ y ] = md1 [ d1 ] = md2 [ d2 ] = false ;
- }
- // 不放置皇后
- solution [ x ][ y ] = false ;
- backtrack ( x , y + 1 );
- }
改进
由于一条线必须刚好摆放一只皇后,故可以以线为单位来递回穷举。重新设计solution vector为一条一维int阵列,solution[0] = 5表示第零个直行上的皇后,摆在第五个位置。
- int solution [ 8 ];
- void backtrack ( int x ) //每次都换一排格子
- {
- // it's a solution
- if ( x == 8 )
- {
- print_solution ();
- return ;
- }
- // 分别放置皇后在每一格,并各自递回下去。
- solution [ x ] = 0 ;
- backtrack ( x + 1 );
- solution [ x ] = 1 ;
- backtrack ( x + 1 );
- ......
- solution [ x ] = 7 ;
- backtrack ( x + 1 );
- }
缩成回圈是一定要的啦!
- int solution [ 8 ];
- void backtrack ( int x ) //每次都换一排格子
- {
- // it's a solution
- if ( x == 8 )
- {
- print_solution ();
- return ;
- }
- // 分别放置皇后在每一格,并各自递回下去。
- for ( int y = 0 ; y < 8 ; ++ y )
- {
- solution [ x ] = y ;
- backtrack ( x + 1 );
- }
- }
接着要避免列举出不可能出现的答案。
- int solution [ 8 ];
- bool my [ 8 ], md1 [ 15 ], md2 [ 15 ]; //初始值都是false
- // x这条线可以不用检查了
- void backtrack ( int x ) //每次都换一排格子
- {
- // it's a solution
- if ( x == 8 )
- {
- print_solution ();
- return ;
- }
- // 分别放置皇后在每一格,并各自递回下去。
- for ( int y = 0 ; y < 8 ; ++ y )
- {
- int d1 = ( x + y ) % 15 , d2 = ( x - y + 15 ) % 15 ;
- if (! my [ y ] && ! md1 [ d1 ] && ! md2 [ d2 ])
- {
- // 这只皇后占据了四条线,记得标记起来。
- my [ y ] = md1 [ d1 ] = md2 [ d2 ] = true ;
- solution [ x ] = y ;
- backtrack ( x + 1 );
- // 递回结束,回复到原本的样子,要记得取消标记。
- my [ y ] = md1 [ d1 ] = md2 [ d2 ] = false ;
- }
- }
- }
改进
8 Queen Problem的答案是上下、左右、对角线对称的。排除对称的情形,可以节省列举的时间。这里不加赘述。
另一种左右斜线判断方式
比用阵列纪录还麻烦。自行斟酌。
- void backtrack ( int x ) //每次都换一排格子
- {
- for ( int i = 0 ; i < x ; ++ i )
- if ( abs ( x - i ) == abs ( solution [ x ] - solution [ i ]))
- return ;
- ......
- }
这里是练习题。
Sudoku
数独
解决方法和8 Queen Problem十分相似。设计solution vector为二维的int阵列,solution[0][0] = 2表示(0,0)的位置填了数字2。
- int solution [ 9 ][ 9 ];
- void backtrack ( int x , int y )
- {
- if ( y == 9 ) x ++, y = 0 ; //换到下一排格子
- // it's a solution
- if ( x == 9 )
- {
- print_solution ();
- return ;
- }
- // 分别填入一到九的数字,并各自递回下去。
- solution [ x ][ y ] = 1 ;
- backtrack ( x , y + 1 );
- solution [ x ][ y ] = 2 ;
- backtrack ( x , y + 1 );
- ......
- solution [ x ][ y ] = 9 ;
- backtrack ( x , y + 1 );
- }
缩成回圈是一定要的啦!
- int solution [ 9 ][ 9 ];
- void backtrack ( int x , int y )
- {
- if ( y == 9 ) x ++, y = 0 ; //换到下一排格子
- // it's a solution
- if ( x == 9 )
- {
- print_solution ();
- return ;
- }
- // 分别填入一到九的数字,并各自递回下去。
- for ( int n = 1 ; n <= 9 ; ++ n )
- {
- solution [ x ][ y ] = n ;
- backtrack ( x , y + 1 );
- }
- }
接着要避免列举出不可能出现的答案:直线、横线、3x3方格内不能有重复的数字。分别建立三个bool阵列,纪录数字在各地方使用的情形,这个手法很常见,请见程式码。
- int solution [ 9 ][ 9 ];
- bool mx [ 9 ][ 10 ], my [ 9 ][ 10 ], mg [ 3 ][ 3 ][ 10]; //初始值为false
- void backtrack ( int x , int y )
- {
- if ( y == 9 ) x ++, y = 0 ; //换到下一排格子
- // it's a solution
- if ( x == 9 )
- {
- print_solution ();
- return ;
- }
- // 分别填入一到九的数字,并各自递回下去。
- for ( int n = 1 ; n <= 9 ; ++ n )
- if (! mx [ x ][ n ] && ! my [ y ][ n ] && ! mg [ x /3 ][ y / 3 ][ n ])
- {
- mx [ x ][ n ] = my [ y ][ n ] = mg [ x / 3 ][ y/ 3 ][ n ] = true ;
- solution [ x ][ y ] = n ;
- backtrack ( x , y + 1 );
- mx [ x ][ n ] = my [ y ][ n ] = mg [ x / 3 ][ y/ 3 ][ n ] = false ;
- }
- }
再加上原本格子里就有数字的判断。
- int board [ 9 ][ 9 ]; //没有值时为0
- int solution [ 9 ][ 9 ];
- bool mx [ 9 ][ 10 ], my [ 9 ][ 10 ], mg [ 3 ][ 3 ][ 10]; //初始值为false
- void initialize ()
- {
- for ( int x = 0 ; x < 9 ; ++ x )
- for ( int y = 0 ; y < 9 ; ++ y )
- if ( board [ x ][ y ])
- {
- int n = board [ x ][ y ];
- mx [ x ][ n ] = my [ y ][ n ] = mg [ x / 3][ y / 3 ][ n ] = true ;
- solution [ x ][ y ] = board [ x ][ y ];
- }
- }
- void backtrack ( int x , int y )
- {
- if ( y == 9 ) x ++, y = 0 ; //换到下一排格子
- // it's a solution
- if ( x == 9 )
- {
- print_solution ();
- return ;
- }
- // 判断格子里有没有先填入值
- if ( board [ x ][ y ])
- {
- // solution vector和bool阵列已经在initialize()填写过了
- backtrack ( x , y + 1 );
- return ;
- }
- // 分别填入一到九的数字,并各自递回下去。
- for ( int n = 1 ; n <= 9 ; ++ n )
- if (! mx [ x ][ n ] && ! my [ y ][ n ] && ! mg [ x /3 ][ y / 3 ][ n ])
- {
- mx [ x ][ n ] = my [ y ][ n ] = mg [ x / 3 ][ y/ 3 ][ n ] = true ;
- solution [ x ][ y ] = n ;
- backtrack ( x , y + 1 );
- mx [ x ][ n ] = my [ y ][ n ] = mg [ x / 3 ][ y/ 3 ][ n ] = false ;
- }
- }
这里是练习题。
0/1 Knapsack Problem
0/1背包问题
问题:将一群各式各样的物品尽量塞进背包里,令背包里物品总价值最高。
这个问题当数值范围不大时,可用Dynamic Programming快速的解决掉。可以参考上面几篇文章。
一个简单的想法:每个物品都有「要」和「不要」两种选择,穷举所有可能,并避免列举出背包超载的情形。设计solution vector为一个一维bool阵列,solution[0] = true表示第零个物品有放进背包,即是set的概念(本站文件「Set: 另一种资料结构」)。
- bool solution [ 10 ]; //十个物品
- int weight [ 10 ] = { 4 , 54 , 1 , ..., 32 }; //十个物品分别的重量
- int cost [ 10 ] = { 3 , 3 , 11 , ..., 23 }; //十个物品分别的价值
- const int maxW = 100 ; //背包承载上限
- int maxC = 0 ; //出现过的最高总值
- void backtrack ( int n , int w , int c )
- {
- // it's a solution
- if ( n == 10 )
- {
- if ( c > maxC ) //纪录总值 最高的
- {
- maxC = c ;
- store_solution ();
- }
- return ;
- }
- // 放进背包
- if ( w + weight [ n ] < maxW ) //检查背包超载
- {
- solution [ n ] = true ;
- backtrack ( n + 1 , w + weight [ n ], c + cost[ n ]);
- }
- // 不放进背包
- solution [ n ] = false ;
- backtrack ( n + 1 , w , c );
- }
- bool answer [ 10 ]; //正确答案
- void store_solution ()
- {
- for ( int i = 0 ; i < 10 ; ++ i )
- answer [ i ] = solution [ i ];
- }
检查背包超载的部分可以修改成更美观的样子。
- void backtrack ( int n , int w , int c )
- {
- if ( w > maxW ) return ; //背包超载
- // it's a solution
- if ( n == 10 )
- {
- if ( c > maxC ) //纪录总值 最高的
- {
- maxC = c ;
- store_solution ();
- }
- return ;
- }
- // 放进背包
- solution [ n ] = true ;
- backtrack ( n + 1 , w + weight [ n ], c + cost [ n]);
- // 不放进背包
- solution [ n ] = false ;
- backtrack ( n + 1 , w , c );
- }
Pruning
各位可尝试将物品重量排序,再执行backtracking程式码,看看效率有何不同。
Inclusion-Exclusion Principle
排容原理
![](http://acm.nudt.edu.cn/~twcourse/Inclusion-ExclusionPrinciple1.png)
类似于列举所有子集合(本站文件「Backtracking ─Enumerate All Subsets」),但是每个子集合有正负号之别──奇数个集合的交集为正号、偶数个集合的交集为负号。
举例:求出1到100当中可被3或5或8整除的整数,且除数均两两互质。
![](http://acm.nudt.edu.cn/~twcourse/Inclusion-ExclusionPrinciple2.png)
- int array [ 3 ] = { 3 , 5 , 8 };
- // 排容,weight为正负号,divisor为各种可能的除数
- int backtrack ( int n , int weight , int divisor )
- {
- // it's a solution
- if ( n == 3 ) return weight * ( 100 / divisor );
- int value = 0 ;
- /* 不选。正负号维持不变,除数维持不变。*/
- // solution[n] = false;
- value += backtrack ( n + 1 , weight , divisor );
- /* 选。须变号,并逐步累计除数 */
- // solution[n] = true;
- // 因逐步累计除数,故不需要具体的solution vector记录选到的数字
- value += backtrack ( n + 1 , - weight , divisor *array [ n ]);
- return value ;
- }
- int main ()
- {
- cout << "answer: " << backtrack ( 0 , + 1 , 1 ) << endl ;
- return 0 ;
- }
考虑数字之间不互质的一般情形:
- int array [ 5 ] = { 3 , 5 , 6 , 7 , 9 };
- // 最大公因数
- int gcd ( int a , int b ) {
- return b ? gcd ( b , a % b ) : a ;
- }
- // 最小公倍数
- int lcm ( int a , int b ) {
- return a / gcd ( a , b ) * b ;
- }
- // 精简过后的排容程式码,w为正负号,d为各种可能的除数
- int backtrack ( int n , int w , int d )
- {
- if ( n == 5 ) return w * ( 100 / d );
- return backtrack ( n + 1 , w , d ) + backtrack ( n +1 , - w , lcm ( d , array [ n ]));
- }
另一种实作方法
列举所有子集合有两种穷举方法,排容原理亦有两种对应的实作方法。此方法并非backtracking,故不赘述。
- int array [ 5 ] = { 3 , 5 , 6 , 7 , 9 };
- int recursion ( int n , int d ) // d为各种可能的除数
- {
- int value = 0 ;
- value += 100 / d ; //目前凑出来的集合
- // 继续列举之后的数字,记得变号
- for ( int i = n ; i < 5 ; ++ i )
- {
- int next_divisor = lcm ( d , array [ i ]);
- value -= recursion ( i + 1 , next_divisor );
- }
- return value ;
- }
- int main ()
- {
- cout << "answer: " << recursion ( 0 , 1 ) << endl ;
- return 0 ;
- }
UVa 10325