数学/数论算法

===

Index

大数取模

取模运算的性质

  • 因为 (a%n) - (b%n) 可能小于 n,所以 +n
  • 因为 (a%n)(b%n) 可能溢出,计算前应该强转为 long long
  • a * b % m = (a % m) * (b % m) % m

输入 a 为长度小于 1000 的字符串,b 为小于 100000 的整数

    int big_mod(const string& a, int b) {
        long long ret = 0;
        for (auto c : a) {
            ret = ((ret * 10) % b + (c - '0') % b) % b;
        }
        return (int)ret;
    }

快速幂运算

计算 a^n % mod

  • 时间复杂度:O(log(n))
    int qmi(int a, int n, int mod) {
        long long res = 1;
        while (n) {
            if (n & 1)
                res = res * a % mod;
            a = (long long)(a * a) % p;
            n >>= 1;
        }
        return (int)res;
    }

64位数乘法

ll mul(ll a, ll b, ll p) {
    ll ans = 0;
    for (; b; b >>= 1) {
        if (b & 1) ans = (ans + a) % p;
        a = (ll) a * 2 % p;
    }
    return ans;
}

最大公约数

int gcd(int a, int b) {
    return b == 0 ? a : gcd(b, a % b);
}

最小公倍数

int lcm(int a, int b) {
    return a / gcd(a, b) * b;
}

试除法判定质数

bool isPrime(int x) {
    if (x < 2) return false;
    for (int i = 2; i <= x / i; i ++ )
        if (x % i == 0)
            return false;
    return true;
}

试除法分解质因数

vector<pair<int,int>> getDivisors(int x) {
    vector<pair<int,int>> res;
    for (int i = 2; i <= x / i; ++i) {
        if (x % i == 0) {
            int s = 0;
            while (x % i == 0) 
                x /= i, s++;
            res.emplace_back(i,s);
        }
    }
    if (x > 1) res.emplace_back(x,1);
    return res;
}

试除法求所有约数

vector<int> getDivisors(int x) {
    vector<int> res;
    for (int i = 1; i <= x / i; ++i) 
        if (x % i == 0) {
            res.push_back(i);
            if (i != x / i) res.push_back(x / i);
        }
    sort(res.begin(), res.end());
    return res;
}

素数筛法

1.朴素筛法求素数

  • 时间复杂度 O(nlog(n))
int primes[N], cnt; // primes[]存储所有素数
bool st[N];   //st[x]存储x是否被筛掉
void getPrimes(int n) {
    for (int i = 2; i <= n; ++i) {
        if (st[i]) continue;    //st[i]说明i不是素数
        primes[cnt++] = i;
        for (int j = i + i; j <= n; j += i)
            st[j] = true;
    }
}

2.埃氏筛素数

  • 时间复杂度:O(nloglogn)
int primes[N], cnt; // primes[]存储所有素数
bool st[N];   //st[x]存储x是否被筛掉
void getPrimes(int n) {
    for (int i = 2; i <= n; ++i) {
        if (st[i]) continue;
        primes[cnt ++ ] = i;
        for (int j = 2 * i; j <= n; j += i)
            st[j] = true;
    }
}

3.线性筛法求素数

  • 时间复杂度 O(n)
  • n只会被最小的质因子筛掉,每个数只有一个最小质因子,所以每个数只会被筛一次
  • 当pj∣i时,pj一定是i的最小质因子,也一定是pj∗i的最小质因子
  • 当pj∤i时,pj一定小于i的最小质因子,pj也一定是pj∗i的最小质因子
int primes[N], cnt;     // primes[]存储所有素数
bool st[N];
void getPrimes(int n) {
    for (int i = 2; i <= n; ++i) {
        if (!st[i]) 
            primes[cnt++] = i;
        for (int j = 0; primes[j] <= n / i; ++j) {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) 
                break;
        }
    }
}

约数个数和约数之和

如果 N = p1^c1 * p2^c2 * ... *pk^ck
约数个数: (c1 + 1) * (c2 + 1) * ... * (ck + 1)
约数之和: (p1^0 + p1^1 + ... + p1^c1) * ... * (pk^0 + pk^1 + ... + pk^ck)

欧拉函数

定义:1 ~ N  N 互质的数的个数被称为欧拉函数,记为ϕ(N)
 N = p1^a1 * p2^a2 * ... * pm^am 则:
ϕ(N) = N * (p1-1)/p1 * (p2-1)/p2 * ... * (pm-1)/pm
    int phi(int x) {
        int res = x;
        for (int i = 2; i <= x / i; i ++ )
            if (x % i == 0) {
                res = res / i * (i - 1);
                while (x % i == 0) x /= i;
            }
        if (x > 1) res = res / x * (x - 1);
        return res;
    }

