ACM 学习笔记(三) 数学入门

快速幂

  1. 快速幂:求 a a a b b b次方,对 m m m取模后的值。即 a b  mod  m a^{b}\ \text{mod}\ m ab mod m。还有一个限制就是 b ≤ 1 0 18 b \leq 10^{18} b1018,算法竞赛中计算机默认计算速度为1秒 1 0 8 10^{8} 108次方运算。因此用循环来计算的话,ac不过,因此需要考虑降低算法时间复杂度。

  考虑对 b b b进行二进制分解。假设 b = 5 b=5 b=5,它的二进制为 101 101 101,其第 0 0 0位有一个 1 1 1,第二位有一个 1 1 1,那么此时 a 5 = a 4 ∗ a 1 a^{5}=a^{4}*a^{1} a5=a4a1。因为 b b b一定会有二进制形式,因此它一定可以分解为 a a a 2 2 2的整数次幂 a 2 n a^{2^{n}} a2n相乘得到。

  那么现在就需要考虑将 b b b分解为二进制形式,并且计算其二进制位数中为1的那个位置的 a 2 n a^{2^{n}} a2n值,其中 n n n表示二进制中1的位数。那么我们需要计算多少次 a 2 n a^{2^{n}} a2n呢? b b b分解为二进制之后有多少位呢?可以知道 n ≤ l o g 2 b n \leq log_{2}b nlog2b,因此这种做法的复杂度是 O ( l o g b ) O(log b) O(logb)。快速幂的写法如下:

long long quick_pow(int a, int b){
    int res = 1;
    if(b&1){
        res = res * a;
    }
    b >> 1;
    a = a * a;
    return res;
}

  严谨写法:

class Solution {
public:
    double myPow(double x, int n) {
        if(x==0) return 0;
        double res = 1;
        long n_long = n;
        if(n_long < 0){
            x = 1/x;
            n_long = -n_long;
        }
        while(n_long > 0){
            if(n_long & 1) res = res * x;
            n_long = n_long >> 1;
            x = x * x;
        }
        return res;
    }
};

  但是题目要求还需要对 m m m取模,所以我们需要利用取模运算的一些性质:

( a + b ) % m = ( a % m + b % m ) % m (a + b) \% m = (a \% m +b \% m) \% m (a+b)%m=(a%m+b%m)%m

( a ∗ b ) % m = ( a % m ∗ b % m ) % m (a*b) \%m = (a \% m *b \% m) \% m (ab)%m=(a%mb%m)%m

  此时程序可改写为:

long long quick_pow(int a, int b, int mod){
    int res = 1;
    if(b&1){
        res = res * a % mod;
    }
    b >> 1;
    a = a * a % mod;
    return res;
}

  这里不能调用pow()函数,因为它返回的是一个浮点数,不能存下 a a a18次方再取模。

欧几里得算法

  求gcd(x,y),就是求 x x x y y y的最大公约数。

  c++中有__gcd(x, y)可以直接输出两个数的gcd。这里我们采用不调用库函数的方式实现。欧几里得算法实际上是在辗转相减法的基础上稍微改进一下变成辗转相除法。

  假设 x x x y y y的最大公约数为 d d d,那么可以表示为: g c d ( x , y ) = d gcd(x, y)=d gcd(x,y)=d。通过以上定义,我们可以推到出: d d d能够整除 x x x d d d也能够整除 y y y。 也就是说 x x x d d d的倍数, y y y也是 d d d的倍数,所以 d d d也会是 x − y x-y xy的最大公约数, d d d也就能够整除 x − y x-y xy了。那么我们可以这样一直做下去,并且为了保证 g c d ( ) gcd() gcd()中参数的非负,所以 x − y x-y xy可以用 x % y x\%y x%y来表示。

  也就是说 x x x y y y的最大公约数也是 x % y x\%y x%y y y y的最大公约数。并且 x % y x\%y x%y一定是小于 y y y的。所以可以推导出:

  1. 大数对小数进行取余操作, 如果取余后的结果为0, 小数为大数的约数。
  2. 大数对小数取余, 如果结果不为0, 则取余后的结果必然是导致小数不能成为大数约数的因子。
#include<bits/stdc++.h>
using namespace std;
int gcd(int x, int y){
    if(!y) return x;
    return gcd(y, x%y);
}
int main(){
    cout << gcd(4, 2) << "\n";
}

取整的经典题目

∑ k = 1 n ⌊ n k ⌋ , n ≤ 1 0 9 \sum_{k=1}^{n} \lfloor \frac{n}{k} \rfloor, n \leq 10^{9} k=1nkn,n109

  这道题目展开就是如下操作:

