题目如下:
* 十个射击运动员打靶,靶一共有10环,10人打中90环的可能性有多少种? 请用递归算法编程实现。
关于递归,其求解过程类似一个stack,先进后出。先是由上而下调用,直至寻找到出口,然后自下而上计算返回结果。
其关键点:
1.寻找到一个“出口”,就是递归表达式在这一步可以计算出一个值。
2.定义递归表达式。注意在定义表达式n的时候,下一步n-1往往被考虑成可解的结果。(类似数学的推论证明)
一。第一步 尝试
拿到这个题目分析了一下,然后写了第一版代码:
- /**
- * 递归计算函数
- *
- * @param personId 人数,从0开始计算
- * @param currentScore 当前人打的环数,从0--10
- * @param scoreHistory 环数记录
- */
- private static void Caculate(int personId,int currentScore,
- int[] scoreHistory) {
- //出口
- if( 0 == personId) {//.最后一个人的时候,离90还剩下0-10环即可以满足一次
- if(90-sumup(scoreHistory)> =0 && 90-sumup(scoreHistory)<=10) {
- currentScore = 90-sumup(scoreHistory);
- scoreHistory[personId] = currentScore;
- printlog(scoreHistory);
- }
- }else { //迭代
- scoreHistory[personId] = currentScore;
- //计算下一个人,下个人可能打出0--10环,因此需要循环
- for(int i =0; i<=10;i++) {
- Caculate(personId-1,i,scoreHistory);
- }
- }
- }
这个递归函数的出口也找到了,表达式也找到了,应该可以求解,对吧。继续下一步,貌似可以调用函数求解咯:)。。
- public static void main(String[] args) {
- for(int i =0; i<=10;i++) {
- Caculate(9,i,store);
- }
- }
做到这里才发现,原来这个递归居然没有入口,这里是犯了第一个错误。也就是调用函数的时候,以personId=9人为起点,可是这个人的currentScore是多少呢。。。居然还需要用一个for循环去尝试,效率显然让人无语。
二。第二步 修改
这时候考虑修改入口,虽然无法猜出第一个人打几环,但是可以肯定的是,总剩余环数为90。修改递归函数如下:
- /**
- * 递归函数
- * @param personId 人数,从0开始计算
- * @param remanentScore 当前剩余环数 <=90
- * @param scoreHistory 环数记录
- */
- private static void Caculate(int personId,int remanentScore
- ,int[] scoreHistory) {
- if(0 == personId) { //出口,最后一个人打出剩余环数就ok
- if(remanentScore>=0 && remanentScore<=10) {
- scoreHistory[personId] = remanentScore;
- printlog(scoreHistory);
- counter++;
- }
- }else {
- for(int i=0; i<=10 ; i++) {//这个人可能打出0-10环
- scoreHistory[personId] = i;
- remanentScore -= i;
- Caculate(personId-1,remanentScore,scoreHistory);
- }
- }
- }
这里就犯第二个错误了。。呵呵。18,19行的一个非常隐蔽的小bug,debug调了n久才发现!
错就错在将remanentScore进行了自减。通常在函数结尾处理一个不再使用的变量,没有什么影响的。但是这个递归函数进入到for循环,代表的是当前递归并未到出口,就是没有结束!变量的错误操作会影响到后面的计算。
解决办法就是将18,19两行改成 Caculate(personId - 1, remanentScore - i, scoreHistory); 传递正确的参数而不影响当前递归值栈
三。第三步 优化
第二步实现的递归,跟套10个for循环的效率应该是一样差。每种组合都需要跑一遍,判断。 而我们知道,如果前面2个人打了不到10环,后面8个人全中10也不可能达到90环。那么来优化一下递归函数。
- /**
- * 递归函数
- * @param personId 人数,从0开始计算
- * @param remanentScore 当前剩余环数 <=90
- * @param scoreHistory 环数记录
- */
- private static void Caculate(int personId,int remanentScore
- ,int[] scoreHistory) {
- if(remanentScore > (personId+1)*10) {
- return;
- }
- if(0 == personId) { //出口,最后一个人打出剩余环数就ok
- if(remanentScore>=0 && remanentScore<=10) {
- scoreHistory[personId] = remanentScore;
- printlog(scoreHistory);
- counter++;
- }
- }else{
- for (int i = 0; i <= 10; i++) {//这个人可能打出0-10环
- scoreHistory[personId] = i;
- // remanentScore-=i;
- // Caculate(personId-1,remanentScore,scoreHistory);
- Caculate(personId - 1, remanentScore - i, scoreHistory);
- }
- }
- }
在函数开始就增加分数判断,不合适的直接pass 。
总共的循环次数为10的十次方10000000000 ,满足条件的次数仅92378,因此这一个简单的判断是惊天地泣鬼神的!
在我机器上运行优化过的递归时间为8688ms,而没有优化过的需要时间35766ms,显然是数量级的提高。
下面是最终JAVA代码:(改日写个Python的。。。好歹也算是混了个眼熟)
- package datastructure;
- /**
- * 十个射击运动员打靶,靶一共有10环,10人打中90环的可能性有多少种? 请用递归算法编程实现
- * 请用递归算法编程实现
- * @author wei.songw
- * Sep 20, 2008 9:28:46 PM
- */
- public class Shot {
- static int counter = 0;
- /**
- * 打印函数,输出一次可能性的所有环数
- * @param arr
- */
- private static void printlog(int[] arr) {
- if (arr!=null && arr.length>0) {
- for (int i = 0; i < arr.length; i++) {
- System.out.print(arr[i]+" ");
- }
- System.out.println("/n");
- }
- }
- /**
- * 计算一个一个数组中,所有元素的总和
- * @param scores
- * @return
- */
- private static int sumup(int[] scores) {
- int sum = 0;
- if(scores!=null && scores.length>0) {
- for(int i=0;i<scores.length;i++) {
- sum += scores[i];
- }
- }
- return sum;
- }
- // /**
- // * 递归计算函数 try-1
- // *
- // * @param personId 人数,从0开始计算
- // * @param currentScore 当前人打的环数,从0--10
- // * @param scoreHistory 环数记录
- // */
- // private static void Caculate(int personId,int currentScore,
- // int[] scoreHistory) {
- //
- // //出口
- // if( 0 == personId) {//.最后一个人的时候,离90还剩下0-10环即可以满足一次
- // if(90-sumup(scoreHistory)> 0 && 90-sumup(scoreHistory)<10) {
- // currentScore = 90-sumup(scoreHistory);
- // scoreHistory[personId] = currentScore;
- // printlog(scoreHistory);
- // }
- // }else { //迭代
- // scoreHistory[personId] = currentScore;
- // //计算下一个人,下个人可能打出0--10环,因此需要循环
- // for(int i =0; i<=10;i++) {
- // Caculate(personId-1,i,scoreHistory);
- // }
- // }
- // }
- // /**
- // * 递归函数 try-2
- // * @param personId 人数,从0开始计算
- // * @param remanentScore 当前剩余环数 <=90
- // * @param scoreHistory 环数记录
- // */
- // private static void Caculate(int personId,int remanentScore
- // ,int[] scoreHistory) {
- // if(0 == personId) { //出口,最后一个人打出剩余环数就ok
- // if(remanentScore>=0 && remanentScore<=10) {
- // scoreHistory[personId] = remanentScore;
- // printlog(scoreHistory);
- // counter++;
- // }
- // }else {
- // for(int i=0; i<=10 ; i++) {//这个人可能打出0-10环
- // scoreHistory[personId] = i;
- // remanentScore -= i;
- // Caculate(personId-1,remanentScore,scoreHistory);
- // }
- // }
- // }
- /**
- * 递归函数 try-3 正确版
- * @param personId 人数,从0开始计算
- * @param remanentScore 当前剩余环数 <=90
- * @param scoreHistory 环数记录
- */
- private static void Caculate(int personId,int remanentScore
- ,int[] scoreHistory) {
- // if(remanentScore > (personId+1)*10) {
- // return;
- // }
- if(0 == personId) { //出口,最后一个人打出剩余环数就ok
- if(remanentScore>=0 && remanentScore<=10) {
- scoreHistory[personId] = remanentScore;
- printlog(scoreHistory);
- counter++;
- }
- }else{
- for (int i = 0; i <= 10; i++) {//这个人可能打出0-10环
- scoreHistory[personId] = i;
- // remanentScore-=i;
- // Caculate(personId-1,remanentScore,scoreHistory);
- Caculate(personId - 1, remanentScore - i, scoreHistory);
- }
- }
- }
- public static void main(String[] args) {
- int[] store = new int[10];
- long start = System.currentTimeMillis();
- Caculate(9,90,store);
- System.out.println(counter);
- long end = System.currentTimeMillis();
- long cost = end-start;
- System.out.println("用时:ms "+ cost);
- }
- }