递推式与主定理与均摊分析与势能法

共 18 题
📑 题目列表 18 题
#
★★★

1. 主定理(Master Theorem)三种情况的完整陈述与典型示例中 Case 1 T(n)=8T(n/2)+n² → O(n³)、Case 2 T(n)=2T(n/2)+n → O(n log n)、Case 3 T(n)=2T(n/2)+n² → O(n²),以及正则条件的验证

请完整陈述主定理的三种情况,验证 Case 1 的 T(n)=8T(n/2)+n² → O(n³)、Case 2 的 T(n)=2T(n/2)+n → O(n log n)、Case 3 的 T(n)=2T(n/2)+n² → O(n²),并说明正则条件(regularity condition)的作用?

  • 主定理三个分支(case1/case2/case3)的适用条件与结论形式
  • 对三个具体递推式正确识别 a、b、log_b a 并代入
  • 正则条件 a·f(n/b) ≤ c·f(n) 的验证与意义

主定理针对形如 T(n)=a·T(n/b)+f(n) 的递推式(a≥1,b>1,f(n) 渐近非负)。令 c = log_b a(即 n^(log_b a) 为临界函数),分三种情况:Case 1,若 f(n)=O(n^(c−ε))(ε>0),则 T(n)=Θ(n^c)。对 T(n)=8T(n/2)+n²,a=8、b=2、c=log₂8=3,f(n)=n²=O(n^(3−1)),故 T(n)=Θ(n³)。Case 2,若 f(n)=Θ(n^c·log^k n)(k≥0),则 T(n)=Θ(n^c·log^(k+1) n)。对 T(n)=2T(n/2)+n,a=2、b=2、c=1,f(n)=n=Θ(n·log⁰n),故 T(n)=Θ(n log n)。Case 3,若 f(n)=Ω(n^(c+ε))(ε>0)且满足正则条件 a·f(n/b) ≤ c·f(n)(c<1),则 T(n)=Θ(f(n))。对 T(n)=2T(n/2)+n²,a=2、b=2、c=log₂2=1,f(n)=n²=Ω(n^(1+ε)),且 2·(n/2)²=n²/2 ≤ c·n²(取 c=1/2 成立),故 T(n)=Θ(n²)。

主定理的本质是比较"递归分裂代价" n^(log_b a) 与"合并代价" f(n) 的渐近大小:Case 1 分裂主导、Case 3 合并主导、Case 2 两者同阶故多乘一个 log。正则条件仅出现在 Case 3,它保证 f(n) 不会随递归加深而"缩水",从而递推式整体被 f(n) 主导,这是正确应用 Case 3 的必要前提。

// Case 3 的正则条件验证示例:a=2, b=2, f(n)=n^2
// a*f(n/b) = 2*(n/2)^2 = n^2/2,取 c=1/2 (<1) 满足 n^2/2 <= c*n^2
public class MasterVerify {
    static boolean regularity(int a, int b, double fOfN, double fOfNOverB) {
        double c = 0.5; // 任意满足 0<c<1 的常数
        return a * fOfNOverB <= c * fOfN;
    }
    public static void main(String[] args) {
        // a=2, b=2, f(n)=n^2, f(n/b)=(n/2)^2
        System.out.println(regularity(2, 2, 100, 25)); // 2*25=50 <= 0.5*100=50,true
    }
}
#
★★★

2. 动态数组(ArrayList/vector)的均摊分析中扩容因子 2 时 push_back 的均摊 O(1) 证明(聚合法与势能法两种路径),扩容因子 1.5 的均摊代价变化

用聚合法和势能法两种路径证明扩容因子为 2 时动态数组 push_back 的均摊代价为 O(1),并分析扩容因子改为 1.5 时均摊代价如何变化?

  • 聚合法(Aggregate Method)对动态数组扩容的总代价求和
  • 势能法构造势函数 Φ = 2·size − capacity 的推导
  • 扩容因子从 2 变为 1.5 时均摊代价的差异

设初始容量为 1,每次容量翻倍。聚合法:前 n 次 push_back 中,扩容发生在满容量时,总扩容代价为 1+2+4+…+达到可容纳 n 的 2^k,约 2n;加上 n 次普通赋值,总代价约 3n,故均摊 O(1)。势能法:定义势函数 Φ = 2·size − capacity(size 为元素数,capacity 为容量)。扩容前 size=capacity,Φ=capacity;扩容后 size=capacity/2,Φ=0,势能减少 size(=capacity),恰好摊销扩容时复制 O(capacity) 的代价。每次 push_back 的均摊代价 = 实际代价 + 势能变化:容量未满时 ≤ 1(放元素)+ 2(势增)= 3,扩容时复制代价被势能下降抵消,均为 O(1)。若扩容因子为 1.5(容量 c→1.5c),令势函数 Φ = 3·size − 2·capacity 可证均摊仍为 O(1),但常数变大;且 1.5 扩容能复用已释放的内存(内存分配器可将 1.5 与 2 的倍数交错),实际内存利用率更高,但均摊常数略增。

