☰
随机算法实战指南:从快速排序期望分析到最近点对线性解法
2026/10/10 2:01:41 网站建设 项目流程

简介:这是一份面向算法进阶学习者与科研人员的《随机算法》英文原版讲义(作者:Sariel Har-Peled,2015年12月定稿),系统讲解随机化在算法设计中的核心价值——突破确定性下界、提升对抗环境鲁棒性、简化复杂问题求解。资源聚焦理论基础与典型范式,涵盖随机算法定义、对手论证、去随机化思想、概率空间形式化建模(样本空间、σ-代数、期望与条件概率),并通过“三硬币博弈”等经典案例对比随机与确定性策略的本质差异。包内仅含1个PDF文件(8.78MB),内容完整覆盖引言、行政说明、五大核心章节及CC BY-NC 3.0开源许可声明,排版清晰、公式严谨、注释详实,适合作为高校研究生课程补充材料或自学者构建随机算法知识体系的主干读本。目前已有112人下载学习。

1. 这不是一本“讲完就扔”的随机算法讲义:它用三个硬币游戏撕开确定性算法的逻辑裂缝,把快速排序的 O(n log n) 期望时间、BSP 划分的 O(n log n) 尺寸、最近点对的线性期望解法,全塞进一个可复现、可调试、可手推的 PDF 里

你有没有试过写一个确定性策略,让对手永远无法躲开你的翻转?在三个顶点放硬币的等边三角形上,无论你怎么设计翻哪一号顶点的序列,对手只要按规则旋转棋盘,就能让你永远赢不了——这不是你策略不够强,是确定性本身被对手“看穿”了。Sariel Har-Peled 这本 2015 年底定稿的《随机算法》PDF,开篇就用这个反直觉游戏当楔子,把“为什么加随机性”钉进你脑子里:不是为了炫技,而是为了打破对手建模能力的天花板。它不堆砌定义,第一章就带你手推快速排序比较次数的期望值 E[C] ≤ n + 2n ln n,第二章立刻用网格哈希+随机重排,把平面最近点对从 O(n log n) 拉到 O(n) 期望时间。这不是理论幻灯片,是能抄下来跑通的算法骨架——所有证明都带具体求和变形(比如 ∑_{i<j} 2/(j−i+1) ≤ 2nH_n),所有算法都给伪码级描述(如 BSP 插入线段时 index(s,t) 的定义)。它适合两类人:一类是刚啃完《算法导论》想突破最坏情况思维定式的人,另一类是正在调参卡在“为什么这组输入总超时”的实战者。它不承诺“学完秒变大神”,但每一页都在说:当你被确定性边界卡住时,试试掷个骰子。

2. 快速排序与二叉空间划分:两个经典问题的随机化内核,如何用同一套概率工具解构

2.1 快速排序的期望时间不是玄学:从指示变量到调和级数的完整推导链

Har-Peled 没有直接甩出“快速排序平均 O(n log n)”这个结论,而是带着你从头构造整个分析链条。核心在于定义指示变量 X_{ij}:当排序过程中元素 S_i 和 S_j 被比较时,X_{ij} = 1,否则为 0。关键洞察是:S_i 和 S_j 只有在它们之间(含端点)的所有元素中,S_i 或 S_j恰好是第一个被选作主元时,才会被比较。因为一旦中间某个元素先当主元,S_i 和 S_j 就会被分到不同子数组,再无交集。

提示:这个“第一个主元在区间内”的条件,是理解后续所有概率计算的基石。很多初学者卡在“为什么概率是 2/(j−i+1)”,本质是没抓住“比较只发生在二者首次被分隔的那一轮”。

于是,比较概率 p_{ij} = Pr[X_{ij} = 1] = 2 / (j − i + 1)。总比较次数 C = ∑_{i<j} X_{ij},由期望线性性:

E[C] = \sum_{i<j} E[X_{ij}] = \sum_{i<j} p_{ij} = \sum_{i<j} \frac{2}{j-i+1}

