Loading

数论

模运算

取模操作有三个性质(模数为 p):

加:(a + b) % p = (a % p + b % p) % p

减:(a - b + p) % p = (a % p - b % p + p) % p,因为我们不知道 a % pb % p 的大小关系,可能减出负数,所以需要加上一个 p 再取模。

乘:(a * b) % p = (a % p) * (b % p) % p

但是,需要注意,取模操作是不能对除法进行类似运算的,需要用逆元,下文会讲到。

快速幂

对于幂运算 \(a ^ n\),如果我们暴力的一个一个乘,时间复杂度是 \(O(n)\) 的,当 \(n\) 足够大时,会超时。

所以,我们需要快速幂来解决这个问题。

有两种解法,一种是分治,另一种是利用位运算实现快速幂。

分治

首先,我们可以想到,当 \(n\) 为偶数时,\(a ^ n = a ^ {\frac{n}{2}} \times a ^ {\frac{n}{2}}\) 的,也就是说,我们只要求出 \(a ^ {\frac{n}{2}}\) 就行了。

\(n\) 为奇数时,\(a ^ n = a ^ {\lfloor \frac{n}{2} \rfloor} \times a ^ {\lfloor \frac{n}{2} \rfloor} \times a\),所以我们需要求出 \(a ^ {\lfloor \frac{n}{2} \rfloor}\)

所以,代码如下:

using ll = long long;

ll P(int a, int b) {
  if (!b) {
    return 1;
  }
  ll tmp = P(a, b / 2);
  return tmp * tmp % mod * (b % 2 ? a : 1) % mod;
}

位运算

假设我们要求 \(a ^ {10}\),我们先把 \(10\) 转成二进制,也就是 \(1010\)

我们不难发现,\(a ^ {10} = a ^ 8 \times a ^ 2\),而刚好,\(10\) 的二进制表示的两个 \(1\) 所对应的位权刚好也是 \(8, 2\)

所以,代码如下:

using ll = long long;

ll P(int a, int b) {
  ll ret = 1;
  while (b) {
    if (b & 1) {  // b 的最后一位是 1
      ret = ret * a % mod;
    }
    a = a * a % mod, b >>= 1;
  }
  return ret;
}

两种实现的时间复杂度都是 \(O(\log b)\) 的,但是分治的做法会稍微慢一点点(常数更大),递归是需要一些时间的。

GCD 和 LCM

最大公约数(GCD)和最小公倍数(LCM),研究整除的性质。

整除:

\(a\) 可以整除 \(b\),记为 \(a \mid b\),其中 \(a, b\) 均为整数,\(a \neq 0\)\(b\)\(a\) 的倍数,\(a\)\(b\) 的因子。

性质:

  1. \(a, b, c\) 均为整数,且 \(a \mid b, b \mid c\),则 \(a \mid c\)

  2. \(a, b, m, n\) 均为整数,且 \(c \mid a, c\mid b\),则 \(c \mid (m \times a + n \times b)\)

  3. 定理:带余除法。如果 \(a, b\) 均为整数且 \(b > 0\),则存在唯一的整数 \(1, r\),使得 \(a = b \times q + r\)\(0 \le r < b\)

GCD

gcd(a, b) 指的是 \(a, b\) 的最大公因数,也就是最大的可以同时整除 \(a, b\) 的数。

注意:gcd(a, b) = gcd(|a|, |b|)

欧几里得算法

辗转相除法,时间复杂度为 \(O(\log b)\)

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

LCM

lcm(a, b) 指的是 \(a, b\) 的最小公倍数,从算术基本定理得到。

算术基本定理

任意一个大于 \(1\) 的正整数 \(n\) 都可以分解成有限个素数的乘积:\(n = p_1 ^ {a_1} \times p_2 ^ {a_2} \times \dots \times p_k ^ {a _ k}\),其中 \(a_i\) 都是正整数,\(p_i\) 都是素数且从小到大。

\(x = p_1 ^ {a_1} \times p_2 ^ {a_2} \times \dots \times p_k ^ {a_k}, y = p_1 ^ {b_1} \times p_2 ^ {b_2} \times \dots \times p_k ^ {b_k}\)

那么 \(\gcd (a, b) = p_1 ^ {\min(a_1, b_1)} \times p_2 ^ {\min(a_2, b_2)} \times \dots \times p_k ^ {\min(a_k, b_k)}, lcm(a, b) = p_1 ^ {\max(a_1, b_1)} \times p_2 ^ {\max(a_2, b_2)} \times \dots \times p_k ^ {\max(a_k, b_k)}\)

