#import "labtemplate.typ": * #show: nudtlabpaper.with( author1: "程景愉", id1: "202302723005", advisor: "罗磊", jobtitle: "教授", lab: "306-707", date: "2026.1.19", header_str: "回溯与分支限界算法分析实验报告", minimal_cover: true, ) #set page(header: [ #set par(spacing: 6pt) #align(center)[#text(size: 11pt)[《算法设计与分析》实验报告]] #v(-0.3em) #line(length: 100%, stroke: (thickness: 1pt)) ],) #show heading: it => box(width: 100%)[ #v(0.50em) #set text(font: hei) #it.body ] #outline(title: "目录",depth: 3, indent: 1em) // #pagebreak() #outline( title: [图目录], target: figure.where(kind: image), ) #show heading: it => box(width: 100%)[ #v(0.50em) #set text(font: hei) #counter(heading).display() #it.body ] #set enum(indent: 0.5em,body-indent: 0.5em,) #pagebreak() = 实验介绍 #para[ 回溯法(Backtracking)和分支限界法(Branch and Bound)是求解组合优化问题的两种重要算法。回溯法通过深度优先搜索状态空间树,利用剪枝函数避免无效搜索;分支限界法则常采用广度优先或最佳优先策略,利用代价函数(Bound)计算结点的上界(或下界),以剪除不可能产生最优解的分支,从而加速搜索。本实验旨在通过完全背包问题和多重背包问题,深入理解这两种算法的原理,特别是代价函数的设计对算法性能的影响,并掌握蒙特卡洛方法在估算搜索树规模中的应用。 ] = 实验内容 #para[ 本实验主要包含以下内容: ] + 针对完全背包问题,实现回溯法与分支限界算法。 + 利用蒙特卡洛方法对搜索树的分支数量进行估计。 + 分析分支限界法中代价函数的准确性,通过与真实值(由动态规划求得)对比,分析不同层级和不同输入规模下的近似效果。 + 设计并对比两种不同的代价函数(朴素界与分数背包界),分析其剪枝效果与计算开销。 + (附加)针对多重背包问题,实现分支限界算法,并对比不同代价函数的性能。 = 实验要求 #para[ 具体要求如下: ] + 以物品种类数 $n$ 为输入规模,随机生成测试样本。 + 统计不同算法的运行时间、访问结点数。 + 使用 Python 绘制数据图表,展示蒙特卡洛估计结果、代价函数近似比、以及不同算法的性能对比。 + 分析实验结果,验证理论分析。 = 实验步骤 == 算法设计 === 完全背包问题的分支限界法 #para[ 完全背包问题允许每种物品选择无限次。在分支限界法中,我们构建状态空间树。为了便于剪枝,我们将物品按价值密度($v_i/w_i$)降序排列。 ] ```cpp struct Item { int id; int weight; int value; double density; int limit; Item(int id, int w, int v, int l = -1) : id(id), weight(w), value(v), limit(l) { density = (double)v / w; } }; bool compareItems(const Item& a, const Item& b) { return a.density > b.density; } ``` #para[ 每个结点包含当前价值 $V_"cur"$、当前重量 $W_"cur"$ 和当前考虑的物品层级 $"level"$。我们使用二叉分支策略: ] 1. *左孩子*:选择当前物品一件,状态更新为 $("level", W_"cur"+w_i, V_"cur"+v_i)$,前提是未超重。 2. *右孩子*:不再选择当前物品,转而考虑下一件物品,状态更新为 $("level"+1, W_"cur", V_"cur")$。 #para[ 为了进行剪枝,我们需要计算当前结点的价值上界(Upper Bound, UB)。如果 $"UB" <= "current_best"$,则剪枝。 我们实现了两种代价函数: ] 1. *朴素界 (Simple Bound)*:假设剩余容量全部以全局最大单位价值填充。 $ "UB" = V_"cur" + (W - W_"cur") times max(v_i/w_i) $ 该界计算简单,但较为松弛。 2. *分数背包界 (Fractional Bound)*:即标准的分支限界法上界。将剩余空间用分数背包问题的贪心解填充(即优先装入密度大的物品,最后一件可分割)。由于物品已排序,该界能提供更紧密的上界。 ```cpp double bound_fractional(int level, int current_val, int rem_cap, const vector& items) { double bound = current_val; int w = rem_cap; for (int i = level; i < items.size(); ++i) { if (w >= items[i].weight) { // Take as many as possible (for complete knapsack fractional) bound += (double)w * items[i].density; return bound; } } return bound; } ``` === 蒙特卡洛方法估算搜索树规模 #para[ 对于大规模问题,直接遍历搜索树是不现实的。蒙特卡洛方法通过随机采样路径来估算树的结点总数。 设路径上第 $i$ 层结点的度数为 $m_i$,则该路径代表的树规模估计值为: ] $ N = 1 + m_0 + m_0 m_1 + m_0 m_1 m_2 + dots $ ```cpp long long monte_carlo_estimate(const vector& items, int capacity, int samples = 1000) { long long total_nodes = 0; for (int k = 0; k < samples; ++k) { long long current_multiplier = 1; // ... (traversal logic) ... int branching_factor = moves.size(); total_nodes += current_multiplier; current_multiplier *= branching_factor; // ... } return total_nodes / samples; } ``` #para[ 通过多次采样取平均值,可得到搜索树规模的无偏估计。在完全背包问题中,由于分支因子变化较大(取决于剩余容量),该方法能有效预估问题难度。 ] === 多重背包问题的分支限界法 #para[ 多重背包问题中,每种物品的数量有限制 $k_i$。算法结构与完全背包类似,但在分支时需考虑物品数量限制。 此处同样对比了两种代价函数: ] 1. *松弛界 (Loose Bound)*:忽略数量限制,视为完全背包求分数界。 2. *紧致界 (Tight Bound)*:考虑数量限制求解分数背包问题。即在贪心填充时,不仅受容量限制,也受物品数量 $k_i$ 限制。 ```cpp double bound_mk_tight(int level, int current_val, int rem_cap, const vector& items) { double bound = current_val; int w = rem_cap; for (int i = level; i < items.size(); ++i) { if (items[i].weight == 0) continue; int can_take_weight = items[i].limit * items[i].weight; if (w >= can_take_weight) { w -= can_take_weight; bound += items[i].value * items[i].limit; } else { bound += (double)w * items[i].density; return bound; } } return bound; } ``` == 实验环境 - 操作系统:Linux - 编程语言:C++ (G++) - 数据分析:Python (Pandas, Seaborn, Matplotlib) - 硬件环境:标准 PC = 实验结果与分析 == 蒙特卡洛搜索树规模估计 #para[ 图 1 展示了随物品种类数 $n$ 增加,完全背包问题搜索树结点数的蒙特卡洛估计值(对数坐标)。 ] #figure( image("mc_estimation.png", width: 80%), caption: [搜索树规模的蒙特卡洛估计], ) #para[ 结果表明,搜索树规模随 $n$ 呈指数级增长。蒙特卡洛方法能够快速给出问题规模的数量级估计,对于判断是否能在有限时间内求出精确解具有指导意义。对于整数背包问题,当 $n$ 较大时,建议先使用蒙特卡洛方法预估,若规模过大则应考虑近似算法或启发式搜索。 ] == 代价函数准确性分析 #para[ 为了评估代价函数(上界)的质量,我们记录了搜索过程中各结点的上界值与该状态下的真实最优值(通过动态规划预先计算得到)的比值。比值越接近 1,说明上界越紧致。 ] #figure( image("cost_ratio_level.png", width: 80%), caption: [不同层级下代价函数的近似比 (n=20)], ) #figure( image("cost_ratio_n.png", width: 80%), caption: [平均近似比随输入规模 n 的变化], ) #para[ 从图 2 可以看出,随着搜索深度的增加(Level 增大),剩余问题规模变小,代价函数的近似比逐渐趋向于 1,说明上界越来越精确。这是符合预期的,因为随着物品确定的越多,不确定性越小。 图 3 展示了输入规模 $n$ 对平均近似比的影响。通常情况下,平均近似比相对稳定,不会随 $n$ 剧烈波动,这表明分数背包界具有良好的鲁棒性。 ] == 不同代价函数的性能对比 #para[ 我们对比了“分数背包界 (Fractional)”与“朴素界 (Simple)”在完全背包问题上的性能。 ] #figure( image("new_cost_nodes.png", width: 80%), caption: [不同代价函数下的访问结点数对比], ) #figure( image("new_cost_time.png", width: 80%), caption: [不同代价函数下的运行时间对比], ) #para[ 实验结果显著: ] 1. *剪枝效果*:分数背包界(Fractional)的访问结点数远少于朴素界(Simple),常常相差数个数量级(注意图 4 为对数坐标)。这是因为分数背包界提供了更紧的上界,能更早地剪除无效分支。 2. *运行时间*:尽管分数背包界的计算复杂度略高于朴素界(需要遍历剩余物品,而朴素界仅需常数/一次乘法),但由于其极强的剪枝能力,总运行时间反而大幅降低。 #para[ 这说明在分支限界法中,设计一个计算稍复杂但更紧致的代价函数通常是值得的。 ] = 附加:多重背包问题分析 #para[ 在多重背包问题中,我们对比了考虑物品数量限制的“紧致界 (TightBound)”与忽略数量限制的“松弛界 (LooseBound)”。 ] #figure( image("mk_nodes.png", width: 80%), caption: [多重背包:不同代价函数的结点数对比], ) #figure( image("mk_time.png", width: 80%), caption: [多重背包:不同代价函数的运行时间对比], ) #para[ 结果显示,紧致界(TightBound)在性能上优于松弛界。因为忽略数量限制会导致上界过大,无法有效剪除那些虽然总重量满足但单种物品数量超标的分支。通过在代价函数中精确建模约束条件,可以显著提高算法效率。 ] = 实验总结 #para[ 本实验通过实现和分析完全背包及多重背包问题的分支限界算法,得出以下结论: ] 1. *代价函数的重要性*:代价函数的紧致程度直接决定了分支限界法的剪枝效率。更紧的界(如分数背包界)虽然单次计算开销稍大,但能指数级减少搜索空间,从而获得更好的总性能。 2. *蒙特卡洛方法的实用性*:该方法能有效评估大规模组合优化问题的解空间大小,为算法选择提供依据。 3. *真实值对比分析*:通过与 DP 得到的真实值对比,验证了分支限界法随着搜索深度增加,对问题最优解的估计越来越准确的特性。