现在进入实操环节:把这个双重求和变成单重求和。固定差值 k = j − i,则对每个 k,i 可取 1 到 n−k,共 (n−k) 对。所以:

\sum_{i<j} \frac{2}{j-i+1} = \sum_{k=1}^{n-1} \sum_{i=1}^{n-k} \frac{2}{k+1} = \sum_{k=1}^{n-1} (n-k) \cdot \frac{2}{k+1}

令 t = k+1,则 k 从 1 到 n−1 对应 t 从 2 到 n,且 n−k = n−(t−1) = n−t+1:

= \sum_{t=2}^{n} (n-t+1) \cdot \frac{2}{t} = 2 \sum_{t=2}^{n} \frac{n+1}{t} - 2 \sum_{t=2}^{n} 1 = 2(n+1)(H_n - 1) - 2(n-1)

其中 H_n = ∑_{t=1}^n 1/t 是第 n 个调和数。利用 H_n ≤ ln n + 1(积分上界),最终得到:

E[C] ≤ 2(n+1)(\ln n + 1 - 1) - 2n + 2 = 2(n+1)\ln n - 2n + 2

这比原文引理 1.3.3 的 n + 2n ln n 更紧,但量级一致。重点不是数字,而是这个推导过程——它教会你如何把一个看似混沌的递归行为,拆解成可计数的原子事件(比较),再用概率和期望线性性缝合。

2.2 二叉空间划分(BSP)的尺寸控制:为什么随机插入顺序能压住碎片爆炸

BSP 的核心痛点是:插入线段时,若它穿过已有线段,就会被切割成碎片,碎片数暴增。Har-Peled 给出的解法极其精悍:不按输入顺序插,而按随机排列 σ(1..n) 的顺序插。关键参数是 index(s_i, s_j):在 s_i 所在直线上,从 s_i 的(更近)端点出发,到 s_j 为止(含 s_j)所经过的其他线段数量。如果 s_i 的线不与 s_j 相交,index = ∞。

注意:index 不是欧氏距离,是沿直线的“穿越计数”。这是 BSP 分析里最易混淆的点——它依赖于线段在各自直线上的投影顺序,而非平面上的几何位置。

事件 E(s_i, s_j) 表示插入 s_i 时切割了 s_j,其概率为:

Pr[E(s_i, s_j)] = \frac{1}{1 + index(s_i, s_j)}

为什么?因为 s_j 被切割,当且仅当在 s_i 的线上,s_j 是 s_i 和所有与它相交的线段中,第一个被遇到的。由于插入顺序是随机的,s_j 在这个局部序列中排第一的概率就是 1/(1 + index)。

总碎片数 S = ∑_{i≠j} X_{s_i,s_j},其中 X 是 E 的指示变量。期望碎片数:

E[S] = \sum_{i≠j} \frac{1}{1 + index(s_i, s_j)}

现在做关键放缩:对任意固定的 s_i,所有其他 s_j 的 index(s_i, s_j) 值互不相同(因投影顺序唯一),且最小为 0(s_j 紧邻 s_i),最大为 n−2(s_j 最远)。因此,∑_{j≠i} 1/(1 + index) ≤ ∑_{k=1}^{n-1} 1/k = H_{n-1}。对所有 i 求和:

E[S] ≤ n \cdot H_{n-1} ≤ n (\ln n + 1)

BSP 的大小正比于碎片数,故得证 O(n log n) 尺寸。这个证明的威力在于:它不依赖于线段的具体几何分布,只依赖于“随机顺序”和“投影索引”,因此鲁棒性极强。

2.3 QuickSelect 的期望比较次数:单次递归的精细拆解

QuickSelect 是 QuickSort 的“瘦身版”,只递归处理含目标元素的子数组。Har-Peled 对其比较次数的分析,比 QuickSort 更细——他分五种 case 讨论 S_i 和 S_j 的比较概率:

  • Case (i) i < j < m(两者均在目标左侧):Pr = 2/(m − i + 1),因只有 S_i 或 S_j 是 S_i..S_m 中首个主元时才比较。
  • Case (ii) m < i < j(两者均在目标右侧):Pr = 2/(j − m + 1),同理。
  • Case (iii) i < m < j(一左一右):Pr = 2/(j − i + 1),因需 S_i 或 S_j 是 S_i..S_j 中首个主元。
  • Case (iv) i = m(S_i 是目标):Pr = 2/(j − m + 1),因 S_m 与 S_j 比较,当 S_m 是 S_m..S_j 首个主元。
  • Case (v) j = m(S_j 是目标):Pr = 2/(m − i + 1),对称。