可以推出 \(lcm(a, b) = a \times b \div \gcd(a, b)\)

注意:在计算 \(lcm\) 时,写 \(lcm(a, b) = a \div \gcd(a, b) \times b\),如果先乘再除,可能会溢出。

所以,代码如下:

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

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

裴蜀定理

一个关于 GCD 的定理。

裴蜀定理:如果 \(a, b\) 均为整数,则有整数 \(x, y\) 使得 \(ax + by = \gcd(a, b)\)

推论:整数 \(a, b\) 互质当且仅当存在整数 \(x, y\),使得 \(ax + by = 1\)

理解:对于任意 \(x, y\)\(d = ax + by\)\(d\) 肯定是 \(\gcd(a, b)\) 的整数倍。

洛谷 P4549

我们先看一对数的情况:\(A_1 X_1 = A_2 X_2\),把它改成 \(ax + by\) 的形式。

根据裴蜀定理,有整数 \(x, y\) 使得 \(ax + by = \gcd(a, b)\),也就是说,\(ax + by\) 的最小值就是 \(\lvert \gcd(a, b) \rvert\),所以 \(A_1 X_1 + A_2 X_2\) 的最小值就是 \(gcd(A_1, A_2)\)

再继续合并 \(A_3, A_4, A_5, \dots , A_N\)

代码如下:

#include <bits/stdc++.h>

using namespace std;

const int N = 25;

int n, a[N];
long long ans;

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

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n;
  for (int i = 1; i <= n; i++) {
    cin >> a[i];
  }
  for (int i = 1; i <= n; i++) {
    ans = abs(gcd(ans, a[i]));  // gcd(ans, a[i]) 可能是负数
  }
  cout << ans;
  return 0;
}

同余

同余:设 \(m\) 是正整数,若 \(m \mid (a - b)\),则说明 \(a \equiv b \ \pmod m\)

定理和性质

\(m\) 为正整数,模 \(m\) 的同余满足以下性质:

  1. 自反性:若 \(a\) 是整数,则 \(a \equiv a \pmod m\)

  2. 对称性:若 \(a, b\) 均为整数,且 \(a \equiv b \pmod m\),则 \(b \equiv a \pmod m\)

  3. 传递性:若 \(a, b, c\) 均为整数,且 \(a \equiv b \pmod m, b \equiv c \pmod m\),则 \(a \equiv c \pmod m\)

\(a, b, c, m\) 均为整数,\(m > 0\),且 \(a \equiv b \pmod m, c \equiv d \pmod m\),则有以下性质:

  1. 加:\(a + c \equiv b + d \pmod m\)

  2. 减:\(a - c \equiv b - d \pmod m\)

  3. 乘:\(ac \equiv bd \pmod m\)

  4. 同余的幂:若 \(k\) 为整数,且 \(k > 0\),则有 \(a ^ k \equiv b ^ k \pmod m\)

逆的概念

给定一个整数 \(a\),且 \(a\)\(m\) 互质,则称 \(ax \equiv 1 \pmod m\) 的一个解为 \(a\) 在模 \(m\) 意义下的逆,记为 \(a ^ {-1}\)

求逆

扩展欧几里得算法(求单个逆)

洛谷 P1082

\(ax \equiv 1 \pmod m\),即 \(ax + my = 1\),先求出这个方程的一个特解 \(x_0\),通解是 \(x = x_0 + mn\)。然后计算最小整数解 \(((x_0 \mod m) + m) \mod m\)

代码如下:

#include <bits/stdc++.h>

using namespace std;
using ll = long long;

ll a, b;

void gcd(ll a, ll b, ll &x, ll &y) {
  if (!b) {
    x = 1, y = 0;
    return ;
  }
  gcd(b, a % b, y, x);
  y -= a / b * x;
}

ll Solve(ll a, ll b) {
  ll x, y;
  gcd(a, b, x, y);
  return (x % b + b) % b;
}

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> a >> b;
  cout << Solve(a, b);
  return 0;
}

时间复杂度为 \(O(\log b)\)

费马小定理(求单个逆)

\(p\) 为质数,且 \(p \nmid a\),有 \(a ^ {p - 1} \equiv 1 \pmod p\)

不给证明,当结论来记。

  1. \(\frac{x}{a}\) 在模 \(p\) 意义下为 \(\frac{x}{a} \equiv \frac{x}{a} \cdot a ^ {p - 1} \equiv x \cdot a ^ {p - 2} \pmod p\)

  2. \(a\) 在模 \(p\) 意义下的乘法逆元为 \(a ^ {p - 2}\)\(p\) 为质数,\(p \nmid a\)),记作:\(a ^ {-1} \nmid a ^ {p - 2} \pmod p\)

