算法入门——1 数学基础

导引问题:整数求和

  • 任务:给定一个正整数 n,请计算 1+2+3+…+n 的结果,其中 n <= 50000

    • Sample input:10
    • Sample output:55
  • 常见实现方式:

    #include<stdio.h>
    int main()
    {
        int n, i, sum;
        while(scanf("%d", &n) == 1)
        {
            sum = 0;
            for(i = 1; i <= n; i++)
                sum = sum + i;
            printf("%d\n\n", sum);
        }
        return 0;
    }
    
  • 其它方法:等差数列

    • SUM(n) = 1 + 2 + 3 + …… + n = n * (n + 1) / 2
    • 会有风险!假设用int存储变量n,当n = 50000时,50000 * (50000 + 1) ≈ 25亿,已经超出了int的存储范围;
    • 解决方案:
      • long long存储,输入时使用的占位符是%lld
      • 可以先 / 2,但是会有奇偶问题(如果 n 是奇数,除以 2 就会影响精度)。所以可以先用if…else…做一个判断
        • 如果 n 是偶数,就先 / 2,再 * (n + 1);
        • 如果 n 是奇数,就让 (n + 1) / 2,再 * n。

例1:辗转相除法(欧几里得算法)

  • 任务:给定两个正整数,计算这两个数的最小公倍数;

    • 样例输入:

      10 14
      4 6
      
    • 样例输出:

      70 12
      
  • 解法:

    • 暴力枚举。从 14 开始,15、16……一直枚举下去,判断各个数除以二者能否得到一个整数;
    • 优雅一点的暴力枚举。以 14 的倍数开始枚举;
    • 辗转相除法。LCM(A, B) = A * B / GDC(A, B),即 A 和 B 的最小公倍数 = A * B / A 和 B 的最大公约数;
      • 注意:A 和 B 相乘可能会爆int,解决方案见导引问题:整数求和
      • 将问题从求解 A 和 B 的最小公倍数,转变成了求解 A 和 B 的最大公约数;
  • GCD求解过程:辗转相除法(欧几里得算法)

    在这里插入图片描述

    1. 以求解 10 和 14 的最大公约数为例,假设这个最大公约数为 x ,那么 10 和 14 都是这个 x 的倍数;
    2. 可以推出:若将 14 拆分为 a1 和 a2 这两个数相加后的数,那么其中 a1 就是要求解最大公约数的另一个数的整数倍(此处是 10),a2 这个数也是 x 的倍数(此处就是 14 - 10 = 4)。所以求解 10 和 14 的最大公约数,就等价于求解 10 和 4 的最大公约数;
    3. 由于 10 和 4 做除法发现得到的余数依然不为 0 ,那么继续重复第二步:将 10 拆分成 4 的整数倍(此处是 8)和另一个数(此处是 2)相加,即求解 10 和 4 的最大公约数,就等价于求解 2 和 4 的最大公约数;
    4. 此时可以发现,用 4 / 2 = 2 余 0,所以 2 和 4 的最大公约数就是 2,所以 10 和 4 的最大公约数也是2,所以 10 和 14 的最大公约数也是2;
  • 为什么叫辗转相除法?

    • 可以发现,原本是除数的 10 (还有 4),变成了被除数;
    • 原本是余数的 4(还有 2) ,变成了除数;
  • 伪代码:

    int gcd(int da, int xiao)
    {
        int temp;
        while (xiao != 0)
        {
            temp = da % xiao;
            da = xiao;
            xiao = temp;
        }
        return(da);
    }
    
    • 思考?如果将 10 传给了 da,将14 传给了 xiao,会不会有什么影响?
      • 答:不会,第一次循环就交换回来了,xiao 为 10,da 为 14。