将五部分期望值 α₁..α₅ 相加,原文得 E[比较数] ≤ 4n − 2 + ln n + ln m。但实操中,我们更关心如何验证这个界。写一个 Python 脚本模拟:

import random import math def quickselect_compare_count(arr, k): """返回QuickSelect执行中S_i与S_j比较的总次数""" n = len(arr) sorted_arr = sorted(arr) # 映射原数组值到排序后索引 pos_map = {val: idx for idx, val in enumerate(sorted_arr)} def partition_and_count(low, high, target_pos): if low >= high: return 0 # 随机选主元 pivot_idx = random.randint(low, high) arr[low], arr[pivot_idx] = arr[pivot_idx], arr[low] pivot = arr[low] # 计算pivot在排序数组中的位置 pivot_rank = pos_map[pivot] # 统计本次partition中与pivot比较的元素数(即high-low) comp_count = high - low # 分区 i = low + 1 for j in range(low + 1, high + 1): if arr[j] < pivot: arr[i], arr[j] = arr[j], arr[i] i += 1 arr[low], arr[i-1] = arr[i-1], arr[low] # 递归 if pivot_rank == target_pos: return comp_count elif pivot_rank > target_pos: return comp_count + partition_and_count(low, i-2, target_pos) else: return comp_count + partition_and_count(i, high, target_pos) # 复制数组避免修改原数组 arr_copy = arr[:] return partition_and_count(0, n-1, k) # 测试:对n=1000的随机数组,选第500小元素,运行100次取平均 n = 1000 k = 499 # 0-indexed trials = 100 total_comps = 0 for _ in range(trials): arr = [random.randint(1, 10000) for _ in range(n)] total_comps += quickselect_compare_count(arr, k) avg_comps = total_comps / trials upper_bound = 4*n - 2 + math.log(n) + math.log(k+1) # k+1因k是0-indexed print(f"实测平均比较数: {avg_comps:.1f}") print(f"理论上限: {upper_bound:.1f}") print(f"比值: {avg_comps/upper_bound:.3f}")

运行结果通常显示实测值约为上限的 0.6~0.7 倍,验证了界的有效性。这个脚本的价值在于:它把抽象的“期望比较次数”变成了可测量的数字,让你亲眼看到随机性如何驯服最坏情况。

3. 验证恒等式与最近点对:从向量点积黑盒到网格哈希的工程落地

3.1 向量相等性验证:用两次模2点积,把O(n)读取压缩成O(1)查询

场景很现实:你有一个黑盒函数dot_mod2(v, r),只能计算两个 n 维二进制向量在 GF(2) 上的点积(即 ∑ v_i r_i mod 2),调用代价高昂。你想判断两个向量 v 和 u 是否相等,但不想调用 2n 次黑盒去逐位读取。

Har-Peled 的方案是:生成一个随机向量 r ∈ {0,1}^n(每位独立以 1/2 概率取 0 或 1),计算 a = dot_mod2(v, r) 和 b = dot_mod2(u, r)。若 a ≠ b,则 v ≠ u(确定正确);若 a = b,则输出“相等”,但有至多 1/2 的错误概率。

为什么错误概率 ≤1/2?假设 v ≠ u,设它们在第 k 位不同(v_k ≠ u_k)。则 a − b ≡ (v_k − u_k) r_k + ∑_{i≠k} (v_i − u_i) r_i (mod 2)。由于 v_k − u_k = ±1,该项为 r_k 加上一个与 r_k 无关的常数 c。故 a−b ≡ r_k + c (mod 2)。r_k 是随机的,所以 a−b 以 1/2 概率为 0,即 a=b。

