原文地址:https://blog.csdn.net/sinat_27908213/article/details/80599460
回溯法是很重要的一种算法,在it企业笔试中经常会遇到。事实上,在各种编程题中,大家或多或少都会接触到这些题目,但是很多人没有对这类题目有个系统性的总结。接下来就对回溯法进行详尽,通俗易懂的分析。
回溯法有通用解法的美称,对于很多问题,如迷宫等都有很好的效果。回溯算法实际上一个类似枚举的深度优先搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回(也就是递归返回),尝试别的路径。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。回溯法说白了就是穷举法。回溯法一般用递归来解决。
回溯法一般都用在要给出多个可以实现最终条件的解的最终形式。回溯法要求对解要添加一些约束条件。总的来说,如果要解决一个回溯法的问题,通常要确定三个元素:
1、选择。对于每个特定的解,肯定是由一步步构建而来的,而每一步怎么构建,肯定都是有限个选择,要怎么选择,这个要知道;同时,在编程时候要定下,优先或合法的每一步选择的顺序,一般是通过多个if或者for循环来排列。
2、条件。对于每个特定的解的某一步,他必然要符合某个解要求符合的条件,如果不符合条件,就要回溯,其实回溯也就是递归调用的返回。
3、结束。当到达一个特定结束条件时候,就认为这个一步步构建的解是符合要求的解了。把解存下来或者打印出来。对于这一步来说,有时候也可以另外写一个issolution函数来进行判断。注意,当到达第三步后,有时候还需要构建一个数据结构,把符合要求的解存起来,便于当得到所有解后,把解空间输出来。这个数据结构必须是全局的,作为参数之一传递给递归函数。
对于回溯法来说,每次递归调用,很重要的一点是把每次递归的不同信息传递给递归调用的函数。而这里最重要的要传递给递归调用函数的信息,就是把上一步做过的某些事情的这个选择排除,避免重复和无限递归。另外还有一个信息必须传递给递归函数,就是进行了每一步选择后,暂时还没构成完整的解,这个时候前面所有选择的汇总也要传递进去。而且一般情况下,都是能从传递给递归函数的参数处,得到结束条件的。
递归函数的参数的选择,要遵循四个原则:
1、必须要有一个临时变量(可以就直接传递一个字面量或者常量进去)传递不完整的解,因为每一步选择后,暂时还没构成完整的解,这个时候这个选择的不完整解,也要想办法传递给递归函数。也就是,把每次递归的不同情况传递给递归调用的函数。
2、可以有一个全局变量,用来存储完整的每个解,一般是个集合容器(也不一定要有这样一个变量,因为每次符合结束条件,不完整解就是完整解了,直接打印即可)。
3、最重要的一点,一定要在参数设计中,可以得到结束条件。一个选择是可以传递一个量n,也许是数组的长度,也许是数量,等等。
4、要保证递归函数返回后,状态可以恢复到递归前,以此达到真正回溯。
结合几个例题来分析。
一、给出n对括号,求括号排列的所有可能性。
这是一个很经典的回溯法问题,代码如下,结合代码,对上面总结出的特点进行分析。
- public class BackTracking {
- public static void main(String[] args) {
- int n= 3;
- int leftnum=n,rightnum=n; //左括号和右括号都各有n个
- ArrayList<String> results= new ArrayList<String>(); //用于存放解空间
- parentheses( "", results, leftnum, rightnum);
- for(String s:results)
- System.out.println(s);
- }
- public static void parentheses(String sublist, ArrayList<String> results, int leftnum, int rightnum){
- if(leftnum== 0&&rightnum== 0) //结束
- results.add(sublist);
- if(rightnum>leftnum) //选择和条件。对于不同的if顺序,输出的结果顺序是不一样的,但是构成一样的解空间
- parentheses(sublist+ ")", results, leftnum, rightnum- 1);
- if(leftnum> 0)
- parentheses(sublist+ "(", results, leftnum- 1, rightnum);
- }
- }
输出如下:
()()()
()(())
(())()
(()())
((()))
对于回溯法来说,必须齐备的三要素:
1、选择。在这个例子中,解就是一个合法的括号组合形式,而选择无非是放入左括号,还是放入右括号;
2、条件。在这个例子中,选择是放入左括号,还是放入右括号,是有条件约束的,不是随便放的。而这个约束就是括号的数量。只有剩下的右括号比左括号多,才能放右括号。只有左括号数量大于0才能放入左括号。这里if的顺序会影响输出的顺序,但是不影响最终解;
3、结束。这里的结束条件很显然就是,左右括号都放完了。
回溯法中,参数的设计是一大难点,也是很重要的地方。而递归参数的设计要注意的四个点:
1、用了一个空字符串来作为临时变量存储不完整解;
2、用了一个ArrayList<String> results来存放符合要求的解。在后面可以看到,不一定要这样做,也可以直接打印结果;
3、把leftnum和rightnum传入给递归函数,这样可以用于判断结束条件;
4、这个例子不明显。但是事实上也符合这个条件。可以仔细观察代码,可以发现由于使用了两个if,所以当一次递归退出后,例如从第一个if退出,第二个递归直接递归的是leftnum-1和rightnum,这其实是已经恢复状态了(如果没有恢复状态,那就是leftnum, rightnum-1)。因此不需要人为让他恢复状态。但是恢复状态这点是很重要的,因为回溯法,顾名思义要回溯,不恢复状态,怎么回溯呢。
- if(rightnum>leftnum) //选择和条件。对于不同的if顺序,输出的结果顺序是不一样的,但是构成一样的解空间
- parentheses(sublist+ ")", results, leftnum, rightnum- 1);
- if(leftnum> 0)
- parentheses(sublist+ "(", results, leftnum- 1, rightnum);
从后面的例子可以看出,对于一些题,是必须要恢复递归前状态的。
二、给出一个不重复大于0数字的数组和一个目标,求数组中数的组合的和得到该目标(数字不同组合顺序当做一个解)。
- public class BackTracking {
- public static void main(String[] args){
- int[] num= new int[]{ 2, 3, 7, 6};
- int target= 9;
- find(num, target, "");
- }
- public static void find(int[] num, int target, String temp){
- if(issolution(temp,target)){
- System.out.println(temp);
- return;
- }
- for( int i= 0;i<num.length;i++){
- if(num[i]!=- 1){ //如果取过这个数字了,就置为-1
- int k=num[i];
- num[i]=- 1;
- find(num, target, temp+k);
- num[i]=k;
- }
- }
- }
- public static boolean issolution(String temp, int target){
- boolean result= false;
- int count= 0;
- for( int i= 0;i<temp.length();i++){
- count=count+Integer.valueOf(temp.charAt(i)+ "");
- }
- if(count==target)
- result= true;
- return result;
- }
- }
这题不同的是,它在递归后,必须把数组恢复。这也是参数的特征的第四点所说的内容。
三、给一个字符串,给出他的所有排列
- public class BackTracking {
- public static void main(String[] args){
- String s= "abc";
- pailie(s, "");
- }
- public static void pailie(String s, String temp){ //参数设计地尽量地简洁
- if(s.length()== 0){
- System.out.println(temp);
- return;
- }
- for( int i= 0;i<s.length();i++){
- String news=s.substring( 0, i)+s.substring(i+ 1,s.length()); //去掉String中的某个字母
- pailie(news, temp+s.charAt(i));
- }
- }
- }
,
回溯法是很重要的一种算法,在it企业笔试中经常会遇到。事实上,在各种编程题中,大家或多或少都会接触到这些题目,但是很多人没有对这类题目有个系统性的总结。接下来就对回溯法进行详尽,通俗易懂的分析。
回溯法有通用解法的美称,对于很多问题,如迷宫等都有很好的效果。回溯算法实际上一个类似枚举的深度优先搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就“回溯”返回(也就是递归返回),尝试别的路径。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。回溯法说白了就是穷举法。回溯法一般用递归来解决。
回溯法一般都用在要给出多个可以实现最终条件的解的最终形式。回溯法要求对解要添加一些约束条件。总的来说,如果要解决一个回溯法的问题,通常要确定三个元素:
1、选择。对于每个特定的解,肯定是由一步步构建而来的,而每一步怎么构建,肯定都是有限个选择,要怎么选择,这个要知道;同时,在编程时候要定下,优先或合法的每一步选择的顺序,一般是通过多个if或者for循环来排列。
2、条件。对于每个特定的解的某一步,他必然要符合某个解要求符合的条件,如果不符合条件,就要回溯,其实回溯也就是递归调用的返回。
3、结束。当到达一个特定结束条件时候,就认为这个一步步构建的解是符合要求的解了。把解存下来或者打印出来。对于这一步来说,有时候也可以另外写一个issolution函数来进行判断。注意,当到达第三步后,有时候还需要构建一个数据结构,把符合要求的解存起来,便于当得到所有解后,把解空间输出来。这个数据结构必须是全局的,作为参数之一传递给递归函数。
对于回溯法来说,每次递归调用,很重要的一点是把每次递归的不同信息传递给递归调用的函数。而这里最重要的要传递给递归调用函数的信息,就是把上一步做过的某些事情的这个选择排除,避免重复和无限递归。另外还有一个信息必须传递给递归函数,就是进行了每一步选择后,暂时还没构成完整的解,这个时候前面所有选择的汇总也要传递进去。而且一般情况下,都是能从传递给递归函数的参数处,得到结束条件的。
递归函数的参数的选择,要遵循四个原则:
1、必须要有一个临时变量(可以就直接传递一个字面量或者常量进去)传递不完整的解,因为每一步选择后,暂时还没构成完整的解,这个时候这个选择的不完整解,也要想办法传递给递归函数。也就是,把每次递归的不同情况传递给递归调用的函数。
2、可以有一个全局变量,用来存储完整的每个解,一般是个集合容器(也不一定要有这样一个变量,因为每次符合结束条件,不完整解就是完整解了,直接打印即可)。
3、最重要的一点,一定要在参数设计中,可以得到结束条件。一个选择是可以传递一个量n,也许是数组的长度,也许是数量,等等。
4、要保证递归函数返回后,状态可以恢复到递归前,以此达到真正回溯。
结合几个例题来分析。
一、给出n对括号,求括号排列的所有可能性。
这是一个很经典的回溯法问题,代码如下,结合代码,对上面总结出的特点进行分析。
- public class BackTracking {
- public static void main(String[] args) {
- int n= 3;
- int leftnum=n,rightnum=n; //左括号和右括号都各有n个
- ArrayList<String> results= new ArrayList<String>(); //用于存放解空间
- parentheses( "", results, leftnum, rightnum);
- for(String s:results)
- System.out.println(s);
- }
- public static void parentheses(String sublist, ArrayList<String> results, int leftnum, int rightnum){
- if(leftnum== 0&&rightnum== 0) //结束
- results.add(sublist);
- if(rightnum>leftnum) //选择和条件。对于不同的if顺序,输出的结果顺序是不一样的,但是构成一样的解空间
- parentheses(sublist+ ")", results, leftnum, rightnum- 1);
- if(leftnum> 0)
- parentheses(sublist+ "(", results, leftnum- 1, rightnum);
- }
- }
输出如下:
()()()
()(())
(())()
(()())
((()))
对于回溯法来说,必须齐备的三要素:
1、选择。在这个例子中,解就是一个合法的括号组合形式,而选择无非是放入左括号,还是放入右括号;
2、条件。在这个例子中,选择是放入左括号,还是放入右括号,是有条件约束的,不是随便放的。而这个约束就是括号的数量。只有剩下的右括号比左括号多,才能放右括号。只有左括号数量大于0才能放入左括号。这里if的顺序会影响输出的顺序,但是不影响最终解;
3、结束。这里的结束条件很显然就是,左右括号都放完了。
回溯法中,参数的设计是一大难点,也是很重要的地方。而递归参数的设计要注意的四个点:
1、用了一个空字符串来作为临时变量存储不完整解;
2、用了一个ArrayList<String> results来存放符合要求的解。在后面可以看到,不一定要这样做,也可以直接打印结果;
3、把leftnum和rightnum传入给递归函数,这样可以用于判断结束条件;
4、这个例子不明显。但是事实上也符合这个条件。可以仔细观察代码,可以发现由于使用了两个if,所以当一次递归退出后,例如从第一个if退出,第二个递归直接递归的是leftnum-1和rightnum,这其实是已经恢复状态了(如果没有恢复状态,那就是leftnum, rightnum-1)。因此不需要人为让他恢复状态。但是恢复状态这点是很重要的,因为回溯法,顾名思义要回溯,不恢复状态,怎么回溯呢。
- if(rightnum>leftnum) //选择和条件。对于不同的if顺序,输出的结果顺序是不一样的,但是构成一样的解空间
- parentheses(sublist+ ")", results, leftnum, rightnum- 1);
- if(leftnum> 0)
- parentheses(sublist+ "(", results, leftnum- 1, rightnum);
从后面的例子可以看出,对于一些题,是必须要恢复递归前状态的。
二、给出一个不重复大于0数字的数组和一个目标,求数组中数的组合的和得到该目标(数字不同组合顺序当做一个解)。
- public class BackTracking {
- public static void main(String[] args){
- int[] num= new int[]{ 2, 3, 7, 6};
- int target= 9;
- find(num, target, "");
- }
- public static void find(int[] num, int target, String temp){
- if(issolution(temp,target)){
- System.out.println(temp);
- return;
- }
- for( int i= 0;i<num.length;i++){
- if(num[i]!=- 1){ //如果取过这个数字了,就置为-1
- int k=num[i];
- num[i]=- 1;
- find(num, target, temp+k);
- num[i]=k;
- }
- }
- }
- public static boolean issolution(String temp, int target){
- boolean result= false;
- int count= 0;
- for( int i= 0;i<temp.length();i++){
- count=count+Integer.valueOf(temp.charAt(i)+ "");
- }
- if(count==target)
- result= true;
- return result;
- }
- }
这题不同的是,它在递归后,必须把数组恢复。这也是参数的特征的第四点所说的内容。
三、给一个字符串,给出他的所有排列
- public class BackTracking {
- public static void main(String[] args){
- String s= "abc";
- pailie(s, "");
- }
- public static void pailie(String s, String temp){ //参数设计地尽量地简洁
- if(s.length()== 0){
- System.out.println(temp);
- return;
- }
- for( int i= 0;i<s.length();i++){
- String news=s.substring( 0, i)+s.substring(i+ 1,s.length()); //去掉String中的某个字母
- pailie(news, temp+s.charAt(i));
- }
- }
- }