例2:找规律(循环节)

  • 任务:给定一个正整数N,请计算N个N相乘(NN)的结果的个位数是多少(1 <= N <= 1,000)
    • Sample Input:3
    • Sample Output:7
  • 显然不可能直接求解 NN,因为在 N 很大的情况下,最后得出来的数肯定是一个天文数字,计算机中的数据类型存储不下;
  • 解法:
    • 暴力枚举:因为 N 个 N 相乘,只需要做 N-1 次乘法,每一次相乘都只取个位数(而不用整个数都算出来);
      • 缺点:在 N 很大的情况下,可能会超时;
    • 找规律:
      • 分析 N 从 1 到 10 时 NN 的个位数情况:
        • 当 N = 1 时,1 ^ 1 = 1 ,个位数是 1 ;
        • 当 N = 2 时,2 ^ 2 = 4 ,个位数是 4 ;2 ^ 3 = 8 ,个位数是 8 ;2 ^ 4 = 16 ,个位数是 6 ;2 ^ 5 = 32 ,个位数是 2 ,开始循环,周期为 4 ;
        • 当 N = 3 时,3 ^ 1 = 3 ,3 ^ 2 = 9 , 3 ^ 3 = 27 ,个位数是 7 ; 3 ^ 4 = 81 ,个位数是 1 ;3^5 = 243 ,个位数是 3 ,开始循环,周期为 4 ;
        • 当 N = 4 时,4 ^ 1 = 4 ,4 ^ 2 = 16 ,个位数是 6 ; 4 ^ 3 = 64 ,个位数是 4 ,周期为 2 ;
        • 当 N = 5 时,5 的任何正整数次幂个位数都是 5 ;
        • 当 N = 6 时,6 的任何正整数次幂个位数都是 6 ;
        • 当 N = 7 时,7 ^ 1 = 7 ,7 ^ 2 = 49 , 7 ^ 3 = 343 ,个位数是 3 ; 7 ^ 4 = 2401 ,个位数是 1 ; 7 ^ 5 = 16807 ,个位数是 7 ,周期为 4 ;
        • 当 N = 8 时,8 ^ 1 = 8 ,8 ^ 2 = 64 ,个位数是 4 ; 8 ^ 3 = 512 ,个位数是 2 ; 8 ^ 4 = 4096 ,个位数是 6 ; 8 ^ 5 = 32768 ,个位数是 8 ,周期为 4 ;
        • 当 N = 9 时,9 ^ 1 = 9 ,9 ^ 2 = 81 ,个位数是 1 ; 9 ^ 3 = 729 ,个位数是 9 ,周期为 2 ;
        • 当 N = 10 时,10 的任何正整数次幂个位数都是 0 ;
      • 对于任意的 N ,先看 N 的个位数,根据上述规律来确定 NN 的个位数;
        • 比如 N = 13 ,只看个位数 3 ,按照 3 的幂次个位数规律来计算;
        • N = 26 ,只看个位数 6 ,6 的任何正整数次幂个位数都是 6 。

例3:找规律(循环节)

  • 题目:

    • 有一种 fibonacci 数列,定义如下:
      • F(0) = 7,
      • F(1) = 11,
      • F(n) = F(n - 1) + F(n - 2) (n>=2)
    • 给定一个 n (n < 1,000,000),请判断 F(n) 能否被3整除,分别输出 yes 和 no ;
  • 解法:

    • 递归:但是能不用递归就不用,有超时和报栈的风险;

    • 找规律:

      • F(n) % 3 = (F(n - 1) + F(n - 2)) % 3 = (F(n - 1) % 3 + F(n - 2) % 3) % 3,即每一项对 3 取余,都是前两项对 3 取余的和对 3 取余;

        • 当 n = 0 时,F(0) % 3 = 7 % 3 = 1;

        • 当 n = 1 时,F(1) % 3 = 11 % 3 = 2;

        • 当 n = 2 时,F(2) % 3 = (F(2 - 1) + F(2 - 2)) % 3 = (F(2 - 1) % 3 + F(2 - 2) % 3) % 3 = (11 % 3 + 7 % 3) % 3 = 0;

        • 当 n = 3 时,F(3) % 3 = (F(3 - 1) % 3 + F(3 - 2) % 3) % 3 = (0 + 2) % 3 = 2;

        • 当 n = 4 时,F(4) % 3 = (F(4 - 1) % 3 + F(4 - 2) % 3) % 3 = (2 + 0) % 3 = 2;

        • 当 n = 5 时,F(5) % 3 = (F(5 - 1) % 3 + F(5 - 2) % 3) % 3 = (2 + 2) % 3 = 1;

        • 当 n = 6 时,F(6) % 3 = (F(6 - 1) % 3 + F(6 - 2) % 3) % 3 = (1 + 2) % 3 = 0;

        • 当 n = 7 时,F(7) % 3 = (F(7 - 1) % 3 + F(7 - 2) % 3) % 3 = (0 + 1) % 3 = 1;

        • 当 n = 8 时,F(8) % 3 = (F(8 - 1) % 3 + F(8 - 2) % 3) % 3 = (1 + 0) % 3 = 1

        • 当 n = 9 时,F(9) % 3 = (F(9 - 1) % 3 + F(9 - 2) % 3) % 3 = (1 + 1) % 3 = 2

        • 当 n = 10 时,F(10) % 3 = (F(10 - 1) % 3 + F(10 - 2) % 3) % 3 = (2 + 1) % 3 = 0

        • ……

      • 周期为8;

      • 且可以发现:

        • 结果都是 0 1 2 三个数字中的一个,三个数字组成一个两位数字有 9 种可能(个位有 3 种,十位有 3 种,3 * 3 = 9);
        • 在上面的找规律过程中得出的十个结果,每相邻两个结果组成一个两位数字,即12、20、02、22、21、10、01、11、12、20;
        • 可以发现,从第九个两位数开始就开启新的一轮循环,所以通过这种方式也可以发现周期为8。

