两道递归/回溯好题的分析:printSquares,maxSum

来源:互联网 发布:pp助手网络连接失败 编辑:程序博客网 时间:2024/06/02 03:23

回溯的题目总是有套路的,结合《递归分解的一些方法 和 回溯 笔记》一文,基本上可以套用这个模板来解决回溯题目。

bool solve(configuration conf){    if(no more choices){         // BASE CASE        return (conf is goal state);    }        for(all available choices){        try one choice c:        // Solve from here, if works out, you're done!        if(solve(conf with choice c made))  return true;  // Stop early        unmake choice c;    }        return false;       // tried all choices, no solution found}

注意观察上面的伪代码,我总结了一下:

1)在Base case,处理的是最小的情况,如果满足题意成功了(比如8皇后中,找到最后一列的不会碰撞的位置时)返回true。如果不满足题意了(比如8皇后中,如果坐标值超出了棋盘的范围)则返回false。所以Base case是有可能既要处理成功的情况,也要处理失败的情况!


2)一般下面就是一个for循环,来遍历所有的选择(在8皇后中,所有的选择就是当前列的所有位置)。注意在try one choice c的时候,通常会改变全局变量(比如isVisited数组),然后才去处理子问题。所以在做下一个选择时,必须先撤销刚才所做的操作(在Java中常常是重置全局变量如isVisited数组,或者al.remove(al.size()-1))!一定要恢复原状。这一点在下面的题目中会体现。(注意:如果在回溯的过程中,我自己新建了临时变量如String或者AL还作为子调用的参数,则不必撤销动作!这一点在求回溯问题最优值时常常会用得上!如Sorting_Searching 叠罗汉 @CareerCup)


3)注意到我标有Stop early的return true语句!回溯会有两种问题,一种是只要找到一种解决方案,证明是有解的即可;另一种是穷举所有的解打印出来或者从中比较选择最优解。而return true是针对着前一种的情况。下面的题目中会具体分析有没有这一句的不同!


4)最后就是要注意到最后有一句return false。这一句也颇有玄机,用来表示当前的子调用函数,穷尽了所有办法,但是还是没办法,所以只能返回false代表这不是子调用函数的问题,而是当前函数的选择有问题!

另外要注意,这句也不是一定就是return false的,在下面的例题中可以看到!



================================================================

例题1:

http://practiceit.cs.washington.edu/problem.jsp?category=Building+Java+Programs%2C+3rd+edition%2FBJP3+Chapter+12&problem=bjp3-12-e22-printSquares


Write a method printSquares that uses recursive backtracking to find all ways to express an integer as a sum of squares of unique positive integers. For example, the call of printSquares(200); should produce the following output:

1^2 + 2^2 + 3^2 + 4^2 + 5^2 + 8^2 + 9^21^2 + 2^2 + 3^2 + 4^2 + 7^2 + 11^21^2 + 2^2 + 5^2 + 7^2 + 11^21^2 + 3^2 + 4^2 + 5^2 + 6^2 + 7^2 + 8^21^2 + 3^2 + 4^2 + 5^2 + 7^2 + 10^22^2 + 4^2 + 6^2 + 12^22^2 + 14^23^2 + 5^2 + 6^2 + 7^2 + 9^26^2 + 8^2 + 10^2

Some numbers (such as 128 or 0) cannot be represented as a sum of squares, in which case your method should produce no output. Keep in mind that the sum has to be formed with unique integers. Otherwise you could always find a solution by adding 1^2 together until you got to whatever number you are working with.

As with any backtracking problem, this one amounts to a set of choices, one for each integer whose square might or might not be part of your sum. In many of our backtracking problems we store the choices in some kind of collection. In this problem you can instead generate the choices by doing a for loop over an appropriate range of numbers. Note that the maximum possible integer that can be part of a sum of squares for an integer n is the square root of n.

Like with other backtracking problems, you still need to keep track of which choices you have made at any given moment. In this case, the choices you have made consist of some group of integers whose squares may be part of a sum that will add up to n. Represent these chosen integers as an appropriate collection where you add the integer i to the collection to consider it as part of an answer. If you ever create such a collection whose values squared add up to n, you have found a sum that should be printed.

To help you solve this problem, assume there already exists a method printHelper that accepts any Java collection of integers (such as a list, set, stack, queue, etc.) and prints the collection's elements in order. For example, if a set s stores the elements [1, 4, 8, 11], the call of printHelper(s); would produce the following output:

1^2 + 4^2 + 8^2 + 11^2

这是我的代码:

