几个经典递归问题(放苹果,红与黑,八皇后,木棍)

9.5 例题:放苹果(一次枚举)
问题描述
把 M 个同样的苹果放在 N 个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?(用 K 表示)注意:5,1,1 和 1,5,1 是同一种分法。
输入数据
第一行是测试数据的数目 t(0 <= t <= 20)。以下每行均包含两个整数 M 和 N,以空格分开。1<=M,N<=10。
输出要求
对输入的每组数据 M 和 N,用一行输出相应的 K。
输入样例
1
7 3
输出样例

8

解题思路
所有不同的摆放方法可以分为两类:至少有一个盘子空着和所有盘子都不空。我们可以分别计算这两类摆放方法的数目,然后把它们加起来。对于至少空着一个盘子的情况,则 N 个盘子摆放 M 个苹果的摆放方法数目与 N-1 个盘子摆放 M 个苹果的摆放方法数目相同。对于所有盘子都不空的情况,则 N 个盘子摆放 M 个苹果的摆放方法数目等于 N 个盘子摆放 M-N 个苹果的摆放方法数目。我们可以据此来用递归的方法求解这个问题。  设 f(m, n)  为 m个苹果,n 个盘子的放法数目,则先对 n 作讨论,如果 n>m,必定有 n-m个盘子永远空着,去掉它们对摆放苹果方法数目不产生影响;即 if(n>m) f(m,n) = f(m,m)。当 n <= m时,不同的放法可以分成两类:即有至少一个盘子空着或者所有盘子都有苹果,前一种情况相当于 f(m , n) = f(m , n-1); 后一种情况可以从每个盘子中拿掉一个苹果,不影响不同放法的数目,即 f(m , n) = f(m-n , n)。总的放苹果的放法数目等于两者的和,即 f(m,n) =f(m,n-1)+f(m-n,n)  。整个递归过程描述如下:
 
1.  int f(int m , int n){
2.      if(n == 1 || m == 0)  return 1;
3.      if(n > m)  return f (m, m);
4.      return f (m , n-1)+f (m-n , n);
5.  }
出口条件说明:当 n=1时,所有苹果都必须放在一个盘子里,所以返回1;当没有苹果可放时,定义为1种放法;递归的两条路,第一条 n 会逐渐减少,终会到达出口n==1;  第二条 m会逐渐减少,因为 n>m时,我们会 return f(m , m) 所以终会到达出口m==0.

import java.util.Scanner;

//放苹果问题,和组合数相似,m个苹果放入n个盘子中,每个盘子可以放多个苹果,盘子可以空
public class Test9_5 {

    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Scanner  sc=new Scanner(System.in);
        int m=sc.nextInt();//苹果数
        int n=sc.nextInt();//
        System.out.println(f(m,n));
    }
    public static int f(int m,int n)
    {
        if(m==0||n==1) return 1;
        if(n>m) return f(m,m);
        return f(m,n-1)+f(m-n,n);
    }

}

实现中常见的问题:
问题一:没有想清楚如何递归,用循环模拟逐一枚举的做法时考虑不周出错;
问题二:出口条件判断有偏差,或者没有分析出当盘子数大于苹果数时要处理的情况;
9.6 例题:红与黑   (全局变量但不回溯)
问题描述
有一间长方形的房子,地上铺了红色、黑色两种颜色的正方形瓷砖。你站在其中一块黑色的瓷砖上,只能向相邻的黑色瓷砖移动。请写一个程序,计算你总共能够到达多少块黑色的瓷砖。
输入数据
包括多个数据集合。每个数据集合的第一行是两个整数 W 和 H,分别表示 x 方向和 y 方向瓷砖的数量。W 和 H 都不超过 20。在接下来的 H 行中,每行包括 W 个字符。每个字符表示一块瓷砖的颜色,规则如下  
1)‘.’:黑色的瓷砖;  
2)‘#’:白色的瓷砖;  
3)‘@’ :黑色的瓷砖,并且你站在这块瓷砖上。该字符在每个数据集合中唯一出现一
次。  
当在一行中读入的是两个零时,表示输入结束。
输出要求
对每个数据集合,分别输出一行,显示你从初始位置出发能到达的瓷砖数(记数时包括初始位置的瓷砖)。
输入样例
6 9  

....#.  
.....#  
......  
......  
...... 
......  
......  
#@...#  
.#..#.  
0 0
 
输出样例
45
 
