数学是算法基础。我们在写题时会经常碰到数论、概率、组合计数方面的问题。如何重复利用已知的数学概念和知识来解决问题,非常关键。
这篇文章是在阅读《算法竞赛入门经典》一书后的总结。如果读者已经清楚且明白其中的知识点,那么可以忽略本文。
记得从高中数学竞赛开始那会就接触过数论,仅仅是接触。数论给我的感觉是很难,里面逻辑性很强,有时候绕不弯来就懵逼了。但是既然算法里面需要,那无论如何也得翻越这座大山。本文只介绍几个小的数论知识,也是经常会出现的基础问题。
1. 唯一分解定理
唯一分解定理,就是说任何一个正整数 N>1 都可以分解成有限个素数的乘积。即
2. 欧几里德算法
乍一看,根本没想起来这个是啥,换个名字大家就知道了,辗转相除法。欧几里德算法就是计算两个数的最大公约数(gcd)。具体计算过程我就不详细说了,下面贴出代码(a与b的顺序无关):
int gcd(int a, int b) {
return b == 0 ? a : gcd(b, a % b);
}
既然是递归,那必然要考虑会不会栈溢出的问题。答案是不会,gcd递归层数不会超过 4.785lgN+1.6723 , 其中 N=max{a,b} 。值得说明的是,让gcd函数递归层数最多的是 gcd(Fn,Fn−1) , Fn 是后文会提到的 Fibonacci 数。因此,时间复杂度为 O(lgN) 。
现在让我们根据以上两个知识点求最小公倍数(lcm),也就方便了。聪明的你应该已经知道, gcd(a,b)∗lcm(a,b)=a∗b
由唯一分解定理,设
void primer_select(int n) {
memset(vis, 0, sizeof(vis));
for (int i = 2; i <= n; i ++)
for (int j = i * 2; j <= n; j += i)
vis[j] = 1;
}
我们来分析一下时间复杂度。每个 i 内部循环的次数是 ni ,这样 O(p)=n2+n3+n4+⋅⋅⋅+nn=O(nlogn) 。我们知道调和级数公式:
void primer_select(int n) {
int m = sqrt(n + 0.5);
memset(vis, 0, sizeof(vis));
for (int i = 2; i <= m; i ++) if (!vis[i])
for(int j = i * i; j <= n; j += i)
vis[j] = 1;
}
另外,如果问给定 n ,不超过 n 的素数个数是多少?根据素数定理:
4. 扩展欧几里德算法
对于不完全为0的非负整数, a,b ,扩展欧几里德算法是在求解最大公约数同时,得到方程 ax+by=gcd(a,b) 的一个解。
思路:
看到 gcd(a,b) 我们立马应该想到 gcd(a,b)=gcd(b,a mod b) 。那么如果我们将原方程变成后者的形式, gcd 一定会有解,然后观察 x,y 的变化。因此我们设原方程解是 x1,y1 ,方程 bx+(a mod b)y=gcd(b,a mod b) 解为 x2,y2
因为
void gcd(int a, int b, int &g, int &x, int &y) {// g是最大公约数的解
if (!b) {
g = a;
x = 1;
y = 0;
}
else {
gcd(b, a % b, g, y, x);
y -= x * (a / b);
}
}
另外,我们通过以上程序求得了方程的一个解 (x1,y1) 。求其他的解,我们得到以下方程
应用:
1. 求不定方程 ax+by=c 的整数解。通过扩展欧几里德算法,我们很容易求解,如果 c 是 gcd(a,b) 的整数倍,有解,且解是方程 ax+by=gcd(a,b) 解的整数倍;如果 c 不是 gcd(a,b) 整数倍,无解。
2. 求模线性方程 ax≡b(mod n)实际上是求方程 ax−b=ny 的解。
5. 模运算
关于模运算,首先我们要知道以下几个公式
应用:
1. 大整数取模
大整数 n 超过long long 能表示的范围,因为 1234=((1∗10+2)∗10+3)∗10+4 ,利用以上的公式,每步取模即可,代码:
scanf("%s%d", n, &m); int len = strlen(n); int ans = 0; for (int i = 0; i < len; i ++) { ans = (int)((long long)ans * 10 + n[i] - '0') % m; printf("%d\n", ans); }
2. 幂取模
输入正整数 a,n,m , 输出 anmodm 。
同样利用以上公式,但是直接遍历复杂度高,我们可以使用分治算法,代码:
int pow_mod(int a, int n, int m) { if (n == 0) return 1; int x = pow_mod(a, n / 2, m); long long ans = (long long) x * x % m; if (n % 2 == 1) ans = ans * a % m; return (int) ans; }
下面,我们通过一个例子来实战一下。
问题描述
: (Uva11582)巨大的斐波那契数。输入两个非负整数a、b和正整数n(0 ≤ a,b< 2^64, 1 ≤ n ≤ 1000),你的任务是计算 f(ab) 除以n的余数。 其中 f(0)=f(1)=1 ,且对于所有非负整数 i , f(i+2)=f(i+1)+f(i) 。 思路
:由于这个数很大,直接运用模运算策略是不可行的。那么我们可以先试着把数取得小一点,看看有什么好的办法,比如,n = 3,斐波那契数列前10项的余数是 1,1,2,0,2,2,1,0,1,1 ,我们会发现,当出现 1,1 时,数列的余数就开始重复,因此,我们只需要针对当前的 n 求出重复长度 m ,那么对于大数 ab 我们 求得 abmodm 则可以对应于 前面的重复余数数列。由题意,n < 1000, 我们知道余数对的个数不会超过 n2 。我们需要预处理 F(0)...F(n2) 。
代码(c++)
:
#define LOCAl
#include
#include
#include
#define LL long long
const int maxn = 1000 + 5;
int F[maxn];
int vis[maxn][maxn];
LL a, b;
int n, M;
int pow_mod(LL a, LL n, int m) {
if (n == 0) return 1;
int x = pow_mod(a, n / 2, m);
LL ans = (LL)x * x % m;
if (n % 2 == 1)
ans = (ans * (a % m)) % m;
return (int) ans;
}
int main() {
#ifdef LOCAl
freopen("input.txt", "r", stdin);
freopen("output.txt", "w", stdout);
#endif // LOCAl
int T;
scanf("%d", &T);
while (T --) {
memset(vis, 0, sizeof(vis));
memset(F, 0, sizeof(F));
scanf("%lld %lld %d", &a, &b, &n);
F[0] = F[1] = 1;
M = 0;
vis[F[0]][F[1]] = 1;
for (int i = 2; i <= n * n; i ++) {
F[i] = (F[i-2] % n + F[i-1] % n) % n;
if (vis[F[i-1]][F[i]]) {
M = i-1;
break;
}
vis[F[i-1]][F[i]] = 1;
}
int index = pow_mod(a, b, M);
printf("%d\n", F[index-1]);
}
return 0;
}
关于数论的知识,算法里面用的还很多,很多题目应该有意识的用已知的知识来解决。另外,代码能力很重要,代码能力很重要,代码能力很重要!重要的事情说三遍。
[1]. 刘汝佳,算法竞赛入门经典,第二版,2014.