21、一步步学算法(算法题解)---1

时间:2021-10-07 00:17:58

一步步学算法(算法题解)---1

数值处理相关问题。


1。19头牛

问题描述:有一个老人在临死前把三个儿子叫到跟前,告诉他们把19头牛分了,老大分1/2,老二分1/4,老三分1/5,说完就死了.按当地习俗,不能宰牛.问三个儿子各能分多少?

问题分析:由于19与2、4、5都不能整除,所以就不能用平常的方法来解决这个问题。但是,如果仔细一点就可以发觉到:1/2+1/4+1/5=19/20,而牛的数量刚好为19。 

由此,不难写出下面的代码。

[cpp] view plaincopy
  1. #include <stdio.h>  
  2.   
  3. int main()  
  4. {  
  5.     int i;  
  6.     for (i=1; i<=10; i++) //遍历查找  
  7.     {  
  8.         if (i + i/2 + 2*i/5 == 19)   //找到满足条件的个数  
  9.         {  
  10.             printf("三个儿子分别分得:%d头,%d头,%d头", i, i/2, 2*i/5);  
  11.         }  
  12.     }  
  13.     return 0;  
  14. }  
[cpp] view plaincopy
  1. //打印结果<p style="margin-top: 0px; margin-bottom: 0px; ">三个儿子分别分得:10头,5头,4头</p>  

2.分钱

问题描述:一元钱分成1分、2分、5分的,问有多少种分法? 

问题分析:从题目就能看出,分发肯定是有很多种,常规的逐个判断显然不适合。考虑到不是所有的面值都得用到,并且1分得特殊性(最小因子)。我们先从5分开始考虑,遍历5分所有可能个数,然后制约2分的个数,最后1分的无需考虑,缺了全部补一分的就行了。

由此,不难写出下面的代码

[cpp] view plaincopy
  1. #include <stdio.h>  
  2.   
  3. int main()  
  4. {  
  5.     int i, j;  
  6.     int count_ = 0;  
  7.       
  8.     for (i=0; i<=20; i++)   //遍历5分的所有情况。 0<=i<=20  
  9.     {  
  10.         for (j=0; j<=(100-5*i)/2; j++)  //制约2分的可能情况  一  
  11.         {  
  12.             count_++;                                  //二  
  13.         }                                            //三  
  14.           
  15.         //或者。上述 一二三语句可以替换成如下语句  
  16.         //for (j=0; j<=50; j++)  
  17.         //{  
  18.         //    if (5*i + j*2 <= 100)  
  19.         //    {  
  20.         //        count_++;  
  21.         //    }  
  22.         //}  
  23.     }  
  24.       
  25.     printf("共有%d种分法", count_);  
  26.   
  27.     return 0;  
  28. }  
  29.   
  30. //打印结果  共有541种分法  

3.队列人数

问题描述:在爱尔兰守神节那天,举行每年一度的庆祝游戏,指挥者若将乐队排成10人、9人、8人、7人、6人、5人、4人、3人和2人时,最后的一排总是缺少一个人,那些人想这个位置大概是给数月前死去的乐队成员凯西还留着位置。指挥者见到总缺一人恼火了,叫大家排成一列纵队前进。假定人数不超过7000人,那么乐队究竟有多少人? 

问题分析:问题不难,无非遍历,找到满足所有分布情况的那个值就好了。

[cpp] view plaincopy
  1. #include <stdio.h>  
  2.   
  3. int main()  
  4. {  
  5.     int i, j;  
  6.     for (i=9; i<=7000; i+=10) //用最大步长取最外层循环变量值  
  7.     {  
  8.         for (j=9; j>=2; j--)  //模拟排队过程  
  9.         {  
  10.             if ((i+1)%j != 0)  //不满足条件  
  11.             {  
  12.                 break;  
  13.             }  
  14.         }  
  15.         if (j==1)  //满足条件  
  16.         {  
  17.             printf("乐队共有%d人", i);    
  18.             return 0; //找到最小的  
  19.         }  
  20.           
  21.     }  
  22.   
  23.     return 0;  
  24. }  
  25.   
  26. //打印结果  乐队共有2519人  

4.里程碑种数

问题描述:甲、乙两个城市有一条999公里长的公路。公路旁每隔一公里竖立着一个里程碑,里程碑的半边写着距甲城的距离,另半边写着距乙城的距离。有位司机注意到有的里程碑上所写的数仅用了两个不同的数字,例如000/999仅用了0和9,118/881仅用了1和8。算一算具有这种特征的里程碑共有多少个,是什么样的? 

