258 lines
11 KiB
Typst
258 lines
11 KiB
Typst
#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<Item>& 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<Item>& 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<Item>& 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 得到的真实值对比,验证了分支限界法随着搜索深度增加,对问题最优解的估计越来越准确的特性。
|