1. 项目概述:为什么我们需要Miller-Rabin素检测?
在密码学、数据安全乃至一些数学研究领域,判断一个数是否为素数是一个基础且关键的问题。你可能听说过最朴素的试除法——从2一直除到这个数的平方根。对于小数字,这没问题,但想象一下,当你要处理一个长达数百位、用于RSA加密的“大整数”时,试除法所需的时间将是天文数字,完全不现实。这时,我们就需要一种“概率性”但极其高效的算法,Miller-Rabin素检测算法就是其中的佼佼者,它也是目前许多加密库(如OpenSSL)在实际中采用的默认素性测试方法。
简单来说,Miller-Rabin算法是一种基于概率的素性测试。它不能百分之百确定一个数是素数(对于合数,它能给出确定性判断),但对于一个通过多次测试的数,我们可以说“它极大概率是素数”,这个概率高到在实际应用中完全可以接受,比如出错概率小于2的负几十次方,比你电脑硬件出错的概率还低得多。它的核心优势在于速度,时间复杂度是O(k log³ n),其中k是测试轮数,这使得它能够处理巨大的整数。今天,我们就来彻底拆解这个算法,从数学原理到代码实现(C++和Python),让你不仅能理解它为什么快,更能亲手实现它。
2. 算法核心原理与数学背景拆解
要理解Miller-Rabin,我们不能绕过其背后的数学定理。它本质上是费马小定理的强化版,并引入了“二次探测定理”。
2.1 从费马小定理到Miller-Rabin的演进
费马小定理指出:如果p是一个素数,且a是任意不被p整除的整数,那么 a^(p-1) ≡ 1 (mod p)。这个定理的逆命题并不总是成立。存在一些合数n,对于某些a,也满足 a^(n-1) ≡ 1 (mod n),这些数被称为“费马伪素数”。这意味著仅靠费马测试会误判。
Miller-Rabin算法通过更严格的检验避免了大部分费马伪素数。它的核心思想是:如果n是奇素数(大于2的素数),那么对于n-1的分解式 n-1 = 2^s * d(其中d是奇数),对于任意与n互质的基数a(1 < a < n-1),以下两个条件至少有一个成立:
- a^d ≡ 1 (mod n)
- 存在某个r(0 ≤ r < s),使得 a^( (2^r) * d ) ≡ -1 (mod n)
如果对于某个a,以上两个条件都不满足,那么n肯定是一个合数,此时a被称为n是合数的一个“证据”(witness)。如果对于某个a,条件之一成立,那么n可能是素数,a被称为一个“强伪证”(strong liar)。
2.2 关键参数:测试基数a的选择与误差分析
算法是概率性的,那么它的误差到底有多大?对于一个合数n,至少75%的基数a(在1到n-1之间)会是它的“证据”。这意味着,如果我们随机选择一个a进行测试,判定n为合数的成功率至少是75%。如果我们独立随机选择k个不同的a进行测试,并且n实际上是一个合数,但它全部通过了k次测试(即每次a都是“强伪证”),那么这种情况发生的概率最多是 (1/4)^k。
这就是Miller-Rabin算法的威力所在:通过增加测试轮数k,我们可以将错误概率(将一个合数误判为素数)降到任意低。例如:
- k=1, 错误概率 ≤ 25%
- k=5, 错误概率 ≤ 0.09765625%
- k=10,错误概率 ≤ 0.00009536%
- k=20,错误概率 ≤ 9.0949e-13
在实际的密码学应用中,通常取k=10到20就足够了,对于随机选取的大数,这已经比硬件故障的概率还要低得多。
注意:这里存在一类极其特殊的合数,叫做“强伪素数”。对于某些特定的基数集合,它们能欺骗Miller-Rabin测试。但如果我们随机选择基数,遇到这种数的概率微乎其微。为了达到确定性测试,我们可以使用一组经过验证的、固定的基数集合。例如,对于32位整数,只需测试基数a = {2, 7, 61};对于64位整数,测试基数a = {2, 3, 5, 7, 11, 13, 17} 即可给出确定性结果。我们后续的实现会兼顾这两种策略。
3. 算法步骤详解与手动演算
理解了原理,我们把它分解成可执行的步骤。假设我们要测试的数是n(n > 2,且是奇数,偶数直接判断)。
3.1 步骤分解:从n到判决
步骤1:分解出s和d。计算 n-1 = 2^s * d。方法是不断将n-1除以2,直到结果为奇数,除的次数就是s,最后得到的奇数就是d。示例:假设 n = 29。 n-1 = 28。 28 / 2 = 14 (s=1, d=14? 不,14还是偶数,继续) 14 / 2 = 7 (s=2, d=7, 7是奇数,停止) 所以,s=2, d=7。验证:2^2 * 7 = 4 * 7 = 28。
步骤2:选择测试基数a。在范围 (1, n-1) 内随机选择一个整数a,或者从一组固定的可信基数列表中选取。
步骤3:计算模幂序列。计算 x = a^d mod n。
- 如果 x == 1 或 x == n-1 (即 -1 mod n),那么对于这个a,n可能是素数,进入下一轮测试(回到步骤2选择新的a)或直接返回“可能是素数”。
- 否则,我们进行最多s-1次平方测试。
步骤4:二次探测循环。循环 r 从 1 到 s-1: 计算 x = (x * x) mod n。
- 如果 x == n-1,那么对于这个a,n可能是素数,跳出循环,进行下一轮测试。
- 如果 x == 1,那么n肯定是合数,直接返回“是合数”。 循环结束后,如果x始终不等于n-1,那么n肯定是合数,返回“是合数”。
步骤5:重复与判决。重复步骤2到步骤4共k轮(k是我们设定的测试次数)。如果所有k轮测试都未能证明n是合数,那么我们就判定n“极大概率是素数”。
3.2 手动演算实例:以n=29和n=25为例
例1:测试n=29(它确实是素数)s=2, d=7 (已算出)。 假设我们选择基数 a=2。
- 计算 x = 2^7 mod 29。2^7=128。128 / 29 = 4...12。所以 x = 12。x既不是1也不是28。
- 进入循环 (r=1): x = (1212) mod 29 = 144 mod 29。294=116,144-116=28。所以 x = 28 (即 n-1)。
- 发现 x == n-1,所以对于a=2,29通过测试。再进行几轮随机测试(如a=3, 5等)都通过后,可判定29为素数。
例2:测试n=25(它是合数)n-1 = 24 = 2^3 * 3。所以 s=3, d=3。 假设我们“不幸地”选择了基数 a=7(一个强伪证?我们来验证)。
- 计算 x = 7^3 mod 25。7^3=343。343 / 25 = 13...18。所以 x = 18。
- 循环 r=1: x = 1818 mod 25 = 324 mod 25。2512=300,324-300=24。x=24 (即 n-1)。
- 发现 x == n-1,所以对于a=7,25欺骗了测试。这说明单次测试可能出错。
- 让我们换一个基数 a=2。
- x = 2^3 mod 25 = 8。
- r=1: x = 8*8 mod 25 = 64 mod 25 = 14。
- r=2: x = 14*14 mod 25 = 196 mod 25 = 21。 (循环结束,s-1=2次已做完)
- 最终x=21,既不是1也不是24。因此,a=2是n=25为合数的“证据”,算法正确判定25为合数。
这个例子清晰地展示了随机选择基数的重要性,以及多次测试如何降低误判风险。
4. 核心模块实现:快速模幂运算
Miller-Rabin算法中,最核心、最频繁的操作就是计算 a^d mod n,即模幂运算。直接先计算a^d再取模,对于大数是不可能的(中间结果会溢出)。因此,我们必须实现一个高效的快速模幂算法(Exponentiation by Squaring)。
4.1 快速模幂算法原理与迭代实现
原理基于二进制和模运算的性质:(a * b) mod n = [(a mod n) * (b mod n)] mod n。 我们将指数d用二进制表示。例如计算 a^13 mod n,13的二进制是1101,即 13 = 8+4+1 = 2^3 + 2^2 + 2^0。 那么 a^13 = a^8 * a^4 * a^1。 我们可以通过反复平方来计算出这些分量:
- 初始化结果 res = 1。
- 从指数d的最低位开始,设当前底数为 base = a % n。
- 如果d的当前二进制位是1,则 res = (res * base) % n。
- 无论该位是否为1,都让 base = (base * base) % n(为下一位计算平方)。
- d右移一位(除以2)。 重复直到d为0。
这种迭代方法的时间复杂度是O(log d),完美解决了大数幂运算的问题。
// C++ 快速模幂实现 (迭代法) long long mod_pow(long long base, long long exponent, long long mod) { long long result = 1; base = base % mod; // 防止base大于mod的情况 while (exponent > 0) { // 如果当前指数位为1,则将当前的base乘入结果 if (exponent & 1) { result = (result * base) % mod; } // 将base平方,为下一位做准备 base = (base * base) % mod; // 指数右移一位 exponent >>= 1; } return result; }# Python 快速模幂实现 (迭代法) def mod_pow(base, exponent, mod): result = 1 base = base % mod # 防止base大于mod while exponent > 0: # 如果当前指数位为1,则将当前的base乘入结果 if exponent & 1: result = (result * base) % mod # 将base平方,为下一位做准备 base = (base * base) % mod # 指数右移一位 exponent >>= 1 return result实操心得:在C/C++中,即使使用了
long long,在计算(result * base)或(base * base)时,中间结果仍然可能溢出,尤其是在模数n很大的时候(比如64位上限附近)。为了绝对安全,在实际的加密库实现中,会使用编译器提供的__int128类型(如果支持)或专门的大整数库(如GMP)来进行中间计算。我们的示例为了简洁和可读性,假设输入在long long安全范围内。在生产环境中,处理任意大整数是必须的。
4.2 算法主流程的代码实现
有了快速模幂这个利器,我们就可以实现完整的Miller-Rabin测试函数了。我们将实现一个函数is_probable_prime(n, k),其中k是测试轮数。同时,我们会实现一个针对特定范围的确定性测试版本。
#include <iostream> #include <cstdlib> #include <ctime> #include <cmath> // 快速模幂函数,同上,此处省略... // 使用随机基数的Miller-Rabin概率性测试 bool is_probable_prime(long long n, int k) { // 处理小数字和偶数 if (n <= 1) return false; if (n == 2 || n == 3) return true; if (n % 2 == 0) return false; // 分解 n-1 = 2^s * d long long s = 0; long long d = n - 1; while (d % 2 == 0) { d /= 2; s++; } // 进行k轮测试 for (int i = 0; i < k; i++) { // 随机选择基数a,范围在[2, n-2] long long a = 2 + rand() % (n - 3); long long x = mod_pow(a, d, n); // 如果x==1或x==n-1,本轮通过,继续下一轮 if (x == 1 || x == n - 1) { continue; } // 否则进行s-1次平方探测 bool continue_outer_loop = false; for (long long r = 1; r < s; r++) { x = (x * x) % n; // 这里用普通乘法,实际应考虑防溢出 if (x == n - 1) { continue_outer_loop = true; break; // 跳出内层循环,本轮通过 } if (x == 1) { return false; // 肯定是合数 } } if (continue_outer_loop) { continue; // 本轮通过,进行下一轮测试 } // 如果执行到这里,说明所有次平方后x都不等于n-1 return false; // 肯定是合数 } // 通过所有k轮测试 return true; } // 针对64位以内整数的确定性Miller-Rabin测试 bool is_prime_deterministic(long long n) { if (n <= 1) return false; if (n <= 3) return true; if (n % 2 == 0) return false; // 适用于2^64范围内的确定性子集 long long bases[]; if (n < 2047LL) bases = {2}; else if (n < 1373653LL) bases = {2, 3}; else if (n < 9080191LL) bases = {31, 73}; // ... 为了示例简洁,这里省略更大的范围判断 // 实际应包含 {2, 3, 5, 7, 11, 13, 17} 等 else { bases = {2, 3, 5, 7, 11, 13, 17}; // 适用于 < 2^64 } long long s = 0, d = n - 1; while (d % 2 == 0) { d /= 2; s++; } for (long long a : bases) { if (a % n == 0) continue; // 如果a是n的倍数,跳过,但通常a很小,n很大,不会发生 long long x = mod_pow(a, d, n); if (x == 1 || x == n - 1) continue; bool composite = true; for (long long r = 1; r < s; r++) { x = (x * x) % n; if (x == n - 1) { composite = false; break; } } if (composite) return false; } return true; }import random def mod_pow(base, exponent, mod): # 同上,此处省略... pass def is_probable_prime(n, k=5): """使用Miller-Rabin算法进行概率性素性测试""" if n <= 1: return False if n <= 3: return True if n % 2 == 0: return False # 分解 n-1 = 2^s * d s, d = 0, n - 1 while d % 2 == 0: d //= 2 s += 1 # 进行k轮测试 for _ in range(k): a = random.randrange(2, n - 1) x = pow(a, d, n) # Python内置的pow支持模幂,且自带防溢出优化,比手写快 if x == 1 or x == n - 1: continue composite = True for _ in range(s - 1): x = (x * x) % n if x == n - 1: composite = False break if composite: return False # 确定是合数 return True # 很可能是素数 def is_prime_deterministic(n): """针对特定范围的确定性Miller-Rabin测试 (适用于 < 2^64)""" if n <= 1: return False if n <= 3: return True if n % 2 == 0: return False # 根据n的大小选择确定的基数集 if n < 2047: bases = [2] elif n < 1373653: bases = [2, 3] elif n < 9080191: bases = [31, 73] elif n < 25326001: bases = [2, 3, 5] elif n < 3215031751: bases = [2, 3, 5, 7] elif n < 4759123141: bases = [2, 7, 61] elif n < 1122004669633: bases = [2, 13, 23, 1662803] elif n < 2152302898747: bases = [2, 3, 5, 7, 11] elif n < 3474749660383: bases = [2, 3, 5, 7, 11, 13] elif n < 341550071728321: bases = [2, 3, 5, 7, 11, 13, 17] else: # 对于更大的n,使用随机基数概率测试,或扩展确定性基数集 # 这里为了通用性,退回概率测试 return is_probable_prime(n, k=10) s, d = 0, n - 1 while d % 2 == 0: d //= 2 s += 1 for a in bases: if a % n == 0: continue x = pow(a, d, n) if x == 1 or x == n - 1: continue composite = True for _ in range(s - 1): x = (x * x) % n if x == n - 1: composite = False break if composite: return False return True5. 性能优化与生产环境实践
上面的代码清晰地展示了算法流程,但在实际生产环境中(比如生成RSA密钥),我们需要处理数百位甚至数千位的大整数,并且对性能有极高要求。这里有几个关键的优化点和实践考量。
5.1 大整数运算与防溢出处理
在C++中,long long通常只有64位,这对于密码学应用远远不够。我们必须使用大整数库。
- GMP (GNU Multiple Precision Arithmetic Library):这是C/C++领域最著名、性能最优的大数库。OpenSSL等众多软件都依赖它或类似的实现。
- Boost.Multiprecision:提供了方便的C++接口,可以后端链接GMP。
- Python的
int类型:Python天生支持任意精度整数,这是其在进行算法教学和原型验证时的巨大优势。pow(a, d, n)内置的三参数形式就是优化的模幂运算。
在C++中使用GMP,我们的模幂和乘法运算就安全了:
#include <gmpxx.h> bool miller_rabin_gmp(const mpz_class& n, int k) { if (n <= 1) return false; if (n == 2) return true; if (n % 2 == 0) return false; mpz_class s = 0; mpz_class d = n - 1; while (d % 2 == 0) { d /= 2; s++; } gmp_randclass rng(gmp_randinit_default); rng.seed(time(NULL)); for (int i = 0; i < k; i++) { mpz_class a = rng.get_z_range(n - 3) + 2; // [2, n-2] mpz_class x; mpz_powm(x.get_mpz_t(), a.get_mpz_t(), d.get_mpz_t(), n.get_mpz_t()); // 快速模幂 if (x == 1 || x == n - 1) continue; bool continue_test = false; for (mpz_class r = 1; r < s; r++) { mpz_mul(x.get_mpz_t(), x.get_mpz_t(), x.get_mpz_t()); mpz_mod(x.get_mpz_t(), x.get_mpz_t(), n.get_mpz_t()); if (x == n - 1) { continue_test = true; break; } if (x == 1) return false; } if (!continue_test) return false; } return true; }5.2 测试轮数k与基数选择的策略
- 对于随机大数生成:通常k=10到20次随机测试就足够了。例如,OpenSSL在生成素数时默认使用多次Miller-Rabin测试。
- 对于确定性测试:如果知道待测数n的上限,可以使用一组固定的、经过验证的基数集。如前所述,对于64位整数,
{2, 3, 5, 7, 11, 13, 17}这7个基数足以给出确定性结果。这比做几十次随机测试更快。 - 基数的随机性:在概率测试中,基数的随机性非常重要。应使用密码学安全的随机数生成器(CSPRNG),如
/dev/urandom或操作系统提供的安全API,而不是简单的rand()函数。Python的random模块对于一般应用可以,但密码学应用应使用secrets模块。
5.3 与其它素性测试算法的对比
- 试除法:最简单,但时间复杂度O(√n),仅适用于非常小的数(如<10^12)。
- Fermat测试:比试除法快,但存在大量的卡迈克尔数(Carmichael numbers)能通过所有基数的费马测试,因此不安全。
- Miller-Rabin测试:概率性,但速度快,错误概率可控,是实际应用的标准。
- AKS素性测试:第一个被证明的、通用的、多项式时间的确定性素性测试算法。但其理论意义大于实践,因为它的常数因子很大,对于实际使用的大数,速度远慢于Miller-Rabin。
- Baillie-PSW测试:一种结合了Miller-Rabin和Lucas序列的测试,至今未发现反例,被认为是实际应用中非常可靠的确定性测试(对于64位以内整数)。许多数学软件(如Mathematica)用它作为默认测试。
在实际中,生成一个可能用于RSA的素数,标准做法是:随机生成一个大的奇数,然后用小素数试除过滤掉明显有因子的数,最后进行足够多次(如20次)的Miller-Rabin测试。如果要求绝对确定(且数字在特定范围内),则使用确定性的基数集。
6. 常见问题、调试技巧与实战心得
即使理解了算法,在实现和调试过程中也难免会遇到问题。这里记录一些典型的坑和解决思路。
6.1 典型错误与排查清单
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 对小素数(如5,7,11)判断错误 | 基数a选择范围错误,包含了0,1,n-1或n | 确保基数a在区间(1, n-1)内随机选取。例如a = 2 + rand() % (n - 3)。 |
| 算法对某些合数(如9,15)永远返回“可能是素数” | 测试轮数k太少,或随机数生成器种子固定,导致每次都选到“强伪证”基数。 | 增加测试轮数k(如10以上)。使用时间或其他熵源初始化随机种子。对于小范围,使用确定性测试。 |
| 程序在处理稍大的数时速度极慢 | 模幂运算实现效率低下,可能是直接计算a^d导致溢出或计算量巨大。 | 务必使用快速模幂算法(反复平方法)。检查你的mod_pow函数实现是否正确。 |
| C++版本在处理大数时结果错误或崩溃 | 整数溢出。(a * b) % mod中的a*b可能超出long long范围。 | 使用__int128中间类型(如果编译器支持),或实现“慢速乘法”(通过加法循环),或直接使用大数库(GMP)。 |
| Python版本一切正常,C++版本不对 | 随机数生成质量不同,或溢出处理不同。 | 在C++中,使用<random>库的mt19937等更好的生成器。统一使用大数库进行运算以排除溢出。 |
| 确定性测试函数对某个已知素数返回false | 使用的确定性基数集不适用于该数所在的范围。 | 核对你的确定性基数表。确保覆盖了待测数n的范围。例如,对于n<2^32,{2, 7, 61}是足够的;对于n<2^64,需要{2, 3, 5, 7, 11, 13, 17}等。 |
6.2 调试与验证策略
- 从小开始:先用2, 3, 5, 7, 11这些显而易见的素数,以及4, 9, 15, 21这些显而易见的合数测试你的函数。
- 使用已知的伪素数:用一些已知的强伪素数来测试你的概率版本。例如,
2047是一个以2为基的强伪素数。你的算法在k=1且a=2时,会错误地认为它是素数。增加测试轮数或更换基数应能检测出来。 - 交叉验证:用你的Python实现和C++实现测试同一组数字,看结果是否一致。Python的
pow(a, d, n)是经过高度优化的标准实现,可以作为参考基准。 - 性能剖析:对于大数,使用计时工具。如果速度不符合O(k log³ n)的预期,检查模幂运算是否是瓶颈。
6.3 一个完整的测试用例示例
def test_miller_rabin(): test_cases = [ (2, True), (3, True), (4, False), (5, True), (9, False), (11, True), (15, False), (17, True), (25, False), (29, True), (49, False), (97, True), (561, False), # 卡迈克尔数,费马测试会失败,MR测试应能检测 (1105, False), # 另一个卡迈克尔数 (2047, False), # 以2为基的强伪素数 (7919, True), # 一个较大的素数 ] print("Testing probabilistic version (k=5):") for n, expected in test_cases: result = is_probable_prime(n, 5) status = "OK" if result == expected else "FAIL" print(f" is_probable_prime({n}) = {result}, expected {expected} [{status}]") print("\nTesting deterministic version:") for n, expected in test_cases: result = is_prime_deterministic(n) status = "OK" if result == expected else "FAIL" print(f" is_prime_deterministic({n}) = {result}, expected {expected} [{status}]") if __name__ == "__main__": test_miller_rabin()运行这个测试,你可以快速验证你的实现基本是否正确。
7. 应用场景延伸与代码整合示例
Miller-Rabin算法不仅是课堂上的数学玩具,它在真实世界中扮演着关键角色。
7.1 在RSA密钥生成中的应用
RSA算法的第一步就是生成两个大素数p和q。这个过程大致如下:
- 随机生成一个大的奇数n(通常是1024位或2048位)。
- 用一系列小素数(比如前1000个素数)对n进行试除。如果能整除,则n是合数,回到步骤1。这一步能快速过滤掉大部分有明显因子的数。
- 对通过试除的n,进行多次(比如20次)Miller-Rabin测试。如果全部通过,则认为n是素数。如果任何一次测试失败,则n是合数,回到步骤1。
- 重复以上过程,直到找到两个素数p和q。
7.2 一个简单的“寻找下一个素数”函数
有时我们需要找到一个大于某个数N的素数。一个简单的方法是逐个测试奇数。
def next_prime(n): """返回大于n的最小素数(使用概率性测试)""" if n < 2: return 2 if n == 2: return 3 # 从下一个奇数开始 candidate = n + 1 if n % 2 == 0 else n + 2 while True: if is_probable_prime(candidate, k=10): # 使用10轮测试 return candidate candidate += 2 # 只测试奇数实操心得:在需要连续生成多个素数的场景下,上述方法效率不高。更专业的做法是使用“素数间隙”的统计知识,或者使用“筛法”的变种来预生成一个区间的候选数,然后再用Miller-Rabin测试。对于极其关键的应用,在Miller-Rabin测试之后,有时还会加上一次Lucas测试(构成Baillie-PSW测试)以增加信心,尽管对于随机大数,多次MR测试已经足够可靠。
7.3 性能对比:Python内置math.isqrt与试除法
Python标准库math模块里有一个isqrt函数用于计算整数平方根,我们可以结合它实现一个简单的试除法来对比性能。
import math import time def is_prime_trial_division(n): """试除法判断素数""" if n <= 1: return False if n == 2: return True if n % 2 == 0: return False limit = math.isqrt(n) # 计算整数平方根 for i in range(3, limit + 1, 2): if n % i == 0: return False return True # 性能测试 def benchmark(): test_number = 9999999967 # 一个10位的素数 iterations = 100 # 测试试除法 start = time.time() for _ in range(iterations): is_prime_trial_division(test_number) trial_time = time.time() - start # 测试Miller-Rabin (10轮) start = time.time() for _ in range(iterations): is_probable_prime(test_number, 10) mr_time = time.time() - start print(f"测试数字: {test_number}") print(f"试除法 ({iterations}次) 耗时: {trial_time:.4f} 秒") print(f"Miller-Rabin 10轮 ({iterations}次) 耗时: {mr_time:.4f} 秒") print(f"MR比试除法快: {trial_time/mr_time:.2f} 倍") benchmark()在我的机器上运行,对于一个10位的素数,Miller-Rabin算法(10轮)通常比试除法快数十倍甚至上百倍。随着数字位数的增加,这个差距会呈指数级扩大。这就是为什么对于密码学规模的大数(几百位),试除法完全不可行,而Miller-Rabin仍然是首选。
最后,无论是学习算法思想,还是将其应用于实际项目,理解Miller-Rabin的每一个细节——从数学原理到防溢出的编程技巧——都能让你更从容地面对需要处理素数的场景。记住,在大多数情况下,相信概率(一个经过充分测试的概率)比试图去追求绝对的确定性(在有限时间内)要实用得多。