装配线(工作站)问题的两种解法

本文探讨了使用动态规划法解决装配线问题的方法,并对比了穷举法的解决方案。介绍了如何定义最优解及其结构,利用递归公式进行求解,并提供了具体的算法实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

上学的时候有一道题目一直困扰着我,那就是厨师摆盘子问题,问题的描述是这样的:

厨师的所有盘子都放在一个架子上,每天工作结束他都要将盘子按照从小到大的顺序排好,问题是架子不太稳,如果一次拿出一个或几个盘子,架子可能要倒掉,所以他必须只能从一边翻动盘子,由于他只有两只手,所以只能用两只手将拿起的盘子一起翻转。问题是当给出一个杂乱的盘子序列时,如何以最小的翻转次数将其排序。

当时用穷举的方法解决了这个问题,但是看到很多资料都说此类最优解的问题还可以用动态规划法解决,但是我一直没有找到分解最优子问题的方法,所以只好放弃了。前一段时间看《编程之美》,其中“一摞烙饼问题”那一章提到的一摞烙饼问题其实和厨师摆盘子是一回事儿,文中提到了可用动态规划法解决,但是没有给出解法,这又勾起了我的念头,我又翻出了《算法导论》,并动用了Google,百度和雅虎,但是仍然没有找到用动态规划法的解法,倒是看到一些关于这个问题无法用动态规划法解决的讨论。虽然没有找到答案,但是把以前的老代码翻出来了,看到了当时解决“装配线(工作站)问题”的代码,当时用动态规划法和穷举法两种方法解决了这个问题,现在就晒晒代码,先看看“装配线(工作站)问题”的描述:

Colonel汽车公司在有两条装配线的工厂内生产汽车,一个汽车底盘在进入每一条装配线后,在每个装配站会在汽车底盘上安装不同的部件,最后完成的汽车从装配线的末端离开。如图1.所示:


图1. 装配线示意图

每一条装配线上有n个装配站,编号为j=1,2,...,n,将装配线i(i为1或2)的第j个装配站表示为S(i,j)。装配线1的第j个站S(1,j)和装配线2的第j个站S(2,j)执行相同的功能。然而这些装配站是在不同的时间建造的,并且采用了不同的技术,因此,每个站上完成装配所需要的时间也不相同,即使是在两条装配线相同位置的装配站也是这样。把每个装配站上所需要的装配时间记为a(i,j),并且,底盘进入装配线i需要的时间为e(i),离开装配线i需要的时间是x(i)。正常情况下,底盘从一条装配线的上一个站移到下一个站所花费的时间可以忽略,但是偶尔也会将未完成的底盘从一条装配线的一个站移到另一条装配线的下一站,比如遇到紧急订单的时候。假设将已经通过装配站S(i,j)的底盘从装配线i移走所花费的时间为t(i,j),现在的问题是要确定在装配线1内选择哪些站以及在装配线2内选择哪些站,以使汽车通过工厂的总时间最小,如图2.所示,最快的时间是选择装配线1的1,3和6装配站以及装配线2的2,4和5装配站。


图2. 装配线的一个最快时间路线

按照《算法导论》书中的讨论,我首先给出了使用动态规划法的算法。动态规划的第一步是描述最优解的结构特征,也就是要先定义什么是最优解。对于装配线问题,通过S(i,j)的最优解就是通过S(i,j)的前一站的最优解加上用最短的时间通过S(i,j)站。对于有两条装配线的Colonel汽车公司来说,通过装配站S(1,j)的最快路线只能是以下二者之一:
1、通过装配站S(1,j-1)的最快路线,然后直接通过装配线S(1,j);
2、通过装配站S(2,j-1)的最快路线,然后从装配线2移到装配线1上,再通过装配线S(1,j);

同理类推,对于装配站S(2,j)的最快路线与之对称相反。由此可以看出,对于S(1,j)站的子问题就是通过S(1,j-1)和S(2,j-1)的最快路线,建立最优解的结构后,就可以执行动态规划的第二步,用子问题的最优解递归定义一个问题的最优解,根据前面的分析,很容易得到关于第j个站的最优解递归公式:




剩下的就是写算法实现问题了,下面就给出动态规划法的算法,首先定义一个数据结构,这个数据结构是为了方便中间数据存储和减少函数参数而定义的,并不是动态规划方法的一部分:

#define MAX_LINE_STATION 20

typedef struct tagFixLinePara
{
int a[2][MAX_LINE_STATION];
int t[2][MAX_LINE_STATION];
int e[2];
int x[2];
int l[2][MAX_LINE_STATION];
int ls;
int fs;
}FixLinePara;