例4:快速幂运算

  • 任务:求 A ^ B 的最后三位数表示的整数(1 <= A,B <= 10000)

    • Sample Input

      2 3
      12 6
      
    • Sample 0utput

      984
      
  • 解法:

    • 如果 A 和 B 的值的范围很大,用暴力就无法求解;

    • 使用快速幂(二分加速);

      • 递归实现(递归先写出口,即不能递归的情况):

        int power(int a, int n) // a表示底数,n表示指数,求解a的n次方
        {
            int ans;
            if(n == 0) ans = 1; //结束条件
            else
            {
                ans = power(a * a, n / 2); //递归调用。a^n = (a^2)^(n/2)
                if(n % 2 == 1) ans *= a; //n为奇数,就再乘一次底数
            }
            return ans;
        }
        
        • 例:求解 123234 ,要算 233 次乘法;
        • 但是由于 123234 = (1232)117,就只需要算 117 次乘法
        • 但是由于 (1232)117 = ((1232)2)58 * 1232,就只需要算 58 + 1 次乘法
        • ……
      • 非递归(循环)实现:

        int power(int a, int n) // a表示底数,n表示指数,求解a的n次方
        {
            int ans = 1;
            while(n) // 只要 n 不为0,就一直循环下去
            {
                if(n % 2) ans *= a;  //奇数情况(if成立的条件是n % 2 = 1,即 n 为奇数)
                a = a * a;  //底数平方
                n = n / 2;  //指数减半
            }
            return ans;
        }
        
      • 对于以上两种方式,在一些数很大的情况下,只要是有乘法运算的地方,都要取模。

例5:二分查找

  • 给出若干个(可以很多)有序的整数,请查找某个元素是否存在,比如:

    2 3 4 5 6 8 12 20 32 45 65 74 86 95 100
    
    • 请查找以上数列中是否存在某个整数(比如25);
  • 解决:二分查找

    • 2 的下标是 0,被head指针指向;
    • 100 的下标是 14,被tail指针指向;
    • 20是中间值,下标是7,被mid指针指向;
    • 将要查找的数(25),与中间值进行比较;
    • 比中间值大,那么将head指针指向mid所指的数的下一个(32),而mid去指向此时headtail之间的数的中间值(74);
    • 然后继续重复上面两步(注意也有可能移动的是tail指针),直到head指针指向的数大于tail指针指向的数;

    在这里插入图片描述

  • 非递归实现:

    int BiSearch(int a[], int n, int x) //a数组中有n个有序元素,查找x是否在a数组中
    {
        int left = 0, right = n - 1;
        while(left <= right)  // 注意=不能少
        {
            int middle = (left + right)/2;  //整数除法
            if(a[middle] == x)  //找到的情况
                return middle;
            if(x > a[middle])  //如果比中值大
                left = middle + 1;
            else  //如果比中值小
                right = middle - 1;
        }
        
        return -1; //循环结束都没找到,说明找不到
    }
    
  • 递归实现:

    int BiSearch(int a[], int x, int left, int right)
    {
        if(left > right)  //注意不能有=号
            return -1;
        else
        {
            int mid = (left + right) / 2;
            if(a[mid] == x)
                return mid;
            else if(x > a[mid]) // 如果在右边
                return BiSearch(a, x, mid + 1, right);
            else if(x < a[mid]) // 如果在左边
                return BiSearch(a, x, left, mid - 1);
        }
    }
    
  • 思考:在一百万个元素里查找某个元素大约需要比较多少次?

    • 时间复杂度:O(logN)