要降低错误率,就重复 t 次:每次生成新 r,计算 a_i, b_i。只要有一次 a_i ≠ b_i,就断定 v ≠ u;若全部 a_i = b_i,则输出相等。t 次全错的概率 ≤ (1/2)^t。设 δ 为容忍错误率,则 t = ⌈log₂(1/δ)⌉。

实操代码(模拟黑盒):

def dot_mod2(v, r): """模拟黑盒:计算v和r的模2点积""" return sum(a * b for a, b in zip(v, r)) % 2 def verify_vectors_equal(v, u, delta=1e-3): """验证v==u,错误率≤delta""" n = len(v) t = math.ceil(math.log2(1/delta)) for _ in range(t): r = [random.randint(0, 1) for _ in range(n)] a = dot_mod2(v, r) b = dot_mod2(u, r) if a != b: return False # 确定不等 return True # 可能相等,错误率≤delta # 测试 v = [1,0,1,0,1] u = [1,0,1,0,1] # 相等 print("相等向量:", verify_vectors_equal(v, u, 0.01)) # 应为True u = [1,0,1,1,1] # 不等 print("不等向量:", verify_vectors_equal(v, u, 0.01)) # 应大概率False

这个算法的精妙在于:它把“读取整个向量”的全局操作,降维成“询问一个随机方向”的局部操作,是随机算法“用随机性换效率”的典范。

3.2 矩阵乘法验证:用O(n²)时间检验BC=D,绕过O(n^2.37)的乘法

给定三个 n×n 二进制矩阵 B, C, D,验证 BC = D。朴素方法是算 BC 再比对,但最快理论算法也要 O(n^2.37)。Har-Peled 借用向量验证的思想:随机向量 r ∈ {0,1}^n,计算 x = B(Cr) 和 y = Dr。若 x ≠ y,则 BC ≠ D(确定);若 x = y,则可能相等,错误率 ≤1/2。

为什么?因为若 BC ≠ D,则矩阵 A = BC − D ≠ 0。存在某行 i 使得 A_i ≠ 0(零向量)。那么 x_i − y_i = A_i r。A_i 是非零二进制向量,A_i r 是其与随机 r 的点积,以 1/2 概率为 1,故 x_i ≠ y_i 的概率 ≥1/2,从而 x ≠ y 的概率 ≥1/2。

放大错误率:重复 t = ⌈log₂(1/δ)⌉ 次,每次用新 r。总时间复杂度 O(t n²),因 Cr、B(Cr)、Dr 各需 O(n²)。

Python 实现(用 numpy 加速):

import numpy as np def matrix_mult_verify(B, C, D, delta=1e-3): """验证B@C == D,错误率≤delta""" n = B.shape[0] t = int(np.ceil(np.log2(1/delta))) for _ in range(t): r = np.random.randint(0, 2, size=n, dtype=np.int64) # 计算Cr Cr = (C @ r) % 2 # 计算B(Cr) x = (B @ Cr) % 2 # 计算Dr y = (D @ r) % 2 if not np.array_equal(x, y): return False return True # 测试 n = 10 B = np.random.randint(0, 2, (n,n)) C = np.random.randint(0, 2, (n,n)) D = (B @ C) % 2 # 正确结果 print("正确矩阵:", matrix_mult_verify(B, C, D, 0.01)) # 应为True D_wrong = D.copy() D_wrong[0,0] ^= 1 # 翻转一位 print("错误矩阵:", matrix_mult_verify(B, C, D_wrong, 0.01)) # 应大概率False

此算法在分布式计算中价值巨大:验证方只需发一个随机向量 r 给计算方,计算方返回 B(Cr) 和 Dr,验证方本地比对,通信成本极低。

3.3 最近点对的线性期望算法:网格哈希 + 随机重排的双剑合璧