int FindFastStationSequence(FixLinePara *para, int n)
{
int f[2][MAX_LINE_STATION];
if((para == NULL) || (n <= 0))
{
return -1;
}
f[0][0] = para->a[0][0] + para->e[0];
f[1][0] = para->a[1][0] + para->e[1];

for(int j = 1; j < n; j++)
{
if((f[0][j - 1] + para->a[0][j]) <= (f[1][j - 1] + para->t[1][j] + para->a[0][j]))
{
f[0][j] = f[0][j - 1] + para->a[0][j];
para->l[0][j] = 1;
}
else
{
f[0][j] = f[1][j - 1] + para->t[1][j] + para->a[0][j];
para->l[0][j] = 2;
}
if((f[1][j - 1] + para->a[1][j]) <= (f[0][j - 1] + para->t[0][j] + para->a[1][j]))
{
f[1][j] = f[1][j - 1] + para->a[1][j];
para->l[1][j] = 2;
}
else
{
f[1][j] = f[0][j - 1] + para->t[0][j] + para->a[1][j];
para->l[1][j] = 1;
}
}

if(f[0][n - 1] + para->x[0] <= f[1][n - 1] + para->x[1])
{
para->fs = f[0][n - 1] + para->x[0];
para->ls = 1;
}
else
{
para->fs = f[1][n - 1] + para->x[1];
para->ls = 2;
}

return 0;
}

求解完成后para->ls存放最有使用的装配站所属的装配线编号,para->l存放底盘在两条装配线上的转移记录,结合para->ls可以向前递推出底盘的装配顺序。para->fs保存最短时间。下面是打印结果的函数:
void PrintStations(FixLinePara *para, int n)
{
int i = para->ls;

printf("Line %d , Station %d/n", i, n);
for(int j = n - 1; j > 0; j--)
{
i = para->l[i - 1][j];
printf("Line %d , Station %d/n", i, j);
}
}

这个打印结果刚好和实际装配顺序是反的,因为它是通过para->ls向前递推得出的结果。《算法导论》提示可以通过递归的方法得出正序列的结果,但是是作为练习题提出的,没有给出算法,其实也不难,这里就给出一个:

void PrintNextStations(FixLinePara *para, int n, int line)
{
if(n == 0)
{
return;
}
int i = para->l[line - 1][n];
PrintNextStations(para, n - 1, i);
printf("Line %d , Station %d/n", i, n);
}

void PrintStations2(FixLinePara *para, int n)
{
int i = para->ls;

PrintNextStations(para, n - 1, i);
printf("Line %d , Station %d/n", i, n);
}


《算法导论》提到了可以通过穷举(通常使用递归)方法解决这个问题,这里也一并给出使用穷举搜索的方法,在装配站个数n比较小的情况下还是可以可以用的。同样,先定义一个数据结构:

typedef struct tagFixLineEnumeratePara
{
int a[2][MAX_LINE_STATION];
int t[2][MAX_LINE_STATION];
int e[2];
int x[2];
int l[MAX_LINE_STATION];
int fs;
int fl[MAX_LINE_STATION];
int ffs;
}FixLineEnumeratePara;

para->l存放搜索过程中的一个中间结果的装配站序列,para->fs是中间结果的装配时间,para->fl是最终的最优解的装配站序列,para->ffs就是最优解的装配时间。

int FindEnumerateStationSequence(FixLineEnumeratePara *para, int line, int station, int n)
{
if(station >= n) //到头了,整理一次结果
{
para->fs += para->a[line][station - 1];
para->fs += para->x[line];
para->l[station - 1] = line + 1;
if(para->fs < para->ffs)
{
para->ffs = para->fs;
memmove(para->fl, para->l, n * sizeof(int));
}
return 0;
}

int curCost = para->fs + para->a[line][station - 1];
para->l[station - 1] = line + 1;

station++;

para->fs = curCost;
FindEnumerateStationSequence(para, line, station, n);

para->fs = curCost;
int nextline = (line + 1) % 2;
para->fs += para->t[line][station - 1];
FindEnumerateStationSequence(para, nextline, station, n);

return 0;
}


int FindFastStationSequenceEnumerate(FixLineEnumeratePara *para, int n)
{
para->ffs = 0x0FFFFFFF;
para->fs = para->e[0];
FindEnumerateStationSequence(para, 0, 1, n);
para->fs = para->e[1];
FindEnumerateStationSequence(para, 1, 1, n);

return 0;
}

搜索是按照装配站的顺序递归的,所以结果就是正序列,打印结果的函数就很简单了:

void PrintStations3(FixLineEnumeratePara *para, int n)
{
for(int i = 0; i < n; i++)
{
printf("Line %d , Station %d/n", para->fl[i], i + 1);
}
}

以上就是两种方法的解,晒完收工。

