上12周 考试和考试周一起考 18 19周
24节课
考察内容:
考虑正确性证明(常用循环不变性+归纳法证明)

循环不变式(Loop Invariant)是算法设计与分析中的一个核心概念,尤其在证明迭代算法 (如排序、查找、图遍历等)的正确性时起着至关重要的作用。它本质上是一种在循环执行过程中始终保持为真的逻辑断言或性质。
一、循环不变式的定义
循环不变式是指:在循环的每次迭代开始前(有时也包括结束后),某个特定条件始终成立。这个条件描述了算法在该时刻所维护的关键状态或结构性质。
更形式化地说,设有一个循环结构,其循环变量为 jj ,循环不变式 I(j)I(j) 是一个关于程序状态(如数组内容、变量值等)的谓词,满足:
- 在首次进入循环前, I(j)I(j) 成立;
- 若某次迭代前 I(j)I(j) 成立,则执行完该次迭代后,下一次迭代开始前 I(j+1)I(j+1) 仍成立;
- 当循环终止时, I(j)I(j) 与终止条件共同蕴含算法的最终目标已达成。
二、为什么循环不变式能用于证明算法正确性?
循环不变式之所以有效,是因为它的验证过程严格对应于数学归纳法的逻辑结构,并通过三个关键步骤确保算法从初始状态到终止状态的每一步都"走在正确的轨道上"。
1. 初始化(Initialization)
证明在第一次循环迭代之前,循环不变式成立。
这相当于数学归纳法中的基础情形(base case)。例如,在插入排序中,当 j=1j=1 时,子数组 A0..j−1=A0A0..j−1=A0 只包含一个元素,显然是有序的,因此不变式"A0..j-1 是非降序排列的"成立。
2. 保持性(Maintenance)
假设在第 kk 次迭代开始前 不变式成立,证明在第 k+1k+1 次迭代开始前它仍然成立。
这对应于数学归纳法的归纳步骤。例如,在插入排序中,若 A0..j−1A0..j−1 已有序,算法将 AjAj 插入到合适位置,使得 A0..jA0..j 依然有序。这一操作由内层 while 循环完成(向右移动大于 key 的元素),从而保证了不变式的延续。
3. 终止(Termination)
当循环结束时,结合循环终止条件和不变式,能够推出算法达到了预期目标。
这是循环不变式区别于纯数学归纳法的关键------归纳在此处停止,并用于推导最终结果。例如,插入排序的外层循环在 j=nj=n (数组长度)时终止,此时不变式表明 A0..n−1A0..n−1 整体有序,即整个数组已排序完成。
正如《算法导论》所强调:"第三条性质或许是最重要的,因为我们将使用循环不变式来证明正确性。"
三、循环不变式的实际意义
-
提供清晰的算法理解框架
它迫使程序员明确回答:"我在循环中试图维护什么不变的性质?"这种思维习惯能显著减少逻辑错误。
-
支持形式化验证
在高可靠系统(如航天、金融)中,可通过断言(assertion)在代码中嵌入不变式检查,实现运行时验证。例如
1assert is_sorted(A, j) # 每次循环前后检查 A[0:j] 是否有序 -
适用于多种算法范式
- 排序:插入排序(前缀有序)、冒泡排序(后缀已排好最大元素);
- 查找:线性查找(已检查部分不含目标值)、二分查找(目标若存在必在 l, r 区间内);
- 链表处理:删除重复节点(当前指针前无重复)。
四、举例说明:插入排序的循环不变式
以升序插入排序为例:
1def INSERTION_SORT(A):
2 j = 1
3 while j < len(A):
4 key = A[j]
5 i = j - 1
6 while i >= 0 and key < A[i]:
7 A[i+1] = A[i]
8 i -= 1
9 A[i+1] = key
10 j += 1
- 循环不变式 :在每次外层循环开始时,子数组
A[0..j-1]是原数组中前j个元素的有序排列。 - 初始化 :
j=1时,A[0..0]单元素,自然有序。 - 保持 :假设
A[0..j-1]有序,将A[j]插入后,A[0..j]仍有序。 
- 终止 :当
j = len(A)时,A[0..len(A)-1]全部有序,算法正确。
算法的效率------空间时间复杂度
独立的,于机器无关
关注相对的近似的时间
考虑在大规模输入下的增长趋势

常数证明法:
证明2^(2n) budengyu O(2^n)


证明:
定义证明法:(和常数证明法一致,就是定义证明法)

常数证明法:

极限证明法:


洛必达:0/0型或∞/∞