n / 1 + n / 2 + n / 3 + … + n / n \mathrm{n} / 1+\mathrm{n} / 2+\mathrm{n} / 3+\ldots+\mathrm{n} / \mathrm{n} n/1+n/2+n/3++n/n

  这个数其实是不太好算的,因为它涉及到一个取整操作,并且这个枚举的 k k k在分母,而不在分子。也没有什么数学上的公式使其达到 O ( 1 ) O(1) O(1)。如果用 O ( n ) O(n) O(n)的算法去做的话,肯定会超时。

  考虑以下两种情况:

  1. k ≤ n k \leq \sqrt{n} kn 时,此时的多项式表达展开为: n 1 + ⋯ + n n \frac{n}{1} + \cdots + \frac{n}{\sqrt{n}} 1n++n n,这总共有 n \sqrt{n} n 项。
  2. k > n k > \sqrt{n} k>n 时,此时多项式值的范围可表示为:[1, n \sqrt{n} n ),这里总共也是有 n \sqrt{n} n 项。

  因此这道题目的总共可能的情况就是 2 × n 2 \times \sqrt{n} 2×n 种。因此是可以将算法的时间复杂度降到 O ( n ) O(\sqrt{n}) O(n )的。能降下来的原因就在于这个向下取整的这个操作会使得某些数不需要计算就能知道结果。

  比如当 n = 10 n=10 n=10时,整个的求和过程可以表示为:

10 1 + 10 2 + 10 3 + 10 4 + 10 5 + 10 6 + 10 7 + 10 8 + 10 9 + 10 10 \frac{10}{1} + \frac{10}{2}+ \frac{10}{3}+ \frac{10}{4}+ \frac{10}{5}+\frac{10}{6}+\frac{10}{7}+\frac{10}{8}+\frac{10}{9}+\frac{10}{10} 110+210+310+410+510+610+710+810+910+1010

10 + 5 + 3 + 2 + 2 + 1 + 1 + 1 + 1 + 1 10 + 5 + 3 + 2 + 2+1+1+1+1+1 10+5+3+2+2+1+1+1+1+1

  我们能够做的就是将这个重复的2,重复的1的计算次数减少。我们需要去计算重复的区间长度,和这个区间里面的数值(向下取整后的结果)是多少:

  假设区间的左端点为 L L L,那么区间中的数就是 ⌊ n L ⌋ \lfloor \frac{n}{L} \rfloor Ln,那么右端点 R R R怎么算呢?右端点的数值也为 ⌊ n L ⌋ \lfloor \frac{n}{L} \rfloor Ln,即 n R = ⌊ n L ⌋ \frac{n}{R}=\lfloor \frac{n}{L} \rfloor Rn=Ln,并且需要使得右端点 R R R最大。刚好 ⌊ n L ⌋ \lfloor \frac{n}{L} \rfloor Ln是向下取整,会使得其结果最小,因此右端点的值 R = n / ⌊ n L ⌋ R = n / \lfloor \frac{n}{L} \rfloor R=n/Ln。下一次循环左端点就是右端点 R R R1

#include<bits/stdc++.h>
using namespace std;
int main(){
    freopen("E:\\ACM\\in.txt", "r", stdin);
    int n;
    cin >> n;
    long long ans = 0;
    for(int L=1, R=1; L<=n; L=R+1){
        R = n/(n/L);
        ans = ans + (n/L)*(R-L+1);
    }
    return ans;
}

取整与取模的转换

∑ k = 1 n n   m o d   k , n ≤ 1 0 9 \sum_{k=1}^{n} n \bmod k, n \leq 10^{9} k=1nnmodk,n109

  同样这道题目无法暴力求解。但是取模运算可以转换为取整运算: n   m o d   m = n − m ⌊ n m ⌋ n \bmod m=n-m \lfloor \frac{n}{m} \rfloor nmodm=nmmn。到这一步,这道题就与上一道题一样,就可以解了。