筛法求欧拉函数(求1 ~ n中每个数的欧拉函数之和是多少)

  • 借鉴线性筛法的思路
  • 当i为质数时,ϕ(i)=i−1
  • 枚举质数过程中:
  • 当pj∣i时,pj一定是i的最小质因子,则
  • ϕ(pj*i) = pj * ϕ(i)
  • 当pj∤i时,pj一定小于i的最小质因子,pj也一定是pj∗i的最小质因子, 则
  • ϕ(pj*i) = (pj - 1) * ϕ(i)
int primes[N], cnt;     // primes[]存储所有素数
int euler[N];           // 存储每个数的欧拉函数
bool st[N];         // st[x]存储x是否被筛掉

void get_eulers(int n) {
    euler[1] = 1;
    for (int i = 2; i <= n; i ++ ) {
        if (!st[i]) {
            primes[cnt ++ ] = i;
            euler[i] = i - 1;
        }
        for (int j = 0; primes[j] <= n / i; j ++ ) {
            int t = primes[j] * i;
            st[t] = true;
            if (i % primes[j] == 0) {
                euler[t] = euler[i] * primes[j];
                break;
            }
            euler[t] = euler[i] * (primes[j] - 1);
        }
    }
}

扩展欧几里得算法

求x, y,使得ax + by = gcd(a, b)

    int exgcd(int a, int b, int &x, int &y) {
        if (!b) {
            x = 1; y = 0;
            return a;
        }
        int d = exgcd(b, a % b, y, x);
        y -= (a/b) * x;
        return d;
    }

高斯消元

m 个方程,n个未知数的线性方程组

a11 * x1 + ... + a1n * xn = b1
a21 * x1 + ... + a2n * xn = b2
...
an1 * x1 + ... + ann * xn = bn

初等行变换:把某一行乘一个非零的数,交换某两行,把某行的若干倍加到另一行 解的个数:根据最后得到的上三角矩阵判断

  • 如果是完美的阶梯型矩阵,则有唯一解;
  • 如果存在0=b(b≠0)的行,则无解;
  • 否则有无穷解(存在0=0的行)

步骤

  • 枚举每一列c:
  • 找到当前这一列绝对值最大的一行;
  • 将该行换到未固定的方程的最上方,之后将该方程固定;
  • 将该行第一个非零数变成1;
  • 将下方所有行的第c列消成0
   // a[N][N]是增广矩阵
    int gauss() {
        int c, r;

        for (c = 0, r = 0; c < n; c++) {
            int t = c;

            for (int i = r; i < n; i++)
                if (abs(a[i][c]) > abs(a[t][c]))
                    t = i;

            if (abs(a[t][c]) < eps) continue;

            for (int i = c; i < n + 1; i++) swap(a[t][i], a[r][i]);
            for (int i = n; i >= 0; i--) a[r][i] /= a[r][c];

            for (int i = r + 1; i < n; i++)
                if (abs(a[i][c]) > eps)
                    for (int j = n; j >= c; j--)
                        a[i][j] -= a[r][j] * a[i][c];

            r++;
        }

        if (r < n) {
            for (int i = r; i < n; i++)
                if (abs(a[i][n]) > eps)
                    return 0;

            return 1;
        }

        for (int i = n - 1; i >= 0; i--)
            for (int j = i + 1; j < n; j++)
                a[i][n] -= a[j][n] * a[i][j];

        return 2;
    }

组合计数

数据不同范围,对时间复杂度要求不同

  • 求 c(a, b) mod (1e9 + 7)
  • 1 <= b <= a <= 2000
  • 时间复杂度 O(n^2)
    for (int i = 0; i < N; i ++ )
        for (int j = 0; j <= i; j ++ )
            if (!j) c[i][j] = 1;
            else 
                c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
  • 求 c(a, b) mod (1e9 + 7)
  • 1 <= b <= a <= 10^5
  • 时间复杂度 O(n(log(n)))
  • 预处理出所有数的阶乘和逆元,mod是质数,可以用费马小定理
    void init() {
        fact[0] = infact[0] = 1;
        for (int i = 1; i < N; i ++ ) {
            fact[i] = fact[i - 1] * i % mod;
            infact[i] = infact[i - 1] * qmi(i, mod - 2, mod) % mod;
        }
    }
  • 求 c(a, b) mod p
  • 1 <= b <= a <= 10^18, 1 <= p <= 10^5, p 是质数
  • O(p * log(p) * log_p(N))
  • Lucas定理:c(a,b) ≡ c(a mod p, b mod p) * c(a/p, b/p) (mod p)