算法优化问题
迭代和递归哪个更快
一般迭代反而更快,因为递归要调用函数产生开销,但是迭代只是loop
★流程题 给定递归函数,求输出值,用树状图
★属性归纳法证明递归正确性


★递归的要求:截至条件 有progress 不能fn=fn
分治
要求每个子任务互相独立' mergesort quick sort 子问题不重叠
动态规划
由于斐波那契每次都会计算f0 f1,很大部分会重复计算,因此考虑设计一个表,存储各个子问题的答案,会从O(N*2)->O(n),空间复杂度是n

但这样还要递归调用,调用会花费视觉空间
因此使用自底向上的调用

什么样的问题适合DP解决呢?
满足:
解是由子问题的解组成的
由很多子问题是重复运输的
如何实现?
记住各个子问题的答案
时间复杂度=每个子问题的时间=subpro*time
分治和动态规划的区别

子问题不重叠------分治
子问题重叠------动态规划
如何减少计算的:保存结果、bonttom to top从低到上的计算
DP的步骤:

0/1背包问题
递归解决:
问题转换为,如果第i个物品不选,则从前i-1个物品中选w的物品;
如果选,则从前i-1个物品中选w-wi的物品;


递归做法:

此外为了记录旋量哪些内容,开辟一个bi,w记录选了哪些内容
装不下就保持上一个状态,如果装得下,就看选择原来的东西,还是换新的东西


先横着填,再往下填


但凡放不下(当前重量放不放的下自己),就使用其上面一个的值!!!
但凡放得下,就计算max【a,b+v】,即不选自己和选自己的最大值(上一个值,自己的值+选择了自己后还能装得下的最大值)


同时还需要填写表,表示是从哪里来的(记录选择了哪些物品),然后从最后一个往前找递推链到0,0即可





LCS问题 最长公共子序列
只要顺序一致即可,不要求连续。
长度为n的字符串,最多有2^n种子字符串

如果Xm和Yn是相等的,也就是两个字符串的末尾字符一致
LCS(Xm,Yn)=LCS(Xm-1,Yn-1)+1

如果Xm和Yn是不相等的,也就是两个字符串的末尾字符不一致,那么缩减后一定是要么Xm-1,Yn,要么压缩为Xm,Yn-1

得到递推公式:


依然使用表格法:
如果末尾字符相同,则从对角线来,如果不相同,则选择从上方或左侧来的最大值
先看相不相同,如果相同选择右上的值++,如果不同看上方和左方谁最大,一样默认选上方,也可以走左侧(else if ci-1,j>=ci,j-1)结果的长度是唯一的,但是结果序列不唯一。

最后的步数就是斜着的格子个数。

在只需要计算长度,不需要具体内容的情况下:
如果需要空间优化,还可以只记录本行和前一行,因为推导只需要这两行。



最优二叉树
引入n+1个叶子,n和1是为了约束找不到的场景

概率越高的应该放在越浅的位置

| i | 0 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|---|
| pᵢ | 0.15 | 0.1 | 0.05 | 0.1 | 0.2 | |
| qᵢ | 0.05 | 0.1 | 0.05 | 0.05 | 0.05 | 0.1 |
内部节点(关键字):
- k₂: depth=1 → 1 × 0.1 = 0.1
- k₁: depth=2 → 2 × 0.15 = 0.3
- k₄: depth=2 → 2 × 0.1 = 0.2
- k₃: depth=3 → 3 × 0.05 = 0.15
- k₅: depth=3 → 3 × 0.2 = 0.6
→ 内部节点总和 = 0.1 + 0.3 + 0.2 + 0.15 + 0.6 = 1.35
外部节点(失败节点):
- d₀: depth=3 → 3 × 0.05 = 0.15
- d₁: depth=3 → 3 × 0.1 = 0.3
- d₂: depth=4 → 4 × 0.05 = 0.2
- d₃: depth=4 → 4 × 0.05 = 0.2
- d₄: depth=4 → 4 × 0.05 = 0.2
- d₅: depth=4 → 4 × 0.1 = 0.4
→ 外部节点总和 = 0.15 + 0.3 + 0.2 + 0.2 + 0.2 + 0.4 = 1.45
总期望代价:
E(T) = 1.35 + 1.45 = 2.8 ✅

ei,j------最优期望值
wi,j------区间总权重

物理意义清晰:每一层的代价 = 子树代价 + 本层所有节点概率和(因深度+1)

沿着对角线,从低往右上填





贪心算法
保证choise正确 可被拆分为最优子结构
字符编码
定长编码
可变长编码
☆哈夫曼树