问题分析:从题意中可知每对数仅用了两个不同的数字,并且两个数字之和衡等于9.并且,每对数之和也应衡等于999. 通过这两个限制条件,我们也不难写出代码。

[cpp] view plaincopy
  1. #include <stdio.h>  
  2.   
  3. int main()  
  4. {  
  5.     int i, j, k, sum_, count_ = 0;  
  6.       
  7.     for (i=0; i<=9; i++)  
  8.     {  
  9.         for (j=0; j<=9; j++)  
  10.         {  
  11.             for (k=0; k<=9; k++)  
  12.             {  
  13.                 if (((i==j)&&(9-i==k))||((i==k)&&(9-i==j))||((j==k)&&(9-k==i))||((i==j)&&(j==k)))  
  14.                 {  
  15.                     sum_ = i*100 + j*10 + k;  
  16.                     printf("%d---%d\n", sum_, 999-sum_);  
  17.                     count_++;  
  18.                 }  
  19.             }  
  20.         }  
  21.     }  
  22.       
  23.     printf("满足条件的里程碑个数: %d", count_);  
  24.   
  25.     return 0;  
  26. }  
  27.   
  28. //打印结果  满足条件的里程碑个数: 40 (具体省略)  


5.同等遗产

问题描述:父亲临终时,让按下列方式分配他的遗产:大儿子分得100克朗和剩下财产的1/10,二儿子分得200克朗和剩下财产的1/10,三儿子分得300克朗和剩下财产的1/10。依此类推,最后发现这种分法好极了,因为所有儿子分得的钱数恰好相等。问他共有几个儿子?每

个儿子分得多少遗产? 


问题分析:  还是遍历。不难,直接上代码

[cpp] view plaincopy
  1. #include <stdio.h>  
  2.   
  3. int main()  
  4. {  
  5.     int i, j, k, m, n;  
  6.     for (n=600; ; n=n+10) //由题意得,遗产最少600,并且最小步长10  
  7.     {  
  8.         k = 100 + (n-100)/10;  //遍历寻找满足条件的  
  9.         m = n - k;         //每个儿子遗产都一样,直接利用大儿子分得的带入计算  
  10.         for (i=2; m>0; i++)  
  11.             if ((m%10!=0)||(k!=i*100+(m-i*100)/10))  
  12.                 break;  
  13.             else m = (m-i*100) - (m-i*100)/10;  
  14.         if (m==0)  
  15.         {  
  16.             printf("他共有%d个儿子,每个儿子分得%d克朗.", i, k);  
  17.             exit(0); //找出后退出程序,不然无限循环  
  18.         }  
  19.     }  
  20.     return 0;  
  21. }  
  22.   
  23. //打印结果 他共有10个儿子,每个儿子分得900克  


6** 寻找丑数 (来源:博客园-笨鸟先飞)

问题描述:

我们把只包含因子235的数称作丑数(Ugly Number)。例如68都是丑数,但14不是,因为它包含因子7。习惯上我们把1当做是第一个丑数。求按从小到大的顺序的第1500个丑数。

分析:这是一道在网络上广为流传的面试题,据说google曾经采用过这道题。

所谓一个数m是另一个数n的因子,是指n能被m整除,也就是n % m == 0。根据丑数的定义,丑数只能被2、3和5整除。也就是说如果一个数如果它能被2整除,我们把它连续除以2;如果能被3整除,就连续除以3;如果能被5整除,就除以连续5。如果最后我们得到的是1,那么这个数就是丑数,否则不是。

基于前面的分析,我们可以写出如下的函数来判断一个数是不是丑数:

[cpp] view plaincopy
  1. bool IsUgly(int number)  
  2. {  
  3.     while(number % 2 == 0)  
  4.         number /= 2;  
  5.     while(number % 3 == 0)  
  6.         number /= 3;  
  7.     while(number % 5 == 0)  
  8.         number /= 5;  
  9.     return (number == 1) ? true : false;  
  10. }  

接下来,我们只需要按顺序判断每一个整数是不是丑数,即:

[cpp] view plaincopy
  1. int GetUglyNumber_Solution1(int index)  
  2. {  
  3.     if(index <= 0)  
  4.         return 0;  
  5.     int number = 0;  
  6.     int uglyFound = 0;  
  7.     while(uglyFound < index)  
  8.     {  
  9.         ++number;  
  10.         if(IsUgly(number))  
  11.         {  
  12.             ++uglyFound;  
  13.         }  
  14.     }  
  15.     return number;  
  16. }  