解题思路
这个题目可以描述成给定一点,计算它所在的连通区域的面积。需要考虑的问题包括矩阵的大小,以及从某一点出发向上下左右行走时,可能遇到的三种情况:出了矩阵边界、遇到’.’、遇到’#’。 设 f(x, y)为从点(x,y)出发能够走过的黑瓷砖总数,则 f(x, y) = 1 + f(x - 1, y) + f(x + 1, y) + f(x, y - 1) + f(x, y + 1) 这里需要注意,凡是走过的瓷砖不能够被重复走过。可以通过每走过一块瓷砖就将它作标记的方法保证不重复计算任何瓷砖。

import java.util.Scanner;

//红与黑
public class Test9_6 {

    private static char[][] a;//保存输入的数据
    private static int m;//行
    private static int n;//列
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub
        Scanner sc=new Scanner(System.in);
        m=sc.nextInt();
        n=sc.nextInt();
        a=new char[m][n];
        int startx=0,starty=0;//用来记录初始位置的位置
        for(int i=0;i<m;i++)
        {
            String s=sc.next();
            for(int j=0;j<n;j++)
            {
                a[i][j]=s.charAt(j);
                if(a[i][j]=='@')// 记录初始位置
                {
                    a[i][j]='.';
                    startx=i;
                    starty=j;
                }
            }
        }
        System.out.println(f(startx,starty));
                

    }
    //判断从i,j位置能移动多少块次瓷砖,包括该位置
    public static int f(int i,int j)
    {
        if(i<0||j<0||i>=m||j>=n)//越界的情况
            return 0;
        if(a[i][j]=='#')//该位置是红色瓷砖不能走
            return 0;
        else
        {
            a[i][j]='#';//标记为红色瓷砖,说明该黑色瓷砖已经走过
            return 1+f(i,j-1)+f(i,j+1)+f(i-1,j)+f(i+1,j);
        }
        
    }

}

实现中常见的问题
问题一:走过某块瓷砖后没有将它标记,导致重复计算或无限递归;
问题二:在递归出口条件判断时,先判断该网格点是否是’#’,再判断是否出边界,导致数组越界;
问题三:读入数据时,用 scanf 一个字符一个字符读入,没有去掉数据中的行尾标记,导致数据读入出错。
 
在上面放苹果的例题中可以看出,在寻找从 f(x)  向出口方向的递归方法时,我们是对可能的情况做了一步枚举,即将所有可能情况划分为至少有一个盘子空着和所有盘子至少有一个苹果两种情况。这种通过一步枚举进行递归的方法是很常用的。例如在例题“红与黑”中,我们枚举了在一个方格点上的四种可能的走法。例题“红与黑”与前几个例题不同的地方在于,在该问题中有一个记录地图的全局量,在每一个格点行走时,我们会改变这个全局量的状态。我们在处理每个格点时按上下左右的顺序依次走向相邻格点,当我们走过左边的格点时,改变了全局量的状态,只是这种改变不影响我们继续走向右边的格点。但是对于另外一类问题,情况可能会不同,在我们尝试了前面的分支情况后,要将全局量恢复成进入分支前的状态,然后再尝试其它的分支情况。下面几个例题就是这种情况。

9.7 例题:八皇后问题  (回溯)

问题描述
会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。如何将 8 个皇后放在棋盘上(有 8 * 8个方格),使它们谁也不能被吃掉!这就是著名的八皇后问题。   对于某个满足要求的 8 皇后的摆放方法,定义一个皇后串 a 与之对应,即 a=b1b2...b8,其中 bi 为相应摆法中第 i行皇后所处的列数。已经知道 8 皇后问题一共有 92 组解(即 92 个不同的皇后串)。给出一个数 b,要求输出第 b 个串。串的比较是这样的:皇后串 x 置于皇后串 y之前,当且仅当将 x 视为整数时比 y小。  
输入数据
第 1 行是测试数据的组数 n,后面跟着 n 行输入。每组测试数据占 1 行,包括一个正整
数 b(1 <= b <= 92)
输出要求
n 行,每行输出对应一个输入。输出应是一个正整数,是对应于 b 的皇后串
输入样例
2
1
92
输出样例
15863724
84136275
解题思路三
这个题目也可以不用仿真棋盘来模拟已放置棋子的控制区域,而只用一个有 8 个元素的数组记录已经摆放的棋子摆在什么位置,当要放置一个新的棋子时,只需要判断它与已经放置的棋子之间是否冲突就行了。

import java.util.Scanner;

//八皇后问题
public class Test9_7 {