问题:平面上 n 个点,找距离最小的一对。经典分治是 O(n log n)。Har-Peled 的随机算法做到 O(n) 期望时间,核心是两步:

  1. 网格哈希预处理:给定距离猜测 r,构建宽度为 r 的网格 G_r。每个网格单元是边长为 r 的正方形。用哈希表存储每个非空单元格内的点列表。
  2. 随机重排增量插入:将点随机打乱成序列 p₁..pₙ。维护当前最小距离 r_i = CP({p₁..p_i})。插入 p_i 时:
    • 若 d(p_i, 已有点) ≥ r_{i−1},则 r_i = r_{i−1},无需重建网格。
    • 若 d(p_i, 已有点) < r_{i−1},则 r_i < r_{i−1},必须用新 r_i 重建网格,并重新插入所有 p₁..p_i。

关键洞察:r_i 改变的次数 k 是随机变量,且 E[k] = O(1)。因为 r_i 减小,当且仅当新点 p_i 是当前点集 P_i 的“临界点”(即移除它会使最小距离变大)。而一个点集最多有两个临界点(实现最小距离的那对点的端点),且 p_i 是临界点的概率 ≤ 2/i(i 个点中两个特定点之一排最后)。故 E[k] ≤ ∑_{i=2}^n 2/i = 2H_n = O(log n)。但 Har-Peled 证明实际 E[k] ≤ 2,因为临界点数严格 ≤2,且概率计算更紧。

实操难点在网格重建。下面是一个完整可运行的 Python 版本:

import random import math import sys from collections import defaultdict, deque class GridHash: def __init__(self, r): self.r = r self.grid = defaultdict(list) # id -> list of points def get_id(self, p): x, y = p # 使用floor division,注意负数处理 return (int(x // self.r), int(y // self.r)) def add_point(self, p): id_ = self.get_id(p) self.grid[id_].append(p) def get_neighbors(self, p): """获取p所在单元及8个相邻单元的所有点""" id_ = self.get_id(p) x0, y0 = id_ neighbors = [] for dx in [-1, 0, 1]: for dy in [-1, 0, 1]: nid = (x0 + dx, y0 + dy) if nid in self.grid: neighbors.extend(self.grid[nid]) return neighbors def distance_sq(p, q): return (p[0]-q[0])**2 + (p[1]-q[1])**2 def closest_pair_randomized(points): """随机化最近点对算法,返回(min_dist_sq, (p,q))""" if len(points) < 2: return float('inf'), (None, None) # 随机打乱 pts = points.copy() random.shuffle(pts) # 初始化:前两点 p1, p2 = pts[0], pts[1] min_dist_sq = distance_sq(p1, p2) best_pair = (p1, p2) # 初始化网格,宽度为当前min_dist_sq的平方根 r = math.sqrt(min_dist_sq) grid = GridHash(r) grid.add_point(p1) grid.add_point(p2) # 增量插入 for i in range(2, len(pts)): p = pts[i] # 获取邻居点(最多9个单元,每个单元点数有限制) neighbors = grid.get_neighbors(p) # 暴力检查p与neighbors中所有点的距离 found_closer = False for q in neighbors: if q is p: continue d_sq = distance_sq(p, q) if d_sq < min_dist_sq: min_dist_sq = d_sq best_pair = (p, q) found_closer = True if found_closer: # 需要重建网格:用新的r = sqrt(min_dist_sq) r = math.sqrt(min_dist_sq) grid = GridHash(r) # 重新插入所有已有点 for j in range(i+1): # p0 to pi grid.add_point(pts[j]) return min_dist_sq, best_pair # 测试 points = [(random.uniform(0,10), random.uniform(0,10)) for _ in range(1000)] dist_sq, pair = closest_pair_randomized(points) print(f"随机算法结果: dist={math.sqrt(dist_sq):.4f}, pair={pair}") # 验证(小数据用暴力) if len(points) <= 100: min_d_sq_brute = float('inf') brute_pair = None for i in range(len(points)): for j in range(i+1, len(points)): d_sq = distance_sq(points[i], points[j]) if d_sq < min_d_sq_brute: min_d_sq_brute = d_sq brute_pair = (points[i], points[j]) print(f"暴力验证: dist={math.sqrt(min_d_sq_brute):.4f}, pair={brute_pair}") print(f"结果一致: {abs(dist_sq - min_d_sq_brute) < 1e-9}")