\(p\)素数\(a\) 是正整数且 \(a, p\) 互质,则有 \(a ^ {p - 1} \equiv 1 \pmod p\)

\(a \times a ^ {n - 2} \equiv 1 \pmod p\),则 \(a ^ {p - 2}\) 就是 \(a\) 在模 \(p\) 意义下的逆,需要用快速幂求解。

using ll = long long;

ll P(int a, int b) {
  if (!b) {
    return 1;
  }
  ll tmp = P(a, b / 2);
  return tmp * tmp % mod * (b % 2 ? a : 1) % mod;
}

ll F(int x) {
  return P(x, mod - 2);
}

时间复杂度为 \(O(\log mod)\)

递推(求多个逆)

求出 \(1 \sim n\) 的所有逆,用递推,时间复杂度为 \(O(n)\)

洛谷 P3811

首先,\(i = 1\) 的逆是 \(1\),若 \(i > 1\),则这样求:

  1. \(p \div i = k\),余数为 \(r\),则 \(k \times i + r \equiv 0 \pmod p\)

  2. 在等式两边乘上 \(i ^ {-1} \times r ^ {-1}\),得到 \(k \times r ^ {-1} + i ^ {-1} \equiv 0 \pmod p\)

  3. 移项可得 \(i ^ {-1} \equiv -k \times r ^ {-1} \pmod p\),即 \(i ^ {-1} \equiv -p \div i \times r ^ {-1} \pmod p\),也就是 \(i ^ {-1} \equiv (p - p \div i) \times r ^ {-1} \pmod p\)

代码如下:

#include <bits/stdc++.h>

using namespace std;

const int N = 3e6 + 10;

int n, p;
long long inv[N];

int main() {
  ios::sync_with_stdio(0), cin.tie(0);
  cin >> n >> p, inv[1] = 1, cout << "1\n";
  for (int i = 2; i <= n; i++) {
    inv[i] = (p - p / i) * inv[p % i] % p;
    cout << inv[i] << '\n';
  }
  return 0;
}

逆与除法取模

如果我们要求 \((a \div b) \mod m\),可以先将 \(a \div b\),再对 \(m\) 取模。

但是,如果 \(a, b\) 都是很大的数呢?比如说 \(2000!\),会溢出,导致计算错误。

所以,我们记 \(k = b ^ {-1}\),则有 \(b \times k \equiv 1 \pmod m\)

\(a \div b \equiv a \times 1 \div b \equiv a \times (1 \div b) \equiv a \times k \pmod m\)

所以,我们可以求出 \(b\) 的逆,再求 \((a \div b) \mod m\)

素数(质数)

素数:只能被 \(1\) 和自己整除的正整数。

素数的分布:随着整数的增大,素数的分布越来越稀疏。

哥德巴赫猜想:每个大于 \(2\) 的正偶数可以写成两个素数的和。

试除法

我们应该如何判断 \(x\) 是不是一个质数呢?

由于约数是成对出现的,所以我们只需要枚举 \(1 \sim \sqrt{x}\) 中的所有 \(i\),判断 \(x\) 是否可以被 \(i\) 整除即可。

bool Is_Prime(int x) {
  if (x == 2) return 1;
  for (int i = 2; i * i <= x; i++) {
    if (x % i == 0) return 0;
  }
  return 1;
}

素数筛

给定一个 \(n \ (1 \le n \le 10 ^ 6)\),请你求出 \(2 \sim n\) 的所有质数。

如果我们一个一个的判断是不是质数,时间复杂度就是 \(O(n \times \sqrt{n})\),最高会达到 \(10 ^ 9\) 级别,不够优秀。

我们需要快速的筛出所有的质数。

埃氏筛

埃氏筛的本质是利用了质数的定义,也就是说,对于一个质数 \(x\) 来说,\(2 \sim x - 1\) 都不是它的约数,那么,对于 \(2 \sim x - 1\) 中的所有数,它们的倍数中都没有 \(x\),所以,在遍历到某一个质数 \(i\) 时,我们给 \(1 \sim n\) 中所有的 \(i\) 的倍数都打上标记就可以了。

但是,其实,我们是不用从 \(2 \times i\) 开始枚举的,因为 \(2 \times i\) 已经被 \(2\) 筛过了,只需要从 \(i \times i\) 开始即可。

void Eratosthenes(int n) {
  f[1] = 1;
  for (int i = 2; i <= n; i++) {
    if (!f[i]) {
      prime[++c] = i;
      for (int j = i * i; j <= n; j += i) {
        f[j] = 1;
      }
    }
  }
}