    private static int[] result=new int[92];//保存92种方案的结果
    private static int num=0;//保存方案数
    private static int[] flag=new int[8];//保存八个皇后的位置,下标代表列,元素值代表行
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        putQueen(0);//在输入之前将所有方案已经求出来
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        for(int i=0;i<n;i++)
        {
            int m=sc.nextInt();
            System.out.println(result[m-1]);
        }
        
    }
    
    /*将第i个皇后找个合适的位置放下,然后再计算第i+1个皇后可以放的位置,直到八个皇后都找到合
     * 适的位置后将该方案保存。
    */
    public static void putQueen(int i)
    {
        if(i==8)
        {
            for(int j=0;j<8;j++)
            {
                //System.out.print(flag[j]+" ");
                result[num]=result[num]*10+flag[j]+1;    
            }
            //System.out.println(num);
            num++;
            return;
        }
        else
        {
            for(int j=0;j<8;j++)
            {
                int k=0;
                //判断能被前i-1个皇后吃掉
                for( k=0;k<i;k++)
                {
                    //冲突继续
                    if(j==flag[k]||Math.abs(k-i)==Math.abs(flag[k]-j))
                        break;        
                }
                if(k==i)//满足条件
                {
                    flag[i]=j;
                    putQueen(i+1);
                }
                
            }
        }
    }

}

 
实现中常见的问题
问题一:  使用枚举法,穷举 8 个皇后的所有可能位置组合,逐一判断是否可以互相被吃掉,得到超时错误;
问题二:对于多组输入,有多组输出,没有在每组输出后加换行符,得到格式错;
问题三:对输入输出的函数不熟悉,试图将数字转换成字符或者将 8 个整数转换成 8位的十进制整数来完成输出,形成不必要的冗余代码。
9.8 例题:木棍问题  (回溯)
问题描述
乔治拿来一组等长的木棒,将它们随机地裁断,使得每一节木棒的长度都不超过50个长度单位。然后他又想把这些木棒恢复到裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。请你设计一个程序,帮助乔治计算木棒的可能最小长度。每一节木棒的长度都用大于零的整数表示。
输入数据
由多个案例组成,每个案例包括两行。第一行是一个不超过64的整数,表示裁截之后共有多少节木棒。第二行是经过裁截后,所得到的各节木棒的长度。在最后一个案例后,是零。  
输出要求
为每个案例,分别输出木棒的可能最小长度,每个案例占一行。  
输入样例
9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0
 
输出样例
6
5

解题思路
假设输入的 k 根木棒的长度之和是 M,并且它们之中最长的一根的长度是 p,原来的木棒长度是 L,则 L 应该能整除 M 并且大于等于 p。假设 M 除以 L 的商是N,则求解这个问
题的过程就是将 k 根木棒拼回到 N根长度是 L的木棒的过程。  我们可以分几个层次来思考这个问题,第一层,L 是大于等于 p 的整数,所以从 p开始逐一枚举 L 的可能取值,直到有一个取值使得 k 根木棒成功地拼成该长度的木棒若干。这个长度就是我们要求的解。第二层,判断某个长度 L 是否能被 k 根木棒成功地拼出。事实上我们需要成功拼出 M / L 根长度是 L 的木棒。具体做法是依次判断第 1 根到第M / L 根木棒能否被拼出。只有所有 M / L 根都被成功拼出,才能断言 L 是满足题意的解。这里就需要我们尝试 k 根木棒的不同组合,只有所有组合都尝试到了还不能拼出 M / L 根长度是L 的木棒才能断言 L 不是问题的解。我们用递归的思想可以这样看待这个问题:设 f(k, left, L, N)表示能否将 k 根木棒拼成 N 根长度是 L 的木棒加一根长度是 left 的木棒。这个 left 表示我们当前正在拼的一根木棒。我们从k根木棒里从大到小寻找一根长度为t并且t小于等于left的木棒,将它拼入当前正在拼的木棒,则原问题化成 f(k-1, left-t, L, N) 即能否将 k-1根木棒拼成 N根长度是 L 的木棒加一根长度是 left-t 的木棒。随着递归层次的加深,k 会不断地减小直到等于 0。如果当 k递归到 0 时,left==N==0,则表示我们可以将 k根木棒拼成 N根长度是 L 的木棒加一根长度是 left 的木棒。如果在递归的过程中出现从 k 根木棒里找不到长度小于等于 left 的木棒,则表明这条递归的道路不通,需要回到上一层,将已经拼到半路的木棒从末尾拆掉一根,换用新的一根再继续尝试。需要说明的是当 left==0 时,我们将left置成 L,并将 N的值减 1。表明我们已经成功拼出一根程度是 L 的木棒,准备拼下一根长度是 L 的木棒。这样一种做法可以尝试输入木棒的所有组合,不会出错,但是非花时间。我们需要想些办法来加快这个求解过程。首先,为了使得尝试更加有序,也为了及早发现不可能的组合,我们先将输入的 k 根木棒按长度从大到小排序。当某个小木棒在拼某个长度为L 的木棒时是第一个或者最后一个时,如果沿着这条路不能最后拼出所有的长度为 L 的木棒,则在这根小木棒所在位置不必尝试它后面的比它更短的小木棒,直接回到这根小木棒的上一根小木棒所在位置尝试其它可能。如果一根木棒放在某个长度为 L 的木棒的第一个位置并且沿着这条路走不下去,这根木棒如果出现在其后的任何一根度为 L 的木棒中都会有同样的问题,所以只能向上退回。如果一根木棒放在某个长度为 L 的木棒的最后个位置并且沿着这条路走不下去,如果用更小的木棒填充它所在的位置,则这根木棒要出现在其后拼出的长度为 L 的木棒中,此时交换这个木棒与此前替换它的更小的木棒组合,拼接效果不变,这就产生了矛盾,所以如果一根木棒放在某个长度为 L 的木棒的最后一个位置并且沿着这条路走不下去,就不用在这个位置尝试其他木棒,而应直接回到上一根小木棒的位置尝试其它可能。

