迷宫
题目要求:
Extense 处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense 想要从点A走到点 B,问在不走出迷宫的情况下能不能办到。如果起点或者终点有一个不能通行(为#),则看成无法办到。
输入要求:
输入迷宫的行(行和列是相等的),然后输入迷宫的状态,然后再输入起始位置和终止位置的行和列
输出要求:
如果可以走到,就输出成功
输入样例
2
..
..
0 0 1 1
输出样例
成功!
import java.util.Scanner;
//迷宫
public class LX9_3 {
private static int n;//迷宫的行和列数,行列相等
private static char[][] a;//保存迷宫的状态
private static int endx;
private static int endy;
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc=new Scanner(System.in);
n=sc.nextInt();
a=new char[n][n];
String l=sc.nextLine();
//得到迷宫的状态
for(int i=0;i<n;i++)
{
String s=sc.nextLine();
for(int j=0;j<n;j++)
{
//System.out.println(s.charAt(j));
a[i][j]=s.charAt(j);
}
}
int startx=sc.nextInt();//初始位置的行
int starty=sc.nextInt();//初始位置的列
endx=sc.nextInt();//结束位置的行
endy=sc.nextInt();//结束位置的列
f(startx-1,starty-1);
}
//判断从i,j 位置是否到达想要到达的位置
public static void f(int i,int j)
{
if(i<0||j<0||i>=n||j>=n)//走出迷宫
{
return;
}
if(a[i][j]=='#')//不能通过
return;
if(i==endx-1&&j==endy-1)//成功
{
System.out.println("成功!");
}
else//继续递归
{
a[i][j]='#';//将走过的格子置为走过,否则会出现死循环
f(i-1,j);
f(i+1,j);
f(i,j-1);
f(i,j+1);
a[i][j]='.';
}
}
}
注意:回溯 枚举
算24
题目要求:
给出 4 个小于 10 个正整数,你可以使用加减乘除 4 种运算以及括号把这 4 个数连接起来得到一个表达式。现在的问题是,是否存在一种方式使得得到的表达式的结果等于 24。
这里加减乘除以及括号的运算结果和运算的优先级跟我们平常的定义一致(这里的除法定义是实数除法)。 比如,对于 5,5,5,1,我们知道 5 * (5 – 1 / 5) = 24,因此可以得到 24。又比如,对于 1,1,4,2,我们怎么都不能得到 24。
输入要求
输入四个小于10的整数
输出
如果有则输出成功,如果没有不输出
输入样例
1 5 5 5输出样例
成功!
import java.util.Scanner;
//算24
public class LX9_4 {
private static int[] a=new int[4];//保存输入的4个整数
private static boolean[] flag=new boolean[4];//标记数是否用过
private static float sum=0;//保存结果
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc=new Scanner(System.in);
//得到4个小于10的整数
for(int i=0;i<4;i++)
{
a[i]=sc.nextInt();
}
for(int i=0;i<4;i++)
{
sum=0;
if(flag[i]) continue;//该数用过 直接跳过
else
{
flag[i]=true;
sum=a[i];
if(f(1))
break;
else
sum-=a[i];
sum-=a[i];//此处实现-1/5的情况(5-1/5)*5=24
if(f(1))
break;
else
sum+=a[i];
sum=a[i];
if(f(1))
break;
else
sum=a[i];
sum=a[i];
if(f(1))
break;
else
sum=a[i];
}
flag[i]=false;
}
}
//从第i个数到最后的结果是否为24
public static boolean f(int i)
{
if(i==4)
{
if(sum==24)
{
System.out.println("成功!");
return true;
}
else
{
return false;
}
}
else
{
for(int j=0;j<4;j++)
{
if(flag[j]) continue;//该数用过 直接跳过
else
{
flag[j]=true;
sum+=a[j];
if(f(i+1))
return true;
else
sum-=a[j];//回溯
sum-=a[j];
if(f(i+1))
return true;
else
sum+=a[j];//回溯
sum*=a[j];
if(f(i+1))
return true;
else
sum/=a[j];//回溯
sum/=a[j];
if(f(i+1))
return true;
else
sum*=a[j];//回溯
}
flag[j]=false;//回溯
}
return false;
}
}
}
注意:枚举 回溯
小游戏(和迷宫很像)
题目要求:
一天早上,你起床的时候想:“我编程序这么牛,为什么不能靠这个赚点小钱呢?”因此你决定编写一个小游戏。 游戏在一个分割成 w * h个正方格子的矩形板上进行。如图所示,每个正方格子上可以有一张游戏卡片,当然也可以没有。 当下面的情况满足时,我们认为两个游戏卡片之间有一条路径相连: 路径只包含水平或者竖直的直线段。路径不能穿过别的游戏卡片。但是允许路径临时的离开矩形板。下面是一个例子:
注意:图中牌的位置行和列反着
这里在 (1, 3)和 (4, 4)处的游戏卡片是可以相连的。而在 (2, 3) 和 (3, 4) 处的游戏卡是不相连的,因为连接他们的每条路径都必须要穿过别的游戏卡片。 你现在要在小游戏里面判断是否存在一条满足题意的路径能连接给定的两个游戏卡片。
输入要求;
输入矩形板的的行),然后输入矩形板的状态(1代表有牌,0代表没牌),然后再输入起始位置和终止位置的行和列
输出要求:
如果可以走到,就输出成功
输入样例
4 5
10001
11101
01110
3 2 3 5
输出样例
成功! 注意:此处和迷宫不同的是,矩形板外的区域也可以走,思路是扩展矩形板将行和列都增加2,并且将其置为可通过。这样问题就可以和迷宫一样解决了
import java.util.Scanner;
//小游戏(迷宫的变形)
public class LX9_6 {
private static int w;//行
private static int h;//列
private static int[][] a;//保存矩形板的情况 0代表可以通行,1代表不可以通行
private static int endx;//结束位置的行
private static int endy;//结束位置的列
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc=new Scanner(System.in);
w=sc.nextInt();
h=sc.nextInt();
/*因为在矩形框的外边也可以通过,所以将矩形板的行和列都增加2*/
a=new int[w+2][h+2];
// 得到矩形板的情况
String l=sc.nextLine();
for(int i=1;i<w+1;i++)
{
String s=sc.nextLine();
for(int j=1;j<h+1;j++)
{
a[i][j]=s.charAt(j-1)-'0';
}
}
int startx=sc.nextInt();//初始位置的行
int starty=sc.nextInt();//初始位置的列
a[startx][starty]=0;//将起始位置置为可以通过
endx=sc.nextInt();//结束位置的行
endy=sc.nextInt();//结束位置的列
a[endx][endy]=0;//将结束位置置为可以通过
for(int i=1;i<w+1;i++)
{
for(int j=1;j<h+1;j++)
{
System.out.print(a[i][j]+" ");
}
System.out.println();
}
f(startx,starty);
}
//判断从i,j 位置是否到达想要到达的位置
public static void f(int i,int j)
{
if(i<0||j<0||i>=w+2||j>=h+2)//走出扩展后的矩形板
{
return;
}
if(a[i][j]==1)//不能通过
{
//System.out.println(i+" "+j+" "+a[i][j]);
return;
}
else
{
if(i==endx&&j==endy)
{
System.out.println("成功!");
}
else
{
//System.out.println(i+" "+j);
a[i][j]=1;//将走过的格子置为走过(也就是置为不能通过),否则会出现死循环
f(i-1,j);
f(i+1,j);
f(i,j-1);
f(i,j+1);
a[i][j]=0;//回溯
}
}
}
}
注意:枚举 回溯
碎纸机
题目要求:
你现在负责设计一种新式的碎纸机。一般的碎纸机会把纸切成小片,变得难以阅读。而你设计的新式的碎纸机有以下的特点:
1.每次切割之前,先要给定碎纸机一个目标数,而且在每张被送入碎纸机的纸片上也需要包含一个数。
2.碎纸机切出的每个纸片上都包括一个数。
3.要求切出的每个纸片上的数的和要不大于目标数而且与目标数最接近。
举一个例子,如下图,假设目标数是50,输入纸片上的数是12346。碎纸机会把纸片切成 4块,分别包含 1,2,34和 6。这样这些数的和是 43 (= 1 + 2 + 34 + 6),这是所有的分割方式中,不超过 50,而又最接近 50的分割方式。又比如,分割成1,23,4和 6是不正确的,因为这样的总和是 34 (= 1 + 23 + 4 + 6),比刚才得到的结果 43 小。分割成 12,34 和 6 也是不正确的,因为这时的总和是52 (= 12 + 34 + 6),超过了50。
还有三个特别的规则:
1.如果目标数和输入纸片上的数相同,那么纸片不进行切割。
2.如果不论怎样切割,分割得到的纸片上数的和都大于目标数,那么打印机显示错误信息。
3.如果有多种不同的切割方式可以得到相同的最优结果。那么打印机显示拒绝服务信息。比如,如果目标数是 15,输入纸片上的数是 111,那么有两种不同的方式可以得到最优解,分别是切割成 1 和11 或者切割成 11 和1,在这种情况下,打印机会显示拒绝服务信息。 为了设计这样的一个碎纸机,你需要先写一个简单的程序模拟这个打印机的工作。给定两个数,第一个是目标数,第二个是输入纸片上的数,你需要给出碎纸机对纸片的分割方式。
输入要求
输入目标数和纸片上的数(都不超过整型所能表示的范围)
输出要求
输出最接近目标数且不大于目标数的值
输入样例
50
12346
输出样例
43
import java.math.BigInteger;
import java.util.Scanner;
//碎纸机
public class LX9_7 {
private static int[] a;//保存纸片上的数字的每一位
private static int len;//纸片上的数字的位数
private static int target;//目标数
private static int[] temp;//放置隔板的位置,元素存的是隔板前的元素的下标
private static int[] temp2;//保存输出结果的每个隔板的位置
private static int large=0;//保存最后输出的那个值
private static int flag=0;//标记最接近数的个数
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc=new Scanner(System.in);
target=sc.nextInt();//得到目标数字
sc.nextLine();
String s=sc.nextLine();//得到纸片上的数据
//System.out.println(s);
len=s.length();//纸片上数据的长度
a=new int[len];
temp=new int[len];
temp2=new int[len];
//获得数字的每一位
int sum3=0;
for(int i=0;i<len;i++)
{
//System.out.println(s.charAt(i)-'0');
a[i]=s.charAt(i)-'0';
sum3=sum3*10+a[i];
}
if(sum3==target)
{
System.out.println("目标数和纸片上数据相等!");
return;
}
for(int i=1;i<=len-1;i++)
{
f(i,0,0);
}
if(flag>=1)
{
System.out.println("最接近目标数有多重分割方法!");
return;
}
if(large==0)
{
System.out.println("所有的分割都大于目标数!");
}
else
{
System.out.println(large);
//输出结果值的每个隔板的位置
for(int i=0;i<len;i++)
{
System.out.print(temp2[i]+" ");
}
System.out.println();
}
}
//num隔板的个数,index代表隔板可以放在从该下标表示的元素后面, cur当前放的是第几个隔板
public static void f(int num,int index,int cur)
{
if(cur==num)//num个隔板都放置完毕
{
temp[num]=len-1;
int sum=0;
int t=0;
for(int i=0;i<=num;i++)
{
int sum2=0;
for(int j=t;j<=temp[i];j++)
{
sum2=sum2*10+a[j];
}
sum+=sum2;
t=temp[i]+1;
}
//System.out.println(sum);
if(sum>target)//和大于目标值,返回
return;
else
{
if(sum>large)//和接近目标值
{
flag=0;
large=sum;
for(int i=0;i<=num;i++)
{
temp2[i]=temp[i];
}
}
else if(sum==large)
{
flag++;
}
return;
}
}
if(index>=len-1)//隔板数没有放完,但是能放置的位置已经没有了
{
//System.out.println("0000");
return;
}
else
{
for(int i=index;i<=len-2;i++)
{
//System.out.println(i);
temp[cur]=i;
f(num,i+1,cur+1);
}
}
}
}
注意:思路(放隔板的个数) 递归函数
=======================================================
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:你还能想出哪些情况,是能及早发现做下去没有前途,不应再尝试的?
9.2 例题:菲波那契数列
问题描述
菲波那契数列是指这样的数列:数列的第一个和第二个数都为 1,接下来每个数都等于前面 2 个数之和。给出一个正整数 a,要求菲波那契数列中第 a 个数是多少。
输入数据
第 1 行是测试数据的组数 n,后面跟着 n 行输入。每组测试数据占 1 行,包括一个
正整数 a(1 <= a <= 20)。
输出要求
n 行,每行输出对应一个输入。输出应是一个正整数,为菲波那契数列中第 a 个数
的大小。
输入样例
4
5
2
19
1
输出样例
5
1
4181
1
解题思路:这个题目要求很明确,因为 a的规模很小,所以递归调用不会产生栈溢
出的问题。设第 n 项值为 f(n),则 f(n) = f(n-1)+f(n-2)。已知 f(1)=1,f(2)=1,则从第3
项开始,可以用公式求。
import java.util.Scanner;
/*斐波那契数列
* 1 1 2 3 5 8 13.....
* */
public class Test9_2 {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc=new Scanner(System.in);
int n=sc.nextInt();
System.out.println(f(n));
}
public static int f(int n)
{
if(n==1||n==2)
return 1;
return f(n-1)+f(n-2);
}
}
思考题9.2:在n比较大(比如,n>10)的时候,函数f的执行次数和下面哪一项比
较接近?
A) n^2 B) n^3 C) 2^n D) n!
9.3 例题:二叉树
问题描述
如上图所示,由正整数 1, 2, 3, ...组成了一棵无限大的二叉树。从某一个结点到根结点(编号是 1 的结点)都有一条唯一的路径,比如从 10 到根结点的路径是(10, 5, 2, 1),
从 4 到根结点的路径是(4, 2, 1),从根结点 1 到根结点的路径上只包含一个结点 1,因此路径就是(1)。对于两个结点 x 和 y,假设他们到根结点的路径分别是(x1, x2, ... ,1)和(y1,
y2, ... ,1)(这里显然有 x = x1,y = y1),那么必然存在两个正整数 i 和 j,使得从 xi 和 yj开始,有 xi = yj ,xi + 1 = yj + 1,xi + 2 = yj + 2,... 现在的问题就是,给定 x 和y,要求 xi(也就是 yj)。
输入数据
输入只有一行,包括两个正整数 x 和 y,这两个正整数都不大于 1000。
输出要求
输出只有一个正整数 xi。
输入样例
10 4
输出样例
2
解题思路
这个题目要求树上任意两个节点的最近公共子节点。分析这棵树的结构不难看出,不论奇数偶数,每个数对 2 做整数除法,就走到它的上层结点。 我们可以每次让较大的一个数(也就是在树上位于较低层次的节点)向上走一个结点,直到两个结点相遇。如果两个节点位于同一层,并且它们不相等,可以让其中任何一个先往上走,然后另一个再往上走,直到它们相遇。设 common(x, y)表示整数 x 和 y的最近公共子节点,那么,根据比较 x 和y 的值,我们得到三种情况:1) x 与y 相等,则 common(x, y)等于 x 并且等于y;2)x 大于 y,则 common(x, y)等于 common(x/2, y);3)x 大于 y,则 common(x, y)等于 common(x y/2);
import java.util.Scanner;
//二叉树,求另个结点到根节点的路径中重复的位置
public class Test9_3 {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
Scanner sc=new Scanner(System.in);
int x=sc.nextInt();
int y=sc.nextInt();
System.out.println(f(x,y));
}
public static int f(int x,int y)
{
if(x==y)
return x;
else if(x>y)
return f(x/2,y);
else
return f(x,y/2);
}
}
实现中常见的问题
问题一:有一种比较直观的解法是对于两个给定的数,分别求出它们到根节点的通路上的所有节点的值,然后再在两个数组中寻找数码最大的公共节点。这种做法的代码
比较繁琐,容易在实现中出错;
问题二:代码实现逻辑不明晰,造成死循环等错误,例如,有人只将其中一个数不停地除以 2,而不理会另外一个数。
9.4 例题:逆波兰表达式
问题描述
逆波兰表达式是一种把运算符前置的算术表达式,例如普通的表达式 2 + 3 的逆波兰表示法为+ 2 3。逆波兰表达式的优点是运算符之间不必有优先级关系,也不必用括号改变运算次序,例如(2 + 3) * 4 的逆波兰表示法为* + 2 3 4。本题求解逆波兰表达式的值,其中运算符包括 + - * / 四个。
输入数据
输入为一行,其中运算符和运算数之间都用空格分隔,运算数是浮点数
输出要求
输出为一行,表达式的值。
输入样例
* + 11.0 12.0 + 24.0 35.0
输出样例
1357.000000
解题思路
这个问题看上去有些复杂,如果只是简单地模拟计算步骤不太容易想清楚,但是如果用递归的思想就非常容易想清楚。让我们根据逆波兰表达式的定义进行递归求解。在递归函数中,针对当前的输入,有五种情况:1)输入是常数,则表达式的值就是这个常数;2)输入是’+’,则表达式的值是再继续读入两个表达式并计算出它们的值,然后将它们的值相加;3)输入是’-’;4)输入是’*’; 5)输入是’/’;后几种情况与 2)相同,只是计算从’+’变成’-’,’*’,’/’。
import java.util.Scanner;
//给一个用逆波兰式表示的数学式子,求出结果
public class Test9_4 {
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
System.out.println(cal());
}
public static float cal()
{
Scanner sc=new Scanner(System.in);
String c=sc.nextLine();//每当输入一个字符后要按回车键
//System.out.println(c);
if(c.equals("+"))
//System.out.print("("+cal()+"+"+cal()+")")将逆波兰式转化成常规表达式输出
return cal()+cal();
if(c.equals("-"))
return cal()-cal();
if(c.equals("*"))
return cal()*cal();
if(c.equals("/"))
return cal()/cal();
return Float.parseFloat(c);
}
}
实现中常见的问题
问题一:不适应递归的思路,直接分析输入的字符串,试图自己写进栈出栈的程序,写得逻辑复杂后,因考虑不周出错;
问题二:不会是用 atof()函数,自己处理浮点数的读入,逻辑复杂后出错。
思考题9.4:改写此程序,要求将逆波兰表达式转换成常规表达式输出。可以包含多余的括号。
http://blog.csdn.net/li1500742101/article/details/8866985