接手这个“无人机联盟选取与能耗最优追踪”项目时,我第一反应是:这不就是一个带约束的组合优化问题吗?实战中很多人的做法是拍脑袋指定几架无人机出去执行目标任务,结果有的无人机飞了大半圈能耗爆表,有的明明就在任务点旁边却闲置着,整个联盟的总能耗高得离谱。
我这次用 0-1 整数规划来做无人机联盟的自动选取,目标是在完成追踪/攻击任务的前提下,让所有参战无人机的总能耗最小。核心思路一句话:把“选谁出去、不选谁出去”的决策写成数学变量,把能耗、任务覆盖、续航限制写成约束和目标函数,然后用 Matlab 自带的 intlinprog 求解器在秒级给出全局最优解。整个项目从建模到代码落地大概用了两天时间,这篇文章把每个环节展开讲清楚,含可直接复用的 Matlab 代码片段。
1. 项目要解决的痛点:能耗浪费与人工分配的低效
背景是这样的:某个多无人机协同场景里,地面控制系统需要从十几架待命无人机中挑选一个“作战联盟”,去覆盖几个分散的目标点执行攻击任务。每个目标必须被至少一架无人机追踪到,但并不是所有无人机都要出动,也不是每架无人机都能胜任所有目标。
1.1 人工分配为什么不行
直觉式的分配方式存在三个典型问题。
一是能耗完全不可控。手动指派无人机时,很容易忽略起飞阶段的高能耗代价,让一架距离目标很远的无人机长途奔袭,而旁边一架具备同等能力的无人机却在待命池里闲着。最终总能耗远超必要值。
二是约束容易冲突。无人机各有各的续航上限,有人可能 45 分钟就得返航,有人能飞 90 分钟。如果某个目标位置偏远,续航不足的无人机即便“想去”也“去不了”,人工方案很难把所有续航约束一次性全部满足。
三是联盟规模不固定。不是说 12 架无人机就要 12 架全部出动。最优解可能是只派 4 架、5 架或 6 架,数量需要根据目标位置和能耗代价来“自动决定”。人工预判联盟规模,基本靠猜。
1.2 0-1 整数规划在这个问题中的适用性
0-1 整数规划是混合整数线性规划的特殊情况,决策变量只能取 0 或 1,对应“不选”与“选中”。这个工具被广泛应用在人员排班、物流配送、生产调度等领域,核心优势就两条:全局最优、可证明最优。
对比遗传算法或粒子群算法,0-1 整数规划不会因为随机种子不同而给出不同结果,而且 GLPK、intlinprog、Gurobi 这类求解器都采用了分支定界与割平面算法,对 100 个左右的决策变量可以秒级求解。无人机的联盟选取问题通常只有几十架飞机、几个任务点,决策变量规模几十到几百个,完全在可控范围内。
所以这个项目本质上不是“怎么飞”的问题,而是“派谁去”的问题。飞行路径可以让底层飞控去完成,但顶层的高价值决策——哪些无人机组成联盟、谁去哪个目标——交给 0-1 整数规划来做再合适不过。
2. 0-1 整数规划建模:把“选谁执行”写成数学语言
建模是整个项目最核心、也最容易出错的地方。数学公式写清楚了,后面的代码就是翻译问题。
2.1 决策变量:一个二维的 0-1 矩阵
定义决策变量 x(i,j),含义是“第 i 架无人机是否被选中去执行第 j 个目标点的攻击/追踪任务”。
如果无人机总数为 N,任务点总数为 M,那么决策变量总数为 N × M。每个变量只有两种取值:
- x(i,j) = 1:第 i 架无人机被选中,分配给目标 j
- x(i,j) = 0:不选
例如 N=12、M=3,就有 36 个决策变量。这 36 个变量组合起来形成一个逻辑上的 12×3 矩阵,每一行代表一架无人机,每一列代表一个任务目标。矩阵中所有 1 的位置,就是最终选出的“联盟”。
2.2 约束条件:任务覆盖、能力匹配与续航限制
约束条件直接决定了可行解空间长什么样。我用了四条核心约束。
第一条约束:每个目标至少被一架无人机覆盖。
用数学公式表达是:
sum_i x(i,j) >= 1,对每个任务点 j这条约束保证“没有任务被漏掉”。实际项目中,如果你希望某个目标被多架无人机同时追踪以提高冗余度,可以把右边的 1 改成 2 或更大值。
第二条约束:每架无人机最多只去一个目标点。
sum_j x(i,j) <= 1,对每架无人机 i理由是多数场景下,一架无人机执行完一个目标后剩余电量已无法转向第二个目标。如果你想让一架无人机顺路串访多个目标,这个约束就要删掉,同时把变量扩展成带顺序编号的三维形式。那属于带时间窗的路径规划问题,复杂度会上一个台阶。
第三条约束:能力匹配限制。
x(i,j) = 0,如果无人机 i 无法执行任务类型 j有些目标需要携带特定载荷的无人机去处理,比如具备夜视能力的机型才能追踪夜间目标。能力不匹配的无人机,直接通过预计算把对应变量限制为 0,不加入求解器。
第四条约束:续航限制。
无人机从出发位置飞到目标点再返回,总飞行距离对应的能耗不能超过电池容量。这一步需要预估飞行路线,通常会乘以一个 1.2 到 1.5 的安全系数。
2.3 目标函数:总能耗最小化
既然要“能耗最小”,就需要构建能耗代价函数。这里我用的是分阶段线性能耗模型。每架无人机 i 被派往目标 j 的总能耗 estim(i,j) 由四个部分组成:
estim = 起飞爬升能耗 + 水平巡航能耗 + 任务悬停追踪能耗 + 返航降落能耗目标函数的数学形式是:
minimize sum_i sum_j C(i,j) * x(i,j)C(i,j) 就是预计算好的 estim(i,j)。由于 C(i,j) 是常数,这个目标函数是线性的,配合 0-1 整数变量和线性约束,就是标准整数线性规划问题,求解器可以直接处理。
3. 能耗模型拆解:让目标函数真正可用
很多初学者拿到这个问题的第一反应是:能耗太复杂了,是不是得仿真气动数据?其实在顶层任务分配阶段,能耗不需要精确到电池电压纹波,只需要一个“线性可信”的估算。
3.1 四个阶段的能耗估算公式
我把无人机执行一个目标任务的全程拆成四段。
第一段:起飞爬升。无人机从地面升高到巡航高度,这个阶段能耗基本固定,取决于机型和起飞重量。我用的模拟参数中,起飞爬升能耗大约占总能耗的 10%,这个阶段没有距离变量,是常数。
第二段:水平巡航。无人机以巡航速度从起始位置飞到目标上空。这里能耗等于单位里程能耗乘以飞行距离。
第三段:任务悬停或盘旋追踪。到达目标上方后,无人机需要悬停或小半径盘旋来执行攻击追踪任务。悬停时间由任务需求决定,悬停功率乘以任务时间即为能耗。
第四段:返航与降落。返航距离通常假设为从目标点直接飞回起降平台,能耗公式与巡航段一致,但降落阶段又有固定能耗。
把四个部分写在一起,C(i,j) 的计算公式如下:
C(i,j) = E_takeoff(i) + e_cruise(i) * dist(i,j) + P_hover(i) * T_task(j) + e_cruise(i) * dist_ret(i,j)其中 dist(i,j) 是出发位置到目标 j 的水平距离,dist_ret(i,j) 是目标 j 到返航点的水平距离。如果起降点是同一个机场,这两个距离相等。
3.2 参数如何标定:实测与估算的折中
很多团队的误区是试图拿到“精确”功率曲线。实战经验是,在任务分配阶段只需要把参数标定到误差在 15% 以内即可,真正精细的能耗优化应该在飞行控制层做。
我用的方式是给无人机平台做 3 次实测标定:
- 电池充满,起飞到 100 米高度悬停 10 秒,记录压降,推出起飞段能耗。
- 以巡航速度往返飞行一段已知距离,推出单位里程能耗。
- 悬停 60 秒,记录压降,推出悬停功率。
实测两次取平均值,再乘以一个安全系数。标定的参数整理成表格:
| 无人机编号 | 起飞能耗 (Wh) | 巡航能耗 (Wh/km) | 悬停功率 (W) | 最大航程 (km) | 最高速度 (m/s) |
|---|---|---|---|---|---|
| UAV-01 | 2.8 | 3.5 | 180 | 15 | 18 |
| UAV-02 | 3.2 | 4.1 | 210 | 12 | 16 |
| UAV-05 | 1.9 | 2.6 | 140 | 20 | 22 |
实际项目里参数可以从出厂手册查一个初值,再用一次小规模飞行测试修正。关键是建立一套参数表管理机制,每架无人机入库时的标定数据直接进配置表,代码里只需读表即可。
3.3 为什么非线性能耗一定要线性化
有朋友问过:能耗和距离如果是超线性关系,是不是要用非线性规划?答案是可以做,但代价非常大。
非线性规划的求解速度慢、容易陷入局部最优、并且无法证明全局最优。在顶层任务分配场景中,能耗模型的小幅非线性完全可以用分段线性来逼近,或者直接乘以一个高估系数。
举例来说,真实能耗曲线可能是“距离的 1.08 次方”,那你直接用 1.1 倍的线性系数,误差也就两三个百分点,求解难度却从 NP-Hard 的非线性问题变成了标准整数线性规划。这就是工程上常见的“能线性就线性,线性不了就分段线性”的取舍。
4. Matlab 代码实现:从公式到可运行脚本
Matlab 实现这个项目最大的坎不是算法本身,而是把数学模型“翻译”成 intlinprog 需要的矩阵输入格式。
4.1 主脚本结构设计
完整的代码我放在这个项目里,整体结构分为四层:
- 参数定义层:无人机参数表、任务点坐标、能耗系数。
- 代价计算层:根据公式计算 C(i,j) 矩阵。
- 约束构建层:把所有约束写成 Ax <= b 和 Aeqx = beq 的矩阵形式。
- 求解与输出层:调用 intlinprog,重构联盟分配结果并绘制航线图。
主脚本的核心骨架如下:
% 主脚本 clear; clc; % 第1步:加载无人机参数与任务点定义 drones = loadDroneParams('drone_params.csv'); % 无人机参数表 targets = loadTargetList('target_list.csv'); % 任务点坐标列表 % 第2步:计算每架无人机去每个目标的能耗代价 N = size(drones, 1); % 无人机数量 M = size(targets, 1); % 目标数量 C = zeros(N, M); % 能耗代价矩阵 for i = 1:N for j = 1:M dist = norm(drones(i).pos - targets(j).pos); C(i, j) = drones(i).E_takeoff + drones(i).e_cruise * dist ... + drones(i).P_hover * targets(j).T_task ... + drones(i).e_cruise * dist; % 假设起降点在同一位置 end end % 第3步:构建约束矩阵并求解 [x_opt, fval, exitflag] = solveAssignment(C, drones, targets); % 第4步:重构结果并输出 [selected_alliance, assignment_map] = reconstructSolution(x_opt, N, M); plotMissionMap(drones, targets, selected_alliance, assignment_map);4.2 变量压平与约束矩阵搭建
intlinprog 要求的决策变量是一维向量,因此必须把二维的 x(i,j) 按列压平成一维。顺序可以自己定,但一旦定下来,后续所有约束和目标函数都必须用同一套顺序。
这里很容易出问题,建议定义一个映射函数:
% 将二维索引(i,j)映射到一维索引k % 采用按列优先:k = (j-1)*N + i function k = idx(i, j, N) k = (j - 1) * N + i; end按列优先的意思是,一维向量前 N 个元素对应任务点 1 的所有无人机分配变量,接着 N 个元素对应任务点 2,以此类推。
每个目标至少被一架无人机覆盖的约束可以这样构建:
% 约束:每个目标点至少有一架无人机执行 A_cover = zeros(M, N*M); for j = 1:M for i = 1:N k = idx(i, j, N); A_cover(j, k) = -1; % 移项到左边:-sum_i x(i,j) <= -1 end b_cover(j, 1) = -1; end注意我用了 -1 和 -1 将“>=”约束转换成了标准形式“Ax <= b”中允许的“<=”约束。这是新手最容易踩坑的地方,Matlab 的 intlinprog 默认只接受 Ax <= b 形式,想要大于等于约束就得在两边乘负号。
每架无人机最多去一个目标点的约束:
% 约束:每架无人机最多执行一个目标 A_single = zeros(N, N*M); for i = 1:N for j = 1:M k = idx(i, j, N); A_single(i, k) = 1; end end b_single = ones(N, 1); % 每架无人机至多选一个目标4.3 调用 intlinprog 求解
约束矩阵、目标函数系数、整数变量列表都准备好后,调用求解器非常直接。
% 目标函数系数,从C矩阵按列压平 f = reshape(C, [], 1); % 整数变量:所有N*M个变量都需要是0-1整数 intcon = 1:N*M; % 变量边界:0 <= x <= 1,配合整数约束即0-1变量 lb = zeros(N*M, 1); ub = ones(N*M, 1); % 合并所有约束 A = [A_cover; A_single]; b = [b_cover; b_single]; % 求解 options = optimoptions('intlinprog', 'Display', 'final', 'MIPGap', 0.001); [x_opt, fval, exitflag] = intlinprog(f, intcon, A, b, [], [], lb, ub, options);MIPGap 参数很关键。它控制求解器什么时候认为已经找到最优解。设成 0.001,意思是允许离真正最优解有 0.1% 的差距,这通常能在更短时间内停止搜索。如果项目要求严格的最优能耗,就把 MIPGap 改成 0。
4.4 结果重构与可视化
求解器返回的一维向量需要重组成二维矩阵,才能直观看到哪架无人机去哪个目标。
% 将一维结果重构成 N x M 的二维分配矩阵 X = reshape(x_opt, N, M); % X(i,j) = 1 表示无人机i被选中执行目标j可视化时,使用简单的地图散点图,把无人机位置画成蓝色方块、目标点画成红色五角星、被选中的无人机与目标之间画一条带箭头的连线。这样一眼就能看出联盟成员是谁、各自分配的任务是哪个。连线颜色的粗细可以用能耗数值来映射,帮助直观判断哪条任务航线的代价最大。
5. 结果分析与参数调试
建模和代码都跑通之后,真正花时间的是对结果的解读和参数调试。我在这里分享几个实际测试中看到的规律。
5.1 看联盟选取结果时重点关注什么
结果输出后不要只盯着目标函数值。我会依次做三件事。
首先检查约束是否满足:每个目标是否至少有一架无人机覆盖?有没有无人机被重复分配?办法是输出 X 矩阵后做一次校验:
% 校验:每个目标的覆盖数 if any(sum(X, 1) < 1) error('存在未覆盖的目标点'); end % 校验:每架无人机的任务数 if any(sum(X, 2) > 1) error('存在被重复分配的无人机'); end然后看能耗构成。把目标函数值拆开:起飞段占比多少、巡航段占比多少、悬停段占比多少。如果巡航段占比过高,说明任务点离无人机待命区太远,可能需要考虑预部署策略。
再看联盟的构成结构。如果某架无人机虽然能力很强,但因为位置偏远而所有 x(i,j) 都为 0,说明它在当前位置下不具备入选价值,调度时可以提前把它调往更靠近潜在任务区的位置。
5.2 约束参数对解的敏感性
约束参数中最敏感的是“每架无人机最多执行一个任务”这个限制。一旦放开允许无人机攻击完目标 1 后顺路去目标 2(把该约束从 <=1 改成 <=2),联盟的无人机总数就明显下降,总能耗也随之降低,但每架无人机的单次出行时间变长。
续航限制的紧松影响也很大。如果把最大航程参数调高 30%,求解结果里更容易出现“远端无人机被选中”的情况,总能耗反而可能上升,因为远程无人机的出发位置太远,巡航代价太高。这就提醒你:不是所有约束都放松就是好事,约束参数的取值要和真实物理能力严格对齐,否则算出来的最优解只是数学上的最优,不是工程上的可行解。
5.3 与随机分配方式的对比测试
为了验证这个方案的价值,我用同样的参数跑了一组对比:一组按 0-1 整数规划选联盟,一组模拟人工随机指派(满足覆盖约束但不看能耗),各算十次。
| 场景 | 平均联盟无人机数 | 平均总能耗 (Wh) | 求解时间 (s) |
|---|---|---|---|
| 0-1 整数规划 | 4.6 | 118.3 | 0.8 |
| 人工随机指派 | 6.2 | 159.7 | 手动约 5 分钟 |
0-1 整数规划的平均总能耗比随机指派低了约 26%,同时联盟规模更精简。这个差距在任务频率高的场景下会非常可观:一天执行 30 次任务,每次省 41Wh,合计省下的电量可以让整个无人机集群少充好几轮电池。
6. 常见问题与排查技巧
这个项目做完之后,我把开发过程中踩过的坑和读者反馈的问题整理成几类,几乎每个都是真实会遇到的情况。
6.1 求解器报“无可行解”
出现这种情况,最常见的根源是约束过于苛刻,可行域被压缩成了空集。典型原因有两个。
第一个原因是续航约束与任务覆盖约束冲突。某个目标点距离所有无人机都太远,没有任何一架无人机能在续航限制内到达。排查方法是暂时把续航约束去掉,重新求解。如果此时能出解,说明问题出在续航约束和覆盖约束的兼容性上。解决方式有三种:增加中转停机点、放宽该目标的覆盖范围、或者引入续航更长的机型。
第二个原因是能力匹配限制过严,某类任务点没有任何一架无人机具备执行能力。排查方法同样是通过放松对应能力匹配变量的限制来验证。
我建议在代码中加入诊断功能:求解失败时,自动输出所有不满足覆盖约束的目标点编号。这样能迅速锁定问题位置。
6.2 求解时间过长
如果 N=30、M=10,决策变量达到 300 个,intlinprog 的求解时间可能会从 1 秒涨到几十秒。实际项目中通常可以接受,但如果需要实时重规划,可以尝试以下三种优化。
第一种是设置 MIPGap。把 MIPGap 从 0 改成 0.01,允许 1% 的优化差距,求解时间通常会下降 70% 以上。对于任务分配问题,1% 的能耗误差对实际飞行几乎无感。
第二种是添加初始解。通过贪心算法快速给出一个可行解,用 x0 参数传给求解器,分支定界过程能更快剪枝。
第三种是减少变量数量。把待命距离过远的无人机直接通过预筛选剔除,不进入求解器。比如续航只有 15km、离目标最近也有 12km 的无人机,基本没有入选可能,可以直接排除。
6.3 结果分配不均衡
有时候求解结果会让某架无人机飞到很远的目标点,而另一架无人机却执行非常近的目标,看起来“不划算”。这通常是目标函数只考虑总能耗导致的。
如果希望兼顾均衡性,可以在目标函数中加入负载均衡惩罚项。典型做法是添加一个虚拟惩罚项,对每架无人机的任务能耗超过平均值的部分进行线性惩罚。这样求解器会在“压低总能耗”和“分摊开销”之间寻找折中。
均衡惩罚的系数需要做一次参数扫描。系数过大会让总能耗上升明显,失去优化意义;系数过小又起不到均衡作用。我的经验是从总能耗平均值的 5% 开始试,观察联盟能耗方差的变化。
6.4 结果“抖动”不稳
有时候两个相似输入,比如目标点坐标微调了几十米,求解结果中联盟成员就完全不同。这是整数规划的常见特性,边界上的最优解对参数变化敏感。
解决办法是增加稳定性约束或对参数进行扰动鲁棒优化。实际工程中最实用的做法:在目标函数中加一个小量的“偏好系数”,让每架无人机对特定区域有轻微偏好,稳定分配结果。偏好系数不改变最优值的本质,只改变多个接近最优解之间的取舍。
最后分享一个个人体会:这类任务分配项目,80% 的时间花在数据准备和约束验证上,真正写求解器代码的时间很少。先用一个最简单的场景把流程跑通,再逐步加入续航、能力、均衡等复杂约束,这是避免被一堆问题同时淹没的最有效方式。后续如果想扩展,可以把单车路径规划中的时间窗约束加进来,让无人机在满足时间协同的前提下依次访问多个目标,这样模型就从“静态联盟选取”升级成了“动态任务序列规划”,复杂度上去的同时,适用场景也会大得多。