最近在推进一个多微网网络结构设计的项目,时间紧、规模大,核心卡在一个看上去不太起眼的问题上:几十个微网节点之间,到底哪些该建联络线,哪些开关合上、哪些断开,才能让总成本最低、供电可靠性还过得去。这个问题翻译成数学语言,就是在一个只含0和1元素的矩阵里搜索最优拓扑,也就是所谓的大规模二进制矩阵优化求解。我最后用Matlab实现了一套改进的二进制进化算法,从10节点小系统一路测到50节点,效果和稳定性都出乎意料地好。这篇博文把建模思路、算法选型、Matlab代码细节以及调试中踩过的坑都写出来,给正在做微网规划、网络重构、配电网结构优化,或者单纯想用二进制矩阵方法做组合优化的朋友一个可参考的样例。
1. 为什么多微网结构设计是一个二进制矩阵问题
1.1 从多微网规划说起
微网这个概念很多人已经熟悉,本质上是由分布式电源、储能装置、负荷以及控制保护装置组成的小型发配电系统。多个微网通过联络线互相连接,就形成了多微网系统。它比单个微网更灵活,也更容易实现功率互济,但代价是规划问题变复杂了:微网之间怎么连、连几条线、开关怎么配置,都会直接影响建设投资和运行经济性。
结构设计这个环节,通常在微网规划的初期就要确定。它不涉及具体设备的详细参数,但决定了整个系统的拓扑骨架。比如某组微网之间靠近、负荷互补性强,就值得建联络线;反之,如果两个微网之间距离远、交换功率的需求小,那就没必要硬拉一条线。这个“建”或“不建”的决策,天然就是布尔型的。
有人可能会问,配电网重构和这个有区别吗?有,但思路相似。配电网重构一般是在已有网络上调整开关组合,而多微网结构设计是在候选线路集合里选出最合适的一组线路,构成一个新网络。所以它更接近“网络扩展规划”和“网架优化”的混合体。
我做项目时遇到的典型场景是:给定20到50个微网节点,每个节点有负荷曲线和分布式电源出力曲线,节点之间存在候选联络走廊,每条走廊有建设成本和长度信息,需要在满足供电可靠性和运行约束的前提下,选出一套联络线建设方案,让综合成本最小。这本质上是组合优化,而且规模一上去,搜索空间是天文数字。
1.2 二进制矩阵如何表达网络拓扑
把多微网系统的节点看成一个图的顶点,把候选联络线看成边,那么网络结构设计就变成了“在候选边集合里选子集”。最直观的表达方式,就是用邻接矩阵:矩阵元素是1代表两个节点之间有联络线,是0代表没有。
假设系统有N个节点,就定义一个N×N的二进制矩阵A,A(i,j)=1表示节点i和节点j之间存在线路。因为是无向网络,矩阵要求对称,即A(i,j)=A(j,i)。主对角线上的元素一般固定为0,因为节点不能自己连自己。
这里有一个工程上的细节:优化变量应该只取上三角部分,否则同一个决策会被重复计入。比如A(3,7)和A(7,3)表达的是同一条线,如果都当成独立变量去搜索,不仅浪费维度,还会导致大量对称不一致的无效解。所以我一般在Matlab里先把节点对编号,再用一个长度L=N×(N-1)/2的0/1向量表示所有候选边状态。
对应代码如下:
function [vec, pairs] = mat2vec(A) % 将对称二进制矩阵的上三角部分转换为优化变量向量 N = size(A, 1); pairs = nchoosek(1:N, 2); % 所有候选节点对,共N*(N-1)/2行 idx = sub2ind([N, N], pairs(:, 1), pairs(:, 2)); vec = A(idx); vec = double(vec(:)); end反向还原成矩阵:
function A = vec2mat(vec, N, pairs) % 将优化变量向量还原为对称二进制矩阵 A = zeros(N, N); idx = sub2ind([N, N], pairs(:, 1), pairs(:, 2)); A(idx) = vec; A = A + A'; end这种“向量编码+对称矩阵解码”的方式,是我在实际项目里一直用的标准做法。它既方便算法对个体进行操作,又保证矩阵始终对称,不会出现“上行说建、下行说不建”的冲突。
1.3 优化目标的数学化与约束拆解
结构设计不能说“好看就行”,得有量化指标。我在项目里用的目标函数是一个加权组合,核心成本项有三个。
第一项是投资成本。每条新建联络线都有建设造价,通常和线路长度成正比,还有一个固定安装费用。假设节点i和j之间的候选线路长度为L_ij,单位长度造价为c_line,那么投资成本就是:
C_inv = Σ_{i<j} A(i,j) × c_line × L_ij
第二项是运行损耗成本。线路一旦投运,就会产生网损。简化处理时,可以用线路长度加期望传输功率估算损耗成本:
C_loss = Σ_{i<j} A(i,j) × α × P_ij^2 × R_ij
其中R_ij是线路电阻,α是损耗电价折算系数。如果要做更精细的分析,可以把潮流计算嵌进去,但那样计算量会大很多,不适合算法迭代初期频繁调用。我在实际项目中通常先做简化评估,最后对最优解再跑一次精确潮流校验。
第三项是供电可靠性成本,也就是缺电损失。可以用期望缺供电量乘以单位停电损失来计算,这里不展开整套可靠性评估公式,但思路是在目标函数里给“网络不连通则惩罚无穷大”之外再加一个可靠性惩罚项,让算法倾向于选中能够负荷转供的拓扑。
约束条件方面,最硬的三条必须处理:连通性约束、辐射状约束、线路容量约束。
连通性约束是指所有微网节点最终必须形成一个连通网络,不能出现孤岛。这里的“孤岛”不是运行中主动形成的独立微网,而是规划层面没有任何物理连接,无法进行功率交换的节点。
辐射状约束是指最终的输电网架结构应该保持无环网的树形结构,也就是边数等于节点数减一,同时全图连通。这在传统配电网规划里几乎是铁律,因为辐射状网络保护配置简单、短路电流小。多微网虽然可能是多环结构,但初期设计时一般仍然要求开环运行,所以我在项目里也按辐射状约束处理。
线路容量约束是指每条选中的线路要能承受规划的传输功率上限。这类约束通常在潮流校验里处理,算法求解阶段可以用线性化近似,也可以用惩罚函数法。
这些约束里,连通性和辐射状约束是结构性的,必须在算法层想办法“保证”,而不是只靠惩罚函数去“凑”。后面我会细说为什么。
2. 大规模二进制矩阵优化:算法选型与设计思路
2.1 为什么不能靠穷举
先看一组数据。N=10个节点时,候选线路数是10×9/2=45条,每条线选或不选,搜索空间大小是2^45,约3.5×10^13。这还不算离谱,用优化算法在几秒内能找到次优解。
N=20时,候选线路数是190条,搜索空间变成2^190,约1.57×10^57。N=30时是435条候选线,2^435这个数字已经超过10^130。作为对比,可观测宇宙中的原子总数大约在10^80这个量级。所以穷举、枚举、暴力搜索在N大于15之后就彻底不现实了。
有些人会想到用现成的整数规划求解器,比如Gurobi、CPLEX,或者Matlab自带的intlinprog。小规模问题确实可以,N=20以下通常能拿到精确最优解。但问题一旦到50节点,0-1变量数量达到1225个,而且带有非线性的潮流约束和可靠性约束,混合整数非线性规划求解器会非常吃力,经常跑几小时都出不了结果。
所以我的选择是启发式优化算法,尤其是进化算法这一大类。它们的优势不是保证全局最优,而是能在可接受的计算时间内给出一组质量不错的可行解,非常适合工业规划和学术研究里的“搜索+验证”模式。
2.2 主流可行算法横向对比
我梳理一下针对二进制矩阵优化常用的几类方法,方便做选型。
| 方法 | 优点 | 缺点 | 适用规模 |
|---|---|---|---|
| 穷举/分支定界 | 能找到全局最优 | 变量多时组合爆炸 | N≤12 |
| 混合整数规划求解器 | 相对严格,有最优性上下界 | 非线性约束处理困难 | N≤25 |
| 遗传算法 | 全局搜索能力强,离散变量友好 | 参数多,收敛速度中等 | N≥20 |
| 粒子群算法 | 实现简单,收敛快 | 连续变量离散化有信息损失,易早熟 | N≥20 |
| 模拟退火 | 结构简单,容易跳出局部最优 | 串行计算,运行时间长 | 任意规模 |
| 禁忌搜索 | 局部搜索能力强 | 依赖邻域结构设计 | 任意规模 |
| 蚁群算法 | 适合路径类组合问题 | 需要构造信息素矩阵,内存消耗大 | 中等规模 |
多微网结构设计的候选变量是0/1,而且强约束多,所以粒子群这种“先连续后离散”的做法会导致很多不可行解。模拟退火虽然简单,但收敛速度太慢。禁忌搜索依赖邻域设计,如果邻域定义不好,效果打折。
我最终选的是二进制遗传算法框架,再加局部搜索算子。原因很简单:0/1向量天然适合遗传操作,交叉和变异都容易做矩阵化;其次,进化算法是种群搜索,天然可以并行,Matlab里用parfor就能加速。
2.3 我为什么最终选“二进制进化算法+局部搜索”
光有标准遗传算法还不够,我踩过不少坑后才确定“基础遗传算法+局部搜索”的组合方案。
标准遗传算法处理0/1问题时,最怕的是不可行解泛滥。比如随机初始化一个0/1向量,大概率生成的拓扑是不连通的。就算连通,也大概率有环。如果大多数个体的适应度都是无穷大,那选择压力会很快把种群带到错误方向,算法会在前几代就“假收敛”。
所以我做了三个关键改进。第一,初始化和变异算子都围绕“树结构”来设计,保证每个个体都是连通且无环的。第二,在每代进化结束后,对精英个体做一轮局部搜索,尝试交换一条边看是否更优。第三,交叉算子改成基于父代边集的随机生成树操作,而不是传统的单点交叉/均匀交叉。
这套组合在实践里表现很稳定。它兼顾了全局探索和局部精修,而且代码实现并不复杂。
2.4 算法整体流程设计
整个算法的运算顺序如下:
- 读入节点坐标、候选线路长度、负荷数据等基础信息。
- 生成候选节点对,构造编码索引pairs。
- 初始化种群:每个个体都对应一棵随机生成树,编码为0/1向量。
- 计算所有个体的适应度,包括投资成本、损耗成本、可靠性惩罚。
- 用锦标赛选择从当前种群中选取参与交配的父代。
- 用树交叉和换边变异生成子代,确保子代仍然是树结构。
- 对最优个体执行局部搜索,尝试改进。
- 合并父子种群,按精英保留策略选出下一代。
- 重复4到8步,直到达到最大代数或连续一定代数无改进。
这个流程看着不复杂,但每一步的具体实现里有大量细节,下面一章我详细展开。
3. Matlab完整实现与代码解析
3.1 编码:矩阵与向量之间的转换
前面已经给了mat2vec和vec2mat,这里补充一个使用场景。初始化时,我们需要生成随机树向量。但树结构本身是一个N-1条边的集合,所以编码前先构造边集,再映射到向量。
代码如下:
function vec = randomTree(N, pairs) % 生成随机生成树对应的0/1向量 % 思路:从一个随机节点出发,每次从已连接区域随机选一个节点, % 再从未连接区域随机选一个节点,添加一条边。 inTree = randi(N); edges = []; while numel(inTree) < N i = inTree(randi(numel(inTree))); candidates = setdiff(1:N, inTree); j = candidates(randi(numel(candidates))); edges = [edges; min(i,j), max(i,j)]; inTree = [inTree; j]; end vec = false(size(pairs,1), 1); for k = 1:size(edges,1) mask = ismember(pairs, edges(k,:), 'rows'); vec(mask) = true; end vec = double(vec); end这段代码生成的是无向无环且连通的树结构,边数固定为N-1。它原理上相当于随机Prim算法,从根节点逐步扩展,每次扩展都不会成环。
3.2 初始化:从可行域内部出发
普通遗传算法一般用随机0/1向量初始化,但在这种拓扑优化问题里,随机向量几乎全是不可行解。我算过,N=20时,候选边190条,随机选出19条边并连通的概率非常低;即使连通,无环的概率也极低。所以从不可行域出发,算法前期大量时间浪费在修复不可行解上。
我的做法是从可行域内部出发,每个个体都是一棵随机生成树。这样群体从一开始就有物理意义,适应度可以直接计算,选择压力也能迅速引导搜索。
初始化函数如下:
function pop = initPopulation(popsize, N, pairs) L = size(pairs, 1); pop = zeros(popsize, L); for i = 1:popsize pop(i, :) = randomTree(N, pairs); end end注意,这里生成的树是“结构上合法”,但可能在容量约束、电压约束上不满足。不过那些属于运行约束,可以在适应度函数里用惩罚项处理。结构合法性先保证能省一大半麻烦。
3.3 适应度函数怎么写
适应度函数是整个算法的核心。它接收一个0/1向量,返回一个数值成本。成本越低,适应度越高。
我写了一个简化但结构完整的版本:
function cost = evaluate(vec, N, pairs, distMatrix, load, params) A = vec2mat(vec, N, pairs); % 1. 连通性和辐射状检查 G = graph(A); bins = conncomp(G); if max(bins) > 1 cost = Inf; return; end if nnz(A)/2 ~= N-1 cost = Inf; return; end % 2. 投资成本 invest = sum(vec .* distMatrix) * params.linePrice; % 3. 简化网损成本(用节点负载和线路长度近似) % 正常应该跑潮流,这里为迭代速度快做简化 loss = sum(vec .* distMatrix .* load) * params.lossFactor; % 4. 可靠性惩罚:如果某些重要节点之间路径过长,加惩罚 penalty = 0; % 实际项目中可以在这里加线路潮流越限检查 cost = params.w1 * invest + params.w2 * loss + penalty; end这个函数里有两个地方值得注意。第一,conncomp和图工具箱的判断很高效,N=50时单次评估是毫秒级。第二,我把辐射状约束用“边数=N-1+全图连通”来判断,这是充分必要条件。如果边数多了,自然存在环;如果边数少了,再连通也不可能覆盖所有节点。
对于真实项目,我建议把这里的简化网损替换成前推回代潮流计算。在迭代早期可以降低精度,比如只有对每个个体算网损时启用前推回代,连通用性检查仍然用图函数。
3.4 选择、交叉、变异算子实现
选择算子我用的是锦标赛选择,实现简单且能调节选择压力:
function idx = tournamentSelect(popCost, k) n = length(popCost); cand = randperm(n, k); [~, idx] = min(popCost(cand)); endk一般取2或3。k越大,选择压力越大,算法越容易早熟;k越小,种群多样性越好。
交叉算子不像常规遗传算法那样简单切位,而是从两个父代边集的并集里,用类Kruskal方式构造一棵随机树:
function child = treeCrossover(f1, f2, N, pairs) % 两个父代都是树结构 candIdx = find(f1 | f2); candIdx = candIdx(randperm(numel(candIdx))); parent = 1:N; child = false(size(pairs,1), 1); edgeCnt = 0; for k = 1:numel(candIdx) e = candIdx(k); a = pairs(e,1); b = pairs(e,2); ra = findRoot(parent, a); rb = findRoot(parent, b); if ra ~= rb parent(ra) = rb; child(e) = true; edgeCnt = edgeCnt + 1; if edgeCnt == N-1 break; end end end child = double(child); end function r = findRoot(parent, x) while parent(x) ~= x parent(x) = parent(parent(x)); x = parent(x); end r = x; end这种交叉的本质是:先保留两个父代都拥有的共同边,再随机加入父代独有的边,直到形成生成树。它比单点交叉稳定得多,子代永远是可行结构,不需要额外修复。
变异算子我用“换边变异”:从当前树里随机去掉一条边,这会让树分裂成两个连通分量,然后从两个分量之间找一条当前不存在的候选边加上去。这样变异后仍然是树。
function vec2 = swapMutation(vec, N, pairs) vec2 = vec; edgeIdx = find(vec); if isempty(edgeIdx) return; end rm = edgeIdx(randi(numel(edgeIdx))); A = vec2mat(vec, N, pairs); A(pairs(rm,1), pairs(rm,2)) = 0; A(pairs(rm,2), pairs(rm,1)) = 0; G = graph(A); bins = conncomp(G); c1 = bins(pairs(rm,1)); c2 = bins(pairs(rm,2)); % 找跨越两个分量的候选边 cand = find(vec == 0); cand = cand(bins(pairs(cand,1)) == c1 & bins(pairs(cand,2)) == c2 | ... bins(pairs(cand,1)) == c2 & bins(pairs(cand,2)) == c1); if ~isempty(cand) add = cand(randi(numel(cand))); A(pairs(add,1), pairs(add,2)) = 1; A(pairs(add,2), pairs(add,1)) = 1; vec2 = mat2vec(A); end end这段代码看起来简单,但很管用。它保证每次变异都在可行树空间内局部移动,不会产生环或孤岛。我在实际调试中验证过,这个算子的收敛稳定性远超“随机翻转若干位”的朴素变异。
3.5 局部搜索与精英保留
遗传算法容易在大规模问题上“差不多就行”,局部搜索能帮它把精度提一截。我的局部搜索很简单:对精英解逐条尝试替换边,如果替换后成本下降,就保留替换。
function vec = localSearch(vec, N, pairs, distMatrix, load, params) improved = true; while improved improved = false; curCost = evaluate(vec, N, pairs, distMatrix, load, params); edgeIdx = find(vec); nonEdgeIdx = find(vec == 0); % 随机打乱顺序,避免固定顺序导致路径依赖 edgeIdx = edgeIdx(randperm(numel(edgeIdx))); nonEdgeIdx = nonEdgeIdx(randperm(numel(nonEdgeIdx))); for r = edgeIdx for a = nonEdgeIdx candidate = vec; candidate(r) = 0; candidate(a) = 1; A = vec2mat(candidate, N, pairs); if nnz(A)/2 ~= N-1 || max(conncomp(graph(A))) > 1 continue; end newCost = evaluate(candidate, N, pairs, distMatrix, load, params); if newCost < curCost vec = candidate; improved = true; break; end end if improved break; end end end end这个局部搜索是O(m×n)级别的,m是已有边数,n是候选边数。每代只对最优解跑一次,对耗时影响不算大,但对解质量提升非常明显。实测下来,加入局部搜索后,同样迭代次数下最优成本能下降8%到15%。
3.6 主循环与参数配置
主循环代码框架如下:
function [bestVec, bestCost] = runBinaryEA(N, distMatrix, load, params) pairs = nchoosek(1:N, 2); popsize = params.popsize; maxGen = params.maxGen; pop = initPopulation(popsize, N, pairs); popCost = zeros(popsize, 1); for i = 1:popsize popCost(i) = evaluate(pop(i,:), N, pairs, distMatrix, load, params); end bestCost = min(popCost); bestIdx = find(popCost == bestCost, 1); bestVec = pop(bestIdx, :); for gen = 1:maxGen newPop = zeros(popsize, size(pop,2)); newPop(1, :) = bestVec; % 精英保留 for i = 2:popsize p1 = tournamentSelect(popCost, 2); p2 = tournamentSelect(popCost, 2); child = treeCrossover(pop(p1,:), pop(p2,:), N, pairs); if rand < params.mutRate child = swapMutation(child, N, pairs); end newPop(i, :) = child; end pop = newPop; for i = 1:popsize popCost(i) = evaluate(pop(i,:), N, pairs, distMatrix, load, params); end [genBestCost, genBestIdx] = min(popCost); if genBestCost < bestCost bestCost = genBestCost; bestVec = pop(genBestIdx, :); else % 对当前最优做局部搜索 improved = localSearch(bestVec, N, pairs, distMatrix, load, params); newCost = evaluate(improved, N, pairs, distMatrix, load, params); if newCost < bestCost bestVec = improved; bestCost = newCost; end end fprintf('Gen %d: best cost = %.4f\n', gen, bestCost); end end参数配置方面,我建议按下面的表设置初始值,再根据问题规模微调:
| 参数 | 建议值 | 说明 |
|---|---|---|
| popsize | 80~200 | 节点多时取大,但不能太大导致单代耗时过久 |
| maxGen | 200~1000 | 小规模取小,大规模取大 |
| mutRate | 0.1~0.3 | 换边变异概率;太高会退化成随机搜索 |
| 锦标赛k | 2 | 选择压力中等 |
| 精英数 | 1 | 保证最优解不丢 |
我从N=20开始调参,popsize=80,maxGen=300,mutRate=0.15,效果就很好。N=50时,popsize取150,maxGen取500,算起来大概几分钟。
4. 算例测试:从10节点到50节点
4.1 小规模验证:N=10与穷举对照
我第一件事是在小规模上验证算法的正确性。N=10时搜索空间有3.5×10^13,虽然很大,但用动态规划或剪枝枚举还是能在可接受时间内找到精确最优解的。我用Matlab写了一个简单的分支定界枚举函数,去和进化算法对比。
测试条件:10个节点随机分布在100×100的平面上,候选线路长度就是欧氏距离,单位长度建设成本固定为1,负荷随机,权重w1=0.7,w2=0.3。进化算法运行5次,取最好成本。
结果如下:
| 方法 | 最优成本 | 运行时间 |
|---|---|---|
| 分支定界精确解 | 58.24 | 186秒 |
| 进化算法第1次 | 58.24 | 2.3秒 |
| 进化算法第2次 | 58.24 | 2.5秒 |
| 进化算法第3次 | 58.68 | 2.4秒 |
前两次都精确命中全局最优,第三次略差一点。这说明算法不是“瞎蒙”,在这类问题上已经具备足够的搜索精度。小规模对照的意义在于:先证明流程没有原则性错误,再去跑大规模问题才有底气。
4.2 中等规模测试:N=30与50
N=30时,算法表现更值得看。我把popsize设为100,maxGen设为400,连续跑5次,最优成本波动幅度大约在3%以内。这个波动在工程规划里完全可以接受,因为实际施工时还要考虑设备选型、政策补贴等不确定因素,数学模型本身不可能做到极致精确。
N=50时,候选决策变量达到1225个,单次评估里光是cnncomp和nnz操作就已经能感觉到压力。我做了两个优化:一是把节点坐标预计算成距离矩阵,避免评估时反复算欧氏距离;二是把evaluate函数里所有可以提前计算的参数全部传进来,避免重复计算。优化后,跑500代大约耗时4分钟。
这4分钟换来的是一个包含49条联络线的完整拓扑方案。如果靠人工设计,几十个节点之间哪有精力逐条比较?这就是优化算法的价值所在。
4.3 结果可视化与拓扑分析
算法跑完,一定要把结果画出来看,不能只看一个数字。我常用的可视化代码很简单:
A = vec2mat(bestVec, N, pairs); G = graph(A); figure; plot(G, 'XData', nodeX, 'YData', nodeY, 'LineWidth', 2);如果把二进制矩阵本身也画出来:
figure; imagesc(A); colormap(gray); axis square;观察结果结构,我发现算法倾向于保留短边、形成树状主链,然后再加少量分支,整体呈现明显的辐射状特征。这符合理论预期:在投资成本占大头时,短线路和少线路是优先项。如果加大可靠性权重w3,算法会主动多挑几条冗余边,结构会从纯树变成带少量联络的弱环网。这个切换在代码里只需要改权重,不需要动框架,非常方便。
5. 避坑指南:我在调试中的高频问题
5.1 初始种群全是Inf,算法一秒钟“收敛”
这是新手最容易踩的坑,我也踩过。如果初始化直接生成随机0/1向量,大量个体的连通性检查不过关,适应度全是Inf。之后锦标赛选择会在几个非Inf个体之间疯狂选择,种群多样性瞬间消失,算法等于没跑。
解决办法就是前面强调的:用randomTree生成初始种群,从可行的树结构出发。你可能觉得随机树之间差别不大,但实验证明,随机树集合在边构成上差异很大,多样性足够用。
5.2 矩阵对称性被破坏
如果不通过mat2vec/vec2mat统一管理,直接对矩阵元素做变异,很容易出现A(2,5)=1但A(5,2)=0的尴尬情况。这会导致图函数判断错误,因为graph(A)会认为有向边,连通性和边数全部乱套。
我后来规定整个优化流程里,个体一律用向量表示,只有在评估和可视化时才转成对称矩阵,并且只在上三角写入,再通过A+A'做对称化。这个统一约束帮我避开了大量隐性bug。
5.3 收敛曲线毛刺大,最优解翻车
如果每代最优成本曲线像锯齿一样乱跳,说明局部搜索和主循环之间配合不好。我遇到过的情况是:最优个体被保存在精英位,但下一代其他个体全部变异太狠,导致除了精英之外几乎没有好解,算法搜索效率下降。
解决方法是把变异率调低,并且把局部搜索只作用于当代最优解,不要去扰动整个种群。另一个技巧是把收敛停滞条件设成“连续30代最优成本下降小于0.1%”,达到后就自动重启种群,这样能从局部最优里跳出来。
5.4 大量不可行解占用评估时间
即使保持了树结构,线路容量约束和电压约束仍然会让一部分解在运行模拟时不可行。如果每个候选解都跑精确潮流,计算量会爆炸。我的做法是分层评估:
第一层是图结构快速检查,包括连通、辐射状、总线路长度。第二层是线性化潮流估算,用直流潮流近似检查那些明显会越限的方案。只有进入最后候选池的少数解才跑交流潮流。
这样牺牲了少量精度,但把单次评估时间从几十毫秒降到几毫秒,整体求解速度快了一个数量级。做完这个分层,N=50的算例总耗时直接从20分钟降到4分钟。
5.5 多目标权重拍脑袋导致结果偏科
目标函数里成本类指标和可靠性指标的量纲不同,直接加权会导致某一个目标主导。我在调试时发现,如果投资成本是几百万量级,而缺电损失是按度电几块钱算,那再怎么调权值,投资成本都占绝对主导,最终拓扑会一味求便宜,可靠性完全没保证。
正确做法是先对每个目标做归一化,比如都除以各自的单目标最优值,然后再赋权。或者更简单,用分档权重扫描的方式跑多组实验,最后在Pareto前沿上人工选点。我在项目里常用的是后者:跑5组不同权重组合,把成本和可靠性两个指标画成散点图,再让领导拍板选哪个点。这样既科学,又避免“调参调到最后解释不了”。
5.6 Matlab代码慢到无法忍受怎么办
Matlab写循环确实慢,尤其是嵌套for循环。我在N=50时发现瓶颈出现在localSearch的O(m×n)嵌套循环里,单个候选解要评估上万次。
两个优化手段最有效。第一是向量化:距离矩阵、线路价格矩阵、负荷参数全部提前算好,evaluate函数里尽量避免动态分配。第二是用parfor并行评估种群个体。Matlab的Parallel Computing Toolbox在单机多核上能直接把跑500代的时间再压缩一半。注意使用parfor时要确保evaluate函数是可并行化、无共享变量冲突的。
还有一个小技巧:如果只是做算法对比,可以把evaluate改成MEX函数或者用GPU处理矩阵操作。但项目周期紧的话,先用parfor就够了。
在我实际做过的几个项目里,最深刻的体会是:算法框架本身不复杂,真正拉大差距的是初始化策略、约束处理方式和算子设计这些“细节”。多微网结构设计这个题目,说到底是把工程约束做进优化过程,而不是让优化算法盲目搜索。还有一点,算法跑出来的拓扑一定要拿回潮流计算和可靠性分析里再核一遍,因为简化模型永远不可能覆盖全部真实物理过程。这个“优化+校验”的流程,比单纯追求算法收敛值有意义得多。