package Recursion;import java.util.ArrayList;public class PrintSquares {public static void main(String[] args) {printSquares(200);}public static void printSquares(int result){int limit = (int)Math.sqrt(result)+1;int[] options = new int[limit+1];for(int i=0; i<=limit; i++){options[i] = i;//System.out.println(options[i]);}ArrayList<ArrayList<Integer>> ret = new ArrayList<ArrayList<Integer>>();rec(result, options, 0, 1, new ArrayList<Integer>(), ret);System.out.println(ret);//for (ArrayList<Integer> al : ret) {//printHelper(al);//}}public static boolean rec(int result, int[] options, int cur, int startIndex, ArrayList<Integer> al, ArrayList<ArrayList<Integer>> ret){if(cur > result){// 如果超过了设定值,不用继续找了,已经错了return false;}if(cur == result){// 如果正好匹配,很好,加入到ret!ret.add(new ArrayList<Integer>(al));return true;}for(int i=startIndex; i<options.length; i++){// 用for来穷举所有选择if(options[i] != -1){// 这里我把options[i]为-1时定义为已经被用过的状态,即不能再被用!int tmp = options[i];// 保存值到临时变量中,为了最后的撤销动作options[i] = -1;// 执行动作,改变了全局变量optionsal.add(tmp);// 先假设当前的选择是正确的,把它添加到al中if(rec(result, options, cur+(int)Math.pow(tmp, 2), i+1, al, ret)){al.remove(al.size()-1);// 即使当前选择是正确的,也要移除!是因为al也是所有调用栈共享一个的,所以必须还原!//return true;}else{// 如果子调用返回false,说明当前做出的选择是错的!al.remove(al.size()-1);// 肯定要撤销当前的选择(因为1当前选择错误,2al是共享的)}options[i] = tmp;// 撤销动作}}return false;}}

打印出的结果是:

[[1, 2, 3, 4, 5, 8, 9], [1, 2, 3, 4, 7, 11], [1, 2, 5, 7, 11], [1, 3, 4, 5, 6, 7, 8], [1, 3, 4, 5, 7, 10], [2, 4, 6, 12], [2, 14], [3, 5, 6, 7, 9], [6, 8, 10]]

但是如果加上回溯中的那句return true,则结果会是:

[[1, 2, 3, 4, 5, 8, 9]]

就只剩下第一个了!


中间的对options和al的设值,撤销的原因是options和al是所有递归调用共享的,所以每次改变后都要复原。

这道题差不多可以当做是回溯的模板了。



---------------------------------------------------------------------------------------------

再来看另一道题:

http://practiceit.cs.washington.edu/problem.jsp?category=Building+Java+Programs%2C+3rd+edition%2FBJP3+Chapter+12&problem=bjp3-12-e21-maxSum

Write a recursive method maxSum that accepts a list of integers L and an integer limit n as its parameters and uses backtracking to find the maximum sum that can be generated by adding elements of L that does not exceed n. For example, if you are given the list of integers [7, 30, 8, 22, 6, 1, 14] and the limit of 19, the maximum sum that can be generated that does not exceed is 16, achieved by adding 7, 8, and 1. If the list L is empty, or if the limit is not a positive integer, or all of L's values exceed the limit, return 0.

Each index's element in the list can be added to the sum only once, but the same number value might occur more than once in a list, in which case each occurrence might be added to the sum. For example, if the list is [6, 2, 1] you may use up to one 6 in the sum, but if the list is [6, 2, 6, 1] you may use up to two sixes.

Here are several example calls to your method and their expected return values:

List LLimit nmaxSum(L, n) returns[7, 30, 8, 22, 6, 1, 14]1916[5, 30, 15, 13, 8]4241[30, 15, 20]4035[6, 2, 6, 9, 1]3024[11, 5, 3, 7, 2]1414[10, 20, 30]70[10, 20, 30]2020[]100

You may assume that all values in the list are non-negative. Your method may alter the contents of the list L as it executes, but L should be restored to its original state before your method returns. Do not use any loops in solving this problem.


注意到这道题是求最优解,如果用回溯来求最优解,就得遍历所有的组合,所以就像我前面说的第3点,在回溯中是不能有return true语句的!还有一点有意思的是,一般来说回溯的模板最后都是return false,但是这道题的最后却必须是return true!因为这道题没有结束条件(比如上一题的结束条件就是cur==result)所以就要用return true来使之一直运行下去,直到穷尽所有的选择。


我的解答:

有for循环版和无for循环版:

package Recursion;import java.util.ArrayList;import java.util.List;public class MaxSum {public static void main(String[] args) {int[] arr = {5, 30, 15, 13, 8};List<Integer> L = new ArrayList<Integer>();for (int i : arr) {L.add(i);}System.out.println(maxSum2(L, 42));}public static int maxSum(List<Integer> L, int limit){boolean[] used = new boolean[L.size()];int[] max = {0};rec(L, limit, 0, used, max);return max[0];}public static boolean rec(List<Integer> L, int limit, int cur, boolean[] used, int[] max){if(cur > limit){// 超过限定的最大值,出错!return false;} for(int i=0; i<L.size(); i++){// 尝试所有选择if(!used[i]){// 因为不能重复选择同一元素,所以要用used来标识已经选择过的used[i] = true;if(rec(L, limit, cur+L.get(i), used, max)){max[0] = Math.max(max[0], cur+L.get(i));//return true;// 因为这道题不是找到一个解就行,而是必须找到最优解,所以不能return true}used[i] = false;// 撤销动作}}return true;// 不像一般的回溯题是return false,这道题是return true,因为如果是return false那么这道题就永远没有地方返回true了}public static int maxSum2(List<Integer> L, int limit){boolean[] used = new boolean[L.size()];int[] max = {0};rec2(L, limit, 0, used, max, 0);return max[0];}// 无for循环版,就是把index提出来然后多做一个递归public static boolean rec2(List<Integer> L, int limit, int cur, boolean[] used, int[] max, int index){if(cur > limit){return false;}if(index == L.size()){return true;}if(!used[index]){used[index] = true;if(rec2(L, limit, cur+L.get(index), used, max, index)){max[0] = Math.max(max[0], cur+L.get(index));//return true;}used[index] = false;}return rec2(L, limit, cur, used, max, index+1);}}


另外,想把一个迭代的改成递归,只要把index提出来作为函数参数,然后多一个尾递归即可。




原创粉丝点击