我们只需要在函数GetUglyNumber_Solution1中传入参数1500,就能得到第1500个丑数。该算法非常直观,代码也非常简洁,但最大的问题我们每个整数都需要计算。即使一个数字不是丑数,我们还是需要对它做求余数和除法操作。因此该算法的时间效率不是很高。

接下来我们换一种思路来分析这个问题,试图只计算丑数,而不在非丑数的整数上花费时间。根据丑数的定义,丑数应该是另一个丑数乘以2、3或者5的结果(1除外)。因此我们可以创建一个数组,里面的数字是排好序的丑数。里面的每一个丑数是前面的丑数乘以2、3或者5得到的。

这种思路的关键在于怎样确保数组里面的丑数是排好序的。我们假设数组中已经有若干个丑数,排好序后存在数组中。我们把现有的最大丑数记做M。现在我们来生成下一个丑数,该丑数肯定是前面某一个丑数乘以2、3或者5的结果。我们首先考虑把已有的每个丑数乘以2。在乘以2的时候,能得到若干个结果小于或等于M的。由于我们是按照顺序生成的,小于或者等于M肯定已经在数组中了,我们不需再次考虑;我们还会得到若干个大于M的结果,但我们只需要第一个大于M的结果,因为我们希望丑数是按从小到大顺序生成的,其他更大的结果我们以后再说。我们把得到的第一个乘以2后大于M的结果,记为M2。同样我们把已有的每一个丑数乘以3和5,能得到第一个大于M的结果M3和M5。那么下一个丑数应该是M2、M3和M5三个数的最小者。

前面我们分析的时候,提到把已有的每个丑数分别都乘以2、3和5,事实上是不需要的,因为已有的丑数是按顺序存在数组中的。对乘以2而言,肯定存在某一个丑数T2,排在它之前的每一个丑数乘以2得到的结果都会小于已有最大的丑数,在它之后的每一个丑数乘以2得到的结果都会太大。我们只需要记下这个丑数的位置,同时每次生成新的丑数的时候,去更新这个T2。对乘以3和5而言,存在着同样的T3和T5。

有了这些分析,我们不难写出如下的代码:

[cpp] view plaincopy
  1. int GetUglyNumber_Solution2(int index)  
  2. {  
  3.     if(index <= 0)  
  4.         return 0;  
  5.     int *pUglyNumbers = new int[index];  
  6.     pUglyNumbers[0] = 1;  
  7.     int nextUglyIndex = 1;  
  8.     int *pMultiply2 = pUglyNumbers;  
  9.     int *pMultiply3 = pUglyNumbers;  
  10.     int *pMultiply5 = pUglyNumbers;  
  11.     while(nextUglyIndex < index)  
  12.     {  
  13.         int min = Min(*pMultiply2 * 2, *pMultiply3 * 3, *pMultiply5 * 5);  
  14.         pUglyNumbers[nextUglyIndex] = min;  
  15.         while(*pMultiply2 * 2 <= pUglyNumbers[nextUglyIndex])  
  16.             ++pMultiply2;  
  17.         while(*pMultiply3 * 3 <= pUglyNumbers[nextUglyIndex])  
  18.             ++pMultiply3;  
  19.         while(*pMultiply5 * 5 <= pUglyNumbers[nextUglyIndex])  
  20.             ++pMultiply5;  
  21.         ++nextUglyIndex;  
  22.     }  
  23.     int ugly = pUglyNumbers[nextUglyIndex - 1];  
  24.     delete[] pUglyNumbers;  
  25.     return ugly;  
  26. }  
  27. int Min(int number1, int number2, int number3)  
  28. {  
  29.     int min = (number1 < number2) ? number1 : number2;  
  30.     min = (min < number3) ? min : number3;  
  31.     return min;  
  32. }  

和第一种思路相比,这种算法不需要在非丑数的整数上做任何计算,因此时间复杂度要低很多。感兴趣的读者可以分别统计两个函数GetUglyNumber_Solution1(1500)GetUglyNumber_Solution2(1500)的运行时间。当然我们也要指出,第二种算法由于要保存已经生成的丑数,因此需要一个数组,从而需要额外的内存。第一种算法是没有这样的内存开销的。