目录
[1. 状态总数](#1. 状态总数)
[2. 每个状态的转移次数](#2. 每个状态的转移次数)
[3. 总操作数](#3. 总操作数)
[4. 随规模增长的趋势](#4. 随规模增长的趋势)


前言
还记得22年的华为杯数模研赛吗,多维邻接矩阵无法解开。在此做一个总结吧。

2022年"华为杯"中国研究生数学建模竞赛C题,核心是设计一个**涂装-总装缓存调序区(PBS)的调度优化方案**
汽车制造厂主要由焊装车间、涂装车间、总装车间构成。每个车间,由于采用了混流生产方式,有不同的生产偏好,对不同类型的车身有一定的配置比例要求。由于车间偏好不同,前一车间的出车序列可能对下一车间的生产效率等方面产生不利影响。特别是涂装车间与总装车间车身序列差异较大,为此需要在这两个车间之间建立一个具有调序功能的缓存区,通过将涂装车间的序列调整为合适的总装车间序列,提高整条生产线的生产效率。此缓存区即汽车制造涂装-总装缓存调序区(Painted Body Store, PBS)。PBS 区域主要由涂装-PBS 出车口、 PBS-总装接车口、进车道 1 至进车道 6、返回道、接车横移机及送车横移机组成。给定涂装-PBS 出车口处的车身序列,要求在给定的约束条件下,优化调度方案,使得经 PBS 缓冲区重排序后的出车序列,在给定的优化目标评价尺度下,能够有较好的表现。相关概念具体描述如下:
- 车身:本题目给出三项车身属性------车型、动力、驱动类型,但结合问题约束及优化目标,仅需考虑动力类型及驱动类型两项。
- 进、出车序列:进车序列即涂装-PBS 出车口出的车辆序列,为已知条件,车身与进车顺序一一对应,因此可以将车身在进车序列中的排序作为其编号;出车序列即经进车序列重排序后的序列,应尽可能满足总装车间的需求。
- 约束条件:主要包括原文件给定的 12 条 PBS 约束,其中包含接车横移机及送车横移机的接、送逻辑,以及车身在进车道及行车道的行进逻辑等。
- 优化目标:主要包括四个方面,出车序列动力类型及驱动类型尽量满足要求、返回道使用次数尽量少, PBS 调度总时长尽可能短,权重依次降低。
- 时间设定:由原文件给出,包括给出的车身在同一车道不同车位间的转移时间,接车、送车横移机在不同车道处接送所用时间等。由以上时间数据,结合文中给出的距离数据,我们可以构造位置矩阵,以描述各移动单位的位置关系,便于得到所需的不同车身位置与时间的关系矩阵。
原文链接:【数模研赛】"华为杯"第十九届中国研究生数学建模竞赛C题分享------(二)问题重述_2022华为杯c题问题重述-CSDN博客
数学建模
数学建模过程(略),本文不关注文档写作。
.......
实验设计
我们自行补全了一个具体的问题:在一个有向路网中,一辆小车从仓库出发,需要遍历所有客户点和充电站(共5个中间节点)各一次,最后返回仓库。小车有电池容量限制,每段路消耗与距离相等的电量,到达充电站可立即充满电。目标是找到满足电量约束的最短行驶路线(即带充电约束的TSP问题)。我们用C语言实现状态压缩动态规划求解,并生成SVG图像显示路线。
实验数据
typedef struct Vertex
Vertex0:Axis((0,0)), Discription:"仓库(起点/终点)"
Vertex1:Axis((10,0)),Discription:"客户A"
Vertex2:Axis((5,10)),Discription:"客户B"
Vertex3:Axis((15,5)),Discription:"客户C"
Vertex4:Axis((3,3)), Discription:"充电站S1"
Vertex5:Axis((12,8)),Discription:"充电站S2"
//如需增加节点数量(>15),状态爆炸,可改用启发式算法(如遗传算法)
Adjacency Matrix(DN,unit: km)

电池参数
电池容量:100 (kWh)
每公里耗电:1 kWh/km
充电站:节点4、节点5(到达后电池立刻充满至100)
初始状态
从仓库(0)出发,电量100
必须遍历中间节点 {1,2,3,4,5} 各一次,最后返回仓库
算法设计思想
核心算法:状态压缩DP + 电量维度
定义 `dpmasklastbatt` 表示已访问节点集合为 `mask`,当前位于节点 `last`,剩余电量为 `batt` 时的最短行驶距离。其中 `mask` 是一个5位二进制数(节点1~5),`last` 取值范围1~5,`batt` 0~100。
状态转移:
从 `(mask, last, batt)` 出发,尝试前往未访问节点 `next`。
消耗 `cost = distlastnext`,需满足 `batt >= cost`。
到达 `next` 后新电量为 `new_batt = (is_charger(next) ? 100 : batt cost)`。
更新 `dpmask\|bit(next)nextnew_batt = min(自身, dpmasklastbatt + cost)`。
初始状态:从仓库0直接到第一个节点 `first`,消耗 `cost = dist0first`,要求初始电量100≥cost,新电量根据 `first` 是否为充电站决定。
最终答案:遍历所有 `last` 和 `batt`,若 `dp全1lastbatt` 可达且 `batt >= distlast0`,则总距离为 `dp + distlast0`,取最小值。
路径回溯:记录前驱 `prev_masklastbatt`, `prev_lastlastbatt`, `prev_battlastbatt` 以还原完整序列。
程序输出
控制台打印最优路径、每段路程、电量变化、总距离。
生成 `path.svg` 矢量图,包含节点、标号、路线箭头,可用浏览器打开。
C语言实现,及关键注释
cpp
include <stdio.h>
include <stdlib.h>
include <string.h>
include <limits.h>
include <math.h>
define N_NODES 6 // 0仓库,1~5客户/充电站
define MID_START 1 // 第一个中间节点编号
define MID_END 5 // 最后一个中间节点编号
define MID_COUNT 5 // 中间节点个数 (1..5)
define BATTERY_MAX 100
define INF 1000000
// 节点坐标 (用于绘图)
const int coord_x[N_NODES] = {0, 10, 5, 15, 3, 12};
const int coord_y[N_NODES] = {0, 0, 10, 5, 3, 8};
// 有向距离矩阵
int dist[N_NODES][N_NODES] = {
{0, 10, 12, 16, 5, 14},
{10, 0, 8, 11, 7, 13},
{12, 7, 0, 9, 8, 6},
{15, 10, 9, 0, 13, 6},
{5, 6, 8, 12, 0, 4},
{13, 12, 7, 5, 4, 0}
};
// 判断节点是否为充电站
int is_charger(int node) {
return (node == 4 || node == 5);
}
// 将中间节点编号(1..5)转换为位掩码 (bit 0对应节点1)
inline int bit(int node) {
return 1 << (node MID_START);
}
// 全局DP数组: dp[mask][last][batt]
// 由于状态数: 2^5 5 101 = 325101 = 16160,用short存距离(最大距离<1000)
short dp[1<<MID_COUNT][MID_COUNT+1][BATTERY_MAX+1];
// 前驱记录
short prev_mask[1<<MID_COUNT][MID_COUNT+1][BATTERY_MAX+1];
char prev_last[1<<MID_COUNT][MID_COUNT+1][BATTERY_MAX+1];
char prev_batt[1<<MID_COUNT][MID_COUNT+1][BATTERY_MAX+1];
// 初始化dp为INF
void init_dp() {
int m, l, b;
for (m = 0; m < (1<<MID_COUNT); m++)
for (l = MID_START; l <= MID_END; l++)
for (b = 0; b <= BATTERY_MAX; b++)
dp[m][l][b] = INF;
}
int main() {
init_dp();
// 初始状态: 从仓库出发到第一个节点
for (int first = MID_START; first <= MID_END; first++) {
int cost = dist[0][first];
if (cost <= BATTERY_MAX) {
int batt = is_charger(first) ? BATTERY_MAX : (BATTERY_MAX cost);
int mask = bit(first);
if (cost < dp[mask][first][batt]) {
dp[mask][first][batt] = cost;
// 前驱: mask=0, last=1, batt=BATTERY_MAX (仓库)
prev_mask[mask][first][batt] = 0;
prev_last[mask][first][batt] = 1; // 表示来自仓库
prev_batt[mask][first][batt] = BATTERY_MAX;
}
}
}
// DP 状态转移
int full_mask = (1<<MID_COUNT) 1;
for (int mask = 1; mask <= full_mask; mask++) {
for (int last = MID_START; last <= MID_END; last++) {
if (!(mask & bit(last))) continue; // last必须在mask中
for (int batt = 0; batt <= BATTERY_MAX; batt++) {
if (dp[mask][last][batt] == INF) continue;
// 尝试去下一个未访问节点
for (int nxt = MID_START; nxt <= MID_END; nxt++) {
if (mask & bit(nxt)) continue;
int cost = dist[last][nxt];
if (batt < cost) continue; // 电量不足
int new_batt = is_charger(nxt) ? BATTERY_MAX : (batt cost);
int new_mask = mask | bit(nxt);
int new_dist = dp[mask][last][batt] + cost;
if (new_dist < dp[new_mask][nxt][new_batt]) {
dp[new_mask][nxt][new_batt] = new_dist;
prev_mask[new_mask][nxt][new_batt] = mask;
prev_last[new_mask][nxt][new_batt] = last;
prev_batt[new_mask][nxt][new_batt] = batt;
}
}
}
}
}
//剩余此处省略,评论区获取
结果输出


算法时间复杂度分析
下面分析所给C代码(带充电约束的TSP,状态压缩DP)的时间复杂度,代码主要耗时在DP状态转移部分。
1. 状态总数
中间节点数量:`MID_COUNT = n = 5`
访问状态掩码 `mask`:\(2^n = 32\) 种
当前所在节点 `last`:\(n = 5\) 种(仅中间节点,节点1~5)
当前剩余电量 `batt`:\(B_{\max} + 1 = 101\) 种(0~100)
因此总状态数:
\ S = 2\^n \\times n \\times (B_{\\max}+1) = 32 \\times 5 \\times 101 = 16160 \\
2. 每个状态的转移次数
对于每个状态 `(mask, last, batt)`,需要枚举所有未被访问的下一个节点 `nxt`。最坏情况下,未访问节点数为 \(n\)(实际上随着 `mask` 增大而减少)。平均约 \(n/2\),但上界取 \(n=5\)。
每次转移的操作(计算新电量、新距离、比较更新)均为 \(O(1)\)。
3. 总操作数
\T = S \\times n = 16160 \\times 5 = 80800\\
加上初始化的 \(S\) 次赋值(`init_dp` 中循环 \(32\times5\times101\) 次):
\T_{\\text{init}} = 16160\\
总时间复杂度:
\O(2\^n \\times n\^2 \\times B_{\\max})\\
代入 \(n=5,\ B_{\max}=100\) 得约 \(8\times 10^4\) 次操作,常数极小,可视为瞬时完成。
4. 随规模增长的趋势
若 \(n=10\):\(2^{10}=1024\),状态数 \(1024\times10\times101\approx 1.03\times10^6\),转移次数约 \(5.15\times10^6\),仍很快。
若 \(n=15\):\(2^{15}=32768\),状态数 \(32768\times15\times101\approx 4.96\times10^7\),转移约 \(2.48\times10^8\),在C语言中可能需几秒。
若 \(n=20\):状态数将超过 \(2^{20}\times20\times101\approx 2.12\times10^9\),内存和时间均不可接受。
因此该算法仅适用于节点数 ≤ 15的小规模问题。