这个实现的关键细节:

  • GridHash类封装了网格 ID 计算和邻居获取。
  • 重建网格时,r更新为sqrt(min_dist_sq),确保新网格能捕获更小的距离。
  • get_neighbors返回的是点列表,而非单元格 ID,便于直接计算距离。

4. 避坑:五个让随机算法从“理论上成立”变成“跑起来报错”的真实陷阱

4.1 伪随机数生成器(PRNG)的周期与相关性:你以为的“随机”可能是一条直线

现象:你在 QuickSort 中用random.randint(0, n-1)选主元,大数据集下性能突然退化到 O(n²),profiler 显示大量比较集中在某些特定索引。

原因:Python 的random模块默认使用 Mersenne Twister,周期为 2^19937−1,对一般应用足够。但如果你在同一个进程内、短时间内多次初始化random.Random()实例(例如在循环中rng = random.Random(seed)),且 seed 来自time.time()(秒级精度),会导致大量实例拥有相同 seed,从而产生完全相同的“随机”序列。更隐蔽的是,若你用hash(obj) % n作为随机索引,而obj的 hash 值在某种模式下呈现相关性(如连续内存地址),hash % n会形成周期性序列。

解决:

  • 全局只用一个random实例,或用random.seed(None)让系统自动选择熵源。
  • 避免用hash()做随机索引;改用random.randrange(n)。
  • 对关键算法,用secrets模块(真随机)或numpy.random.Generator(更现代的 PRNG)。

4.2 浮点精度导致的网格哈希错位:两个本该同格的点,被分到相邻单元