using LL = long long;
LL C(int a, int b) {
    LL ret = 1;
    for (int i = 1, j = a; i <= b; i ++, j -- ) {
        ret = ret * j % p;
        ret = ret * qmi(i, p - 2) % p;
    }
    return ret;
}

LL lucas(LL a, LL b) {
    if (a < p && b < p) return C(a, b);
    return C(a % p, b % p) * lucas(a / p, b / p) % p;
}

前缀和

一维前缀和

 for(int i = 1;i <= n; i++) 
    s[i] = s[i - 1] + a[i];

二维前缀和

for (int i = 1; i <= n; ++i) 
    for (int j = 1; j <= m; ++j) 
        s[i][j] = s[i- 1][j] + s[i][j - 1] - s[i - 1][j - 1] + a[i - 1][j - 1];

约瑟夫环

0,1,...n-1 这n个数排成一个圆圈,从数字0开始,每次从这个圆圈里删除第m个数字,求出这个圆圈里剩下的最后一个数字。

一句话题解:通过剩余x人时的答案,反推x+1人时的答案。由于只剩一个人时答案必然为0,层层反推即可。 首先,我们用f(n)表示剩下n个人的时候,从当前起点开始走f(n)步会到达赢家的位置。 假设现在有n个人活着,那么 当前起点是0,,杀完编号为m-1的人后, 新的起点则是m,假设f(n - 1)已知,那么我们需要走m步到新的起点,再走f(n-1)步走到赢家的位置,即一共需要走的步数为: m + f(n - 1) // m是新起点,f(n-1)是额外步数

int lastRemaining(int n, int m) {
    /*
    设idx表示上一次的结果,l表示当前人数,
    则 f(l) = (m + idx) % l;
    */
    int idx = 0;
    for (int l = 2; l <= n; ++l) {
        idx = (m + idx) % l;
    }
    return idx;
}

卡特兰数

给定n个0和n个1,它们按照某种顺序排成长度为2n的序列.
满足任意前缀中0的个数都不少于1的个数的序列的数量为: 
Cat(n) = C(2n, n) / (n + 1)

施罗德数

在组合数学中,施罗德数用来描述从 (0,0)(0,0) 到 (n,n)(n,n) 的网格中,只能使用 (1,0)、(0,1)、(1,1)(1,0)、(0,1)、(1,1) 三种移动方式,始终位于对角线下方且不越过对角线的路径数。

施罗德数的前几项(从第 00 项算起)为 1,2,6,22,90,394,1806,8558,41586,2060981,2,6,22,90,394,1806,8558,41586,206098…… 下图为 n=1,2,3n=1,2,3 时的施罗德路径



施罗德数的递推公式为: s_i = s_(i-1) + sigma(j = 0, i - 1) s_j * s _ (n-j-1)

不过 O(n2)的递推在 1e5级别的数据面前显然是要超时的

有递推式 (i + 1)F_i = (6n - 3)F_(i-1) - (i - 2)F_(i - 2)

使得 F_0 = S_0, 对于任意的 i >= 1 ,都有 2*F_i = S_i

using LL = long long;
const int N = 100010, mod = 1e9 + 7;

int qmi(int a, int k) {...}

LL f[N], s[N];

void init(int n) {
    f[0] = s[0] = f[1] = 1;
    s[1] = 2;
    for (int i = 2; i <= n; ++i) {
        f[i] =  ((6*i-3) * f[i-1] - (i-2) * f[i-2]) % mod * qmi(i+1, mod-2) % mod;
        s[i] = f[i] * 2 % mod;
    }
}

nim游戏

给定N堆物品,第i堆物品有Ai个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。两人都采取最优策略,问先手是否必胜。

我们把这种游戏称为NIM博弈。把游戏过程中面临的状态称为局面。整局游戏第一个行动的称为先手,第二个行动的称为后手。若在某一局面下无论采取何种行动,都会输掉游戏,则称该局面必败。

所谓采取最优策略是指,若在某一局面下存在某种行动,使得行动后对面面临必败局面,则优先采取该行动。同时,这样的局面被称为必胜。我们讨论的博弈问题一般都只考虑理想情况,即两人均无失误,都采取最优策略行动时游戏的结果。 NIM博弈不存在平局,只有先手必胜和先手必败两种情况。

定理:NIM博弈先手必胜,当且仅当 A1 ^ A2 ^ … ^ An != 0

打赏一下

取消

感谢您的支持,我会继续努力的!

扫码支持
扫码支持
扫码打赏,你说多少就多少

打开支付宝扫一扫,即可进行扫码打赏哦