这样,时间复杂度就是 \(O(n \times \log \log n)\),接近 \(O(n)\),比较优秀。

欧拉筛

我们先来想一想,为什么埃氏筛有一个 \(\log \log n\) 呢?

因为每个数都会被筛很多次,像 \(12\),它会被 \(2, 3\) 各筛到一次。

所以,欧拉筛的原理就是让每个数都只被它的最小质因数筛一次。

也就是说,枚举到 \(i\) 时,我们给 \(i\) 和已经筛出的所有质数相乘所得到的数打上标记即可。

void Euler(int n) {
  for (int i = 2; i <= n; i++) {
    if (!f[i]) prime[++c] = i;
    for (int j = 1; j <= c; j++) {
      if (prime[j] * i > n) break;
      f[prime[j] * i] = 1;
      if (i % prime[j] == 0) break;
    }
  }
}

事实上,由于我们是用最小质因数来筛的每个数,所以可以顺便记录下最小质因数。

欧拉函数

欧拉函数的定义和性质

欧拉函数的定义:设 \(n\) 是一个正整数,欧拉函数 \(\phi(n)\) 定义为不超过 \(n\) 且与 \(n\) 互素的正整数的个数。

\[\phi(n) = \sum_{i = 1} ^ n \left[ \gcd(i, n) = 1 \right] \]

定理 1

\(p, e\) 是互质的正整数,那么 \(\phi(p \times q) = \phi(p) \times \phi(q)\)

所以欧拉函数是一种积性函数,则有以下推论。

\(n = p_1 ^ {a_1} \times \dots \times p_m ^ {a_m}\),其中 \(p_1, p_2, \dots, p_m\) 互素,则 \(\phi(n) = \phi(p_1 ^ {a_1}) \times \dots \times \phi(p_m ^ {a_m})\)

定理 2

\(n\) 为正整数,那么

\[n = \sum _ {d \mid n} \phi(d) \]

这个定理说明了 \(n\)\(\phi(n)\) 的关系:\(n\) 的正因数(包括 \(1\) 和自身)的欧拉函数之和等于 \(n\)

求欧拉函数的通解公式

欧拉定理

\(m\) 是一个正整数,\(a\) 是一个整数且 \(a\)\(m\) 互质,即 \(\gcd(a, m) = 1\),则有 \(a ^ {\phi(m)} \equiv 1 \pmod m\)

定理 3

\(n = p_1 ^ {a_1} \times \dots \times p_m ^ {a_m}\) 为正整数 \(n\) 的质幂因数分解,那么

\[\phi(n) = n \times (1 - \frac{1}{p_1} \times (1 - \frac{1}{p_2}) \times \dots \times (1 - \frac{1}{p_m})) = n \times \prod _ {i = 1} ^ m (1 - \frac{1}{p_i}) \]

上述公式有以下两种特殊情况。

  1. \(n\) 是质数,\(\phi(n) = n - 1\)

  2. \(n = p ^ k\)\(p\) 是质数,有 \(\phi(n) = \phi(p ^ k) = p ^ k - p ^ {k - 1} = p ^ {k - 1}(p - 1) = p ^ {k - 1} \times \phi(p)\)

所以,可以用 \(O(\sqrt{n})\) 的时间复杂度求出单个欧拉函数。

用线性筛(欧拉筛)求 \(1 \times n\) 内的所有欧拉函数

现在我们需要求 \(1 \sim n\) 的所有欧拉函数,如果我们一个一个的求的话,时间复杂度为 \(O(n \times \sqrt{n})\),效率不够高。

所以,我们需要优化。

首先,我们用线性筛求出 \(1 \sim n\) 所有数的最小质因数,再递推求解所有数的欧拉函数。总时间复杂度为 \(O(n)\)

void Get_Phi() {
  f[1] = 1;
  for (int i = 2; i <= n; i++) {
    if (!f[i]) prime[++c] = i, phi[i] = i - 1;
    for (int j = 1; j <= c; j++) {
      if (prime[j] * i > n) break;
      f[prime[j] * i] = 1;
      phi[prime[j] * i] = phi[i] * (i % prime[j] ? prime[j] - 1 : prime[j]);
      if (i % prime[j] == 0) break;
    }
  }
}

Dilworth 定理

最长上升(下降)子序列长度 = 将序列划分为若干个不上升(不下降)子序列的最少序列个数。

posted @ 2023-08-03 19:44  chengning0909  阅读(94)  评论(2编辑  收藏  举报