simulated_annealing不发散的关键是合理设初温t0、慢降温、动态内循环次数及约束内嵌邻域;避免nan需用对数空间比较而非直接exp。

simulated_annealing 函数怎么写才不发散
退火算法跑几十次结果全飘在天边,大概率是温度衰减太猛或初始温度没压住。核心不是堆参数,而是让 accept_probability 在早期真能接受劣解——否则直接退化成贪心。
- 初始温度
T0建议用采样法:随机生成 100 个邻域解,取目标函数差值的 95% 分位数绝对值,再除以log(2)(对应接受概率 ≈ 0.5) - 降温策略别硬写
T *= 0.995,改用T = T0 / log(1 + t)或T = T0 / (1 + t * 0.01),慢降比快降更容易跨过局部峰 - 每次温度下迭代次数不能固定为 10/20,得按当前温度动态设:比如
inner_iters = max(10, static_cast<int>(100 * T / T0))</int>
邻域操作怎么避免越界或非法状态
C++ 里手写邻域一不留神就 std::vector::at 报 out_of_range,或者解向量进了不可行域(比如背包超重、路径重复节点)。关键不是加一堆 if,而是把约束“编译进”邻域生成逻辑里。
- 对数组索引类变量(如 TSP 路径),邻域用
swap或reverse_segment,天然保长度和元素集不变 - 对浮点决策变量,扰动后立刻用
std::clamp截断:x = std::clamp(x + delta, low_bound, high_bound) - 如果约束复杂(如整数线性约束),宁可每次生成后快速验证,验证失败就重试——比在生成时硬推公式更稳
随机数引擎选 std::mt19937 还是 std::ranlux24
退火依赖高质量随机性,但 std::ranlux24 慢三倍以上,而 std::mt19937 在长序列中可能暴露周期性。实际用法很朴素:
- 全局只建一个
std::mt19937实例,用std::random_device{}()初始化种子,别每次调用都 new 引擎 - 概率计算统一走
std::uniform_real_distribution<double>(0.0, 1.0)</double>,别用rand() % 100 / 100.0—— 低比特周期太短 - 如果跑多线程退火,每个线程必须有自己的
std::mt19937实例(种子用 thread_id 混合),否则所有线程输出完全相同
为什么 std::exp((old_cost - new_cost) / T) 经常算出 nan
当 old_cost - new_cost 是很大的负数(比如 -1000),而 T 已降到 1e-5 级别时,exp(-1e8) 直接下溢成 0,后续除法或比较可能触发未定义行为。这不是数学问题,是浮点精度落地问题。
- 改用对数空间比较:不计算接受概率,直接生成
u ~ Uniform(0,1),然后判断log(u) - 或者加保护:当
(old_cost - new_cost) / T 时直接拒绝(<code>exp(-700) ≈ 1e-304,double 下限) - 别用
std::exp算小概率,用std::expm1或手动泰勒展开前几项反而更糟——退火里不需要那么高精度
退火最麻烦的从来不是公式,而是温度掉到 1e-6 后,double 的有效位数已经不够描述两个相近解的微小差异了。这时候该换数据结构,而不是调参数。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











