几个有用的数学概念-数论

数学是算法基础。我们在写题时会经常碰到数论、概率、组合计数方面的问题。如何重复利用已知的数学概念和知识来解决问题,非常关键。

这篇文章是在阅读《算法竞赛入门经典》一书后的总结。如果读者已经清楚且明白其中的知识点,那么可以忽略本文。


数论

记得从高中数学竞赛开始那会就接触过数论,仅仅是接触。数论给我的感觉是很难,里面逻辑性很强,有时候绕不弯来就懵逼了。但是既然算法里面需要,那无论如何也得翻越这座大山。本文只介绍几个小的数论知识,也是经常会出现的基础问题。

1. 唯一分解定理
唯一分解定理,就是说任何一个正整数 N>1 都可以分解成有限个素数的乘积。即

N=Pa11Pa22Pa33...
这一点挺重要的,很多题目比如求约数、倍数、分数。都可以用这个定理解决。但是多个素数的集合,我们怎么得到呢?这需要Eratosthenes筛选法(素数筛选法),见 3。

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,Fn1) Fn 是后文会提到的 Fibonacci 数。因此,时间复杂度为 O(lgN)

现在让我们根据以上两个知识点求最小公倍数(lcm),也就方便了。聪明的你应该已经知道, gcd(a,b)lcm(a,b)=ab
由唯一分解定理,设

a=pe11pe22pe33....perrb=pf11pf22pf33....pfrr
那么,
gcd(a,b)=pmin{e1,f1}1pmin{e2,f2}2pmin{e3,f3}3....pmin{er,fr}rlcm(a,b)=pmax{e1,f1}1pmax{e2,f2}2pmax{e3,f3}3....pmax{er,fr}r

3. 素数筛选
素数是一类很特别的数,它除了1和本身,没有其他的因子。所以素数与素数之间是相互没有公共因子的,这样的性质常用来解决问题,比如无平方因子数。素数筛选法思想很简单,对于给定一个 n , 我们依次删除 不超过 n 的非负整数 p 的 2倍、3倍、4倍 … … 那没有被删除的就是素数。为了方便,我们用数组 vis[i] 表示数 i 是否已经删除,代码如下:

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) 。我们知道调和级数公式:

1+12+13+14++1n=ln(n+1)+γ
,其中 γ0.577218 是欧拉常数。调和级数是发散的。时间复杂度允许短时间得到 106 以内所有素数。但是还可以改进,有些是可以不用继续判断的,如下:

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 的素数个数是多少?根据素数定理:

π(x)=xlnx
它和 xlnx 比较接近。

4. 扩展欧几里德算法
对于不完全为0的非负整数, ab ,扩展欧几里德算法是在求解最大公约数同时,得到方程 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

因为

bx2+(a mod b)y=gcd(b,a mod b)bx2+(a(ab)b)y2=gcd(b,a mod b)ay2+b(x2aby2)=gcd(b,a mod b)
那么,
x1=y2y1=x2aby2
递归代码:

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) 。求其他的解,我们得到以下方程

ax1+by1=ax2+by2a(x1x2)=b(y2y1)
g=gcd(a,b) , 则
ag(x1x2)=bg(y2y1)
由于 ag bg 互素,因此
x1x2=bgy2y1=ag
最后,设k是整数,则原方程任意解可以写成
(x1kbg,y1+kag)

应用:
1. 求不定方程 ax+by=c 的整数解。

通过扩展欧几里德算法,我们很容易求解,如果 c gcd(a,b) 的整数倍,有解,且解是方程 ax+by=gcd(a,b) 解的整数倍;如果 c 不是 gcd(a,b) 整数倍,无解。

2. 求模线性方程 axb(mod n)

实际上是求方程 axb=ny 的解。

5. 模运算
关于模运算,首先我们要知道以下几个公式

(a+b)modn=((amodn)+(bmodn))modn(ab)modn=((amodn)(bmodn)+n)modnabmodn=((amodn)(bmodn))modn

应用:
1. 大整数取模
大整数 n 超过long long 能表示的范围,因为 1234=((110+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;
}

实例

下面,我们通过一个例子来实战一下。

  1. 问题描述: (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)

  2. 思路:由于这个数很大,直接运用模运算策略是不可行的。那么我们可以先试着把数取得小一点,看看有什么好的办法,比如,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)

  3. 代码(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.

你可能感兴趣的:(算法与数据结构)