数论是纯粹数学的一个分支,主要研究整数的性质和整数之间的关系。在信息学奥林匹克竞赛(IO)中,数论问题占有重要的地位,涉及的知识面广,技巧性强。数论问题不仅可以单独作为竞赛题目出现,还可以与其他算法(如动态规划、图论、字符串等)结合,形成综合性较强的竞赛题目。
本文将深入解析IO竞赛中的数论算法,包括数论基础、质数相关算法、约数相关算法、同余方程、原根、组合数学等内容,并结合具体的代码实现和实例,帮助读者理解和掌握这些算法。
数论是研究整数的性质和整数之间的关系的数学分支,是纯粹数学的一个重要组成部分。数论的基本概念包括:
数论中有很多重要的定理,这些定理是解决数论问题的基础。以下是一些常用的数论定理:
数论中有很多常用的函数,这些函数在解决数论问题时经常用到。以下是一些常用的数论函数:
质数是数论中的基本概念,也是IO竞赛中的重要考点。质数相关的算法主要包括质数的判定、质数的筛选、质数的分解等。
质数的判定是判断一个数是否为质数的过程。常用的质数判定方法包括试除法、Miller-Rabin素性测试等。
试除法是最基本的质数判定方法,其基本思想是:对于一个大于1的整数n,如果n能被2到√n之间的任何一个整数整除,那么n是合数;否则,n是质数。
代码实现:
#include <iostream>
#include <cmath>
using namespace std;
// 判断一个数是否为质数(试除法)
bool isPrime(int n) {
if (n <= 1) return false;
for (int i = 2; i <= sqrt(n); i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
int main() {
int n;
cin >> n;
if (isPrime(n)) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
return 0;
}试除法的时间复杂度是O(√n),对于比较小的数来说,效率是比较高的。但对于非常大的数来说,试除法的效率会变得很低。
Miller-Rabin素性测试是一种概率性的质数判定方法,其基本思想是基于费马小定理和二次探测定理。Miller-Rabin素性测试的时间复杂度是O(k log^3 n),其中k是测试的轮数,k越大,测试的准确性越高。
代码实现:
#include <iostream>
#include <cstdlib>
#include <ctime>
using namespace std;
typedef long long ll;
// 快速幂取模
ll pow_mod(ll a, ll b, ll mod) {
ll res = 1;
while (b > 0) {
if (b % 2 == 1) {
res = (res * a) % mod;
}
a = (a * a) % mod;
b /= 2;
}
return res;
}
// Miller-Rabin素性测试
bool miller_rabin(ll n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0) return false;
// 将n-1表示为d*2^s
ll d = n - 1;
int s = 0;
while (d % 2 == 0) {
d /= 2;
s++;
}
// 测试k轮
int k = 5;
for (int i = 0; i < k; i++) {
ll a = rand() % (n - 3) + 2; // 随机选取一个a,范围是[2, n-2]
ll x = pow_mod(a, d, n);
if (x == 1 || x == n - 1) {
continue;
}
for (int j = 0; j < s - 1; j++) {
x = pow_mod(x, 2, n);
if (x == n - 1) {
break;
}
}
if (x != n - 1) {
return false;
}
}
return true;
}
int main() {
srand(time(NULL));
ll n;
cin >> n;
if (miller_rabin(n)) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
return 0;
}Miller-Rabin素性测试是一种概率性的测试方法,存在一定的误判概率。但通过多次测试(通常测试5轮),可以将误判概率降低到非常低的水平,对于实际应用来说是足够的。
质数的筛选是找出一定范围内所有质数的过程。常用的质数筛选方法包括埃拉托斯特尼筛法(Eratosthenes Sieve)、欧拉筛法(Euler Sieve)等。
埃拉托斯特尼筛法是一种古老而有效的质数筛选方法,其基本思想是:对于每个质数p,将p的所有倍数标记为合数,从而筛选出所有的质数。
代码实现:
#include <iostream>
#include <vector>
using namespace std;
// 埃拉托斯特尼筛法,找出1到n之间的所有质数
vector<int> sieve_of_eratosthenes(int n) {
vector<bool> is_prime(n + 1, true);
is_prime[0] = is_prime[1] = false;
for (int p = 2; p * p <= n; p++) {
if (is_prime[p]) {
for (int i = p * p; i <= n; i += p) {
is_prime[i] = false;
}
}
}
vector<int> primes;
for (int i = 2; i <= n; i++) {
if (is_prime[i]) {
primes.push_back(i);
}
}
return primes;
}
int main() {
int n;
cin >> n;
vector<int> primes = sieve_of_eratosthenes(n);
for (int p : primes) {
cout << p << " ";
}
cout << endl;
return 0;
}埃拉托斯特尼筛法的时间复杂度是O(n log log n),空间复杂度是O(n)。对于比较大的n来说,埃拉托斯特尼筛法的效率是比较高的。
欧拉筛法(也称为线性筛法)是一种更高效的质数筛选方法,其基本思想是:每个合数只被它的最小质因数筛掉,从而保证每个合数只被筛一次,时间复杂度为O(n)。
代码实现:
#include <iostream>
#include <vector>
using namespace std;
// 欧拉筛法,找出1到n之间的所有质数
vector<int> euler_sieve(int n) {
vector<bool> is_prime(n + 1, true);
vector<int> primes;
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= n; i++) {
if (is_prime[i]) {
primes.push_back(i);
}
for (int p : primes) {
if (i * p > n) {
break;
}
is_prime[i * p] = false;
if (i % p == 0) {
break;
}
}
}
return primes;
}
int main() {
int n;
cin >> n;
vector<int> primes = euler_sieve(n);
for (int p : primes) {
cout << p << " ";
}
cout << endl;
return 0;
}欧拉筛法的时间复杂度是O(n),空间复杂度是O(n),是一种非常高效的质数筛选方法。对于大规模的数据来说,欧拉筛法的效率要高于埃拉托斯特尼筛法。
质因数分解是将一个合数分解成若干个质数的乘积的过程。常用的质因数分解方法包括试除法、Pollard’s Rho算法等。
试除法是最基本的质因数分解方法,其基本思想是:对于一个合数n,从最小的质数2开始,依次用质数去除n,直到n变为1为止。
代码实现:
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
// 质因数分解(试除法)
vector<int> prime_factorization(int n) {
vector<int> factors;
for (int i = 2; i <= sqrt(n); i++) {
while (n % i == 0) {
factors.push_back(i);
n /= i;
}
}
if (n > 1) {
factors.push_back(n);
}
return factors;
}
int main() {
int n;
cin >> n;
vector<int> factors = prime_factorization(n);
for (int factor : factors) {
cout << factor << " ";
}
cout << endl;
return 0;
}试除法的时间复杂度是O(√n),对于比较小的数来说,效率是比较高的。但对于非常大的数来说,试除法的效率会变得很低。
Pollard’s Rho算法是一种用于质因数分解的概率性算法,其基本思想是基于生日悖论和Floyd判圈算法,用于快速找到一个数的非平凡因数。Pollard’s Rho算法的时间复杂度是O(n^(1/4)),对于非常大的数来说,效率要高于试除法。
代码实现:
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
using namespace std;
typedef long long ll;
// 计算最大公约数\lll gcd(ll a, ll b) {
while (b != 0) {
ll temp = a % b;
a = b;
b = temp;
}
return a;
}
// 快速幂取模
ll pow_mod(ll a, ll b, ll mod) {
ll res = 1;
while (b > 0) {
if (b % 2 == 1) {
res = (res * a) % mod;
}
a = (a * a) % mod;
b /= 2;
}
return res;
}
// Miller-Rabin素性测试
bool is_prime(ll n) {
if (n <= 1) return false;
if (n <= 3) return true;
if (n % 2 == 0) return false;
ll d = n - 1;
int s = 0;
while (d % 2 == 0) {
d /= 2;
s++;
}
int k = 5;
for (int i = 0; i < k; i++) {
ll a = rand() % (n - 3) + 2;
ll x = pow_mod(a, d, n);
if (x == 1 || x == n - 1) {
continue;
}
for (int j = 0; j < s - 1; j++) {
x = pow_mod(x, 2, n);
if (x == n - 1) {
break;
}
}
if (x != n - 1) {
return false;
}
}
return true;
}
// Pollard's Rho算法,用于快速找到一个数的非平凡因数
ll pollards_rho(ll n) {
if (n % 2 == 0) return 2;
if (n % 3 == 0) return 3;
if (n % 5 == 0) return 5;
while (true) {
ll x = rand() % (n - 1) + 1;
ll c = rand() % (n - 1) + 1;
ll y = x;
ll d = 1;
auto f = [&](ll x) {
return (pow_mod(x, 2, n) + c) % n;
};
while (d == 1) {
x = f(x);
y = f(f(y));
d = gcd(abs(x - y), n);
}
if (d != n) {
return d;
}
}
}
// 质因数分解(Pollard's Rho算法)
void prime_factor(ll n, vector<ll>& factors) {
if (n == 1) return;
if (is_prime(n)) {
factors.push_back(n);
return;
}
ll d = pollards_rho(n);
prime_factor(d, factors);
prime_factor(n / d, factors);
}
int main() {
srand(time(NULL));
ll n;
cin >> n;
vector<ll> factors;
prime_factor(n, factors);
for (ll factor : factors) {
cout << factor << " ";
}
cout << endl;
return 0;
}Pollard’s Rho算法是一种概率性的算法,存在一定的随机因素,但对于非常大的数来说,其效率要远高于试除法。在实际应用中,Pollard’s Rho算法通常与Miller-Rabin素性测试结合使用,用于高效地分解大数。
约数是数论中的一个基本概念,也是IO竞赛中的重要考点。约数相关的算法主要包括求最大公约数、求最小公倍数、求约数个数、求约数之和等。
最大公约数(GCD)是指两个或多个整数共有约数中最大的一个。求最大公约数的常用方法包括欧几里得算法(辗转相除法)、更相减损术等。
欧几里得算法(辗转相除法)是求最大公约数的最常用方法,其基本思想是基于定理:gcd(a, b) = gcd(b, a mod b)。
代码实现:
#include <iostream>
using namespace std;
// 欧几里得算法求最大公约数\lint gcd(int a, int b) {
while (b != 0) {
int temp = a % b;
a = b;
b = temp;
}
return a;
}
int main() {
int a, b;
cin >> a >> b;
cout << gcd(a, b) << endl;
return 0;
}欧几里得算法的时间复杂度是O(log min(a, b)),空间复杂度是O(1),是一种非常高效的求最大公约数的方法。
更相减损术是中国古代数学中求最大公约数的一种方法,其基本思想是:如果两个数a和b都是偶数,那么gcd(a, b) = 2 * gcd(a/2, b/2);否则,用较大的数减去较小的数,然后继续求两个新数的最大公约数,直到两个数相等为止。
代码实现:
#include <iostream>
using namespace std;
// 更相减损术求最大公约数\lint gcd(int a, int b) {
if (a == b) return a;
if (a % 2 == 0 && b % 2 == 0) {
return 2 * gcd(a / 2, b / 2);
}
if (a % 2 == 0) {
return gcd(a / 2, b);
}
if (b % 2 == 0) {
return gcd(a, b / 2);
}
if (a > b) {
return gcd(a - b, b);
} else {
return gcd(b - a, a);
}
}
int main() {
int a, b;
cin >> a >> b;
cout << gcd(a, b) << endl;
return 0;
}更相减损术的时间复杂度在最坏情况下是O(max(a, b)),空间复杂度是O(1),对于较大的数来说,其效率要低于欧几里得算法。但在某些情况下(如两个数比较接近),更相减损术的效率可能会更高。
最小公倍数(LCM)是指两个或多个整数公有的倍数中最小的一个。求最小公倍数的常用方法是基于最大公约数的公式:lcm(a, b) = a * b / gcd(a, b)。
代码实现:
#include <iostream>
using namespace std;
// 欧几里得算法求最大公约数\lint gcd(int a, int b) {
while (b != 0) {
int temp = a % b;
a = b;
b = temp;
}
return a;
}
// 求最小公倍数\lint lcm(int a, int b) {
return a * b / gcd(a, b);
}
int main() {
int a, b;
cin >> a >> b;
cout << lcm(a, b) << endl;
return 0;
}需要注意的是,在计算a * b时,可能会发生溢出,因此在处理较大的数时,需要使用更大的数据类型(如long long)或者进行适当的优化。
约数个数是指一个数的所有正约数的个数。求约数个数的常用方法是基于质因数分解的公式:如果一个数n的质因数分解为n = p1^a1 * p2^a2 * … * pk^ak,那么n的约数个数为(a1 + 1) * (a2 + 1) * … * (ak + 1)。
代码实现:
#include <iostream>
#include <vector>
#include <cmath>
#include <map>
using namespace std;
// 质因数分解,返回质因数及其指数的映射
map<int, int> prime_factorization(int n) {
map<int, int> factors;
for (int i = 2; i <= sqrt(n); i++) {
while (n % i == 0) {
factors[i]++;
n /= i;
}
}
if (n > 1) {
factors[n]++;
}
return factors;
}
// 求约数个数
int divisor_count(int n) {
map<int, int> factors = prime_factorization(n);
int count = 1;
for (auto& [p, a] : factors) {
count *= (a + 1);
}
return count;
}
int main() {
int n;
cin >> n;
cout << divisor_count(n) << endl;
return 0;
}求约数个数的时间复杂度取决于质因数分解的时间复杂度,对于试除法来说,时间复杂度是O(√n)。
约数之和是指一个数的所有正约数的和。求约数之和的常用方法是基于质因数分解的公式:如果一个数n的质因数分解为n = p1^a1 * p2^a2 * … * pk^ak,那么n的约数之和为(1 + p1 + p1^2 + … + p1^a1) * (1 + p2 + p2^2 + … + p2^a2) * … * (1 + pk + pk^2 + … + pk^ak)。
代码实现:
#include <iostream>
#include <vector>
#include <cmath>
#include <map>
using namespace std;
// 质因数分解,返回质因数及其指数的映射
map<int, int> prime_factorization(int n) {
map<int, int> factors;
for (int i = 2; i <= sqrt(n); i++) {
while (n % i == 0) {
factors[i]++;
n /= i;
}
}
if (n > 1) {
factors[n]++;
}
return factors;
}
// 求等比数列的和:1 + p + p^2 + ... + p^a
int geometric_sum(int p, int a) {
int sum = 1;
int power = 1;
for (int i = 1; i <= a; i++) {
power *= p;
sum += power;
}
return sum;
}
// 求约数之和
int divisor_sum(int n) {
map<int, int> factors = prime_factorization(n);
int sum = 1;
for (auto& [p, a] : factors) {
sum *= geometric_sum(p, a);
}
return sum;
}
int main() {
int n;
cin >> n;
cout << divisor_sum(n) << endl;
return 0;
}求约数之和的时间复杂度也取决于质因数分解的时间复杂度,对于试除法来说,时间复杂度是O(√n)。
同余方程是数论中的一个重要概念,也是IO竞赛中的重要考点。同余方程的求解涉及到很多数论知识和算法,如扩展欧几里得算法、中国剩余定理等。
扩展欧几里得算法是欧几里得算法的扩展,用于求解形如ax + by = gcd(a, b)的贝祖定理方程,其中a和b是整数,x和y是整数解。扩展欧几里得算法不仅可以求出最大公约数,还可以求出贝祖定理方程的一组解。
代码实现:
#include <iostream>
using namespace std;
typedef long long ll;
// 扩展欧几里得算法,返回gcd(a, b),并求解ax + by = gcd(a, b)
ll exgcd(ll a, ll b, ll& x, ll& y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
ll g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}
int main() {
ll a, b, x, y;
cin >> a >> b;
ll g = exgcd(a, b, x, y);
cout << "gcd(" << a << ", " << b << ") = " << g << endl;
cout << a << "*" << x << " + " << b << "*" << y << " = " << g << endl;
return 0;
}扩展欧几里得算法的时间复杂度是O(log min(a, b)),空间复杂度是O(log min(a, b))(递归调用栈的深度)。
线性同余方程是形如ax ≡ b (mod m)的同余方程,其中a、b、m是整数,x是未知整数。求解线性同余方程的常用方法是基于扩展欧几里得算法。
线性同余方程ax ≡ b (mod m)有解的充分必要条件是gcd(a, m) | b。如果有解,那么解的个数是gcd(a, m),解之间的间隔是m / gcd(a, m)。
代码实现:
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
// 扩展欧几里得算法
ll exgcd(ll a, ll b, ll& x, ll& y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
ll g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}
// 求解线性同余方程ax ≡ b (mod m),返回解的个数,如果无解则返回0
ll linear_congruence(ll a, ll b, ll m, vector<ll>& solutions) {
ll x, y;
ll g = exgcd(a, m, x, y);
if (b % g != 0) {
return 0;
}
ll x0 = (x * (b / g) % (m / g) + (m / g)) % (m / g); // 最小正整数解
solutions.push_back(x0);
for (ll i = 1; i < g; i++) {
solutions.push_back((x0 + i * (m / g)) % m);
}
return g;
}
int main() {
ll a, b, m;
cin >> a >> b >> m;
vector<ll> solutions;
ll count = linear_congruence(a, b, m, solutions);
if (count == 0) {
cout << "No solution" << endl;
} else {
cout << "There are " << count << " solutions:" << endl;
for (ll x : solutions) {
cout << x << endl;
}
}
return 0;
}线性同余方程的求解时间复杂度取决于扩展欧几里得算法的时间复杂度,即O(log min(a, m))。
中国剩余定理(Chinese Remainder Theorem,CRT)是数论中的一个重要定理,用于求解同余方程组。中国剩余定理指出,如果m1, m2, …, mk是两两互质的正整数,那么对于任意的整数a1, a2, …, ak,同余方程组x ≡ ai (mod mi)(i=1,2,…,k)有唯一解模M = m1m2…*mk。
代码实现:
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
// 扩展欧几里得算法
ll exgcd(ll a, ll b, ll& x, ll& y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
ll g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}
// 中国剩余定理,求解同余方程组x ≡ ai (mod mi),其中mi两两互质
ll chinese_remainder(const vector<ll>& a, const vector<ll>& m) {
ll M = 1;
for (ll mi : m) {
M *= mi;
}
ll x = 0;
for (int i = 0; i < a.size(); i++) {
ll Mi = M / m[i];
ll ti, yi;
exgcd(Mi, m[i], ti, yi); // 求解Mi*ti ≡ 1 (mod mi)
x = (x + a[i] * Mi * ti) % M;
}
return (x + M) % M; // 确保x是正数
}
int main() {
int k;
cin >> k;
vector<ll> a(k), m(k);
for (int i = 0; i < k; i++) {
cin >> a[i] >> m[i];
}
ll x = chinese_remainder(a, m);
cout << x << endl;
return 0;
}中国剩余定理的时间复杂度取决于扩展欧几里得算法的时间复杂度,即O(k log M),其中k是同余方程的个数,M是所有模的乘积。
原根是数论中的一个重要概念,在密码学、编码理论等领域有广泛的应用。原根的概念比较抽象,理解起来有一定的难度。
原根的定义:如果a和m是互质的正整数,且a的阶等于欧拉函数φ(m),那么称a是模m的原根。其中,a的阶是指最小的正整数k,使得a^k ≡ 1 (mod m)。
原根存在的条件:模m存在原根的充分必要条件是m = 1, 2, 4, pk或2*pk,其中p是奇质数,k是正整数。
判定一个数a是否是模m的原根,需要满足以下条件:
代码实现:
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
typedef long long ll;
// 快速幂取模
ll pow_mod(ll a, ll b, ll mod) {
ll res = 1;
while (b > 0) {
if (b % 2 == 1) {
res = (res * a) % mod;
}
a = (a * a) % mod;
b /= 2;
}
return res;
}
// 计算欧拉函数φ(n)
ll euler_phi(ll n) {
ll res = n;
for (ll i = 2; i * i <= n; i++) {
if (n % i == 0) {
res -= res / i;
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
res -= res / n;
}
return res;
}
// 质因数分解,返回质因数的集合(去重)
vector<ll> prime_factors(ll n) {
vector<ll> factors;
for (ll i = 2; i * i <= n; i++) {
if (n % i == 0) {
factors.push_back(i);
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
factors.push_back(n);
}
return factors;
}
// 判断a是否是模m的原根
bool is_primitive_root(ll a, ll m) {
if (a == 0 || a % m == 0) {
return false;
}
ll phi = euler_phi(m);
vector<ll> factors = prime_factors(phi);
for (ll p : factors) {
if (pow_mod(a, phi / p, m) == 1) {
return false;
}
}
return true;
}
int main() {
ll a, m;
cin >> a >> m;
if (is_primitive_root(a, m)) {
cout << "Yes" << endl;
} else {
cout << "No" << endl;
}
return 0;
}原根判定的时间复杂度取决于快速幂取模和质因数分解的时间复杂度,即O(log^3 m + √φ(m))。
求解模m的最小原根,可以通过枚举的方法,从2开始依次尝试每个数,直到找到最小的原根为止。
代码实现:
#include <iostream>
#include <vector>
#include <cmath>
using namespace std;
typedef long long ll;
// 快速幂取模
ll pow_mod(ll a, ll b, ll mod) {
ll res = 1;
while (b > 0) {
if (b % 2 == 1) {
res = (res * a) % mod;
}
a = (a * a) % mod;
b /= 2;
}
return res;
}
// 计算欧拉函数φ(n)
ll euler_phi(ll n) {
ll res = n;
for (ll i = 2; i * i <= n; i++) {
if (n % i == 0) {
res -= res / i;
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
res -= res / n;
}
return res;
}
// 质因数分解,返回质因数的集合(去重)
vector<ll> prime_factors(ll n) {
vector<ll> factors;
for (ll i = 2; i * i <= n; i++) {
if (n % i == 0) {
factors.push_back(i);
while (n % i == 0) {
n /= i;
}
}
}
if (n > 1) {
factors.push_back(n);
}
return factors;
}
// 判断a是否是模m的原根
bool is_primitive_root(ll a, ll m) {
if (a == 0 || a % m == 0) {
return false;
}
ll phi = euler_phi(m);
vector<ll> factors = prime_factors(phi);
for (ll p : factors) {
if (pow_mod(a, phi / p, m) == 1) {
return false;
}
}
return true;
}
// 求解模m的最小原根
ll find_min_primitive_root(ll m) {
// 检查m是否存在原根
if (m == 1 || m == 2 || m == 4) {
return 1; // 特殊情况
}
if (m % 2 == 0) {
m /= 2;
if (m % 2 == 0) {
return -1; // 不存在原根
}
}
// 检查m是否是奇质数的幂
bool is_prime_power = true;
ll p = m;
for (ll i = 2; i * i <= p; i++) {
if (p % i == 0) {
is_prime_power = false;
break;
}
}
if (!is_prime_power) {
return -1; // 不存在原根
}
// 枚举寻找最小原根
for (ll a = 2; a < m; a++) {
if (is_primitive_root(a, m)) {
return a;
}
}
return -1; // 不存在原根
}
int main() {
ll m;
cin >> m;
ll g = find_min_primitive_root(m);
if (g == -1) {
cout << "No primitive root exists" << endl;
} else {
cout << "The smallest primitive root of " << m << " is " << g << endl;
}
return 0;
}求解原根的时间复杂度取决于原根判定的时间复杂度和枚举的次数,在最坏情况下是O(m log^3 m),但实际上原根通常比较小,所以枚举的次数不会太大。
组合数学是数学的一个分支,主要研究离散对象的计数、排列和组合等问题。在IO竞赛中,组合数学问题占有重要的地位,涉及的知识面广,技巧性强。
排列与组合是组合数学中的基本概念,也是IO竞赛中的重要考点。
排列是指从n个不同的元素中取出m个元素,按照一定的顺序排成一列,记为P(n, m)或A(n, m)。排列数的计算公式为:P(n, m) = n! / (n - m)!,其中n!表示n的阶乘,即n! = n * (n - 1) * … * 2 * 1。
代码实现:
#include <iostream>
using namespace std;
typedef long long ll;
// 计算排列数P(n, m)
ll permutation(int n, int m) {
ll res = 1;
for (int i = 0; i < m; i++) {
res *= (n - i);
}
return res;
}
int main() {
int n, m;
cin >> n >> m;
cout << permutation(n, m) << endl;
return 0;
}组合是指从n个不同的元素中取出m个元素,不考虑顺序,记为C(n, m)或C(n, m)。组合数的计算公式为:C(n, m) = n! / (m! * (n - m)!)。
代码实现:
#include <iostream>
using namespace std;
typedef long long ll;
// 计算组合数C(n, m)
ll combination(int n, int m) {
if (m < 0 || m > n) {
return 0;
}
if (m == 0 || m == n) {
return 1;
}
m = min(m, n - m); // 优化,C(n, m) = C(n, n - m)
ll res = 1;
for (int i = 1; i <= m; i++) {
res = res * (n - m + i) / i;
}
return res;
}
int main() {
int n, m;
cin >> n >> m;
cout << combination(n, m) << endl;
return 0;
}需要注意的是,在计算排列数和组合数时,可能会发生溢出,因此在处理较大的数时,需要使用更大的数据类型(如long long)或者进行模运算。
二项式定理是组合数学中的一个重要定理,描述了二项式的展开式。二项式定理指出:(a + b)^n = Σ_{k=0}^n C(n, k) * a^{n-k} * b^k,其中C(n, k)是组合数。
二项式系数在IO竞赛中有着广泛的应用,如动态规划中的组合计数问题、概率论中的概率计算问题等。
组合数的递推计算是指利用组合数的递推公式来计算组合数。组合数的递推公式为:C(n, m) = C(n - 1, m - 1) + C(n - 1, m),其中边界条件为C(n, 0) = C(n, n) = 1。
代码实现:
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
// 计算组合数C(n, m)(递推法)
ll combination(int n, int m) {
vector<vector<ll>> C(n + 1, vector<ll>(m + 1, 0));
for (int i = 0; i <= n; i++) {
C[i][0] = 1;
if (i <= m) {
C[i][i] = 1;
}
for (int j = 1; j < i && j <= m; j++) {
C[i][j] = C[i - 1][j - 1] + C[i - 1][j];
}
}
return C[n][m];
}
int main() {
int n, m;
cin >> n >> m;
cout << combination(n, m) << endl;
return 0;
}组合数的递推计算的时间复杂度是O(nm),空间复杂度是O(nm),适用于n和m比较小的情况。
在IO竞赛中,经常需要计算组合数模一个数的结果。由于组合数可能非常大,直接计算会导致溢出,因此需要使用模运算来避免溢出。
预处理阶乘和阶乘的逆元是计算组合数模运算的常用方法。其基本思想是预先计算出阶乘和阶乘的逆元,然后利用组合数的公式C(n, m) = n! / (m! * (n - m)!) = n! * inv(m!) * inv((n - m)!) mod p,其中inv(x)表示x的模p逆元。
代码实现:
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAXN = 1e5 + 5;
vector<ll> factorial(MAXN); // 阶乘
vector<ll> inverse_factorial(MAXN); // 阶乘的逆元
// 快速幂取模
ll pow_mod(ll a, ll b, ll mod) {
ll res = 1;
while (b > 0) {
if (b % 2 == 1) {
res = (res * a) % mod;
}
a = (a * a) % mod;
b /= 2;
}
return res;
}
// 预处理阶乘和阶乘的逆元
void precompute() {
factorial[0] = 1;
for (int i = 1; i < MAXN; i++) {
factorial[i] = (factorial[i - 1] * i) % MOD;
}
inverse_factorial[MAXN - 1] = pow_mod(factorial[MAXN - 1], MOD - 2, MOD);
for (int i = MAXN - 2; i >= 0; i--) {
inverse_factorial[i] = (inverse_factorial[i + 1] * (i + 1)) % MOD;
}
}
// 计算组合数C(n, m) mod MOD
ll combination_mod(int n, int m) {
if (m < 0 || m > n) {
return 0;
}
return factorial[n] * inverse_factorial[m] % MOD * inverse_factorial[n - m] % MOD;
}
int main() {
precompute();
int n, m;
cin >> n >> m;
cout << combination_mod(n, m) << endl;
return 0;
}预处理阶乘和阶乘的逆元的时间复杂度是O(MAXN),之后每次查询组合数的时间复杂度是O(1),适用于需要多次查询组合数的情况。
数论高级算法是数论中的一些复杂算法,通常需要综合运用多种数论知识和技巧。在IO竞赛中,数论高级算法是解决复杂数论问题的关键。
莫比乌斯反演是数论中的一个重要定理,用于将一个函数的前缀和转换为另一个函数的前缀和。莫比乌斯反演在数论问题中有着广泛的应用,如容斥原理、Dirichlet卷积等。
莫比乌斯函数μ(n)的定义为:
莫比乌斯反演定理有两种形式:
代码实现:
#include <iostream>
#include <vector>
using namespace std;
const int MAXN = 1e5 + 5;
vector<int> mu(MAXN); // 莫比乌斯函数
vector<bool> is_prime(MAXN, true); // 标记是否为质数
vector<int> primes; // 存储质数
// 预处理莫比乌斯函数
void precompute_mobius() {
fill(is_prime.begin(), is_prime.end(), true);
is_prime[0] = is_prime[1] = false;
mu[1] = 1;
for (int i = 2; i < MAXN; i++) {
if (is_prime[i]) {
primes.push_back(i);
mu[i] = -1;
}
for (int p : primes) {
if (i * p >= MAXN) {
break;
}
is_prime[i * p] = false;
if (i % p == 0) {
mu[i * p] = 0;
break;
} else {
mu[i * p] = mu[i] * mu[p];
}
}
}
}
int main() {
precompute_mobius();
int n;
cin >> n;
cout << "mu(" << n << ") = " << mu[n] << endl;
return 0;
}预处理莫比乌斯函数的时间复杂度是O(MAXN log log MAXN),空间复杂度是O(MAXN)。
杜教筛是一种高效的数论函数前缀和计算方法,由中国OI选手杜宇华发明。杜教筛的基本思想是利用Dirichlet卷积和莫比乌斯反演,将一个数论函数的前缀和转换为另一个数论函数的前缀和,从而降低时间复杂度。
杜教筛的时间复杂度是O(n^(2/3)),适用于计算大规模数论函数的前缀和。
代码实现:
#include <iostream>
#include <vector>
#include <map>
using namespace std;
typedef long long ll;
const int MAXN = 1e6 + 5;
vector<ll> mu(MAXN); // 莫比乌斯函数
vector<bool> is_prime(MAXN, true); // 标记是否为质数
vector<int> primes; // 存储质数
map<ll, ll> mu_sum_map; // 存储已经计算过的莫比乌斯函数前缀和
// 预处理小范围内的莫比乌斯函数
void precompute_mobius() {
fill(is_prime.begin(), is_prime.end(), true);
is_prime[0] = is_prime[1] = false;
mu[1] = 1;
for (int i = 2; i < MAXN; i++) {
if (is_prime[i]) {
primes.push_back(i);
mu[i] = -1;
}
for (int p : primes) {
if (i * p >= MAXN) {
break;
}
is_prime[i * p] = false;
if (i % p == 0) {
mu[i * p] = 0;
break;
} else {
mu[i * p] = mu[i] * mu[p];
}
}
}
// 计算小范围内的莫比乌斯函数前缀和
for (int i = 2; i < MAXN; i++) {
mu[i] += mu[i - 1];
}
}
// 杜教筛计算莫比乌斯函数前缀和S(n) = Σ_{i=1}^n μ(i)
ll mobius_sum(ll n) {
if (n < MAXN) {
return mu[n]; // 小范围内直接返回预处理的结果
}
if (mu_sum_map.find(n) != mu_sum_map.end()) {
return mu_sum_map[n]; // 已经计算过的结果直接返回
}
ll res = 1; // μ(1) = 1
for (ll i = 2, j; i <= n; i = j + 1) {
j = n / (n / i);
res -= (j - i + 1) * mobius_sum(n / i);
}
return mu_sum_map[n] = res;
}
int main() {
precompute_mobius();
ll n;
cin >> n;
cout << "sum_{i=1}^{" << n << "} μ(i) = " << mobius_sum(n) << endl;
return 0;
}杜教筛的时间复杂度是O(n(2/3)),空间复杂度是O(n(1/3)),适用于计算大规模数论函数的前缀和。
数论分块(也称为除法分块)是一种用于优化数论求和问题的技巧,其基本思想是将相似的项合并,从而减少计算量。数论分块在IO竞赛中有着广泛的应用,如莫比乌斯反演、杜教筛等。
数论分块的核心思想是:对于任意的正整数n和d,当d在某个区间内变化时,n/d的值是不变的。我们可以将这些区间找出来,然后对每个区间进行一次计算,从而减少计算量。
代码实现:
#include <iostream>
using namespace std;
typedef long long ll;
// 数论分块计算Σ_{d=1}^n f(d) * g(n/d),其中f(d)和g(k)是已知函数
ll number_theory_block(ll n, auto f, auto g) {
ll res = 0;
for (ll i = 1, j; i <= n; i = j + 1) {
j = n / (n / i);
// 计算f(i) + f(i+1) + ... + f(j)
ll sum_f = 0;
for (ll k = i; k <= j; k++) {
sum_f += f(k);
}
res += sum_f * g(n / i);
}
return res;
}
int main() {
ll n;
cin >> n;
// 例如,计算Σ_{d=1}^n d * (n/d)
auto f = [](ll d) { return d; };
auto g = [](ll k) { return k; };
ll result = number_theory_block(n, f, g);
cout << "sum_{d=1}^{" << n << "} d*(n/d) = " << result << endl;
return 0;
}数论分块的时间复杂度是O(√n),空间复杂度是O(1),适用于优化各种数论求和问题。
数论是IO竞赛中的一个重要分支,掌握数论算法对于提高竞赛成绩至关重要。为了更好地掌握数论算法,我们需要进行系统的实战训练。
在IO竞赛中,数论的应用非常广泛,常见的题型包括:
在IO竞赛中,解决数论问题需要掌握一些实战技巧,这些技巧可以帮助我们更高效地解决问题,避免常见的错误。
题目描述:给定两个正整数a和b,求它们的最大公约数和最小公倍数。
输入:两个正整数a和b(1 ≤ a, b ≤ 1e9)。
输出:两个数,分别是a和b的最大公约数和最小公倍数。
解题思路:这是一个经典的数论问题,可以使用欧几里得算法来求最大公约数,然后利用公式lcm(a, b) = a * b / gcd(a, b)来求最小公倍数。需要注意的是,在计算a * b时,可能会发生溢出,因此需要使用更大的数据类型(如long long)。
代码实现:
#include <iostream>
using namespace std;
typedef long long ll;
// 欧几里得算法求最大公约数
ll gcd(ll a, ll b) {
while (b != 0) {
ll temp = a % b;
a = b;
b = temp;
}
return a;
}
// 求最小公倍数
ll lcm(ll a, ll b) {
return a / gcd(a, b) * b; // 先除以gcd再乘以b,避免溢出
}
int main() {
ll a, b;
cin >> a >> b;
cout << gcd(a, b) << " " << lcm(a, b) << endl;
return 0;
}题目描述:给定线性同余方程ax ≡ b (mod m),求解x的最小正整数解。如果无解,输出-1。
输入:三个整数a、b、m(1 ≤ a, b, m ≤ 1e9)。
输出:x的最小正整数解,如果无解则输出-1。
解题思路:这是一个经典的数论问题,可以使用扩展欧几里得算法来求解。线性同余方程ax ≡ b (mod m)有解的充分必要条件是gcd(a, m) | b。如果有解,我们可以先求出方程ax + my = gcd(a, m)的一组解(x0, y0),然后将其乘以b / gcd(a, m),得到方程ax + my = b的一组解(x1, y1) = (x0 * b / gcd(a, m), y0 * b / gcd(a, m))。最后,x的最小正整数解为(x1 % (m / gcd(a, m)) + m / gcd(a, m)) % (m / gcd(a, m))。
代码实现:
#include <iostream>
using namespace std;
typedef long long ll;
// 扩展欧几里得算法
ll exgcd(ll a, ll b, ll& x, ll& y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
ll g = exgcd(b, a % b, y, x);
y -= a / b * x;
return g;
}
// 求解线性同余方程ax ≡ b (mod m),返回最小正整数解,如果无解则返回-1
ll solve_linear_congruence(ll a, ll b, ll m) {
ll x, y;
ll g = exgcd(a, m, x, y);
if (b % g != 0) {
return -1; // 无解
}
ll x0 = (x * (b / g) % (m / g) + (m / g)) % (m / g); // 最小正整数解
return x0;
}
int main() {
ll a, b, m;
cin >> a >> b >> m;
ll x = solve_linear_congruence(a, b, m);
if (x == -1) {
cout << "No solution" << endl;
} else {
cout << x << endl;
}
return 0;
}题目描述:给定n和m,计算组合数C(n, m)模1e9+7的结果。
输入:两个整数n和m(0 ≤ m ≤ n ≤ 1e5)。
输出:组合数C(n, m)模1e9+7的结果。
解题思路:这是一个经典的组合数学问题,可以通过预处理阶乘和阶乘的逆元来求解。组合数的公式为C(n, m) = n! / (m! * (n - m)!),在模运算下,可以转换为C(n, m) = n! * inv(m!) * inv((n - m)!) mod 1e9+7,其中inv(x)表示x的模1e9+7逆元。我们可以预先计算出阶乘和阶乘的逆元,然后在查询时直接计算即可。
代码实现:
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
const int MOD = 1e9 + 7;
const int MAXN = 1e5 + 5;
vector<ll> factorial(MAXN); // 阶乘
vector<ll> inverse_factorial(MAXN); // 阶乘的逆元
// 快速幂取模
ll pow_mod(ll a, ll b, ll mod) {
ll res = 1;
while (b > 0) {
if (b % 2 == 1) {
res = (res * a) % mod;
}
a = (a * a) % mod;
b /= 2;
}
return res;
}
// 预处理阶乘和阶乘的逆元
void precompute() {
factorial[0] = 1;
for (int i = 1; i < MAXN; i++) {
factorial[i] = (factorial[i - 1] * i) % MOD;
}
inverse_factorial[MAXN - 1] = pow_mod(factorial[MAXN - 1], MOD - 2, MOD);
for (int i = MAXN - 2; i >= 0; i--) {
inverse_factorial[i] = (inverse_factorial[i + 1] * (i + 1)) % MOD;
}
}
// 计算组合数C(n, m) mod MOD
ll combination_mod(int n, int m) {
if (m < 0 || m > n) {
return 0;
}
return factorial[n] * inverse_factorial[m] % MOD * inverse_factorial[n - m] % MOD;
}
int main() {
precompute();
int n, m;
cin >> n >> m;
cout << combination_mod(n, m) << endl;
return 0;
}为了在IO竞赛中更好地掌握数论算法,建议读者进行以下实战训练:
数论是IO竞赛中一个非常重要的分支,它涉及到整数的性质和整数之间的关系。数论问题通常需要我们运用数论知识和算法技巧来解决,如质数的判定、质因数分解、最大公约数、最小公倍数、线性同余方程、中国剩余定理、组合数的计算等。
本文深入解析了IO竞赛中的数论算法,包括数论基础、质数相关算法、约数相关算法、同余方程、原根、组合数学、数论高级算法以及数论实战训练等内容。通过学习这些算法,读者可以掌握数论的基本思想和方法,提高解决数论问题的能力。
在IO竞赛中,解决数论问题需要注意以下几点:首先,要处理好模运算和溢出问题,避免由于数值过大导致错误的结果;其次,要选择合适的算法和数据结构,提高算法的效率;最后,要进行充分的测试和调试,确保算法的正确性。
希望本文对读者有所帮助,祝大家在IO竞赛中取得好成绩!