质数与欧拉筛法

  质数是指在大于1的自然数中,除了1和它本身以外不再有其他因数的自然数。

  1. 如何判定一个数是否是质数

  如果用枚举的话,复杂度是 O ( n ) O(n) O(n)的。循环 i i i 2 2 2 n − 1 n-1 n1判断一下 n % i n\%i n%i是否等于 0 0 0。这种方法可以优化一下,暴力计算的复杂度可以优化到 O ( n ) O(\sqrt{n}) O(n ),因为如果一个数不是质数,那么它一定存在一个小于 n \sqrt{n} n 的因数。

  1. 整数的唯一分解定理:对于正整数 n n n,仅存在一种分解方式将其分解为若干质数的乘积: n = p 1 t 1 p 2 t 2 … p k t k n=p_{1}^{t_{1}} p_{2}^{t_{2}} \ldots p_{k}^{t_{k}} n=p1t1p2t2pktk

  此时 n n n有多少个因子呢?其因子可表示为: d = p 1 q 1 p 2 q 2 … p k q k d=p_{1}^{q_{1}} p_{2}^{q_{2}} \ldots p_{k}^{q_{k}} d=p1q1p2q2pkqk,其中 q 1 ≤ t 1 ⋯ q 2 ≤ t k q_{1} \leq t_{1} \cdots q_{2} \leq t_{k} q1t1q2tk。所以总共会有 ( t 1 + 1 ) ∗ ( t 2 + 1 ) ∗ ⋯ ( t k + 1 ) (t_{1}+1)*(t_{2}+1)* \cdots (t_{k}+1) (t1+1)(t2+1)(tk+1)个因子。

  1. n ! n! n!末尾有多少个0 n ≤ 1 0 9 n \leq 10^{9} n109

  如果考虑唯一分解的话,那么 n ! n! n!可以表示成2 t 1 t_{1} t1幂乘以5 t 2 t_{2} t2次幂,再乘上其他质数的若干次幂。然后把 t 1 t_{1} t1 t 2 t_{2} t2取个最小值 m i n ( t 1 , t 2 ) min(t_{1},t_{2}) min(t1,t2)就是这样一个 n n n的阶乘末尾 0 0 0的个数。因为25小,所以 t 1 ≥ t 2 t_{1} \geq t_{2} t1t2 m i n ( t 1 , t 2 ) = t 2 min(t_{1},t_{2})=t_{2} min(t1,t2)=t2,所以就需要求5出现的次数即可。

#include<bits/stdc++.h>
using namespace std;
int main(){
    freopen("E:\\ACM\\in.txt", "r", stdin);
    int n;
    cin >> n;
    int ans = 0;
    for(int i=0; i<=n; i=i+5){
        ans = ans + (i/5);
    }
    return ans;
}

  上述代码时间复杂度为 l o g ( n ) log(n) log(n)

  1. n ! n! n!有多少个因子

  考虑唯一分解定义,先求 1 ∼ n 1 \sim n 1n中所有质数,然后对每个质因子 p i p_{i} pi计算其指数 t i t_{i} ti

  埃氏筛法:从 2 2 2 n n n枚举,判断当前枚举的数是否为质数,如果是,则以该数为因子的所有数均标记为合数,判断到 n \sqrt{n} n 的位置即可。这种方式的总体时间复杂度为 n l o g n n logn nlogn

bool prepare(){
    memset(isprime, true, sizeof(isprime));
    isprime[1] = false; // 不是质数
    for(int i=2; i*i < N; ++i){
        if(isprime[i]){ // 如果是质数
            for(int j=2; j*i<N; ++j){
                isprime[j*i]=false;
            }
        }
    }
}

  上述代码在做第二层循环的时候可以进一步优化, j j j没有必要每次都从 2 2 2开始, j j j i i i枚举更加高效,因为 2 ∗ i , 3 ∗ i ⋯ ( i − 1 ) ∗ i 2*i,3*i \cdots (i-1)*i 2i3i(i1)i之前已经分别在枚举的时候标记为合数了。

bool prepare(){
    memset(isprime, true, sizeof(isprime));
    isprime[1] = false; // 不是质数
    for(int i=2; i*i < N; ++i){
        if(isprime[i]){
            for(int j=i; j*i<N; ++j){
                isprime[j*i]=false;
            }
        }
    }
}

  这种方式的总体时间复杂度为 n l o g l o g n n log logn nloglogn

  欧拉筛法:欧拉筛思想的核心是要保证的是每个合数只被这个合数最小的质因子筛除,而且只筛一次,没有重复筛除,时间复杂度能够降低到 O ( n ) O(n) O(n)

#include <iostream>
#include <vector>
using namespace std;
void get_prime(vector<int> & prime,int upper_bound){ // 传引用
    if(upper_bound < 2)return;
    vector<bool> Is_prime(upper_bound+1,true);
    for(int i = 2; i <= upper_bound; i++){
        if(Is_prime[i])
            prime.push_back(i);
        for(int j = 0; j < prime.size() and i * prime[j] <= upper_bound; j++){
            Is_prime[ i*prime[j] ] = false;
            if(i % prime[j] == 0)break;// 某一个素数可以被当前这个数i整除,退出。保证了一个数只被筛一次。
        }
     }
}
int main(){
    vector<int> prime;
    get_prime(prime, 10000001);
    for(vector<int> :: iterator it = prime.begin(); it not_eq prime.end(); it++)
        cout<<*it<<" ";
    return 0;
}

矩阵乘法

高斯消元

排列数与组合数

斯特林数

容斥原理

卡特兰数

概率

期望

数论前置技能

积性函数

欧拉函数

莫比乌斯函数与莫比乌斯反演

积性函数的运用

杜教筛

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值