聚合法关注"总代价",势能法通过"势能账本"把昂贵的扩容提前"预存"进每次便宜的 push_back。关键洞察是扩容只有在容量满时发生,而两次扩容之间至少有一半容量被新元素填满,故扩容代价可被摊薄。换扩容因子后势函数系数需相应调整,但核心"均摊 O(1)"结论不变。

// 动态数组扩容因子 2 的均摊分析(势能法)
public class DynamicArray {
    private int[] arr = new int[1];
    private int size = 0;
    private int capacity = 1;
    void pushBack(int x) {
        if (size == capacity) {           // 扩容:复制 O(capacity)
            int[] newArr = new int[capacity * 2];
            System.arraycopy(arr, 0, newArr, 0, size);
            arr = newArr;
            capacity *= 2;
        }
        arr[size++] = x;
    }
    // 势函数 Phi = 2*size - capacity:扩容前 Phi=capacity,扩容后 Phi=0,
    // 势能减少量恰好抵消复制代价,故均摊 O(1)
}
#
★★★

3. 分治算法递推式的建立与求解中归并排序 T(n)=2T(n/2)+Θ(n)、Strassen T(n)=7T(n/2)+Θ(n²)、最近点对 T(n)=2T(n/2)+Θ(n),从递推到主定理的完整推导链

建立归并排序、Strassen 矩阵乘法、最近点对三种分治算法的递推式,并用主定理求解,给出从递推式到渐近界的完整推导链?

  • 从分治过程正确建立递推式 T(n)=a·T(n/b)+f(n)
  • 用主定理对三个具体递推式求渐近界
  • 理解分治中"分裂、递归、合并"三部分与递推式三项的对应

归并排序:每次把数组分成两半递归排序,合并需 Θ(n),故 T(n)=2T(n/2)+Θ(n)。a=2、b=2、c=log₂2=1,f(n)=Θ(n)=Θ(n^c),属 Case 2,得 T(n)=Θ(n log n)。Strassen 矩阵乘法:把 n×n 矩阵分成 4 个 n/2×n/2 子矩阵,递归做 7 次乘法、Θ(n²) 次加法,故 T(n)=7T(n/2)+Θ(n²)。a=7、b=2、c=log₂7≈2.807,f(n)=n²=O(n^(2.807−ε)),属 Case 1,得 T(n)=Θ(n^(log₂7))≈Θ(n^2.807)。最近点对:把点集按 x 坐标分成两半,递归求左右各自最近对,合并时需在分界线附近长条内检查 O(n) 个候选点,故 T(n)=2T(n/2)+Θ(n),与归并排序同构,得 T(n)=Θ(n log n)。

建立递推式的关键在正确统计"合并"工作量:归并排序合并是 O(n) 线性扫描,最近点对合并因"宽 2δ 长条内每点至多与 6 个点比较"而保持 O(n),Strassen 因 7 次乘法把 a 从 8 减为 7 从而把指数从 3 降到 log₂7。主定理按 c=log_b a 与 f(n) 的关系自动裁决。

// 归并排序合并步骤(合并代价 O(n))
void merge(int[] a, int[] tmp, int lo, int mid, int hi) {
    int i = lo, j = mid + 1, k = lo;
    while (i <= mid && j <= hi)
        tmp[k++] = a[i] <= a[j] ? a[i++] : a[j++];
    while (i <= mid) tmp[k++] = a[i++];
    while (j <= hi)  tmp[k++] = a[j++];
    System.arraycopy(tmp, lo, a, lo, hi - lo + 1);
}
// 递归:T(n) = 2T(n/2) + O(n) = O(n log n)
#
★★

4. Akra-Bazzi 之外的递推求解中 T(n)=T(n/2)+T(n/4)+n 这类非标准递推如何用递归树与猜测代入法求渐近界?

对 T(n)=T(n/2)+T(n/4)+n 这类非标准递推(子问题规模不等且不满足主定理形式),如何用递归树法和猜测代入法求渐近界?

  • 递归树法:每层代价求和
  • 猜测代入法:先猜上界再用数学归纳证明
  • 求解 Σ (1/2+1/4)^k 形式的几何级数

递归树法:第一层代价 n,第二层两个子问题代价 n/2 + n/4 = 3n/4,第三层四个子问题代价 n/4+n/8+n/8+n/16 = 9n/16,依此类推,第 k 层总代价为 n·(3/4)^k。由于 (3/4)^k 是等比级数且公比 3/4<1,总代价 = n·Σ(3/4)^k = n·(1/(1−3/4)) = O(n)。猜测代入法:猜测 T(n)=O(n),即存在 c 使 T(n)≤c·n。代入:T(n) ≤ c·(n/2)+c·(n/4)+n = c·(3n/4)+n = n·(1+3c/4)。要满足 ≤ c·n,需 1+3c/4 ≤ c,即 1 ≤ c/4,取 c≥4 即可,归纳成立,故 T(n)=O(n)。递归树深度为 O(log n)(因为每层规模至少折半的分支),但 (3/4)^k 的几何衰减使总代价收敛于 O(n)。

