《Alogrithms》算法学习笔记——第一章:导语与数论

本文基于《Algorithms》进行学习归纳,本文无论多简单的代码,本人都会尝试用C++将其实现一遍。

零、前言

0.1 本篇介绍

本文是针对于《algorithms》书本进行的学习总结。

0.2 从斐波那契开始了解算法

计算机算法中非常经典的就是Fibonacci算法,他的优化非常的简单,同时也非常的典型。

Fibonacci的定义非常简单,就是当前项的值为前两项之和,所以直接简简单单用递归实现。
F n = { F n − 1 + F n − 2 if n > 1 1 if n = 1 0 if n = 0 F_n = \begin{cases} F_{n-1} + F_{n-2} & \text{if n > 1} \\ 1 & \text{if n = 1} \\ 0 & \text{if n = 0} \\ \end{cases} Fn= Fn1+Fn210if n > 1if n = 1if n = 0

static int fibonacci_base(int x) { // Fibonacci基础递归
  if (x == 0) return 0;
  if (x == 1) return 1;
  return fibonacci_base(x - 1) + fibonacci_base(x - 2);
}

然后我们就会想这个算法是不是不够优,便发现了这个算法的时间消耗,随着项数的增加会飞速增长。

于是我们拆解这个算法就会发现,重复计算太多了,所以我们不如用空间记录下可能会重复计算的值(我一般叫他备忘录),这样可以大幅减少消耗了。

在这里插入图片描述

static int fibonacci_memorandum(int x) { // 采用字典做备忘录的Fibonacci
  vector<int> memorandum;
  memorandum.push_back(0);
  memorandum.push_back(1);
  for (int i = 2; i <= x; i++)
    memorandum.push_back(memorandum[i - 1] + memorandum[i - 2]);
  return memorandum[x];
}

再想更多一点,其实Fibonacci到后面数字会很大,超过整形。如果用大数计算,时间上就不能单纯当成数字计算了,因为大数计算比整形计算要慢许多。

所以Fibonacci很好的说明了算法需要更多的优化。

0.3 O符号的意思

O符号平常也被我们称为时间复杂度,是用来将精确的时间消耗统一化简为几个固定近似值的操作。

例如

  1. 14 n 2 {14n}^{2} 14n2可以说成 O ( n 2 ) O({n}^{2}) O(n2):删除常数;
  2. 3 n 3 + 8 n 2 + 7 {3n}^{3}+{8n}^{2}+7 3n3+8n2+7可以说为 O ( n 3 ) O({n}^{3}) O(n3):删除小项。

除此之外我们建立一些比较的方式

  • O ( 1 ) < O ( l o g n ) < O ( n ) < O ( n l o g n ) < O ( n 2 ) < O ( 2 n ) O(1) < O(logn) < O(n) < O(nlogn) < O({n}^{2}) < O({2}^{n}) O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(2n)
  • 即使是 ( l o g n ) 3 {(logn)}^{3} (logn)3,也会小于 O ( n ) O(n) O(n)

以及书上有一句话很有道理:不要认为是程序员过于傲慢,放弃这么多东西,程序员也会为了减少一倍的时间而精修细改,但没有时间复杂度的概念提供的简单性,确实会导致算法学习时的难以理解

其他人不一定需要了解这件事,但我认为程序员自己必须抱有这种态度去编程。

一、数论

可恶啊,第一章就是数论,我人都看麻了呜呜呜

1.1 基础运算

1.1.1 加法运算

这里讲加法运算,主要是让我们去回归一下加法的原理,同时也在了解计算机是怎么去做加法运算的。

回归小学课本,加法运算是将我们的数字居右写上去,然后逐位相加,如果溢出呢,就把溢出的送给左边的下一位。这里我们用 53 + 35 53+35 53+35的二进制加法举例:

在这里插入图片描述

然后下面的代码,我用位运算符实现了一遍(因为在一个简单加法函数里面有加法可太生草了):

static int addition_base(int x, int y) {
  stack<int> st;

  int overflow = 0; // 进位
  while (x && y) { // 重叠位加法
    // 提取末位
    int low_x = x & 1, low_y = y & 1;
    x >>= 1; y >>= 1;

    // 计算当前位加法结果
    if (low_x && low_y && overflow) { // 全为1
      overflow = 1;
      st.push(1);
    } else if (!low_x && !low_y && !overflow) { // 全为0
      overflow = 0;
      st.push(0);
    } else if ((low_x && low_y) || (low_y && overflow) || (low_x && overflow)) { // 其中有两项为1
      overflow = 1;
      st.push(0);
    } else { // 剩下的就是只有一项为1了
      overflow = 0;
      st.push(1);
    }
  }
  while (x) { // 假如x有剩余
    int low = x & 1;
    x >>= 1;
    if (low && overflow) { // 都为1
      st.push(0);
    } else if (!low && !overflow) { // 都为0
      st.push(0);
    } else { // 其中一个为1
      st.push(1);
      overflow = 0;
    }
  }
  while (y) { // 假如y有剩余
    int low = y & 1;
    y >>= 1;
    if (low && overflow) { // 都为1
      st.push(0);
    } else if (!low && !overflow) { // 都为0
      st.push(0);
    } else { // 其中一个为1
      st.push(1);
      overflow = 0;
    }
  }
  if (overflow) st.push(1); // 加后二进制多了一位

  int mul = 0;
  while (!st.empty()) {
    mul <<= 1;
    mul |= st.top();
    st.pop();
  }
  return mul;
}

所以在理论上其实加法的复杂度是 O ( n ) O(n) O(n)

1.1.2 乘除运算

再次回到小学,我们会想起来我们做乘除运算的时候是一位一位乘,然后按情况移位最后相加,如图:

在这里插入图片描述

既然加法是线性的,那我们也能很快的理解,这样的乘法是 O ( n 2 ) O({n}^{2}) O(n2)的时间复杂度:

static int multiply_base(int x, int y) { // 基础乘法
  int mul = 0; // ans
  for (int i = 1; x; i++) {
    int low = x & 1;
    x >>= 1;
    if (low) mul += y << (i - 1);
  }
  return mul;
}

那么经典问题又来了,能不能优化一下呢?这个时候就发现大佬(Al Khwarizmi)发现了另一种方法。

我们可以通过对一个数一直除2(假设为 x x x),右边相应的进行乘2,直到 x x x变成1了停止,最后把所有 x x x为奇数的那一行的 y y y加起来,就是乘法结果:

在这里插入图片描述

这个方法我们仔细看一下就会发现,三个相加的数分别是 13 × 2 0 13 \times 2^0 13×20 13 × 2 1 13 \times {2}^{1} 13×21 13 × 2 3 13 \times {2}^{3} 13×23,而指数正好与行数对应。这说明什么,说明和我们之前使用的二进制乘法并无什么区别,相当于把十进制的数变成二进制做计算了。这个时候主要解决两个问题:

  1. 为什么要删除行:因为二进制的乘法,如果当前为是0,最后这一行要加的数也就是0;
  2. 为什么要乘2的n次方:这就是向左移位,二进制左移一位就是乘2咯。

那我们来看一下代码:

static int multiply_binary(int x, int y) {
  if (!x) return 0;
  int mul = multiply_binary(x >> 1, y);
  if (x & 1) // 奇数,为需要统计的数值*2,并加上新的数值
    return y + (mul << 1);
  else // 偶数,需要统计的仍旧*2,不加值(因为偶数表示当前二进制最后一位的数值为0,可以略去)
    return mul << 1;
}

这里是采用递归实现的一个简单的乘法。但其实最后发现还是多项相加,最后还是 O ( n 2 ) O({n}^{2}) O(n2)

再次提问:可以更好吗?我们看起来乘法就是需要多项整数相加,似乎已经不能更优了。其实不然,后面章节会给出更优的方法替代。

ps:因为咱实现的就是乘法,所以我没有使用乘法,用位运算代替了。

除法有商和余数,简单来说就是每次除2的时候,如果末尾为1,就把余数记下来,并且按位左移。如果余数过大就删除,然后使商加1。下面展示一下通过二进制的方法实现除法:

static pair<int, int> division_binary(int x, int y) {
  if (!x) return {0, 0};
  pair<int, int> div = division_binary(x >> 1, y);
  div.first <<= 1; // 除数左移
  div.second <<= 1; // 余数左移
  // 奇数表示当前数字除2余数为1(因为末尾为1),则加1(最后一位置1,因为末位经过左移一定为0)
  if (x & 1)  div.second |= 1;
  // 余数比除数大了,就进位
  if (div.second >= y) {
    div.first |= 1;
    div.second -= y;
  }
  return div;
}

1.2 模运算

1.2.0 初步了解模运算

模式什么意思呢?

我们平常在数字超过一定范围之后会开始循环,例如一天24小时,一年12个月这样。在计算机里也是如此,整数类型一共只有32位,超过之后就会在范围内循环,此时,我们就可以简单的去定义[模]的含义了。

对模的定义有两种说法:

  1. 第一种就如我们上面所说的,我们让数字保持在一个范围内,如果大小为 N N N,那就是 { 0 , 1 , . . . , N − 1 } \{0, 1, . . . , N − 1\} {0,1,...,N1} 。就像时钟一样来回往复;
  2. 第二种是把所有的数字化成 N N N个等价类,每个等价类中的数字全部看成是一样的,下图以模3为例。
    … − 9 − 6 − 3 0 3 6 9 … … − 8 − 5 − 2 1 4 7 10 … … − 7 − 4 − 1 2 5 8 11 … \begin{matrix} \dots & -9 & -6 & -3 & 0 & 3 & 6 & 9 & \dots \\ \dots & -8 & -5 & -2 & 1 & 4 & 7 & 10 & \dots \\ \dots & -7 & -4 & -1 & 2 & 5 & 8 & 11 & \dots \\ \end{matrix} 98765432101234567891011

那模怎么求呢,求的方法采用上面讲过的除法取余数。

同时我们将两个模相等的关系记为 x ≡ y ( m o d   N ) ⇐ ⇒ N d i v i d e s ( x − y ) x≡y (mod \ N) ⇐⇒ Ndivides(x−y) xy(mod N)⇐⇒Ndivides(xy),我们也把这种关系称为同余(余数相同)

相同模下的两个值也可以进行四则运算(包含交换、结合、分配律),如:

  1. 前提条件: x ≡ x ′ ( m o d   N ) x \equiv x′ (mod \ N) xx(mod N) and y ≡ y ′ ( m o d   N ) y \equiv y′ (mod \ N) yy(mod N)
  2. x + y ≡ x ′ + y ′ ( m o d   N ) x+y \equiv x′+y′ (mod \ N) x+yx+y(mod N)
  3. x y ≡ x ′ y ′ ( m o d   N ) xy \equiv x′y′ (mod \ N) xyxy(mod N)
  4. x + ( y + z ) ≡ ( x + y ) + z ( m o d   N ) x+(y+z) \equiv (x+y)+z (mod \ N) x+(y+z)(x+y)+z(mod N)
  5. x y ≡ y x ( m o d   N ) xy \equiv yx (mod \ N) xyyx(mod N)
  6. x ( y + z ) ≡ x y + y z ( m o d   N ) x(y + z) \equiv xy + yz (mod \ N) x(y+z)xy+yz(mod N)

这里就提前注意一下,我们是不能进行除法的。

1.2.1 模加与模乘

  • 模加的时间随着数字大小线性增加(因为两个数比模小的数相加不会超过模值的两倍);
  • 模乘的时间是平方的复杂度(因为要通过平方时间的求余);
  • 提前说一下:除法较为麻烦,因为有不合理的情况(除数为0),在合理范围内的时间复杂度基本稳定在立方级别。

1.2.2 指数模

我们考虑一个问题,如果碰到了一个超超超超大数咋办,比如 2 666666 {2}^{666666} 2666666,这可太大了。

我们以最简单的思路来想,那无非就是将指数从1一直增加到666666:
x   m o d   N → x 2   m o d   N → x 3   m o d   N → ⋯ → x y   m o d   N {x \ mod \ N} \rightarrow {x^2 \ mod \ N} \rightarrow {x^3 \ mod \ N} \rightarrow \dots \rightarrow {x^y \ mod \ N} x mod Nx2 mod Nx3 mod Nxy mod N
但是这样时间已经增涨的可快,随着指数大小增减。有啥更快的方法么,还是有的,其实不一定是每次增长1,而是每次增长平方就行。无论是 x x x还是 x % N x \% N x%N,大小都在 N N N​之内。上图的第一步其实就是平方了,所以结果上限已经确定了,后面在用增1反而亏了。
x   m o d   N → x 2   m o d   N → x 4   m o d   N → x 8   m o d   N → ⋯ → x 2 ⌊ l o g   y ⌋   m o d   N {x \ mod \ N} \rightarrow {x^2 \ mod \ N} \rightarrow {x^4 \ mod \ N} \rightarrow {x^8 \ mod \ N} \rightarrow \dots \rightarrow {x^{2^{\lfloor log \ y \rfloor}} \ mod \ N} x mod Nx2 mod Nx4 mod Nx8 mod Nx2log y mod N
其实这样也相当于把他给二进制分解了:
x 25 = x 1100 1 2 = x 1000 0 2 ⋅ x 100 0 2 ⋅ x 1 2 = x 16 ⋅ x 8 ⋅ x 1 x^{25} = x^{11001_2} = x^{10000_2} · x^{1000_2} · x^{1_2} = x^{16} · x^8 · x^1 x25=x110012=x100002x10002x12=x16x8x1
一般也把这个算法叫快速幂,用递归简单来说就是,当前末尾为1表示有这一项,为0表示没有这一项,有的就成上去,没有的就不管咯。
x y = { ( x ⌊ y / 2 ⌋ ) 2 if y is even x ⋅ ( x ⌊ y / 2 ⌋ ) 2 if y is odd x^y = \begin{cases} {(x^{\lfloor y / 2 \rfloor})}^2 & \text{if y is even} \\ x · {(x^{\lfloor y / 2 \rfloor})}^2 & \text{if y is odd} \\ \end{cases} xy={(xy/2)2x(xy/2)2if y is evenif y is odd
那为啥奇偶都要平方呢?因为不管怎么样,前面已经计算过的肯定都要进位咯,下面看代码:

static int modpow(int x, int y, int mod) {
  if (!y) return 1; // x^0=1
  int ans = modpow(x, y >> 1, mod); // 递归采用每次增大一个平方用于计算
  if (y & 1)
    return (ll)ans * ans % mod * x % mod;
  else
    return (ll)ans * ans % mod;
}

这其中递归为先行,求模为平方,所以时间复杂度为 O ( n 3 ) O({n}^{3}) O(n3)

1.2.3 欧几里德最大公因数算法

这个算法被称为最大公因数算法(greatest common divisor)。

从基本概念上我们怎么求呢?

  • 我们可以求出两个数的所有因子,然后把共同的因子乘起来就是最大公因数了。

可这方法太慢了,欧几里德就提出了一条概念:如果 x x x y y y为正整数,且 x ≥ y x \geq y xy,此时 g c d ( x , y ) = g c d ( x % y , y ) gcd(x, y) = gcd(x \% y, y) gcd(x,y)=gcd(x%y,y)

证明一下:

一个整数能整除x和y,一定能整除x-y: g c d ( x , y ) ≤ g c d ( x − y , y ) gcd(x, y) \leq gcd(x − y, y) gcd(x,y)gcd(xy,y)

一个整数能整除x-y和y,一定能整除x: g c d ( x , y ) ≥ g c d ( x − y , y ) gcd(x, y) \geq gcd(x − y, y) gcd(x,y)gcd(xy,y)

这里的等式为什么成立,因为同余满足加减法运算。

同时这里还有一条推论:如果 a ≥ b a \geq b ab,则 a % b < a / 2 a \% b < a / 2 a%b<a/2

证明一下:

如果 b < a / 2 b < a / 2 b<a/2,因为 a % b < b a \% b < b a%b<b,所以 a % b < a / 2 a \% b < a / 2 a%b<a/2

如果 b > a / 2 b > a / 2 b>a/2,因为 a % b = a − b a \% b = a - b a%b=ab a − b < a / 2 a - b < a / 2 ab<a/2,所以 a % b < a / 2 a \% b < a / 2 a%b<a/2

在这里插入图片描述

这样至少每次长度可以减少一半,不过时间上还算是线性,加上求余操作,所以时间复杂度还是 O ( n 3 ) O({n}^{3}) O(n3),看一下代码吧:

static int gcd(int x, int y) {
  return y == 0 ? x : gcd(y, x % y);
}

在这里虽然要求 x x x y y y小,但输入如果不是 x > y x>y x>y也莫的事,因为如果 x < y x<y x<y,第一次会直接换位置( x x x y y y的值互换)

1.2.4 欧几里德算法拓展

这个拓展主要是针对于最大公因数的证明而展开的。

假设一个数为 d d d可以整除 a a a b b b,这并不能证明 d d d a a a b b b的最大公因数。因为只要是因子,都有这个性质。

所以现在提出一条定理:

  • 如果 d d d可以整除 a a a b b b,同时 d = a x + b y d=ax+by d=ax+by(x、y为整数),这可以证明 d = g c d ( a , b ) d=gcd(a, b) d=gcd(a,b)

证明之前,首先我们引出一条定理:裴蜀定理

  • 对于任意的整数a,b,都存在一对整数x,y,使得 a x + b y = g c d ( a , b ) ax+by=gcd(a,b) ax+by=gcd(a,b)成立;

    简单来说是怎么证明的呢:

    因为辗转相除递归到最后一步,一定是可以实现这条理论,因为是 g c d ( d , 0 ) gcd(d, 0) gcd(d,0),只需要 x x x y y y为1、0就行。

    此时我们分解一下过程: g c d ( 𝑎 , 𝑏 ) = g c d ( 𝑏 , 𝑎 % 𝑏 ) gcd(𝑎,𝑏)=gcd(𝑏,𝑎\%𝑏) gcd(a,b)=gcd(b,a%b)。这个时候,如果已经到达了最后一步,也就是 g c d ( d , 0 ) gcd(d, 0) gcd(d,0)。那一定会满足:
    b x + ( a % b ) y = g c d ( b , a % b ) bx+(a\%b)y=gcd(b,a\%b) bx+(a%b)y=gcd(b,a%b)
    又因为:
    𝑎 % 𝑏 = 𝑎 − 𝑏 ⌊ 𝑎 / 𝑏 ⌋ 𝑎\%𝑏=𝑎−𝑏⌊𝑎/𝑏⌋ a%b=aba/b
    所以可以得出以下推导:
    g c d ( 𝑏 , 𝑎 % 𝑏 ) = 𝑏 𝑥 + ( 𝑎 % 𝑏 ) 𝑦 = 𝑏 𝑥 + ( 𝑎 − 𝑏 ⌊ 𝑎 / 𝑏 ⌋ ) 𝑦 = 𝑎 𝑦 − 𝑏 ( 𝑥 − ⌊ 𝑎 / 𝑏 ⌋ 𝑦 ) gcd(𝑏,𝑎\%𝑏) = 𝑏𝑥+(𝑎\%𝑏)𝑦 = 𝑏𝑥+(𝑎−𝑏⌊𝑎/𝑏⌋)𝑦=𝑎𝑦−𝑏(𝑥−⌊𝑎/𝑏⌋𝑦) gcd(b,a%b)=bx+(a%b)y=bx+(aba/b⌋)y=ayb(xa/by)
    然后通过数学归纳法就可以证明出来d=gcd(a,b)=ax+by。

    同时根据上面的公式也提示了我们代码怎么写:每一阶的x=y’和y=𝑥’−⌊𝑎/𝑏⌋𝑦’(不带撇表示当前阶,带撇的表示更小的一阶)

  • 然后可以得到一条推论:如果一个数 m m m满足: a x + b y = m ax+by=m ax+by=m,那么这个 m m m一定是 g c d ( a , b ) gcd(a,b) gcd(a,b)的倍数。(同时也可以说明如果 a x + b y = 1 ax+by=1 ax+by=1,此时 a a a b b b互质。

    简单证明一下:

    其实就是因为a与b都是 g c d ( a , b ) gcd(a,b) gcd(a,b)的倍数,所以无论 a a a还是 b b b,增加或者减少了,结果都还是 g c d ( a , b ) gcd(a,b) gcd(a,b)的倍数。

  • 然后根据我的理解证明,d一定不能是最大公约数其他的因数:
    因为 a x + b y ax+by ax+by一定是 g c d ( a , b ) gcd(a, b) gcd(a,b)的倍数,而且 d = g c d ( a , b ) d=gcd(a, b) d=gcd(a,b)是最大公约数,所以取一个中间不会有其他数的小范围: 0 0 0 d d d 2 d 2d 2d 0 0 0一定不满足, 2 d 2d 2d一定不满足(因为已经是最大公约数了所以一定没有其他共同的因子了)。

然后就可以证明我们最开始提出的那条定理了:

简单证明:

  1. d d d a a a b b b的公约数,所以 d ≤ g c d ( a , b ) d \leq gcd(a, b) dgcd(a,b)
  2. 因为推论 a x + b y ax+by ax+by的结果一定是 g c d ( a , b ) gcd(a, b) gcd(a,b)的倍数,所以 g c d ( a , b ) gcd(a, b) gcd(a,b)一定可以整除 a x + b y ax+by ax+by(也就是 d d d),所以 g c d ( a , b ) ≤ d gcd(a, b) \leq d gcd(a,b)d
  3. 综合一下所以在此条件下 d = g c d ( a , b ) d = gcd(a, b) d=gcd(a,b)

然后拓展欧几里德算法有什么用呢?

  • 经过拓展欧几里德算法求出来的 x x x y y y d d d可以满足 g c d ( a , b ) = d = a x + b y gcd(a, b) = d = ax + by gcd(a,b)=d=ax+by

在上面我们已经证明出来了 x = y ′ x=y' x=y和y= 𝑥 ′ − ⌊ 𝑎 / 𝑏 ⌋ 𝑦 ′ 𝑥'−⌊𝑎/𝑏⌋𝑦' xa/by,所以我们就可以直接将其用在代码中了:

static vector<int> gcd_extended(int x, int y) {
  if (!y) return {1, 0, x};
  vector<int> ans = gcd_extended(y, x % y);
  return {ans[1], ans[0] - (x / y) * ans[1], ans[2]};
}

拓展欧几里德算法因为是递归+取模操作,所以复杂度在 O ( n 3 ) O({n}^{3}) O(n3)

1.2.5 模除

对于每个数来说,都有一个逆元(一般的数字也可以称为倒数),我们给他的定义是: a x ≡ 1 ( m o d   N ) ax \equiv 1 (mod \ N) ax1(mod N),此时可以说x是a模N的逆元(一般不取模的时候就是 a x = 1 ax=1 ax=1),此时的逆元记做 a − 1 {a}^{-1} a1

然而在某些时候,逆元是不存在的,例如2在模6的情况下就不存在逆元。因为这个时候a和N(2和6)都是偶数,所以不管怎么取模,都会是偶数。同理,如果都是3的倍数,那么取模也一定都是3的倍数。

证明一下:

  1. 如果需要满足 a x ≡ 1 ( m o d   N ) ax \equiv 1 (mod \ N) ax1(mod N),那么 a x % N = 1 ax \% N = 1 ax%N=1
  2. a x % N ax \% N ax%N可以写成 a x + k N ax+kN ax+kN,就可以发现这里形如一开始上面学的 a x + b y = 1 ax+by=1 ax+by=1(这里的b为N,y为k),此时x、k可以说是拓展欧几里德算法的一个根。
  3. 所以最初要求 a x % N = 1 ax \% N = 1 ax%N=1,可以理解为 a x + k N = 1 ax + kN=1 ax+kN=1也就是 a x + b y = 1 ax+by=1 ax+by=1,那么显而易见如果要满足 a x ≡ 1 ( m o d   N ) ax \equiv 1 (mod \ N) ax1(mod N)等同于要满足 g c d ( a , N ) = 1 gcd(a, N)=1 gcd(a,N)=1

有一句话特别重要,就是 x x x k k k其实是拓展欧几里德算法中的一个解,也就是说 a x ≡ 1 ( m o d   N ) ax \equiv 1 (mod \ N) ax1(mod N)中的 x x x,其实我们可以用拓展欧几里德算法解出来。同时第三个返回值——最大公约数可以证明这两个数是否互质。

求解逆元的原理我们懂了,此时我们就解决了模除的问题,我们通过逆元将模除转为模乘,这也是这两节的核心内容。代码是依托于拓展欧几里德的,所以时间复杂度也在 O ( n 3 ) O({n}^{3}) O(n3)。下面展示一下代码:

static int inverse(int x, int mod) {
  vector<int> res = gcd_extended(x, mod);
  return res[2] == 1 ? (res[0] + mod) % mod : -1;
}

这里用(res[0] + mod) % mod的方式是为了防止根为负数,用res[0]而不是res[1]是因为我们要的是x的系数。

1.3 素性测试

我们之前求解素数采用的方式是分解因子,那么有没有不用分解就可以快速测试的方法呢?

1.3.1 费马小定理

我们想要快速判断素数,就要看素数有没有其他的性质。在1640年,就有人提出了理论:

  • 费马小定理:如果 p p p是一个素数,那么当 1 ≤ a < p 1 \leq a < p 1a<p时,
    a p − 1 ≡ 1   ( m o d   p ) {a}^{p-1} \equiv 1 \ (mod \ p) ap11 (mod p)

接下来我们来证明一下这个定理:

第一步:用一组特例证明一下;

假设 a = 3 a = 3 a=3 p = 7 p = 7 p=7,此时我们查看一下3的倍数取模的对应值,如图所示:

在这里插入图片描述

这里分别对应了 { 3 × 1   %   7 ,   3 × 2   %   7 , ⋯   ,   3 × 6   %   7 } \{3 \times 1 \ \% \ 7, \ 3 \times 2 \ \% \ 7, \cdots, \ 3 \times 6 \ \% \ 7\} {3×1 % 7, 3×2 % 7,, 3×6 % 7}

此时就会发现,这六个数,正好完全散列在模7的空间里了(这个后面会证明,现在不管),所以我们就可以得到一个公式:
{ 3 × 1   %   7 ,   3 × 2   %   7 , ⋯   ,   3 × 6   %   7 } = { 1 ,   2 , ⋯   ,   6 } \{3 \times 1 \ \% \ 7, \ 3 \times 2 \ \% \ 7, \cdots, \ 3 \times 6 \ \% \ 7\} = \{1, \ 2, \cdots , \ 6\} {3×1 % 7, 3×2 % 7,, 3×6 % 7}={1, 2,, 6}
此时根据同余公式的乘法合并可以得到:
( 3 × 1   %   7 ) × ( 3 × 2   %   7 ) × ⋯ × ( 3 × 6   %   7 ) = 1 × 2 × ⋯ × 6   %   7 (3 \times 1 \ \% \ 7) \times (3 \times 2 \ \% \ 7) \times \cdots \times (3 \times 6 \ \% \ 7) = 1 \times 2 \times \cdots \times 6 \ \% \ 7 (3×1 % 7)×(3×2 % 7)××(3×6 % 7)=1×2××6 % 7
下面我们一步一步慢慢简化(绝不跳步骤):
3 6 × 1 × 2 × ⋯ × 6 ≡ 6 !   ( m o d   7 ) {3}^{6} \times 1 \times 2 \times \cdots \times 6 \equiv 6! \ (mod \ 7) 36×1×2××66! (mod 7)

3 6 × 6 ! ≡ 6 !   ( m o d   7 ) {3}^{6} \times 6! \equiv 6! \ (mod \ 7) 36×6!6! (mod 7)

3 6 ≡ 1   ( m o d   7 ) {3}^{6} \equiv 1 \ (mod \ 7) 361 (mod 7)

这里也就可以写成:
3 7 − 1 ≡ 1   ( m o d   7 ) {3}^{7 - 1} \equiv 1 \ (mod \ 7) 3711 (mod 7)
是不是和我们上面的 a p − 1 ≡ 1   ( m o d   p ) {a}^{p-1} \equiv 1 \ (mod \ p) ap11 (mod p)就对应起来了。

第二步:进行普遍性证明:

通过上面,我们可以发现,如果证明出了 p − 1 p-1 p1个数如果完全散列在 { 1 , 2 , ⋯   , p − 1 } \{1, 2, \cdots , p − 1\} {1,2,,p1}的空间中时,后面的证明都是阶乘相消,非常简单。所以我们现在的核心任务是怎么证明 { a × 1   %   p ,   a × 2   %   p , ⋯   ,   a × ( p − 1 )   %   p } \{a \times 1 \ \% \ p, \ a \times 2 \ \% \ p, \cdots, \ a \times (p - 1) \ \% \ p\} {a×1 % p, a×2 % p,, a×(p1) % p}的值均不相同。

这里我们采用反证法,我假设有两个值 i i i j j j { 1 , 2 , ⋯   , p − 1 } \{1, 2, \cdots , p − 1\} {1,2,,p1}中,且 i ≠ j i \neq j i=j。此时我们假设有下面公式存在:
a × i   %   p = a × j   %   p a \times i \ \% \ p = a \times j \ \% \ p a×i % p=a×j % p
也可以写成下面形式:
a × i ≡ a × j   ( m o d   p ) a \times i \equiv a \times j \ (mod \ p) a×ia×j (mod p)
此时因为 p p p是素数,所以p一定与 { 1 , 2 , ⋯   , p − 1 } \{1, 2, \cdots , p − 1\} {1,2,,p1}中的任何数都互质,那么在这个公式中就有了一个条件: a a a p p p一定互质。

这里我们引入一条定理:

  • 假设有 a × i ≡ a × j   ( m o d   p ) a \times i \equiv a \times j \ (mod \ p) a×ia×j (mod p),当a与p互质时,左右两边可同除 a a a

    证明一下上面这条定理:

    1. 通过上面公式移项可得:
      a × i − a × j ≡ 0   ( m o d   p ) a \times i - a \times j \equiv 0 \ (mod \ p) a×ia×j0 (mod p)

    a × ( i − j ) ≡ 0   ( m o d   p ) a \times (i - j) \equiv 0 \ (mod \ p) a×(ij)0 (mod p)

    1. 此时求模公式也可以写成:
      a × ( i − j )   %   p = 0   %   p a \times (i - j) \ \% \ p = 0 \ \% \ p a×(ij) % p=0 % p

    a × ( i − j )   %   p = 0 a \times (i - j) \ \% \ p = 0 a×(ij) % p=0

    1. 这说明了什么?说明了 a × ( i − j ) a \times (i - j) a×(ij)一定是p的倍数。这个时候a和p的互质就有用了,因为a和p一定没有除了1之外的任何共因子,所以无法相消。那么可以求得: i − j i - j ij一定是 p p p的倍数。

    2. 也就可以写成同上形式:
      ( i − j )   %   p = 0 (i - j) \ \% \ p = 0 (ij) % p=0

    ( i − j ) ≡ 0   ( m o d   p ) (i - j) \equiv 0 \ (mod \ p) (ij)0 (mod p)

    1. 就可以得出:
      i ≡ j   ( m o d   p ) i \equiv j \ (mod \ p) ij (mod p)

那么我们根据上面的定理得出了:
i ≡ j   ( m o d   p ) i \equiv j \ (mod \ p) ij (mod p)
此时因为 i i i j j j的取值范围为 { 1 , 2 , ⋯   , p − 1 } \{1, 2, \cdots , p − 1\} {1,2,,p1},所以也可以说我们证明的结果为 i = j i=j i=j。这与定义的条件 i ≠ j i \neq j i=j不相符,所以有:

  • 假定有素数 p p p,两个在 { 1 , 2 , ⋯   , p − 1 } \{1, 2, \cdots , p − 1\} {1,2,,p1}中不同的值,在 p p p取模时结果一定不同。

在费马小定理中我们知道了素数通过 a p − 1 ≡ 1   ( m o d   p ) {a}^{p-1} \equiv 1 \ (mod \ p) ap11 (mod p)的公式可以判断素数,但是我们并不能通过 a p − 1 ≡ 1   ( m o d   p ) {a}^{p-1} \equiv 1 \ (mod \ p) ap11 (mod p)就得出这个 p p p是素数。

即使 { 1 , 2 , ⋯   , p − 1 } \{1, 2, \cdots , p − 1\} {1,2,,p1}的所有数全部通过了费马测试,也不能说明这个数一定是素数(这种数被称为卡迈克尔数,当然这种数极少),但是我们依旧采用判断 a p − 1 ≡ 1   ( m o d   p ) {a}^{p-1} \equiv 1 \ (mod \ p) ap11 (mod p)的方式来判断素数。

为什么呢?因为一个合数中其实有一半以上的数字都无法通过费马测试(这里我们不考虑卡迈尔数)。

下面我们就要来证明一下,上面所说的一半,是怎么来的。

我们先把这条定理列出来(下面因为我打不出不同余的符号,所以用不等于代替了):

  • 如果 a N − 1 ≠ 1   ( m o d   N ) {a}^{N-1} \neq 1 \ (mod \ N) aN1=1 (mod N)(也就是没通过费马测试),同时已知a与N互质,则至少有一半的底数无法通过费马测试。

我们来证明一下:

加入我们有已知有一个 a N − 1 ≠ 1   ( m o d   N ) {a}^{N-1} \neq 1 \ (mod \ N) aN1=1 (mod N)和一个 b N − 1 ≡ 1   ( m o d   N ) {b}^{N-1} \equiv 1 \ (mod \ N) bN11 (mod N),此时我们可以证明出 a × b a \times b a×b​一定不满足费马测试,证明公式如下:
( a × b ) N − 1   ( m o d   N ) = a N − 1 × b N − 1   ( m o d   N ) {(a \times b)}^{N-1} \ (mod \ N) = {a}^{N-1} \times {b}^{N-1} \ (mod \ N) (a×b)N1 (mod N)=aN1×bN1 (mod N)
此时由于 b N − 1 ≡ 1   ( m o d   N ) {b}^{N-1} \equiv 1 \ (mod \ N) bN11 (mod N),表示这一部分可以被模掉为1,所以:
a N − 1 × b N − 1   ( m o d   N ) = a N − 1   ( m o d   N ) {a}^{N-1} \times {b}^{N-1} \ (mod \ N) = {a}^{N-1} \ (mod \ N) aN1×bN1 (mod N)=aN1 (mod N)
因为我们的条件就是 a N − 1 ≠ 1   ( m o d   N ) {a}^{N-1} \neq 1 \ (mod \ N) aN1=1 (mod N),所以只要是 a × b a \times b a×b的结果一定不满足费马测试,如图:

在这里插入图片描述

这里我当时有一个疑问:为什么 a × b a \times b a×b的值不会重复呢?既不与 a a a重合,也不与 b b b或其他 a × b a \times b a×b重合吗?

这里其实在上面我们就证明过了,如果 a a a N N N互质的话,我们就可以采用反证法证明出来,只要是 a a a { 1 , 2 , ⋯   , N − 1 } \{1, 2, \cdots , N − 1\} {1,2,,N1}中不同的数相乘,结果一定不同,所以 a a a的数量至少和 b b b一样多(证明的时候左右两边把 a a a消掉就行了)。

但是这里没有证明,有没有只有非质数的底数不满足费马测试的情况,这点我就不是很懂了。


所以讲了这么多之后,这只是一个带有概率性的随机性算法。如果我们要判断 N N N是否为素数,我们随机选择 { 1 , 2 , ⋯   , N − 1 } \{1, 2, \cdots , N − 1\} {1,2,,N1}中的一个数进行判断,如果这个数是素数则100%被输出,如果是合数可能有小于50%的概率被输出,那我们怎么办呢?

我们只要多判断几次就好了呗,因为随着我们判断次数多增加,这个合数被输出的概率就变成了 1 2 k \frac{1}{{2}^{k}} 2k1,这个概率随着次数的增加而指数级下降,如果我们判断个100次,那么就只有 2 − 100 {2}^{-100} 2100的概率选中这个合数(这里我们依旧不讨论卡迈克尔数,我们后面再讲)。

那么我们就可以得出以下代码:

static bool primality(int x) {
  default_random_engine e;
  uniform_int_distribution<unsigned> rd(2, x - 1);
  int len = 1e2;
  for (int i = 1; i <= len; i++) {
    int pos = rd(e);
    if (pos == 0) continue;
    if (modpow(pos, x - 1, x) != 1)
      return false;
  }
  return true;
}

1.3.2 生成随机素数

生成随机素数是一个很简单的事情,因为是即使数非常非常大,但是素数的数量依旧非常非常多。一个 n n n位长的数字中,能随机获取到素数的概率大约是 1 n \frac{1}{n} n1(实际上概率大约为 1 l n 2 n ≈ 1.44 n \frac{1}{ln{{2}^{n}}} \approx \frac{1.44}{n} ln2n1n1.44),说明即使我们要生成一个100位长的素数,概率大约也就是 1 100 \frac{1}{100} 1001的概率。

  • 拉格朗日素数定理:设 π ( x ) \pi(x) π(x)表示比 x x x小的素数个数,然后就有:
    π ( x ) ≈ x ln ⁡ x \pi(x) \approx \frac{x}{\ln x} π(x)lnxx
    更严谨来说是:
    lim ⁡ x → ∞ π ( x ) ( x / ln ⁡ x ) = 1 \lim_{x \to \infty} \frac{\pi(x)}{(x / \ln x)} = 1 xlim(x/lnx)π(x)=1
    然后:证明我不会

所以我们创建一个素数的过程就是:

  1. 随机创建一个 n n n位的素数;
  2. 运行素性测试;
  3. 如果没通过测试就换一个再来。

看一下代码:

static int generating_random_primes(int n) {
  int size_low = pow(10, (n - 1));
  int size_high = size_low * 10;
  default_random_engine e;
  uniform_int_distribution<unsigned> rd(size_low, size_high - 1);
  int p = 0;
  do {
    p = rd(e);
  } while(!primality(p));
  return p;
}

这个方法还是非常快,而且非常准确的。书上提到了一组做过的数据:在 N ≤ 25 × 10 9 N \leq 25 \times {10}^{9} N25×109的数据中,有大约 10 9 {10}^{9} 109个素数和大约 2 × 10 4 2 \times {10}^{4} 2×104个合数能够通过底数为2的测验,也就是说,即使只做一组测验,抽到一个合数的概率也就只有 2 × 10 − 5 2 \times {10}^{-5} 2×105。多做几次测验概率就变得非常非常小了,所以即使有卡迈克尔数,抽到也是极小极小概率的事情了。

1.3.3 Miller Rabin算法素性测试

这里为什么要多一种算法做素性测试呢?因为如果仅仅用费马小定理,卡迈克尔数虽然不多,但也确实不算少。Miller Rabin的素性测试可以筛掉卡迈克尔数,并且目前还没有发现可以避开Miller Rabin算法通过素性测试的合数。

Miller Rabin算法在费马测试的基础上由提出了一条定理:

  • 二次探测定理:假设 p p p为素数,同时 a 2 ≡ 1   ( m o d   p ) {a}^{2} \equiv 1 \ (mod \ p) a21 (mod p),则 a a a根一定是 ± 1 \pm 1 ±1,也可以说是 1 1 1 p − 1 p-1 p1。如果有其他的根实现了 a 2   %   p = 1 {a}^{2} \ \% \ p = 1 a2 % p=1,那么这个数一定是合数。

    证明一下:

    通过移位可以得到:
    a 2 − 1 ≡ 0   ( m o d   p ) {a}^{2} - 1 \equiv 0 \ (mod \ p) a210 (mod p)

    ( a + 1 ) ( a − 1 ) ≡ 0   ( m o d   p ) (a+1)(a-1) \equiv 0 \ (mod \ p) (a+1)(a1)0 (mod p)

    所以 a a a的根为 1 1 1 p − 1 p-1 p1

那我们如何去实现Miller Rabin探测呢:

  1. 为了使用二次探测,我们首先要提取出所有的二次方,于是我们将指数的 N − 1 N-1 N1统一写为 u × 2 t u \times {2}^{t} u×2t的形式(不停除2就可以得到 u u u t t t了);
  2. 最后的结果一定要是 ± 1 \pm 1 ±1,那我们设定在计算的时候如果有通过非 ± 1 \pm 1 ±1之外的数的平方变成了1,那么就违反了二次探测定理,说明为合数;
  3. 那如果二次探测到了最后结果依旧不是 ± 1 \pm 1 ±1,那就违反了费马小定理,也为合数。

下面我们来看一下代码:

static bool miller_rabin(int n) {
  if (n < 3 || !(n & 1)) return n == 2;
  int u = n - 1, t = 0;
  while (!(u & 1)) { // n - 1化成u * 2^t的形式
    u >>= 1;
    t++;
  }

  default_random_engine e;
  uniform_int_distribution<unsigned> rd(2, n - 1);
  int len = 1e2;
  for (int i = 1; i < len; i++) { // 随机进行多组判断
    int a = rd(e);
    int res = modpow(a, u, n); // 先计算出a^u % n,方便后面的二次探测
    for (int j = 1; j <= t; j++) {
      if (res == 1 || res == n - 1)
        break;
      res = res * res % n;
      if (res == 1)
        return false; // 不满足二次探测定理
    }
    if (res != 1 && res != n - 1) return false; // 不满足费马小定理
  }
  return true; // 全部判断成功
}

1.3.4 群论

这个书上提了一嘴。

首先我们看一下什么是群论的定义吧:

  • 对于一个任意的整数N,模N的数中,所有与N互质的数的集合被称为群。

我们给这个群赋予了一些性质:

  1. 这个群上我们定义了乘法运算;
  2. 这个群中有一个中性元素(任何数字乘该元素保持不变);
  3. 每个元素都有明确的逆元。

这个特定的群被称为N的乘法群,通常表示为 Z N ∗ {Z}^{*}_{N} ZN

详细的等学到了再补充吧。

1.4 密码学

我们这一张主要内容是讲解Rivest-Shamir-Adelman (RSA) cryptosystem这个公钥+密钥的密码系统。这个算法用到了我们这张所有学到的东西,是集大成者~

1.4.1 密码学的存在意义

我们在讲解密码系统的时候,通常可以使用三个用户来讲述这件事情,这里假设乘A、B、E。

我们假设A、B是想要互相通信的人,而E是一个网络上的拦截者。E想要窃听A和B之间的对话。这个时候我们想到的方法自然就是对信息进行加密,让窃听者E无法看懂其中的意思。

所以我们假设我们的信息内容 x x x,为了保证安全性,自然会设定一个加密函数 e ( x ) e(x) e(x),同时A为了要让B能够看懂,这个函数必须可逆,并且把这个解密函数设为 d ( x ) d(x) d(x)​。所以自然就有:
d ( e ( x ) ) = x d(e(x)) = x d(e(x))=x
并且详细关系如下图:

在这里插入图片描述

接下来我们就会讲解两个方法:AES一次性密钥算法、RSA算法。用于安全的进行信息传输。

1.4.2 一次性密钥算法&AES

首先我们讲一下这个方法的原理:

这个方法在A和B联络之前,必须要进行一次线下约定加密方式。比如A的加密方式选择了按位异或(当然别的也行,只是这个好逆,所以举例)那么此时,我们确定了一个二进制串 r r r,用于加密我们的信息。我们把加密的文本记为 e r ( x ) = x ⨁ r {e}_{r}(x) = x \bigoplus r er(x)=xr,假设我们这里的加密串 r = 01110010 r = 01110010 r=01110010,信息 x = 11110000 x=11110000 x=11110000,那么就有:
e r ( 11110000 ) = 11110000 ⨁ 01110010 = 10000010 {e}_{r}(11110000) = 11110000 \bigoplus 01110010 = 10000010 er(11110000)=1111000001110010=10000010
然后想要解密的话呢,因为 e r {e}_{r} er是从 n n n位字符串到 n n n位字符串之间的双射函数,所以再做一遍即可解密:
e r ( e r ( x ) ) = ( x ⨁ r ) ⨁ r = x ⨁ ( r ⨁ r ) = x ⨁ 0 = x {e}_{r}({e}_{r}(x)) = (x \bigoplus r) \bigoplus r = x \bigoplus (r \bigoplus r) = x \bigoplus 0 = x er(er(x))=(xr)r=x(rr)=x0=x
怎么保证安全呢,首先r需要是随机出来的,然后我们看一下E所看到的情况:如果E获得了一个字符串 y = 10 y=10 y=10,如果E想要获得原字符串 x x x,有下面几种可能:

在这里插入图片描述

所以数字一长可能性就更多了,更难以知道原字符串是什么。

但是!一次性密钥算法在使用了一次以后就必须要丢弃密钥并重新定义。为什么呢?简单来说就是,第一次A、B发消息的时候,完全没有信息可以帮助E判断消息的内容是什么。但如果第二次发消息,E就可以在两次的消息中找到一些相关的数据并进行解析,可能就会暴露一些关键信息。

我们还是采用异或的案例,E可以获得两次的加密信息: x 1 ⨁ r {x}_{1} \bigoplus r x1r x 2 ⨁ r {x}_{2} \bigoplus r x2r。E就可以将这两条信息异或得到 x 1 ⨁ x 2 {x}_{1} \bigoplus {x}_{2} x1x2(里面的 r r r异或抵消了),这就有可能知道重要的信息,例如:

  1. 这可以说明两条信息的开头或结尾是否相同;
  2. 如果 x 1 {x}_{1} x1有很长一段为0(图片就很有可能这样),又因为两条信息的结果等于 x 1 ⨁ x 2 {x}_{1} \bigoplus {x}_{2} x1x2,那么 x 2 {x}_{2} x2对应 x 1 {x}_{1} x1 0 0 0的那一段会展现原原本本的信息,导致新的这条消息被泄漏的一部分。

高级加密标准(AES)与此类似,虽然理论我们已经清楚了,但他的加密方案我们不是很清楚。不过这个并不是和字符串等长,而是确定的128位到128位到双射器。这个加密标准可以重复使用,但是安全性还未得到验证。那为啥还普遍用他呢?因为目前公众还不知道他的加密方式,想要破解他除非一种一种方法试,将所有可能性都试完。

1.4.3 RSA

RSA是通过采用素数的性质与因式分解的高时间复杂度实现的。同时他提出了公钥与私钥的概念,这两把钥匙都由接收信息方提供。

  • 公钥:所有人都可以看见加密方法;
  • 私钥:只有自己可以看见的解密方法。

这个方法简单来说:

  1. 如果A给B发消息,A通过B给出的合数N进行取模加密;
  2. B可以通过这个合数 N N N唯二的素因子 p p p q q q对信息进行解密。

这个方法为什么安全呢?

  1. 首先此时A和E都知道这个合数N,A不需要解密所以不在意;
  2. E想要解密就要知道这两个素因子 p p p q q q是多少,但是E要怎么才能知道?只能因式分解;
  3. 不过因式分解的话时间可太长了,只要这个合数够大,那么全世界的计算机加起来都做不到在短时间内把这两个素因子算出来。

我们已经说明了这个方法确实可行,那么在上述内容中我们只有详细的加密解密方法不清楚了,下面来细讲一下。

性质:选择两个素数 p p p q q q,使 N = p × q N=p \times q N=p×q,并且选一个 e e e,保证 e e e ( p − 1 ) ( q − 1 ) (p-1)(q-1) (p1)(q1)互质。则会有以下特征:

  • x x x映射在 x e   %   N {x}^{e} \ \% \ N xe % N中是可逆的(完全散列)。

    逆映射很好实现:计算出 e e e对于 ( p − 1 ) ( q − 1 ) (p-1)(q-1) (p1)(q1)的逆元 d d d,对于任何 x ∈ { 0 , 1 , … , N − 1 } x \in \{0, 1, \dots , N-1\} x{0,1,,N1}都存在 ( x e ) d ≡ x   ( m o d   N ) {({x}^{e})}^{d} \equiv x \ (mod \ N) (xe)dx (mod N)

根据上面的性质我们知道,加密的时候是没有信息丢失的。然后上面的特性告诉我们,加密的时候采用 x e   %   N {x}^{e} \ \% \ N xe % N,解密的时候采用 ( x e ) d   %   N {({x}^{e})}^{d} \ \% \ N (xe)d % N。所以我们只要把 e e e N N N当成公钥, d d d保留作为私钥就好了。

举例子,看实际效果:

  1. 选取 N = 55 = 5 × 11 N=55=5 \times 11 N=55=5×11,此时 ( p − 1 ) ( q − 1 ) = 40 (p-1)(q-1) = 40 (p1)(q1)=40
  2. 选取 e = 3 e=3 e=3,保证互质: g c d ( e , ( p − 1 ) ( q − 1 ) ) = g c d ( 3 , 40 ) = 1 gcd(e, (p − 1)(q − 1)) = gcd(3, 40) = 1 gcd(e,(p1)(q1))=gcd(3,40)=1
  3. 密钥 d = e − 1   %   ( p − 1 ) ( q − 1 ) = 3 − 1   %   40 = 27 d={e}^{-1} \ \% \ (p-1)(q-1) = {3}^{-1} \ \% \ 40 = 27 d=e1 % (p1)(q1)=31 % 40=27
  4. 原始数据 x = 13 x = 13 x=13

操作(假设 y y y为加密数据):

  1. 加密: y = x e   %   N = 13 3   %   55 = 52 y = {x}^{e} \ \% \ N = {13}^{3} \ \% \ 55 = 52 y=xe % N=133 % 55=52
  2. 解密: x = y d   %   N = 52 27   %   55 = 13 x = {y}^{d} \ \% \ N = {52}^{27} \ \% \ 55 = 13 x=yd % N=5227 % 55=13

看完样例我们发现这个方法确实好,不过我们还有地方没有证明:

  • 为什么 ( x e ) d ≡ x   ( m o d   N ) {({x}^{e})}^{d} \equiv x \ (mod \ N) (xe)dx (mod N)

    证明:

    1. 根据逆元定理:
      e d ≡ 1   ( m o d   ( p − 1 ) ( q − 1 ) ) ed \equiv 1 \ (mod \ (p−1)(q−1)) ed1 (mod (p1)(q1))
      也可以写成:
      e d = 1 + k ( p − 1 ) ( q − 1 ) ed = 1 + k(p - 1)(q - 1) ed=1+k(p1)(q1)
      其中 k ∈ N k \in N kN

    2. 为了证明:
      ( x e ) d ≡ x   ( m o d   N ) {({x}^{e})}^{d} \equiv x \ (mod \ N) (xe)dx (mod N)
      通过逆元定理的转换我们有可以转变为证明:
      x 1 + k ( p − 1 ) ( q − 1 ) ≡ x   ( m o d   N ) {x}^{1 + k(p - 1)(q - 1)} \equiv x \ (mod \ N) x1+k(p1)(q1)x (mod N)

    x × x k ( p − 1 ) ( q − 1 ) ≡ x   ( m o d   N ) x \times {x}^{k(p - 1)(q - 1)} \equiv x \ (mod \ N) x×xk(p1)(q1)x (mod N)

    相当于我们要证明:
    x k ( p − 1 ) ( q − 1 ) ≡ 1   ( m o d   N ) {x}^{k(p - 1)(q - 1)} \equiv 1 \ (mod \ N) xk(p1)(q1)1 (mod N)

    1. 因为 x p − 1 ≡ 1   ( m o d   p ) {x}^{p - 1} \equiv 1 \ (mod \ p) xp11 (mod p) x q − 1 ≡ 1   ( m o d   q ) {x}^{q - 1} \equiv 1 \ (mod \ q) xq11 (mod q),因为p、q为素数,所以 x ( p − 1 ) ( q − 1 ) {x}^{(p - 1)(q - 1)} x(p1)(q1)可以被他们的乘积N整除==(其实这里我不是很理解为什么可以)==,所以可以证明上面公式。

至此,RSA就讲解完了。

1.5 通用哈希

1.5.1 什么是哈希

哈希,我们平常也称作为散列。通过一种数学方法,将一种数据展现为另一种数据,这其中最常用的就是取模。

例如两个数 n n n m m m同样对 q q q取模,这样得到的结果一定是 [ 0 , q − 1 ] [0, q-1] [0,q1]之间的数,如果数据只有q个,并且计算到 [ 0 , q − 1 ] [0, q-1] [0,q1]中不重复,就可以很快的知道这个数是否存在。

本章我们会采用对ip地址的哈希为例,向读者介绍哈希表的使用与构建。

1.5.2 哈希表

我们在此举例,当前有250个ip地址,IP地址的表现形式是xxx.xxx.xxx.xxx,如果换成二进制也就是有 4 × 8 = 32 4 \times 8 = 32 4×8=32位。那也就说明,ip地址一共存在 2 32 {2}^{32} 232个。但如果我们开一个 2 32 {2}^{32} 232的大小的数组用于存放这250个ip地址,未免浪费的也太多了。这里就需要采用我们所说的哈希表。

我们的哈希是通过定义一个hash函数对ip地址进行转换。假设我们在这定义一个 h ( x ) h(x) h(x),用于表示这个hash函数,那我们的映射方式如图所示:

在这里插入图片描述

1.5.3 hash函数

对于 h ( x ) h(x) h(x)这个hash函数,我们首先可以想到的最简单的方法就是直接选取IP地址的后八位,这样就只有 2 8 = 256 {2}^{8} = 256 28=256种可能性,空间浪费很小。

但这个方法的缺点非常明显,其实我们的ip地址最后一位在 [ 0 , 255 ] [0, 255] [0,255]的范围中,大多数时候概率并不是相同的。比如全部集中在 [ 0 , 10 ] [0, 10] [0,10] [ 245 , 255 ] [245, 255] [245,255]这样。那就会造成hash冲突(转换后的结果相同),最坏的可能会导致搜索变成线性时间。那我们怎么办呢?

我们解决问题的关键:

  • 问题的根本是什么?是我们的散列概率不相等。
  • 为什么不相等?因为概率取决于人们对ip地址最后八位的使用习惯。
  • 所以我们的目标就是创造一个概率相等的hash函数。

这里就即将用到我们这一章所学到的数论内容。


  • 第一步:

我们对桶数量的策略采用素数,这里有250个桶,所以我们选取>250的第一个素数257。

  • 第二步:

ip地址一共有四部分,我们就可以将ip分解为 x = ( x 1 , x 2 , x 3 , x 4 ) x=({x}_{1}, {x}_{2}, {x}_{3}, {x}_{4}) x=(x1,x2,x3,x4),也可以说是 [ 0 , 255 ] [0, 255] [0,255]之间整数的四倍体。

  • 第三步:

我们随机选取一组系数 a = ( a 1 , a 2 , a 3 , a 4 ) a=({a}_{1}, {a}_{2}, {a}_{3}, {a}_{4}) a=(a1,a2,a3,a4)​,使其构成一个hash函数:
h a = a 1 x 1 + a 2 x 2 + a 3 x 3 + a 4 x 4   ( m o d   N ) {h}_{a} = {{a}_{1}{x}_{1} + {a}_{2}{x}_{2} + {a}_{3}{x}_{3} + {a}_{4}{x}_{4}} \ (mod \ N) ha=a1x1+a2x2+a3x3+a4x4 (mod N)
也可以写成:
h a = ∑ i = 1 4 ( a i ⋅ x i )   ( m o d   N ) {h}_{a} = \sum_{i = 1}^{4}({a}_{i}·{x}_{i}) \ (mod \ N) ha=i=14(aixi) (mod N)
这里的 N N N也就是上面所说的素数257。


上面这个公式,为什么有用,我们在这里引出一条定理:

  • 对于任意两对不同的ip: x = ( x 1 , x 2 , x 3 , x 4 ) x=({x}_{1}, {x}_{2}, {x}_{3}, {x}_{4}) x=(x1,x2,x3,x4) y = ( y 1 , y 2 , y 3 , y 4 ) y=({y}_{1}, {y}_{2}, {y}_{3}, {y}_{4}) y=(y1,y2,y3,y4)。对于任意的 a = ( a 1 , a 2 , a 3 , a 4 ) a=({a}_{1}, {a}_{2}, {a}_{3}, {a}_{4}) a=(a1,a2,a3,a4) a ∈ [ 0 , n ) a \in [0, n) a[0,n),都有:
    P r { h a ( x 1 , x 2 , x 3 , x 4 ) } = P r { h a ( y 1 , y 2 , y 3 , y 4 ) } = 1 n Pr\{{h}_{a}({x}_{1}, {x}_{2}, {x}_{3}, {x}_{4})\}=Pr\{{h}_{a}({y}_{1}, {y}_{2}, {y}_{3}, {y}_{4})\} = \frac{1}{n} Pr{ha(x1,x2,x3,x4)}=Pr{ha(y1,y2,y3,y4)}=n1

上面的公式表示概率相等且为 1 n \frac{1}{n} n1。这说明我们的预期查找时间是最短的,因为有250个ip,257个桶,所以桶的预期为 250 ÷ 257 < 1 250 \div 257 < 1 250÷257<1

至于这个公式的证明:我没看懂…

二、收尾

数论就到这里,如有补充,后期再改

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值