求质数

(一)质数

质数,又称为素数,指在一个大于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\n", 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\n", 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\n", 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!

(四)短除法

思路:

 
1.png

左边部分的因子相乘,即为最大公约数。
所以,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\n", 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\n", 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\n", 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!

(三)短除法
思路:

 
1.png

左边与底部的因子相乘,即为最小公倍数
所以,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\n", 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\n", pow1(2, 10));
    printf("5 ^ 3 = %f\n", pow1(5, 3));
    printf("10 ^ 0 = %f\n", 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\n", pow2(2, 10));
    printf("5 ^ 3 = %f\n", pow2(5, 3));
    printf("10 ^ 0 = %f\n", 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\n", pow3(2, 10));
    printf("5 ^ 3 = %f\n", pow3(5, 3));
    printf("10 ^ 0 = %f\n", 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\n", pow4(2, 10));
    printf("5 ^ 3 = %f\n", pow4(5, 3));
    printf("10 ^ 0 = %f\n", pow4(10, 0));
    printf("3 ^ 19 = %f\n", 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毫秒\n", (endTime - startTime));

    startTime = clock();
    for (int i = 0; i < COUNT; i++)
    {
        pow1(2.0, 100);
    }
    endTime = clock();
    printf("调用pow1函数计算1亿次,运行时间%d毫秒\n", (endTime - startTime));

    startTime = clock();
    for (int i = 0; i < COUNT; i++)
    {
        pow2(2.0, 100);
    }
    endTime = clock();
    printf("调用pow2函数计算1亿次,运行时间%d毫秒\n", (endTime - startTime));

    startTime = clock();
    for (int i = 0; i < COUNT; i++)
    {
        pow3(2.0, 100);
    }
    endTime = clock();
    printf("调用pow3函数计算1亿次,运行时间%d毫秒\n", (endTime - startTime));


    startTime = clock();
    for (int i = 0; i < COUNT; i++)
    {
        pow4(2.0, 100);
    }
    endTime = clock();
    printf("调用pow4函数计算1亿次,运行时间%d毫秒\n", (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)从推导过程来看,动态规划是贪心的泛化,贪心是动态规划的特例

 


 

posted on 2018-09-08 00:12  Alan_Fire  阅读(453)  评论(0)    收藏  举报