非标准递推的关键在于识别"每层同规模代价的几何衰减"。这里每层总代价按公比 3/4 递减,故和多层叠乘仍为 O(n)。代入法验证猜测时,要保证余项(此处 +n)能被稍大的常数 c 吸收。若公比 ≥1(如两半,公比 1),则每层代价不衰减,总代价会乘 log n。

// 递归树验证:第 k 层代价 = n * (3/4)^k,求和 = O(n)
double totalCost(int n) {
    double sum = 0, k = 0;
    while (n > 1) {
        sum += n * Math.pow(0.75, k);
        n /= 2; k++;
    }
    return sum; // 收敛于 O(n)
}
#
★★

5. 递归与迭代实现归并排序的差异中自顶向下有 O(log n) 递归栈深,自底向上可做到 O(1) 辅助空间,递推式 T(n)=2T(n/2)+n 如何对应两者的运行代价?

比较递归(自顶向下)与迭代(自底向上)实现归并排序的差异,解释为什么自顶向下的递归栈深为 O(log n),自底向上可做到 O(1) 辅助空间,并说明递推式 T(n)=2T(n/2)+n 如何对应两者的运行代价?

  • 递归栈深与递归树高度的关系
  • 自底向上用自然长度的 merge 子程序替代递归
  • 递推式反映两者相等的比较交换总量

自顶向下归并排序把数组递归二分,递归树深度为 O(log n),故调用栈最深处含 O(log n) 个递归帧,栈空间 O(log n);它还需要一个 O(n) 的辅助数组用于合并,总空间 O(n)。自底向上归并排序从长度为 1 的子数组开始,逐轮将相邻子数组两两 merge,子数组长度 1→2→4→…→n,共 O(log n) 轮,每轮总 O(n) 代价,且它不递归、无需调用栈,可只用 O(1) 辅助空间(原地/链表式归并)或 O(n) 辅助数组。两者的总比较与移动次数相同,都满足 T(n)=2T(n/2)+n:自顶向下由递归式本身刻画,自底向上由"每轮合并 n 个元素、共 log n 轮"体现,展开后都是 O(n log n)。

递归栈深取决于递归树高度而非总节点数,二分树高 O(log n)。自底向上消除了"递归到叶子再回溯"的调用栈需求,用循环枚举子数组长度替代,从而把额外空间从递归栈省掉。两者计算量本质相同,递推式以抽象层描述合并代价,与具体实现方式无关。

// 自底向上归并排序(迭代)
void mergeSortBU(int[] a) {
    int n = a.length;
    int[] tmp = new int[n];
    for (int len = 1; len < n; len *= 2) {      // 子数组长度翻倍
        for (int lo = 0; lo < n - len; lo += 2 * len) {
            int mid = lo + len - 1;
            int hi = Math.min(lo + 2 * len - 1, n - 1);
            merge(a, tmp, lo, mid, hi);          // 每轮合并总代价 O(n)
        }
    }
}
#
★★

6. 递推式的求解方法中代入法、递归树、主定理各自的适用条件与局限,如何证明大 O 上界?

比较代入法、递归树、主定理三种递推式求解方法的适用条件与局限,并说明如何用代入法证明大 O 上界?

  • 三种方法的适用场景与优缺点
  • 代入法的归纳证明步骤
  • 主定理的适用限制(等规模子问题、多项式差)

主定理最简洁,但只适用于 T(n)=a·T(n/b)+f(n) 且 f(n) 与 n^(log_b a) 相差多项式因子的情形;当 f(n) 与 n^(log_b a) 只差对数因子、子问题规模不等、或 f(n) 非多项式时主定理失效。递归树适用于各类递推,能直观给出每层代价,便于猜出渐近界,但需要手工求和几何/算术级数,且要处理边界。代入法需要先猜测上界,再用数学归纳证明:假设 T(n)≤c·n^d 对较小规模成立,代入递推式 T(n)≤c·(n/2)^d + f(n) ≤ c·n^d,通过选择足够大的常数 c 吸收余项 f(n) 并满足 n≥n0,从而证明大 O 上界。递归树常用于"猜测",代入法用于"严格证明",两者常配合使用。

三种方法本质是"猜"与"证"的分工:主定理是预设好的模板,递归树是可视化求和,代入法是把猜测变成严格归纳。证明 O 上界时,关键在于余项能否被增大的常数 c 吸收,这是归纳能否闭合的判据。

// 代入法证明 T(n)=2T(n/2)+n 为 O(n log n):
// 猜测 T(n) <= c*n*log n,代入得 T(n) <= 2*c*(n/2)*log(n/2) + n
// = c*n*log n - c*n + n <= c*n*log n  当 c>=1 时成立
public class Substitution {
    static int ceilHalf(int n) { return n / 2 + (n % 2); }
}
#
★★

7. 均摊分析的三种方法中聚合分析、记账法(核算法)、势能法的原理与典型例子(动态数组扩容)?

说明均摊分析的聚合分析、记账法(核算法)、势能法三种方法的原理,并用动态数组扩容作为典型例子解释?

  • 三种均摊方法的定义与差异
  • 记账法中的"存款"思想
  • 势能法中的势函数

