Files
algo2025/backtracking/main.typ
2026-01-19 01:17:31 +08:00

258 lines
11 KiB
Typst
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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 得到的真实值对比,验证了分支限界法随着搜索深度增加,对问题最优解的估计越来越准确的特性。