装配线调度问题的二分分治算法。设计实现装配线调度问题的二分分治算法(即将n个站从中间位置分为两个n/2个站的子问题),分析你所设计算法的计算复杂度,实现该算法及讲义上的动态规划算法,产生测数据对比两种算法的计算时间。请使用C语言代码实现,给用户提供3个选项,第1个选项随机生成数据,并用二分分治发和动态规划解决装配线问题,并向用户输出说明二分分治法每一步都发生了什么,第2个选项随机生成测试这两种算法的测试数据,给出测试结果,第3个选项退出程序。动态规划算法实现部分使用以下代码// 定义结构体来存储各个站点的时间 typedef struct { int a[2][N]; // 每个站点的处理时间 int t[2][N-1]; // 转换时间 int e[2]; // 进入时间 int x[2]; // 退出时间 } AssemblyLine; // 动态规划求解装配线问题 void assemblyLineScheduling(AssemblyLine line) { int f1[N], f2[N]; // 记录到达每个站点的最短时间 int l1[N], l2[N]; // 记录到达每个站点时来自哪条装配线 // 初始化第一个站点的时间 f1[0] = line.e[0] + line.a[0][0]; f2[0] = line.e[1] + line.a[1][0]; // 动态规划计算每个站点的最短时间 for (int j = 1; j < N; j++) { // 计算第一条装配线当前站点的最短时间 if (f1[j-1] + line.a[0][j] <= f2[j-1] + line.t[1][j-1] + line.a[0][j]) { f1[j] = f1[j-1] + line.a[0][j]; l1[j] = 1; } else { f1[j] = f2[j-1] + line.t[1][j-1] + line.a[0][j]; l1[j] = 2; } // 计算第二条装配线当前站点的最短时间 if (f2[j-1] + line.a[1][j] <= f1[j-1] + line.t[0][j-1] + line.a[1][j]) { f2[j] = f2[j-1] + line.a[1][j]; l2[j] = 2; } else { f2[j] = f1[j-1] + line.t[0][j-1] + line.a[1][j]; l2[j] = 1; } } // 计算总时间并确定最优路径 int total1 = f1[N-1] + line.x[0]; int total2 = f2[N-1] + line.x[1]; int optimal = total1 < total2 ? total1 : total2; int lineChoice = total1 < total2 ? 1 : 2; // 输出结果 printf("到达每个站点的最短时间:\n"); printf("f1: "); for (int j = 0; j < N; j++) { printf("%d ", f1[j]); } printf("\n"); printf("f2: "); for (int j = 0; j < N; j++) { printf("%d ", f2[j]); } printf("\n"); printf("到达每个站点时来自的装配线:\n"); printf("l1: "); for (int j = 0; j < N; j++) { printf("%d ", l1[j]); } printf("\n"); printf("l2: "); for (int j = 0; j < N; j++) { printf("%d ", l2[j]); } printf("\n"); printf("总时间:line
最新发布
03-19
#include <stdio.h> #include <stdlib.h> #include <time.h> // 定义最大工位数 #define MAX_STATIONS 100 // 装配线结构体,包含两条装配线的各个工位时间、转移时间、进出时间 typedef struct { int a[2][MAX_STATIONS]; // 两条装配线的工位时间 int t[2][MAX_STATIONS]; // 转移时间 int e[2]; // 进场时间 int x[2]; // 出场时间 int n; // 工位数量 } AssemblyLine; // 打印菜单 void printMenu() { printf("\n===== 装配线调度问题算法比较 =====\n"); printf("1. 查看二分分治动态规划法的具体实现过程以及结果\n"); printf("2. 随机产生测试数据对比两种算法的计算时间\n"); printf("3. 退出\n"); printf("请选择选项: "); } // 初始化装配线数据(用于示例) void initExampleAssemblyLine(AssemblyLine *al) { al->n = 6; al->e[0] = 2; al->e[1] = 4; al->x[0] = 3; al->x[1] = 2; // 装配线1的工位时间 al->a[0][0] = 7; al->a[0][1] = 9; al->a[0][2] = 3; al->a[0][3] = 4; al->a[0][4] = 8; al->a[0][5] = 4; // 装配线2的工位时间 al->a[1][0] = 8; al->a[1][1] = 5; al->a[1][2] = 6; al->a[1][3] = 4; al->a[1][4] = 5; al->a[1][5] = 7; // 转移时间 al->t[0][0] = 2; al->t[0][1] = 2; al->t[0][2] = 3; al->t[0][3] = 1; al->t[0][4] = 3; al->t[0][5] = 4; al->t[1][0] = 4; al->t[1][1] = 2; al->t[1][2] = 1; al->t[1][3] = 2; al->t[1][4] = 2; al->t[1][5] = 1; } // 打印装配线数据 void printAssemblyLine(const AssemblyLine *al) { printf("\n装配线数据:\n"); printf("工位数量: %d\n", al->n); printf("进场时间: line1=%d, line2=%d\n", al->e[0], al->e[1]); printf("出场时间: line1=%d, line2=%d\n", al->x[0], al->x[1]); printf("\n装配线1的工位时间: "); for (int j = 0; j < al->n; j++) { printf("%d ", al->a[0][j]); } printf("\n装配线2的工位时间: "); for (int j = 0; j < al->n; j++) { printf("%d ", al->a[1][j]); } printf("\n转移时间 (line1->line2): "); for (int j = 0; j < al->n - 1; j++) { printf("%d ", al->t[0][j]); } printf("\n转移时间 (line2->line1): "); for (int j = 0; j < al->n - 1; j++) { printf("%d ", al->t[1][j]); } printf("\n"); } // 二分分治算法 void divideAndConquer(AssemblyL
03-19
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值