聚合分析(Aggregate Method):计算 n 次操作的总代价 T(n),再除以 n 得均摊代价 T(n)/n,不区分具体操作。动态数组扩容时,前 n 次 push_back 总代价约 3n,均摊 O(1)。记账法(Accounting Method):给不同操作分配不同的"均摊代价",用"存款"(credit)预存昂贵操作的开销,要求任意时刻存款非负。push_back 收费 3(1 用作放入元素,2 存为存款),扩容时用存款支付复制开销,保证存款始终非负。记账法要求显式给出每类操作的均摊费用。势能法(Potential Method):定义势函数 Φ(D_i) 表示第 i 步后数据结构的"势能",均摊代价 = 实际代价 + Φ(D_i) − Φ(D_{i−1}),要求 Φ(D_i)≥Φ(D_0) 使总均摊代价总和上界总实际代价。动态数组取 Φ=2·size−capacity,扩容时势能下降恰好抵消复制代价。

三种方法殊途同归,都给出 n 次操作的均摊上界。聚合法最省事但掩盖了"哪些操作贵";记账法显式给出每操作费用,直观但需手工维护存款;势能法最通用、便于数学证明,缺点是需要设计势函数。对动态数组,三者都证得 push_back 均摊 O(1)。

// 记账法(核算法)示意:push_back 收费 3,其中 1 立即用,2 存为存款
class Account {
    long credit = 0;
    void push(int size, int capacity) {
        long cost = 3;                  // 每操作均摊收费 3
        if (size == capacity) {         // 扩容复制花费 capacity,用存款支付
            credit -= capacity;
        } else {
            credit += 2;                // 存入 2 备用
        }
    }
}
#
★★

8. T(n)=2T(n/2)+n 这类递推式如何用递归树展开求精确解,主定理不适用的边界条件有哪些?

如何用递归树展开 T(n)=2T(n/2)+n 求精确解,主定理不适用的边界条件有哪些?

  • 递归树展开与逐层求和
  • 精确解的推导(n log n 的精确系数)
  • 主定理失效的边界情形

递归树展开 T(n)=2T(n/2)+n:第一层代价 n,第二层两个子问题各代价 n/2,共 n;第 k 层有 2^k 个节点,每节点代价 n/2^k,层总代价仍为 n。树高 log₂n,共 log₂n+1 层,第 0 到 log₂n−1 层每层代价 n,最后一层 log₂n 层有 n 个叶子、每叶 T(1)=Θ(1),故总代价 = n·log₂n + n = Θ(n log n)。精确解若 T(1)=1,则 T(n)=n(log₂n+1)。主定理不适用的边界条件:一是 f(n) 与 n^(log_b a) 只差对数因子(如 f(n)=n·log n 时,Case 2 的 k 需非负整数,若 f(n)=n/log n 或 n·log(log n) 等非多项式对数差则主定理不直接适用);二是主定理要求 f 与 n^(log_b a) 相差多项式因子,若只差对数因子需用 Case 2 的推广形式或递归树;三是子问题规模不等(如 T(n)=T(n/3)+T(2n/3)+n)不能用标准主定理,需 Akra-Bazzi 或递归树。

递归树把"递归"转成"逐层求和",对 T(n)=2T(n/2)+n 直接得到每层代价恒为 n、共 log n 层,从而精确解 n log n。主定理的局限在于要求 f(n) 与临界函数有多项式量级的差距,或恰好在 Case 2 的整对数形式内;否则需回退到递归树或 Akra-Bazzi。

// 递归树求层和的验证:T(n)=2T(n/2)+n,每层代价 n,共 log2 n 层
int layers(int n) { return (int)(Math.log(n) / Math.log(2)) + 1; }
// 总代价约 n * layers = n(log2 n + 1)
#
★★

9. 均摊分析中的势能函数如何构造,以二进制计数器为例,说明每次 increment 均摊 O(1) 的势能推导(势取 1 的个数)?

以二进制计数器为例,说明如何构造势函数并推导每次 increment 的均摊代价为 O(1)(势取二进制表示中 1 的个数)?

  • 二进制计数器 increment 的最坏代价与总代价
  • 势函数 Φ=数中 1 的个数 的推导
  • 均摊代价 = 实际代价 + 势能变化

二进制计数器从 0 开始,每次 increment 从最低位向高位翻转直到遇到 0 并将其置 1。最坏单次 increment 翻转 O(k) 位(k 为位数),但均摊 O(1)。定义势函数 Φ(b) = 计数器二进制表示中 1 的个数。设第 i 次 increment 实际翻转了 t_i 位(其中 t_i−1 个 1 变 0,1 个 0 变 1),则势能变化 ΔΦ = 1 − (t_i−1) = 2 − t_i。均摊代价 = 实际代价 + ΔΦ = t_i + (2 − t_i) = 2 = O(1)。由于 Φ 恒非负且 Φ(0)=0,累加 n 次均摊代价上限 bound 总实际代价,故 n 次 increment 总代价 O(n),均摊 O(1)。

