前言
前言
"全球校园人工智能算法精英大赛"是江苏省人工智能学会举办的面向全球具有正式学籍的全日制高等院校及以上在校学生举办的算法竞赛。其中的算法巅峰赛属于产业命题赛道,这是第3赛季,这次优化题的主题是 "碳中和"。

回顾
第七届全球校园人工智能算法精英大赛-算法巅峰赛产业命题赛第3赛季优化题--碳中和
在很多工程优化问题里,经常会遇到这样一种场景:
- 决策空间非常大
- 约束复杂
- 想求全局最优很难
- 但我们又希望在有限时间内得到一个"足够好"的可行解
这时,Beam Search(束搜索) 就是一种非常实用的启发式搜索方法。
这篇文章将介绍 Beam Search 的概念、原理,以及针对该问题的 c++解。
束搜索(Beam Search)
Beam Search 可以看作是:
"宽度受限的 BFS + 启发式剪枝"
它的核心思想是:
- 按顺序一步步构造解
- 每一步会产生很多候选中间状态
- 但我们不保留所有状态,只保留其中"最有希望"的前
B个 - 这个
B就叫 beam width(束宽)
可以把它理解成:
- 普通 BFS:每层所有状态都保留,容易爆炸
- 贪心:每层只保留 1 个状态,容易走偏
- Beam Search:每层保留少量优秀状态,在搜索质量和速度之间做平衡
一个简单类比
假设你在走迷宫,每走一步有很多分岔:
- 如果你每条路都探索,那就是全搜索,太慢
- 如果你每次只选当前看起来最优的一条路,那是贪心,容易错
- Beam Search 的做法是:
每一步都保留几条"目前看起来最有希望"的路,继续往下搜索
这样既不像全搜索那么贵,也不像纯贪心那么短视。
Beam Search 的标准流程
假设我们要按顺序处理一批决策变量。
第一步:初始化状态集
开始时只有一个空状态。
第二步:逐步扩展
处理第 i 个决策时,从当前 beam 中每个状态出发,尝试所有合理动作,生成新状态。
第三步:状态打分
对每个新状态计算一个评分,反映它"未来有多大希望成为好解"。
第四步:剪枝
只保留评分最高的前 B 个状态,进入下一轮。
第五步:终止
处理完所有决策后,从剩余状态中选最好的一个作为结果。
Beam Search 和贪心、BFS、DP 的区别
和贪心的区别
贪心每一步只留一个当前最好状态。
优点:
- 快
- 简单
缺点:
- 特别容易因为早期错误决策而毁掉全局结果
Beam Search 则保留多个候选状态,因此比贪心更稳。
和 BFS 的区别
BFS 会把每层所有状态全部保留。
优点:
- 如果状态空间不大,可以找到最优解
缺点:
- 状态爆炸非常严重
Beam Search 相当于把 BFS 的每层状态数限制在一个固定上限。
和 DP 的区别
动态规划通常要求:
- 状态结构清晰
- 最优子结构成立
- 状态数量可控
而这道任务分配问题里:
- 每个任务都可能分配到很多服务器
- 还带有局部热耦合
- 状态维度非常高
很难写出精确 DP。
Beam Search 不要求严密的状态压缩,只需要能:
- 表示当前中间解
- 扩展出新解
- 对状态评分
所以更灵活。
代码
由 LLM 生成
cpp
#include <bits/stdc++.h>
using namespace std;
struct Task {
int id; // 原始编号
int value;
int power;
int heat;
};
struct Server {
int powerCap;
double heatCap;
};
struct State {
long long profit = 0;
vector<int> powerUsed; // size M
vector<int> heatRaw; // size M
vector<int> assign; // 原始任务编号 -> 0..M
double evalScore = 0.0; // 用于排序
bool operator<(const State& other) const {
return evalScore > other.evalScore; // for sort ascending? we'll use custom
}
};
static inline double calcLoadAt(
int p,
const vector<int>& heatRaw,
double K
) {
int M = (int)heatRaw.size();
double load = heatRaw[p];
if (p - 1 >= 0) load += K * heatRaw[p - 1];
if (p + 1 < M) load += K * heatRaw[p + 1];
return load;
}
static inline bool canPlace(
const State& st,
int p,
const Task& task,
const vector<Server>& servers,
double K
) {
int M = (int)servers.size();
if (st.powerUsed[p] + task.power > servers[p].powerCap) return false;
// 只会影响 p-1, p, p+1
// 模拟 heatRaw[p] + task.heat
auto getHeat = [&](int idx) -> int {
if (idx == p) return st.heatRaw[idx] + task.heat;
return st.heatRaw[idx];
};
for (int x = p - 1; x <= p + 1; x++) {
if (x < 0 || x >= M) continue;
double load = getHeat(x);
if (x - 1 >= 0) load += K * getHeat(x - 1);
if (x + 1 < M) load += K * getHeat(x + 1);
if (load > servers[x].heatCap + 1e-12) return false;
}
return true;
}
static inline void placeTask(
State& st,
int p,
const Task& task
) {
st.profit += task.value;
st.powerUsed[p] += task.power;
st.heatRaw[p] += task.heat;
st.assign[task.id] = p + 1; // 输出用 1..M
}
static inline double localSlackAfterPlace(
const State& st,
int p,
const Task& task,
const vector<Server>& servers,
double K
) {
int M = (int)servers.size();
auto getHeat = [&](int idx) -> int {
if (idx == p) return st.heatRaw[idx] + task.heat;
return st.heatRaw[idx];
};
double minSlack = 1e100;
for (int x = p - 1; x <= p + 1; x++) {
if (x < 0 || x >= M) continue;
double load = getHeat(x);
if (x - 1 >= 0) load += K * getHeat(x - 1);
if (x + 1 < M) load += K * getHeat(x + 1);
double slack = servers[x].heatCap - load;
minSlack = min(minSlack, slack);
}
double powerSlack = servers[p].powerCap - (st.powerUsed[p] + task.power);
return minSlack + 0.05 * powerSlack;
}
// 评价函数:当前收益 + 一点剩余空间偏好
static inline double evaluateState(
const State& st,
const vector<Server>& servers,
double K
) {
int M = (int)servers.size();
double thermalSlackSum = 0.0;
double powerSlackSum = 0.0;
for (int p = 0; p < M; p++) {
double load = calcLoadAt(p, st.heatRaw, K);
thermalSlackSum += max(0.0, servers[p].heatCap - load);
powerSlackSum += max(0, servers[p].powerCap - st.powerUsed[p]);
}
// 收益主导,剩余裕量稍微辅助
return (double)st.profit + 1e-4 * powerSlackSum + 1e-4 * thermalSlackSum;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M;
double K;
cin >> N >> M >> K;
vector<Task> tasks(N);
for (int i = 0; i < N; i++) {
cin >> tasks[i].value >> tasks[i].power >> tasks[i].heat;
tasks[i].id = i;
}
vector<Server> servers(M);
for (int i = 0; i < M; i++) {
cin >> servers[i].powerCap >> servers[i].heatCap;
}
// 任务排序:高价值密度优先 + 热/功耗大的稍微优先
vector<Task> order = tasks;
sort(order.begin(), order.end(), [&](const Task& a, const Task& b) {
double da = (double)a.value / (a.power + a.heat + 1.0);
double db = (double)b.value / (b.power + b.heat + 1.0);
if (fabs(da - db) > 1e-12) return da > db;
if (a.value != b.value) return a.value > b.value;
if (a.heat != b.heat) return a.heat > b.heat;
return a.power > b.power;
});
const int BEAM_WIDTH = 40; // 可调
const int CAND_LIMIT = 12; // 每个状态每个任务最多尝试多少台服务器
State init;
init.profit = 0;
init.powerUsed.assign(M, 0);
init.heatRaw.assign(M, 0);
init.assign.assign(N, 0);
init.evalScore = 0.0;
vector<State> beam{init};
for (int step = 0; step < N; step++) {
const Task& task = order[step];
vector<State> nextBeam;
nextBeam.reserve((size_t)beam.size() * (CAND_LIMIT + 1));
for (const State& st : beam) {
// 1) 不选当前任务
{
State ns = st;
ns.assign[task.id] = 0;
ns.evalScore = evaluateState(ns, servers, K);
nextBeam.push_back(std::move(ns));
}
// 2) 选若干候选服务器
vector<pair<double, int>> cand; // (score, serverId)
cand.reserve(M);
for (int p = 0; p < M; p++) {
if (!canPlace(st, p, task, servers, K)) continue;
double score = localSlackAfterPlace(st, p, task, servers, K);
// 加一点收益密度偏好
score += 0.1 * task.value;
cand.push_back({score, p});
}
if (!cand.empty()) {
int keep = min(CAND_LIMIT, (int)cand.size());
nth_element(cand.begin(), cand.begin() + keep - 1, cand.end(),
[&](const auto& A, const auto& B) {
return A.first > B.first;
});
cand.resize(keep);
sort(cand.begin(), cand.end(),
[&](const auto& A, const auto& B) {
return A.first > B.first;
});
for (auto &it : cand) {
int p = it.second;
State ns = st;
placeTask(ns, p, task);
ns.evalScore = evaluateState(ns, servers, K);
nextBeam.push_back(std::move(ns));
}
}
}
// 去重:有时可以只保留 eval 最好的前若干
sort(nextBeam.begin(), nextBeam.end(), [&](const State& a, const State& b) {
if (fabs(a.evalScore - b.evalScore) > 1e-12) return a.evalScore > b.evalScore;
return a.profit > b.profit;
});
if ((int)nextBeam.size() > BEAM_WIDTH) {
nextBeam.resize(BEAM_WIDTH);
}
beam.swap(nextBeam);
}
// 选最终收益最高的可行解
State best = beam[0];
for (const auto& st : beam) {
if (st.profit > best.profit) best = st;
}
for (int i = 0; i < N; i++) {
if (i) cout << ' ';
cout << best.assign[i];
}
cout << '\n';
return 0;
}
效果评估
束搜索主要涉及 beam width 束宽的调参.
这里的 beam width 上限值,需要结合 整体的时间复杂度 O ( b ∗ n ∗ m ) O(b * n * m) O(b∗n∗m), n , m ≤ 1000 n,m\le1000 n,m≤1000,和时限,评测机性能。
| 束宽 | 1 | 2 | 5 | 10 | 20 | 40 | 50 | 100 |
|---|---|---|---|---|---|---|---|---|
| 得分 | 441.78 | 441.13 | 439.44 | 442.44 | 441.31 | 441.52 | 441.89 | 441.01 |
这边束宽的增长,得分并没有预期增长。估计和该算法属于排序主导,且初始即接近最优解有关。
还有一种可能,就是 启发式评估函数 存在缺陷。
static inline double localSlackAfterPlace(
const State& st,
int p,
const Task& task,
const vector<Server>& servers,
double K
) {
int M = (int)servers.size();
auto getHeat = [&](int idx) -> int {
if (idx == p) return st.heatRaw[idx] + task.heat;
return st.heatRaw[idx];
};
double minSlack = 1e100;
for (int x = p - 1; x <= p + 1; x++) {
if (x < 0 || x >= M) continue;
double load = getHeat(x);
if (x - 1 >= 0) load += K * getHeat(x - 1);
if (x + 1 < M) load += K * getHeat(x + 1);
double slack = servers[x].heatCap - load;
minSlack = min(minSlack, slack);
}
double powerSlack = servers[p].powerCap - (st.powerUsed[p] + task.power);
return minSlack + 0.05 * powerSlack;
}
这个函数,只是只代表局部的点,其并不能代表整体,导致了波动和不确定。
泛化能力
- 路径规划 / 迷宫搜索 / 机器人决策
- 棋类 / 博弈 / 决策树搜索
- 旅行商 / 车辆路径 / 路线构造问题
- 组合优化:任务调度 / 资源分配
- 工业排产 / 作业车间调度
- 拼图 / 数独 / 约束满足问题的近似搜索
该算法在这几个 应用场景,也扮演非常重要的角色。
写在最后