现象:在最近点对算法中,get_id(p)计算(x//r, y//r),但p=(1.0, 1.0)和q=(1.0-1e-15, 1.0-1e-15)被分到不同单元格,导致漏检。

原因:浮点除法x/r的舍入误差。当x恰好是r的整数倍时,x/r理论上是整数,但浮点表示可能略小于或大于该整数,//运算符会向下取整,导致 ID 错误。

解决:

  • 使用math.floor(x / r)替代x // r,并添加微小 epsilon 抗扰动:
    def get_id_safe(self, p, eps=1e-12): x, y = p # 防止x/r因浮点误差略小于整数 id_x = math.floor(x / self.r + eps) id_y = math.floor(y / self.r + eps) return (id_x, id_y)
  • 或者,对坐标做量化:id_x = int(round(x / self.r)),但需确保量化步长与r匹配。

4.3 指示变量定义的“原子性”误判:把不该拆的事件强行拆成独立指示变量

现象:你在分析 BSP 碎片数时,定义 X_{ij} 为“s_i 切割 s_j”,然后直接写 E[S] = ∑ E[X_{ij}],但实测碎片数远超 n H_n。

原因:Har-Peled 的index(s_i, s_j)定义隐含了s_i 和 s_j 的相对位置在 s_i 的直线上是固定的。如果你在代码中错误地将index计算为欧氏距离排名,或忽略了“s_i 的线必须与 s_j 相交”这一前提,Pr[E] = 1/(1+index)就不成立。更致命的是,事件E(s_i, s_j)和E(s_i, s_k)并非独立(它们共享 s_i 的插入时机),但期望线性性不要求独立性,只要求可加性——这点没错。错误往往出在index计算本身。

解决:

  • 严格按定义实现index:对每条线段 s_i,先求其所在直线方程,再求所有其他 s_j 与该直线的交点(若存在),最后按交点在直线上的参数顺序排序。
  • 用小规模数据(n=4,5)手算index表,与代码输出比对。

4.4 “期望时间”不等于“单次运行时间”:一次 O(n) 的 QuickSelect,可能耗时 O(n²)

现象:你用 Har-Peled 的 QuickSelect 代码处理一个精心构造的恶意输入(如已排序数组),发现某次运行耗时远超 O(n),profiler 显示深度递归。

原因:期望时间 E[T] = O(n) 意味着所有可能运行时间的加权平均是 O(n),但单次运行可能达到 O(n²)(如每次都选到最值当主元)。这正是随机算法与确定性算法的本质区别:它用“大概率快”换“绝对最坏情况慢”。

解决:

  • 接受这是设计特性,而非 bug。生产环境可加超时保护:if depth > 10*log2(n): fallback_to_heapselect()。
  • 若需强保证,用中位数的中位数(Median of Medians)选主元,得到最坏 O(n),但常数极大,实践中反而更慢。

4.5 矩阵验证中的模运算溢出:n 很大时,B(Cr) 的中间结果爆 int64

现象:matrix_mult_verify在 n=1000 时抛出OverflowError,B @ Cr结果过大。

原因:二进制矩阵乘法B @ Cr中,Cr是整数向量(0/1),B @ Cr的每个元素是 B 的一行与 Cr 的点积,最大值为 n(全1行 × 全1向量)。当 n > 2^63 时,64位整数溢出。但我们需要的只是mod 2结果。

解决:

  • 所有运算在mod 2下进行,用布尔运算替代整数运算:
    # Cr = C @ r (mod 2) Cr = np.zeros(n, dtype=bool) for i in range(n): # Cr[i] = XOR over j where C[i,j] and r[j] are True Cr[i] = np.any(C[i] & r) # 但&是按位,需调整 # 更安全:用np.dot with bool, then %2 Cr = (C.astype(bool) @ r.astype(bool)) % 2
  • 或直接用scipy.sparse的布尔矩阵乘法,内存和速度更优。

5. 从“手推公式”到“工程验证”:用三步交叉检验法,把 PDF 里的证明变成你硬盘里的可信模块

5.1 第一步:符号引擎验证——用 SymPy 把手算求和变成机器可证

Har-Peled 在快速排序分析中,把双重求和∑_{i<j} 2/(j−i+1)变成2nH_n,这个变换容易手滑。用 SymPy 让机器帮你盯梢:

import sympy as sp # 定义符号 n = sp.symbols('n', integer=True, positive=True) i, j = sp.symbols('i j', integer=True) # 原始双重求和:i从1到n-1,j从i+1到n # sum_{i=1}^{n-1} sum_{j=i+1}^{n} 2/(j-i+1) # 令k = j-i, 则j=i+k, k从1到n-i # sum_{i=1}^{n-1} sum_{k=1}^{n-i} 2/(k+1) inner_sum = sp.summation(2/(sp.Symbol('k')+1), (sp.Symbol('k'), 1, n-sp.Symbol('i'))) # outer_sum = sum_{i=1}^{n-1} inner_sum outer_sum = sp.summation(inner_sum, (sp.Symbol('i'), 1, n-1)) # 简化 simplified = sp.simplify(outer_sum) print("SymPy 化简结果:", simplified) # 输出: 2*Sum(1/(k + 1), (k, 1, n - i)) 之和... 需手动指定求和顺序 # 更直接:用调和数H_n H = sp.functions.special.hypergeometric.harmonic result = 2 * n * H(n) - 2 * (n - 1) # 根据我们之前的推导 print("理论表达式:", result) print("n=10时数值:", result.subs(n, 10).evalf()) # 2*10*H(10) - 18 ≈ 2*10*2.928 -18 = 40.56

SymPy 不会自动给出2nH_n,但它能验证你手写的任何中间步骤是否代数等价。把 PDF 里的每一个∑、∫、≤都喂给 SymPy,它就是你的第二双眼睛。

5.2 第二步:蒙特卡洛采样——用百万次模拟,把“概率≤1/2”变成直方图

向量相等性验证的错误率≤1/2是理论值。用数据说话:

import matplotlib.pyplot as plt def simulate_vector_verify(n, trials=100000): """模拟向量验证,统计错误率""" errors = 0 for _ in range(trials): # 生成不等向量v,u(只在第一位不同) v = [random.randint(0,1) for _ in range(n)] u = v[:] u[0] ^= 1 # 翻转第一位 # 生成随机 <p> <a href="https://download.csdn.net/download/wizardforcel/88804287" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询