势函数取"1 的个数"的妙处在于:每次翻转时,每把 1 变 0 都释放一份"存款"(势能减少),这些存款正好预支给下一次可能翻到的高位。势能公式一算,恰好把与 t_i 相关的项抵消,得到常数 2。这展示了势能法的核心:选择一个随操作剧烈变化、且能精确抵消昂贵操作实际代价的势函数。

// 二进制计数器 increment(势能法分析:Phi = 1 的个数)
class BinaryCounter {
    int[] bits; int n;
    BinaryCounter(int k) { bits = new int[k]; n = 0; }
    int increment() {
        int flipped = 0;
        int i = 0;
        while (i < bits.length && bits[i] == 1) { bits[i] = 0; i++; flipped++; }
        if (i < bits.length) { bits[i] = 1; flipped++; }
        // 势能变化 = 1 - (flipped - 1) = 2 - flipped
        // 均摊代价 = flipped + (2 - flipped) = 2 = O(1)
        return flipped;
    }
}
#
★★

10. 均摊分析中势能函数的选择启发式中势能常取数据结构的规模对数或未完成操作数,选取错误会怎样?

说明均摊分析中势能函数的选择启发式:为什么势能常取数据结构的规模对数或未完成操作数,选取错误会怎样?

  • 势函数选取的常见启发式
  • 势函数应衡量的"潜在代价"
  • 势函数选错导致均摊界失效的后果

势函数的选取启发式是:势能应代表"数据结构中已积累、未来可能付出的潜在工作"。常见的天然选择包括:动态数组取 2·size−capacity(衡量"已复制但未付清的成本");二进制计数器取 1 的个数(衡量"能触发高位翻转的存款");Splay 树取 Σ log(size(x))(衡量各节点子树规模,保证访问后势能变化可控);对某些数据结构取"当前规模与容量的差额"或"未完成操作数"。势函数应满足:①Φ 非负且初始为 0(或常数);②昂贵的操作使势能下降,便宜的使势能上升但增幅有界。若选取错误(如势能变化与昂贵操作实际代价不匹配,或势函数不单调可能导致总势能下降量超过预存),则均摊上界会失效:要么得到错误的过大界(如把 O(1) 误判成 O(log n)),要么势能下降发生在昂贵操作之前导致无法抵消其代价,从而均摊界无法成立。

势函数本质是"预付款账本",取规模对数是因为"子问题规模翻倍"这类操作的对数变化正好匹配合并代价;取未完成操作数是因为它度量了"未来还要还的债"。选错的关键后果是势能变化 ΔΦ 无法与"昂贵操作的实际代价"相消,均摊界就失去意义,甚至可能因势能长期为负而违反均摊分析的前提。

// 势函数选取示例:动态数组取 Phi = 2*size - capacity
// 正确的势函数应保证:扩容时势能下降量 >= 复制代价
// 若误取 Phi = size(只随规模线性增长),扩容时势能先增后不降,
// 无法抵消复制开销,均摊分析即失效
#
★★

11. 主定理 case1 与 case3 的边界中当 f(n) 与 n^(log_b a) 只差对数因子时主定理为何失效,需要用什么技巧?

解释当 f(n) 与 n^(log_b a) 只差对数因子时主定理的 Case 1 与 Case 3 为何失效,并说明需要用什么技巧求解?

  • 主定理要求多项式差距(polynomial gap)的原因
  • 对数因子差距时 Case 1/3 条件不满足
  • 用递归树或 Case 2 推广形式求解

主定理 Case 1 要求 f(n)=O(n^(log_b a−ε))(ε>0),Case 3 要求 f(n)=Ω(n^(log_b a+ε)),即 f(n) 与临界函数 n^(log_b a) 必须相差 n^ε 这种"多项式因子"。若 f(n)=n^(log_b a)·log n(只差 log 因子),则 f 既不是 O(n^(log_b a−ε)) 也不是 Ω(n^(log_b a+ε)),Case 1 和 Case 3 的条件都不满足,主定理失效。此时需用递归树:每层代价为 n^(log_b a)·log(n/b^k) 的形式,叠加后总代价为 Θ(n^(log_b a)·log² n)。也即这是 Case 2 的推广——当 f(n)=Θ(n^(log_b a)·log^k n) 时,解为 Θ(n^(log_b a)·log^(k+1) n),k=1 时即 n^(log_b a)·log² n。在主定理的扩展形式中,该情形归入 Case 2 的 k>0 分支。

主定理要求"多项式差距"是因为它的推导依赖 f(n) 与临界函数同阶比较时,若差距不足多项式,则递归树中每层的比较项不会出现"主导项被级数收敛"或"被发散"的干净结果,而是叠加出 log 因子。对数因子差距恰好落入 Case 2 的推广,因而用递归树或 Case 2 推广即可。

// 例:T(n)=2T(n/2)+n log n,f(n)=n^1*log n,与 n^(log2 2)=n 只差 log 因子
// 直接套主定理三个 case 均不满足,需用递归树或 Case 2 推广:
// 每层代价 n log n、n/2 log(n/2)、... 叠加后 T(n)=Theta(n log^2 n)
#

