求质数
(一)质数
质数,又称为素数,指在一个大于1的自然数中,除了1和此整数自身外,无法被其他自然数整除的数(只有1和本身两个因数的数)。
(二)思路
如果m不能被 2~m的平方根 中的任何一个数整除,则m为素数。
证明(反证法):
由i = m/i ==> i = sqrt(m)
这样,对于i属于[2, sqrt(m)],假如i为m的因子,因为i * m/i = m,则m/i也为m的因子。这样,m就不是质数。
反过来,对于i属于[2, sqrt(m)],假如所有的i都不为m的因子,因为i * m/i = m,则m/i也为m的因子。
(三)程序
例1:输入一个数,判断这个数是否为质数
#include <iostream>
#include <math.h>
using namespace std;
bool isPrime(int m)
{
if(m > 1)
{
for(int i = 2; i <= sqrt(m); i++)
{
if(0 == m % i)
{
return false;
}
}
return true;
}
return false;
}
int main()
{
int num;
cin >> num;
if(isPrime(num))
{
cout << num << " is a prime" << endl;
}
else
{
cout << num << " is not a prime" << endl;
}
return 0;
}
运行结果:
23
23 is a prime
例2:求1~100之间的全部质数
#include <iostream>
#include <math.h>
using namespace std;
bool isPrime(int m)
{
if(m > 1)
{
for(int i = 2; i <= sqrt(m); i++)
{
if(0 == m % i)
{
return false;
}
}
return true;
}
return false;
}
int main()
{
for(int i = 2; i <= 100; i++)
{
if(isPrime(i))
{
cout << i << " ";
}
}
return 0;
}
运行结果:
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
最大公约数
一、最大公约数(Greatest Common Divisor)
几个自然数,公有的因数,叫做这几个数的公约数;其中最大的一个,叫做这几个数的最大公约数。例如:12、16的公约数有1、2、4,其中最大的一个是4,4是12与16的最大公约数,一般记为(12、16)=4。12、15、18的最大公约数是3,记为(12、15、18)=3。
二、编程求两个数的最大公约数
求最大公约数有多种方法,没有专门学过方法的人,首先可能会联想到穷举法。
(一)穷举法
#include <stdio.h>
// 穷举法
int gcd(int num1, int num2)
{
// 求最小的那个数
int divisor = num1 < num2 ? num1 : num2;
for(; divisor >= 1; divisor--)
{
if(0 == num1 % divisor && 0 == num2 % divisor)
{
// 找到最大公约数,跳出循环
break;
}
}
return divisor;
}
int main()
{
int a, b;
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
while(a > 0 && b > 0)
{
printf("The greatest common divisor is: %d
", gcd(a,b));
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
}
printf("Loop end! Program will be finished!");
return 0;
}
运行结果:
Please input 2 numbers, seperated by space: 4 6
The greatest common divisor is: 2
Please input 2 numbers, seperated by space: 7 9
The greatest common divisor is: 1
Please input 2 numbers, seperated by space: 9 81
The greatest common divisor is: 9
Please input 2 numbers, seperated by space: 100 128
The greatest common divisor is: 4
Please input 2 numbers, seperated by space: 10000 15000
The greatest common divisor is: 5000
Please input 2 numbers, seperated by space: 0 0
Loop end! Program will be finished!
分析:
穷举法虽然简单,但是有一个很大的缺点,就是效率低。比如咱们输入10000和15000,那么程序是从10000开始自减,一直减到5000,才得出了结果。这个过程for共执行了10000-5000+1 = 5001次。
所以求最大公约数,通常不用穷举法。
那么有没有其他求最大公约数的方法呢?
有的。
常见的有辗转相除法、相减法、短除法等。
(二)辗转相除法
思路:
有两整数a和b
① a%b得余数c
② 若c=0,则b即为两数的最大公约数
③ 若c≠0,则a=b,b=c,再回去执行①
例子: a = 10000 b = 15000,则运算过程为
① c = a % b = 10000 % 15000 = 10000, a = b = 15000, b = c = 10000
② c = a % b = 15000 % 10000 = 5000, a = b = 10000, b = c = 5000
③ c = a % b = 10000 % 5000 = 0, 则b = 5000即为最大公约数
程序:
#include <stdio.h>
// 辗转相除法 + 递归
int gcd(int num1, int num2)
{
if(0 == num2)
{
return num1;
}
return gcd(num2, num1 % num2);
}
int main()
{
int a, b;
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
while(a > 0 && b > 0)
{
printf("The greatest common divisor is: %d
", gcd(a,b));
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
}
printf("Loop end! Program will be finished!");
return 0;
}
运行结果:
Please input 2 numbers, seperated by space: 4 6
The greatest common divisor is: 2
Please input 2 numbers, seperated by space: 7 9
The greatest common divisor is: 1
Please input 2 numbers, seperated by space: 9 81
The greatest common divisor is: 9
Please input 2 numbers, seperated by space: 100 128
The greatest common divisor is: 4
Please input 2 numbers, seperated by space: 10000 15000
The greatest common divisor is: 5000
Please input 2 numbers, seperated by space: 0 0
Loop end! Program will be finished!
分析:
与穷举法相比,求10000和15000的最大公约数,辗转相除法只循环了三次,就得到了结果。效率提高了很多。
(三)相减法
又叫更相减损法、等值算法,起源于《九章算术》。
思路:
有两整数a和b
① 若a>b,则a = a - b
若a<b,则b = b - a
② 若a=b,则a(或b)即为两数的最大公约数
若a≠b,则再回去执行①
例子:求27和15的最大公约数过程为:
① a = a - b = 27-15=12
② b = b - a = 15-12=3
③ a = a - b = 12-3=9
④ a = a - b = 9-3=6
⑤ a = a - b = 6-3=3,此时a = b = 3,则3即为所求。
程序:
#include <stdio.h>
// 相减法
int gcd(int num1, int num2)
{
while(num1 != num2)
{
if(num1 > num2)
{
num1 -= num2;
}
else
{
num2 -= num1;
}
}
return num1;
}
int main()
{
int a, b;
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
while(a > 0 && b > 0)
{
printf("The greatest common divisor is: %d
", gcd(a,b));
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
}
printf("Loop end! Program will be finished!");
return 0;
}
运行结果:
Please input 2 numbers, seperated by space: 4 6
The greatest common divisor is: 2
Please input 2 numbers, seperated by space: 7 9
The greatest common divisor is: 1
Please input 2 numbers, seperated by space: 9 81
The greatest common divisor is: 9
Please input 2 numbers, seperated by space: 100 128
The greatest common divisor is: 4
Please input 2 numbers, seperated by space: 10000 15000
The greatest common divisor is: 5000
Please input 2 numbers, seperated by space: 0 0
Loop end! Program will be finished!
(四)短除法
思路:
左边部分的因子相乘,即为最大公约数。
所以,12与16的最大公约数为2 * 2 = 4
程序:
#include <stdio.h>
// 短除法
int gcd(int m, int n)
{
int min = m < n ? m : n;
int s = 1;
int i;
for(i = 2; i <= min ; i++)
{
// 四个条件只要有一个不满足,while循环结束
while(m > 0 && n > 0 && 0 == m % i && 0 == n % i)
{
m /= i;
n /= i;
s *= i;
}
}
return s;
}
int main()
{
int a, b;
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
while(a > 0 && b > 0)
{
printf("The greatest common divisor is: %d
", gcd(a,b));
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
}
printf("Loop end! Program will be finished!");
return 0;
}
运行结果:
Please input 2 numbers, seperated by space: 4 6
The greatest common divisor is: 2
Please input 2 numbers, seperated by space: 7 9
The greatest common divisor is: 1
Please input 2 numbers, seperated by space: 9 81
The greatest common divisor is: 9
Please input 2 numbers, seperated by space: 100 128
The greatest common divisor is: 4
Please input 2 numbers, seperated by space: 10000 15000
The greatest common divisor is: 5000
Please input 2 numbers, seperated by space: 0 0
Loop end! Program will be finished!
最小公倍数
一、最小公倍数(Least Common Multiple)
几个自然数公有的倍数,叫做这几个数的公倍数,其中最小的一个,叫做这几个数的最小的一个,叫做这几个数的最小公倍数。例如:4的倍数有4、8、12、16,……,6的倍数有6、12、18、24,……,4和6的公倍数有12、24,……,其中最小的是12,一般记为[4、6]=12。
12、15、18的最小公倍数是180。记为[12、15、18]=180。
二、编程求两个数的最小公倍数
(一)穷举法
#include <stdio.h>
// 穷举法
int lcm(int m, int n)
{
// 取两个数较大的那个。因为最小公倍数不可能比大的那个数还小
int num = m < n ? n : m;
// m*n一定是m和n的公倍数,所以做为循环的结束条件
for(; num <= m * n; num++)
{
if(0 == num % m && 0 == num % n)
{
break;
}
}
return num ;
}
int main()
{
int a, b;
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
while(a > 0 && b > 0)
{
printf("The least common multiple is: %d
", lcm(a,b));
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
}
printf("Program will be finished!");
return 0;
}
运行结果:
Please input 2 numbers, seperated by space: 4 6
The least common multiple is: 12
Please input 2 numbers, seperated by space: 7 13
The least common multiple is: 91
Please input 2 numbers, seperated by space: 1000 1500
The least common multiple is: 3000
Please input 2 numbers, seperated by space: 0 0
Program will be finished!
穷举法的优点是思路简单,缺点是效率低。多数情况下,都不能使用穷举法。但是穷举法本身是一种非常重要的思想。
(二)利用最大公约数求最小公倍数
思路:
lcm(a, b) = a * b / gcd(a, b)
例子:
gcd(12, 16) = 4
lcm(12, 16) = 12 * 16 / gcd(12, 16) = 48
程序:
#include <stdio.h>
// 辗转相除法求最大公约数
int gcd(int m, int n)
{
// remainder,余数
int remainder;
while(n != 0)
{
remainder = m % n;
m = n;
n = remainder;
}
return m;
}
// 最小公倍数 = 两数相乘 / 最大公约数
int lcm(int x, int y)
{
return x * y / gcd(x, y);
}
int main()
{
int a, b;
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
while(a > 0 && b > 0)
{
printf("The least common multiple is: %d
", lcm(a,b));
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
}
printf("Program will be finished!");
return 0;
}
运行结果:
Please input 2 numbers, seperated by space: 4 6
The least common multiple is: 12
Please input 2 numbers, seperated by space: 7 13
The least common multiple is: 91
Please input 2 numbers, seperated by space: 1000 1500
The least common multiple is: 3000
Please input 2 numbers, seperated by space: 0 0
Program will be finished!
(三)短除法
思路:
左边与底部的因子相乘,即为最小公倍数
所以,12与16的最小公倍数为2 * 2 * 3 * 4 = 48
程序:
#include <stdio.h>
// 短除法
int lcm(int m, int n)
{
int min = m < n ? m : n;
int s = 1;
int i;
for(i = 2; i <= min ; i++)
{
// 四个条件只要有一个不满足,while循环结束
while(m > 0 && n > 0 && 0 == m % i && 0 == n % i)
{
m /= i;
n /= i;
s *= i;
}
}
return s * m * n;
}
int main()
{
int a, b;
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
while(a > 0 && b > 0)
{
printf("The least common multiple is: %d
", lcm(a,b));
printf("Please input 2 numbers, seperated by space: ");
scanf("%d %d", &a, &b);
}
printf("Program will be finished!");
return 0;
}
运行结果:
Please input 2 numbers, seperated by space: 4 6
The least common multiple is: 12
Please input 2 numbers, seperated by space: 7 13
The least common multiple is: 91
Please input 2 numbers, seperated by space: 1000 1500
The least common multiple is: 3000
Please input 2 numbers, seperated by space: 0 0
Program will be finished!
寻找发帖水王
(一)题目
Tango是微软亚洲研究院的一个试验项目。研究院的员工和实习生们都很喜欢在Tango上面交流灌水。传说,Tango有一大“水王”,他不但喜欢发帖,还会回复其他ID发的每个帖子。坊间风闻该“水王”发帖数目超过了帖子总数的一半。如果你有一个当前论坛上所有帖子(包括回帖)的列表,其中帖子作者的ID也在表中,你能快速找出这个传说中的Tango水王吗?
(二)分析
思路一:
先对ID进行排序,再遍历排序后的序列,统计每个ID的次数,从而寻找到最大次数的ID。
思路二:
如果每次删除两个不同的ID(不管是否包含“水王”的ID),那么,在剩下的ID列表中,“水王”ID出现的次数仍然超过总数的一半。看到这一点之后,就可以通过不断重复这个过程,把ID列表中的ID总数降低(转化为更小的问题),从而得到问题的答案。新的思路,总的时间复杂度只有O(N),且只需要常数的额外内存。
对比思路一和思路二,第一种思路效率更低,实现起来更复杂。所以这里咱们采用思路二。
(三)代码
#include<iostream>
using namespace std;
int Find(int* ID, int N)
{
int candidate;
int nTimes = 0;
int i;
for(i = 0; i < N; i++)
{
if(nTimes == 0)
{
candidate = ID[i];
nTimes = 1;
}
else
{
if(ID[i] == candidate)
{
nTimes++;
}
else
{
nTimes--;
}
}
}
return candidate;
}
int main(void)
{
int id[] = {1, 2, 2, 4, 2, 4, 2, 2};
int cnt = sizeof(id) / sizeof(int);
int res = Find(id, cnt);
cout << "The water king's id is " << res << endl;
return 0;
}
运行结果:
The water king’s id is 2
说明:
int Find(int* ID, int N)等价于int Find(int ID[], int N),这两种写法是一样的。
这是因为,数组名在做形式参数时,自动退化为指针,这个指针指向了数组的首地址。
分析:
这种算法题,要对循环里的数据逐个分析。
i = 0时,ID[0] = 1, candidate = ID[0] = 1, nTimes 赋值 1
i = 1时,ID[1] = 2, nTimes 自减后为 0(至此,相当于把ID[0] 和 ID[1]删掉)
i = 2时,ID[2] = 2, candidate = ID[2] = 2, nTimes 赋值 1
i = 3时,ID[3] = 4, nTimes 自减后为 0 (至此,相当于把ID[2] 和 ID[3]删掉)
i = 4时,ID[4] = 2, candidate = ID[4] = 2, nTimes 赋值 1
i = 5时,ID[5] = 4, nTimes = 自减后为 0 (至此,相当于把ID[4] 和 ID[5]删掉)
i = 6时,ID[6] = 2, candidate = ID[6] = 2, nTimes 赋值 1
i = 7时,ID[7] = 2, nTimes 自加后为 2
i = 8时,for循环结束。
最终, candidate = 2即为所求。此时nTimes = 2,表示删除之后,ID为2的帖子还剩下两个。
(四)结论
在这个题目中,有一个计算机科学中很普遍的思想,就是如何把一个问题转化为规模较小的若干个问题。分治、递推和贪心等都是基于这样的思路。在转化过程中,小的问题跟原问题本质上一致。这样,我们可以通过同样的方式将小问题转化为更小的问题。因此,转化过程是很重要的。
像上面这个题目,我们保证了问题的解在小问题中仍然具有与原问题相同的性质:水王的ID在ID列表中的次数超过一半。
转化本身计算的效率越高,转化之后问题规模缩小得越快,则整体算法的时间复杂度越低。
求幂pow函数的四种实现方式
在math.h中,声明了一个函数pow(x, n),用于求x的n次方。
假如咱们不调用math.h中的pow函数,如何实现求x ^ n的算法呢?
一、while非递归
#include <stdio.h>
double pow1(double x, unsigned int n)
{
int res = 1;
while(n--)
{
res *= x;
}
return res;
}
int main()
{
printf("2 ^ 10 = %f
", pow1(2, 10));
printf("5 ^ 3 = %f
", pow1(5, 3));
printf("10 ^ 0 = %f
", pow1(10, 0));
return 0;
}
运行结果:
2 ^ 10 = 1024.000000
5 ^ 3 = 125.000000
10 ^ 0 = 1.000000
二、递归方法1
#include <stdio.h>
double pow2(double x, unsigned int n)
{
if(0 == n)
{
return 1;
}
if(1 == n)
{
return x;
}
return x * pow2(x, n - 1);
}
int main()
{
printf("2 ^ 10 = %f
", pow2(2, 10));
printf("5 ^ 3 = %f
", pow2(5, 3));
printf("10 ^ 0 = %f
", pow2(10, 0));
return 0;
}
三、递归方法2
#include <stdio.h>
double pow3(double x, unsigned int n)
{
if(0 == n)
{
return 1;
}
if(1 == n)
{
return x;
}
if(n & 1) // 如果n是奇数
{
// 这里n/2会有余数1,所以需要再乘以一个x
return pow3(x * x, n / 2) * x;
}
else // 如果x是偶数
{
return pow3(x * x, n / 2);
}
}
int main()
{
printf("2 ^ 10 = %f
", pow3(2, 10));
printf("5 ^ 3 = %f
", pow3(5, 3));
printf("10 ^ 0 = %f
", pow3(10, 0));
return 0;
}
四、快速幂
上面三种方法都有一个缺点,就是循环次数多,效率不高。举个例子:
3 ^ 19 = 3 * 3 * 3 * … * 3
直接乘要做18次乘法。但事实上可以这样做,先求出3的2^k次幂:
3 ^ 2 = 3 * 3
3 ^ 4 = (3 ^ 2) * (3 ^ 2)
3 ^ 8 = (3 ^ 4) * (3 ^ 4)
3 ^ 16 = (3 ^ 8) * (3 ^ 8)
再相乘:
3 ^ 19 = 3 ^ (16 + 2 + 1)
= (3 ^ 16) * (3 ^ 2) * 3
这样只要做7次乘法就可以得到结果:
3 ^ 2 一次,
3 ^ 4 一次,
3 ^ 8 一次,
3 ^ 16 一次,
乘四次后得到了3 ^ 16
3 ^ 2 一次,
(3 ^ 2) * 3 一次,
再乘以(3 ^ 16) 一次,
所以乘了7次得到结果。
如果幂更大的话,节省的乘法次数更多(但有可能放不下)。
即使加上一些辅助的存储和运算,也比直接乘高效得多。
我们发现,把19转为2进制数:10011,其各位就是要乘的数。这提示我们利用求二进制位的算法:
所以就可以写出下面的代码:
#include <stdio.h>
double pow4(double x, int n)
{
double res = 1;
while (n)
{
if (n & 1) // 等价于 if (n % 2 != 0)
{
res *= x;
}
n >>= 1;
x *= x;
}
return res;
}
int main()
{
printf("2 ^ 10 = %f
", pow4(2, 10));
printf("5 ^ 3 = %f
", pow4(5, 3));
printf("10 ^ 0 = %f
", pow4(10, 0));
printf("3 ^ 19 = %f
", pow4(3, 19));
return 0;
}
运行结果:
2 ^ 10 = 1024.000000
5 ^ 3 = 125.000000
10 ^ 0 = 1.000000
3 ^ 19 = 1162261467.000000
五、效率比较
#include <stdio.h>
#include <math.h>
#include <time.h>
using namespace std;
#define COUNT 100000000
double pow1(double x, unsigned int n)
{
int res = 1;
while(n--)
{
res *= x;
}
return res;
}
double pow2(double x, unsigned int n)
{
if(0 == n)
{
return 1;
}
if(1 == n)
{
return x;
}
return x * pow2(x, n - 1);
}
double pow3(double x, unsigned int n)
{
if(0 == n)
{
return 1;
}
if(1 == n)
{
return x;
}
if(n & 1) // 如果n是奇数
{
// 这里n/2会有余数1,所以需要再乘以一个x
return pow3(x * x, n / 2) * x;
}
else // 如果x是偶数
{
return pow3(x * x, n / 2);
}
}
double pow4(double x, int n)
{
double result = 1;
while (n)
{
if (n & 1)
result *= x;
n >>= 1;
x *= x;
}
return result;
}
int main()
{
int startTime, endTime;
startTime = clock();
for (int i = 0; i < COUNT; i++)
{
pow(2.0, 100.0);
}
endTime = clock();
printf("调用系统函数计算1亿次,运行时间%d毫秒
", (endTime - startTime));
startTime = clock();
for (int i = 0; i < COUNT; i++)
{
pow1(2.0, 100);
}
endTime = clock();
printf("调用pow1函数计算1亿次,运行时间%d毫秒
", (endTime - startTime));
startTime = clock();
for (int i = 0; i < COUNT; i++)
{
pow2(2.0, 100);
}
endTime = clock();
printf("调用pow2函数计算1亿次,运行时间%d毫秒
", (endTime - startTime));
startTime = clock();
for (int i = 0; i < COUNT; i++)
{
pow3(2.0, 100);
}
endTime = clock();
printf("调用pow3函数计算1亿次,运行时间%d毫秒
", (endTime - startTime));
startTime = clock();
for (int i = 0; i < COUNT; i++)
{
pow4(2.0, 100);
}
endTime = clock();
printf("调用pow4函数计算1亿次,运行时间%d毫秒
", (endTime - startTime));
return 0;
}
运行结果:
调用系统函数计算1亿次,运行时间189毫秒
调用pow1函数计算1亿次,运行时间795670毫秒
调用pow2函数计算1亿次,运行时间89756毫秒
调用pow3函数计算1亿次,运行时间6266毫秒
调用pow4函数计算1亿次,运行时间3224毫秒
从运行结果可以看出来,
最快的是math.h提供的函数pow,
接下来依次是pow4、pow3、 pow2,
最慢的是pow1。
六、math.h中的pow函数源码
我使用的编译器是CodeBlocks,没法查看math.h的源码。
但是我在网络上找到了微软的math.h源码 http://www.bvbcode.com/cn/z9w023j8-107349
这里有关于pow函数的实现
template<class _Ty> inline
_Ty _Pow_int(_Ty _X, int _Y)
{unsigned int _N;
if (_Y >= 0)
_N = _Y;
else
_N = -_Y;
for (_Ty _Z = _Ty(1); ; _X *= _X)
{if ((_N & 1) != 0)
_Z *= _X;
if ((_N >>= 1) == 0)
return (_Y < 0 ? _Ty(1) / _Z : _Z); }}
这个实现思路跟pow4的实现思路是一致的。
七、结论
在实际编程时,可以直接调用math.h提供的pow函数;
如果在特定场合需要自己定义的话,使用pow4的方式。
贪心算法与动态规划算法
一、贪心算法
例子
假设有1元,5元,11元这三种面值的硬币,给定一个整数金额,比如28元,最少使用的硬币组合是什么?
分析
碰到这种问题,咱们很自然会想起先用最大的面值,再用次大的面值……这样得到的结果为两个11元,一个5元,一个1元,总共是四个硬币。
C语言实现
#include<stdio.h>
void greed(int m[],int k,int total);
int main(void)
{
int money[] = {11, 5, 1};
int n;
n = sizeof(money)/sizeof(money[0]);
greed(money, n, 28);
return 0;
}
/*
* m[]:存放可供找零的面值,降序排列
* k:可供找零的面值种类数
* total:需要的总金额
*/
void greed(int m[],int n,int total)
{
int i;
for(i = 0; i < n; i++)
{
while(total >= m[i])
{
printf("%d ", m[i]);
total -= m[i];
}
}
}
运行结果:
11 11 5 1
思想
贪心算法(又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,他所做出的是在某种意义上的局部最优解。
不足
上面的例子,total = 28,得到的“11 11 5 1”恰巧是最优解。
假如total = 15呢?
total = 15时,结果为“11 1 1 1 1”,共用了五枚硬币。但是这只能算是较优解,不是最优解。因为最优解是“5 5 5”,共三枚硬币。
所以贪心算法只能保证局部最优(第一枚11就是局部最优),不能保证全局最优。
二、动态规划算法
咱们仍以15为例,换一种思路,看看如何得到最优解。
(1)面值为1时,最少需要一个一元硬币
(2)面值为2时,最少需要两个一元硬币
(3)面值为3时,最少需要三个一元硬币
(4)面值为4时,最少需要四个一元硬币
(5)面值为5时,有两个方案:
① 在面值为4的基础上加一个1元的硬币,需要五个硬币
② 挑一个面值为5元的硬币,需要一个硬币
取最小值,需要一个硬币
(6)面值为6时,两个方案:
① 比1元(一个硬币)多了5元(一个硬币),需要两个硬币
② 比5元(一个硬币)多了1元(一个硬币),需要两个硬币
取最小值,需要两个硬币
(7)面值为7时,两个方案:
① 比1元(一个硬币)多了6元(两个硬币),需要三个硬币
② 比5元(一个硬币)多了2元(两个硬币),需要三个硬币
取最小值,需要三个硬币
(8)面值为8时,两个方案:
① 比1元(一个硬币)多了7元(三个硬币),需要四个硬币
② 比5元(一个硬币)多了3元(三个硬币),需要四个硬币
取最小值,需要四个硬币
(9)面值为9时,两个方案:
① 比1元(一个硬币)多了8元(四个硬币),需要五个硬币
② 比5元(一个硬币)多了4元(四个硬币),需要五个硬币
取最小值,需要五个硬币
(10)面值为10时,两个方案:
① 比1元(一个硬币)多了9元(五个硬币),需要六个硬币
② 比5元(一个硬币)多了5元(一个硬币),需要两个硬币
取最小值,需要两个硬币
(11)面值为11时,三个方案:
① 比1元(一个硬币)多了10元(两个硬币),需要三个硬币
② 比5元(一个硬币)多了6元(两个硬币),需要三个硬币
③ 取面值为11元的硬币,需要一个硬币
取最小值,需要一个硬币
(12)面值为12时,三个方案:
① 比1元(一个硬币)多了11元(一个硬币),需要两个硬币
② 比5元(一个硬币)多了7元(三个硬币),需要四个硬币
③ 比11元(一个硬币)多了1元(一个硬币),需要两个硬币
取最小值,需要两个硬币
(13)面值为13时,三个方案:
① 比1元(一个硬币)多了12元(两个硬币),需要三个硬币
② 比5元(一个硬币)多了8元(四个硬币),需要五个硬币
③ 比11元(一个硬币)多了2元(两个硬币),需要三个硬币
取最小值,需要三个硬币
(14)面值为14时,三个方案:
① 比1元(一个硬币)多了13元(三个硬币),需要四个硬币
② 比5元(一个硬币)多了9元(五个硬币),需要六个硬币
③ 比11元(一个硬币)多了3元(三个硬币),需要四个硬币
取最小值,需要四个硬币
(15)面值为15时,三个方案:
① 比1元(一个硬币)多了14元(四个硬币),需要五个硬币
② 比5元(一个硬币)多了10元(两个硬币),需要三个硬币
③ 比11元(一个硬币)多了4元(四个硬币),需要五个硬币
取最小值,需要三个硬币
最终,得到的最小硬币数是3。并且从推导过程可以看出,计算一个数额的最少硬币数,比如15,必须把它前面的所有数额(1~14)的最少硬币数都计算出来。这够成了一个递推(注意不是递归)的过程。
上述推导过程的Java实现:
public class CoinDP {
/**
* 动态规划算法
* @param values: 保存所有币值的数组
* @param money: 金额
* @param minCoins: 保存所有金额所需的最小硬币数
*/
public static void dp(int[] values, int money, int[] minCoins) {
int valueKinds = values.length;
minCoins[0] = 0;
// 保存1元、2元、3元、……、money元所需的最小硬币数
for (int sum = 1; sum <= money; sum++) {
// 使用最小币值,需要的硬币数量是最多的
int min = sum;
// 遍历每一种面值的硬币
for (int kind = 0; kind < valueKinds; kind++) {
// 若当前面值的硬币小于总额则分解问题并查表
if (values[kind] <= sum) {
int temp = minCoins[sum - values[kind]] + 1;
if (temp < min) {
min = temp;
}
} else {
break;
}
}
// 保存最小硬币数
minCoins[sum] = min;
System.out.println("面值为 " + sum + " 的最小硬币数 : " + minCoins[sum]);
}
}
public static void main(String[] args) {
// 硬币面值预先已经按升序排列
int[] coinValue = new int[] {1,5,11};
// 需要的金额(15用动态规划得到的是3(5+5+5),用贪心得到的是5(11+1+1+1+1)
int money = 15;
// 保存每一个金额所需的最小硬币数,0号单元舍弃不用,所以要多加1
int[] coinsUsed = new int[money + 1];
dp(coinValue, money, coinsUsed);
}
}
运行结果:
面值为1的最小硬币数:1
面值为2的最小硬币数:2
面值为3的最小硬币数:3
面值为4的最小硬币数:4
面值为5的最小硬币数:1
面值为6的最小硬币数:2
面值为7的最小硬币数:3
面值为8的最小硬币数:4
面值为9的最小硬币数:5
面值为10的最小硬币数:2
面值为11的最小硬币数:1
面值为12的最小硬币数:2
面值为13的最小硬币数:3
面值为14的最小硬币数:4
面值为15的最小硬币数:3
三、贪心算法与动态规划的区别
(1)贪心是求局部最优,但不定是全局最优。若想全局最优,必须证明。
dp是通过一些状态来描述一些子问题,然后通过状态之间的转移来求解。般只要转移方程是正确的,答案必然是正确的。
(2)动态规划本质上是穷举法,只是不重复计算罢了。结果是最优的。复杂度高。
贪心算法不一定最优。复杂度一般较低。
(3)贪心只选择当前最有利的,不考虑这步选择对以后的选择造成的影响,眼光短浅,不能看出全局最优;动规是通过较小规模的局部最优解一步步最终得出全局最优解。
(4)从推导过程来看,动态规划是贪心的泛化,贪心是动态规划的特例