import java.util.Scanner;

//木棍问题
public class Test9_8 {

    private static int n;//折断后的木棍总数
    private static int[] a;//保存n根木棍的长度
    private static boolean[] flag;//标记木棍是否已经被用过了
    /**
     * @param args
     */
    public static void main(String[] args) {
        // TODO Auto-generated method stub

        Scanner sc=new Scanner(System.in);
        n=sc.nextInt();
        a=new int[n];
        flag=new boolean[n];
        for(int i=0;i<n;i++)
        {
            a[i]=sc.nextInt();
        }
        //将n根木棍的长度依照从长到短来排序
        QuickSort(0,n-1);
        //得到n根木棍的长度的和
        int sum=0;
        for(int i=0;i<n;i++)
        {
            sum+=a[i];
        }
        //一个一个试常速是否满足
        for(int i=a[0];i<sum;i++)
        {
            if(con(n,0,i))
            {
                System.out.println(i);
                break;
            }
        }
    }
    
    public static void QuickSort(int start,int end)
    {
        if(start>=end)
            return;
        //两个区域的划分
        int temp=a[start];//以开始位置的元素作为基准
        int i=start,j=end+1;
        while(true)
        {
            while(a[++i]<temp&&i<end);
            while(a[--j]>temp);
            if(i>=j) break;
            else
            {
                int t=a[i];
                a[i]=a[j];
                a[j]=t;
            }
        }
        //将基准元素放到合适位置
        a[start]=a[j];
        a[j]=temp;
        //划分过的区域再排序
        QuickSort(start,j-1);
        QuickSort(j+1,end);
    }
    
    /*usenum代表没有被用过的木棍的数量,left当前正在匹配的木棍的剩余长度
    *len当前正在尝试的原始木棍的长度*/
    public static boolean con(int unusednum,int left,int len)
    {
        //尝试成功时
        if(unusednum==0&&left==0)
           return true;
        if(left==0) left=len;//当前木棍匹配成功,匹配下一个
        //找木棍来匹配
        for(int i=0;i<n;i++)
        {
            if(flag[i]) continue;//该木棍已经用过,直接跳过
            
            if(a[i]>left) continue;//长度比当前剩余的长度大,跳过
            flag[i]=true;//尝试
            if(con(unusednum-1,left-a[i],len))//成功返回真
                return true;
            else//失败,回溯,将该木棍置为没有使用过
            {
                flag[i]=false;
            }
            /*如果当前尝试的某个原始木棍的第一个位置或者最后一个位置的话,该位置不用再尝试
             * 剩下的木棍,注解跳出。*/
            if(a[i]==left||left==len)
                break;
        }
        return false;//所有情况尝试玩都失败,返回假
    }

}

 
实现中常见的问题
问题一:在尝试用输入长度的木棒拼出某一长度 L 的木棒时,当尝试到第 i根 L 长度的木棒失败时直接下结论说长度 L 不是问题的解。这时应该将已经拼好的第 i-1 根L 长度的木棒拆开,在最后面的位置尝试其它可能组合;
问题二:没有一根一根的拼出长度 L 的木棒,而是试图一起拼出若干根长度为 L 的木棒,由于组合太多而超时;
问题三:没有做适当的中间判断以减少不必要的组合尝试,造成超时。例如当某个输入长度的木棒作为 L 长度的第一个或最后一个木棒组合时,如果沿着这个路径进行不下去,就应该向上返回,而不应该再在这个位置尝试其它更小的木棒。这里应该指出这种判断的有效性是与测试数据有关的。
 
思考题 9.8:你还能想出哪些情况,是能及早发现做下去没有前途,不应再尝试的?

 



  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值