12. Splay 树的单次操作最坏情况中为什么单次访问可能花费 O(n)(如反复访问叶子),但 m 次操作的总代价仍是 O((m+n)log n)(摊还)?

解释 Splay 树单次操作的最坏情况为何可达 O(n)(如反复访问叶子),而 m 次操作的总代价仍是 O((m+n)log n)(摊还意义)?

  • Splay 树单次操作的摊还界与最坏界区别
  • 势能法中 m 次操作总代价的推导
  • 为什么反复访问叶子不一定导致 O(n) 每次

Splay 树单次访问的最坏情况是 O(n):例如访问树叶后,splay 会把该节点转到根,但整棵树可能被重新组织成一条链,下一次访问另一个叶子又需 O(n) 步。因此单次操作上界是 O(n)。但摊还意义上,m 次操作的总代价是 O((m+n)log n)。势能法取 Φ(T)=Σ_{x∈T} log(size(x)),其中 size(x) 为 x 的子树节点数。每次 splay 时,路径上节点的势能变化被 Access Lemma 界定为 O(log n)(摊还),单次 splay 的摊还代价为 O(log n)。因为初始势能 Φ₀≤n log n,且势能非负,故 m 次操作总代价 = Σ(实际代价) ≤ Σ(摊还代价) + Φ₀ − Φ_m ≤ O(m log n) + O(n log n) = O((m+n)log n)。所以虽然单次操作可能 O(n),但"贵"操作之间必然穿插足够多的"便宜"操作,使总代价摊薄。

摊还分析的关键是"总代价有界而非每次有界"。Access Lemma 保证每次访问的势能增量被 log n 控制,即使某次访问实际走了 O(n) 步,这些步也消耗预存的势能,累积下来总代价仍被 (m+n)log n 约束。这解释了 Splay 树"单次可能慢、长期高效"的自调整特性。

// Splay 树势能函数建模:Phi(T) = sum over x in T of log(size(x))
// m 次操作总代价 bound: O(m log n) + Phi(initial) - Phi(final) <= O((m+n)log n)
// 单次访问最坏 O(n),但摊还 O(log n)
#

13. Akra-Bazzi 定理对主定理的推广中处理 T(n)=T(n/3)+T(2n/3)+n 等子问题规模不等的情形,求解 p 使得 Σa_i·b_i^p=1 的步骤

说明 Akra-Bazzi 定理如何推广主定理处理子问题规模不等的情形,并以 T(n)=T(n/3)+T(2n/3)+n 为例给出求解 p 使 Σa_i·b_i^p=1 的步骤?

  • Akra-Bazzi 定理的适用条件与公式
  • 求解 p 满足 Σa_i·b_i^p=1 的方法
  • 结果代入 Akra-Bazzi 公式

Akra-Bazzi 定理处理 T(n)=Σ_{i=1}^k a_i·T(b_i·n)+f(n) 的情形(子问题规模 b_i·n 可以不相等)。步骤:先求唯一的 p 使 Σ a_i·b_i^p = 1,则 T(n)=Θ(n^p·(1+∫_1^n f(u)/u^(p+1) du))。对 T(n)=T(n/3)+T(2n/3)+n:a₁=a₂=1,b₁=1/3,b₂=2/3,需解 (1/3)^p + (2/3)^p = 1。p=1 时 (1/3)+(2/3)=1 恰成立,故 p=1。于是 T(n)=Θ(n·(1+∫_1^n (u/u^(1+1)) du))=Θ(n·(1+∫_1^n (1/u) du))=Θ(n(1+ln n))=Θ(n log n)。

当子问题规模不等时,标准主定理的"分裂主导 vs 合并主导"比较失效,因为不存在单一临界函数 n^(log_b a)。Akra-Bazzi 用 p 做"加权临界指数",p 由 Σ a_i·b_i^p=1 唯一确定,再通过积分量 ∫ f(u)/u^(p+1) 决定 log 因子。对本例 p=1 恰好是 b₁+b₂=1 的巧合,积分得 n log n。

// 求解 p 满足 (1/3)^p + (2/3)^p = 1,可用二分/牛顿法
double solveP() {
    double lo = 0.0, hi = 10.0;
    for (int it = 0; it < 100; it++) {
        double mid = (lo + hi) / 2;
        if (Math.pow(1.0/3, mid) + Math.pow(2.0/3, mid) > 1) lo = mid;
        else hi = mid;
    }
    return (lo + hi) / 2; // 收敛到 1
}
#

14. Splay 树的势能函数设计中Φ(T)=Σ_{x∈T} r(x)=Σ log(size(x)) 如何保证 m 次操作总代价 O((m+n)log n),Access Lemma 的证明思路

说明 Splay 树势能函数 Φ(T)=Σ_{x∈T} r(x)=Σ log(size(x)) 如何保证 m 次操作总代价 O((m+n)log n),并给出 Access Lemma 的证明思路?

  • 势函数 Φ=Σ log(size(x)) 的定义
  • Access Lemma:单次访问摊还代价 O(log n)
  • 三路 zig-zag / zig-zig / zig 的势能变化分析