例6:二分查找

  • 给出方程:8 * x4 + 7 * x3 + 2 * x2 + 3 * x + 6 = Y,其中实数 Y 满足:fabs(Y) <= 1e10。请输出 x 在区间[0, 100]的解,精确到小数点后4位;

    • 意思就是,x 的取值范围是[0, 100],将 x 代入方程,得到的 Y 的结果要满足:fabs(Y) <= 1e10;
    • fabs(Y)表示求解 Y 的绝对值;
    • 1e10 即 1010
  • 可以发现,该方程满足单调递增,那么就可以使用二分法;

    • 可以先在区间[0, 100]内取一个中间值,即 x = 50,代入方程中运算,得到的 Y 经过fabs(Y)后,若超出 1e10 的范围,说明 x 的取值肯定[0, 50)以内;
    • 继续二分
  • 代码:

    #include <bits/stdc++.h>
    using namespace std;
    double Y;
    double left, right, mid;
    double f(double x)
    {
        return 8*pow(x, 4.0) + 7*pow(x, 3.0) + 2*pow(x, 2.0) + 3*x + 6;
    }
    
    int main()
    {
        int t;
        scanf("%d", &t);
        while( t-- )
        {
            scanf("%lf", &Y );
            if( f(0) <= Y && Y <= f(100) ) {
                left = 0;
                right = 100;
                while( right-left > 1e-6 ) {
                    mid = (left + right) / 2;
                    double ans = f(mid); // 将x的取值范围的中间值代入方程,得到结果
                    if( ans > Y ) // 如果得到的结果超出Y规定的范围,说明Y在左边
                        right = mid - 1e-7; // 移动右指针
                    else // 如果得到的结果超出Y规定的范围,说明Y在右边
                        left = mid + 1e-7; // 移动左指针
                }
                printf("%.4f\n", (left+right) / 2 );
            }
            else
                printf("No solution!\n");
        }
        return 0;
    }
    
    • 在编程和数学的科学计数法表示中,1e-6 代表 1 × 1 0 − 6 1×10^{-6} 1×106 ,也就是 0.000001。其中 e 是指数(exponent)的意思 ,e 前面的数字是基数,e 后面的数字表示 10 的指数幂,这里 -6 表示 10 的 -6 次方。在代码中常用来表示一个极小的数值,比如作为精度控制的值,像判断两个浮点数是否相等或者迭代终止条件等场景。

例7:三分查找

  • 给出函数:F(x) = 6 * x7 + 8 * X6 + 7 * X3 + 5 * X2 - y * X,其中实数 y 满足(0 <y < 1e10)。请输出 x 在区间[0, 100]时函数 F(x) 的最小值,结果精确到小数点后4位;

  • 解法:

    • 该函数的原函数不满足单调性,当 x < 1 时,单调递减。当 x > 1 时,单调递增。但是再仔细观察,该函数的导数是单调递增的,所以将该函数求导后,再按照例6去做二分查找;

    • 三分查找:如果一个函数的曲线,是单调的,但是满足凸性,那么就可以使用三分法;

      • 如果是一个上凸的图形:

        1. 将函数的取值范围三等分化(也可以不三等分);

        2. 若左三分之一点(LeftThird)比右三分之一(RightThird)要,说明极值点不会在最右边那一段,那么可以直接将区间缩小至[Left, RightThird],即将 RightThird 赋值给 Right;

          在这里插入图片描述

        3. 然后重复步骤1和步骤2;

        4. 若左三分之一点(LeftThird)比右三分之一(RightThird)要,说明极值点不会在最左边那一段,那么可以直接将区间缩小至[LeftThird, Right],即将 LeftThird 赋值给 Left;

          在这里插入图片描述

        5. 然后重复步骤1和步骤4。

  • 注意:

    • 二分的前提:单调性;
    • 三分的前提:凸性。同时,凸性并不要求可导。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

木木慕慕

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值