势函数 Φ(T)=Σ_{x∈T} log(size(x)),其中 size(x) 是 x 的子树节点数。Access Lemma 断言:对树中任意节点 x,单次访问 x 的摊还代价 ≤ 3·log(n/size_of_subtree(x)) + 1 ≤ O(log n)(n 为总节点数)。证明思路是分析 splay 的三种旋转:

  1. zig-zig(在 x 的父与祖父同侧时):设 x 的 size 从 s 变化到 s',摊还代价 ≤ 3·(r'(x)−r(x)),其中两次旋转的势能变化被 log 项精确控制。
  2. zig-zag:同样 ≤ 3·(r'(x)−r(x))。
  3. zig(最后一步,x 的父为根):摊还代价 ≤ 3·(r'(x)−r(x)) + 1。 把访问路径上所有步骤的摊还代价相加,中间项(r' 与 r 的交叉项)互相抵消,只剩端点项,得整次访问摊还代价 ≤ 3·log n + O(1)。由于势能初始 Φ₀≤n log n 且非负,m 次操作总摊还代价 ≤ O(m log n) + Φ₀ − Φ_m ≤ O((m+n)log n)。

Access Lemma 的关键是每次旋转的势能变化被它"推进的 ranks(log size)"控制,且 zig-zig 相比两个单旋转能省下势能。交叉项抵消是证明的核心技术,保证 o(log n) 的摊还界。势能函数取 log(size) 正是因为子树大小变化满足对数伸缩可抵消。

// Splay 树势能函数与 Access Lemma 的摊还代价示意
// Phi(T) = sum_{x in T} log(size(x))
// Access(x) 摊还代价 <= 3*log n + O(1)(zig-zig/zig-zag/zig 三种旋转的势能分析)
// 因此 m 次操作总代价 <= O(m log n) + Phi(initial) - Phi(final) = O((m+n)log n)
#

15. 聚合法(Aggregate Method)vs 记账法(Accounting Method)vs 势能法(Potential Method)的适用场景对比中何时选哪种方法最简洁,势能函数选取的启发式原则

对比聚合法、记账法、势能法的适用场景,说明何时选哪种方法最简洁,以及势能函数选取的启发式原则?

  • 三种方法的适用场景
  • 选择方法的原则
  • 势函数选取的一般启发式

聚合法(Aggregate)最简洁,适用于能直接对 n 次操作总代价求和的问题,如动态数组扩容、二进制计数器——它不区分操作类型,只给总均摊界。记账法(Accounting)适用于需要"给每类操作分配均摊费用、用存款覆盖昂贵操作"的场景,如扩展栈、多计数器,比聚合法更细、能体现哪类操作贵。势能法(Potential)最通用、最便于证明,适用于需要严格数学保证或势能变化自然的问题(如 Splay 树、动态表、Fibonacci 堆),因为势函数能衡量"数据结构整体状态"而不只是单次操作。选择原则:若总代价易求和用 Aggregate;若需显式区分操作类型用 Accounting;若需严谨证明或势函数自然用 Potential。势能函数选取启发式:①度量"潜在未来工作"(如容量差、1 的个数、子树规模对数);②要求非负且初始为 0;③昂贵操作时势能下降,便宜操作时势能上升但增幅有界。

三者本质等价(都给出相同均摊上界),区别在抽象层级:Aggregate 最粗、Accounting 中等、Potential 最细。势能函数选取没有机械规则,但"找操作中会剧变的量"是关键——动态数组用容量差、计数器用 1 的个数、Splay 用子树规模对数,这些都是"会大幅变化、可拆解抵消"的指标。

// 三种方法给出同一结论的示意:动态数组 push_back 均摊 O(1)
// Aggregate: 总代价 ~3n, 均摊 3
// Accounting: 每 push 收费 3,储 2 枚
// Potential: Phi = 2*size - capacity,均摊 = 实际 + 势变 = 常数
#

16. 势能法的应用中二进制计数器、Splay 树等场景如何选取势函数,均摊界如何保证?

说明势能法在二进制计数器、Splay 树等场景如何选取势函数,以及均摊界如何保证?

  • 二进制计数器势函数取 1 的个数
  • Splay 树势函数取 Σ log(size(x))
  • 均摊上界由 "总势能变化 + 初始势能" 保证

二进制计数器取 Φ = 二进制表示中 1 的个数。第 i 次 increment 翻转 t_i 位(t_i−1 个 1 变 0、1 个 0 变 1),势能变化 ΔΦ = 1−(t_i−1) = 2−t_i,均摊代价 = t_i + ΔΦ = 2 = O(1)。Splay 树取 Φ = Σ_{x} log(size(x))。每次 splay 的摊还代价由 Access Lemma 保证 ≤ 3 log n + O(1),这是通过 zig-zig/zig-zag/zig 三种旋转的势能变化分析得到的。均摊界保证的通用机制:若势函数 Φ 满足 Φ 非负且 Φ(D₀)=0,则任意 m 次操作的总实际代价 Σ(实际代价) ≤ Σ(均摊代价) = Σ(实际代价 + ΔΦ) = Σ(实际代价) + Φ_m − Φ₀ ≤ Σ(实际代价) + Φ_m。因此 Σ(实际代价) ≤ Σ(均摊代价) − Φ_m ≤ 总摊还代价。简单说,只要每次操作均摊代价有界且势能非负,均摊下界就为总实际代价提供上界。

势能法的统一证明是"总实际代价 ≤ 总均摊代价 − 最终势能 ≤ 总均摊代价",因此只要算出每操作均摊代价上界并保证初始势能非负,就能得到 m 次操作的总代价上界。二进制计数器与 Splay 树都遵循此框架,只是势函数选择不同。

// 势能法统一框架:总实际代价 <= 总均摊代价 - Phi_final + Phi_0
// 二进制计数器:Phi = 1 计数,均摊 2
// Splay 树:Phi = sum log(size(x)),均摊 O(log n)
// 两者都满足 Phi >= 0 且 Phi(init) = 0,故总代价均摊有界
#

17. 主定理的三个分支(case1/2/3)分别适用于哪些增长形态,多项式大于/小于的判定细节?

说明主定理三个分支分别适用于哪些增长形态,并给出多项式大于/小于(f(n) 与 n^(log_b a) 比较)的判定细节?

  • 三个分支对应的增长形态
  • 多项式差距的判定(相差 n^ε)
  • 边界情形(对数因子、同阶)的处理

设 c = log_b a(临界函数 n^c)。Case 1(f 增长慢于 n^c 且差多项式因子):f(n)=O(n^(c−ε)),ε>0,此时递归分裂主导,T(n)=Θ(n^c)。Case 3(f 增长快于 n^c 且差多项式因子,且满足正则条件 a·f(n/b)≤c'·f(n)):f(n)=Ω(n^(c+ε)),此时合并主导,T(n)=Θ(f(n))。Case 2(f 与 n^c 同阶至多差对数因子):f(n)=Θ(n^c·log^k n),k≥0,此时 T(n)=Θ(n^c·log^(k+1) n)。判定细节:①"多项式大于/小于"要求 f(n)/n^c 至少相差 n^ε 的量级,若只差 log 因子则落 Case 2 推广;②主定理要求 f(n) 与 n^c 的差是多项式(不是任意函数),且 Case 3 需验证正则条件;③若 f(n)/n^c 无界但增长慢于任何 n^ε(如 n^c/log n),则 Case 1 的 ε 不存在,需用递归树处理。

三个分支的划分本质是"比较 f(n) 与 n^c 的渐近量级",但必须是"多项式差距"而非"任意差距"。这正是主定理的精确性所在:多项式差距保证递归树中主导项能以几何级数收敛或发散,从而得到干净的 Θ 界;对数因子差距则叠加 log 因子。

// 主定理分支判定辅助函数
String classify(int a, int b, double fexp) {
    double c = Math.log(a) / Math.log(b); // log_b a
    if (fexp < c - 1e-9) return "Case 1: T(n)=Theta(n^c)";
    if (Math.abs(fexp - c) < 1e-9) return "Case 2: T(n)=Theta(n^c log n)";
    return "Case 3: T(n)=Theta(f(n)) (需验证正则条件)";
}
#

18. 主定理中 f(n) 与 n^(log_b a) 的多项式大小比较中当两者同阶(case2)时解的形式?

说明主定理中 f(n) 与 n^(log_b a) 的多项式大小比较方法,以及当两者同阶(Case 2)时解的形式?

  • 多项式大小比较的判定
  • Case 2 的解形式 T(n)=Θ(n^c·log^(k+1) n)
  • 同阶时的 log 因子

主定理比较 f(n) 与 n^(log_b a)(记 c=log_b a)的渐近大小,要求是"多项式差距":若 f(n)/n^c 渐近于 n^ε(ε>0)则 f 大;若 n^c/f(n) 渐近于 n^ε 则 f 小;若 f(n)/n^c 是 log 的多项式(即 f(n)=Θ(n^c·log^k n))则两者同阶。当两者同阶(Case 2)时,f(n)=Θ(n^c·log^k n),k≥0,解的形式为 T(n)=Θ(n^c·log^(k+1) n)。特别地,k=0 时(f(n)=Θ(n^c))解为 Θ(n^c·log n);k=1 时(f(n)=Θ(n^c·log n))解为 Θ(n^c·log² n)。例:T(n)=2T(n/2)+n 中 c=1,f(n)=n=Θ(n^1·log⁰n),故 T(n)=Θ(n·log n)。

Case 2 的精髓是"递归深度 log n 与合并代价 n^c 相乘,再叠加 log^k 的载荷"。当 f(n) 与临界函数同阶时,递归树每层代价相同(都是 Θ(n^c·log^k n)),共 log n 层,每层多一个 log^k 因子,故总代价多一个 log,变成 log^(k+1)。

// Case 2 解的形式:T(n)=Theta(n^c * log^(k+1) n)
// 例:T(n)=2T(n/2)+n  ->  c=1, k=0  ->  T(n)=Theta(n log n)
// 例:T(n)=2T(n/2)+n log n -> c=1, k=1 -> T(n)=Theta(n log^2 n)