流水作业调度(dp)

流水作业调度优化与算法实现
本文探讨了在由两台机器M1和M2组成的流水线上,如何优化n个作业的加工顺序,以最小化从开始到结束的总加工时间。通过给出的代码示例,展示了具体的调度策略,输出的作业顺序表明了这种策略如何降低整体完成时间。

题目:

n个作业{1,2,…,n}要在由2台机器M1和M2组成的流水线上完成加工。每个作业加工的顺序都是先在M1上加工,然后在M2上加工。M1和M2加工作业i所需的时间分别为ai和bi。

流水作业调度问题要求确定这n个作业的最优加工顺序,使得从第一个作业在机器M1上开始加工,到最后一个作业在机器M2上加工完成所需的时间最少。


代码:

#include <bits/stdc++.h>

using namespace std;
const int MAXN=1010;
struct node
{
    int min_cost,index;
    bool flag;
};
node d[MAXN];
bool cmp(node a,node b)
{
    return a.min_cost<b.min_cost;
}
int main()
{
    int n,a[MAXN],b[MAXN],bq[MAXN];
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i]>>b[i];
        d[i].min_cost=min(a[i],b[i]);
        d[i].index=i;
        d[i].flag=a[i]<b[i];
    }
    sort(d+1,d+1+n,cmp);
    int head=1,tail=n;
    for(int i=1;i<=n;i++)
        if(d[i].flag) bq[head++]=d[i].index;
        else bq[tail--]=d[i].index;
    int k,t;
    k=a[bq[1]];
    t=k+b[bq[1]];
    for(int i=2;i<=n;i++)
    {
        k+=a[bq[i]];
        t=k<t?(t+b[bq[i]]):(k+b[bq[i]]);
    }
    for(int i=1;i<=n;i++) cout<<bq[i]<<" ";
    cout<<endl<<t<<endl;
    return 0;
}

测试数据:

6
2 5
7 3
6 2
4 7
6 9
8 2
输出:
1 4 5 2 6 3
35

<think>我们正在讨论流水作业调度问题的动态规划解法。根据Johnson法则,最优调度可以通过将作业分为两类并排序得到。但用户询问的是动态规划的具体实现方法。动态规划方法通常涉及状态定义和状态转移方程。 引用[2]提到:设π={π(1)(2),...,π(n)}是一个最优调度,其加工时间为aπ(1)+T’,其中T’是在机器M2的等待时间为bπ(1)时安排剩余作业的最小时间。这提示我们可以用递归或动态规划来求解。 动态规划的状态定义: 设S为还未安排的作业集合。我们用f(S, t)表示在机器M2还需要等待时间t(即机器M2在t时间之后才可用)之后才开始安排集合S中的作业时,完成剩余作业所需的最少时间(注意,这里t是指机器M2在完成之前作业后,需要等待t时间才能开始下一作业,但实际上t可能是由于M1加工时间导致的等待)。 然而,直接这样定义状态集合S是指数级的。因此,我们需要寻找更高效的方法。实际上,流水作业调度问题有一个著名的Johnson法则,它给出了一个多项式时间的排序方法,但动态规划方法在作业数较小时也是可行的。 根据经典思路,我们可以用状态压缩动态规划(当n较小时)。状态用一个二进制数表示哪些作业已经安排,同时记录机器M2的可用时间(或者机器M1的完成时间,但通常我们关注M2的等待时间)。 状态定义: dp[mask][t]:已经安排了mask表示的作业,且当前机器M2还需要等待t时间(从当前时间算起,t时间后M2才可用)的情况下,完成剩余作业所需的最少时间(注意:这里t是从当前时刻开始,机器M2需要等待的时间,也就是机器M2在完成上一个作业后,还需要等待t才能开始下一个作业的第一个操作?实际上,这里t的含义是机器M2的闲置时间?但更准确地说,在安排下一个作业时,机器M1的开始时间就是当前时间,而机器M2的开始时间则是当前时间+t?) 然而,另一个常见定义是:设机器M1在完成已安排作业后已经花费的总时间为T1,机器M2完成已安排作业后已经花费的总时间为T2。但这样状态是二维的且范围很大。 实际上,我们可以注意到:在最优调度中,机器M1是连续工作的,因此总时间至少是所有ai之和,并且机器M2的开始时间依赖于机器M1的结束时间和上一个作业在M2上的结束时间。 更高效的状态定义(参考经典文献): 我们设dp[mask]表示已经安排mask对应的作业后,机器M2完成这些作业的最早时间。同时,我们还需要记录机器M1完成这些作业的时间吗?实际上,机器M1的时间就是mask中所有作业的ai之和。因为M1是连续工作的。 设sumA[mask]为mask中所有作业的ai之和。 那么,在状态mask下,机器M1完成的时间就是sumA[mask](因为M1是连续加工的)。 现在考虑向mask中加入一个新的作业j,那么: 作业j在M1上加工的开始时间是sumA[mask](即当前mask中作业在M1上加工结束的时刻),加工时间为aj,所以结束时间为sumA[mask] + aj。 作业j在M2上加工的开始时间取决于两个时间:机器M2在mask状态下的结束时间(即dp[mask])和作业j在M1上的结束时间(sumA[mask]+aj),取两者的最大值(因为M2必须等M1加工完并且M2空闲)。所以作业j在M2上的开始时间为max(dp[mask], sumA[mask]+aj)?不对,实际上作业j在M2上加工的开始时间应该是max(机器M2的可用时间(即dp[mask]),作业j在M1上的结束时间(sumA[mask]+aj))。然后加上bj,得到作业j在M2上的结束时间。 因此,转移方程为: dp[mask | (1<<j)] = min_{j not in mask} { max(dp[mask], sumA[mask] + aj) + bj } 但是,这个方程是错误的。因为状态dp[mask]表示安排完mask中的作业后,机器M2的结束时间。那么当我们加入新的作业j时: 机器M1在加工完mask中的作业后,继续加工j,所以j在M1上的结束时间是sumA[mask]+aj(因为sumA[mask]是mask中作业的ai和,加上aj就是j在M1上的结束时间)。 而机器M2在加工完mask中的作业后,其结束时间是dp[mask]。那么加工j时,M2必须等待两个条件满足:一是j在M1上已经完成(时间点为sumA[mask]+aj),二是M2已经空闲(时间点为dp[mask])。所以j在M2上开始的时间是max(dp[mask], sumA[mask]+aj),然后在M2上加工需要bj,所以结束时间是max(dp[mask], sumA[mask]+aj) + bj。 因此,状态转移方程为: dp[mask | (1<<j)] = min { max(dp[mask], sumA[mask] + aj) + bj } (对于所有不在mask中的j) 初始状态:dp[0] = 0, sumA[0]=0。 最终答案:dp[(1<<n)-1] 然而,这个动态规划的状态数是2^n,适用于n比较小(比如n<=20)的情况。 但是,上述动态规划并没有考虑作业的顺序,而是任意顺序加入。为什么这样是正确的?因为加入作业的顺序就是调度的顺序,我们通过状态mask枚举了所有子集,然后对每个子集枚举新加入的作业,实际上就枚举了所有顺序。 因此,动态规划的具体步骤: 1. 预处理每个mask对应的ai之和,可以用循环或递推。 2. 初始化dp[0] = 0。 3. 从小到大枚举mask(从0到(1<<n)-1),对于每个mask,枚举每个不在mask中的作业j,计算: time = max(dp[mask], sumA[mask] + a[j]) + b[j] 然后更新dp[mask|(1<<j)] = min(dp[mask|(1<<j)], time) 4. 最终答案就是dp[(1<<n)-1] 注意:这个动态规划的时间复杂度是O(n*2^n),空间复杂度O(2^n)。 然而,引用[1]中提到了Johnson法则,它可以在O(n log n)时间内解决问题。Johnson法则的步骤: 1. 将作业分成两类: N1 = {i | a_i < b_i} N2 = {i | a_i >= b_i} 2. 对N1中的作业按a_i非递减排序,对N2中的作业按b_i非递增排序。 3. 最优调度顺序是先N1后N2。 因此,如果n比较大,应该使用Johnson法则。但用户明确要求动态规划实现,所以我们可以提供状态压缩动态规划。 下面我们给出动态规划实现的C语言代码框架(假设n不超过20): */ </think>### 动态规划实现流水作业调度的具体方法 流水作业调度问题可通过动态规划解决,核心是 **Johnson法则** 和 **状态转移方程**。以下是具体实现步骤: #### 关键概念 1. **问题定义** $n$个作业在机器$M1$(加工时间$a_i$)和$M2$(加工时间$b_i$)上加工,要求确定最优加工顺序,使总完成时间最小。 2. **Johnson法则**(排序基础)[^1] - 将作业分为两类: $$ \begin{cases} N_1 = \{i \mid a_i < b_i\} \\ N_2 = \{i \mid a_i \geq b_i\} \end{cases} $$ - $N_1$按$a_i$升序排序,$N_2$按$b_i$降序排序 - 最优调度顺序为:$N_1$的作业在前,$N_2$的作业在后 3. **动态规划状态定义**[^2] 设状态$dp[S][t]$表示: - $S$:已调度作业集合(二进制状态压缩) - $t$:机器$M2$的**空闲起始时间**(即$M2$结束当前作业的时刻) - 值:完成剩余作业的最短时间 #### 动态规划步骤 1. **状态转移方程** $$ dp[S][t] = \min_{j \notin S} \left\{ \max\left(t, \sum_{i \in S} a_i + a_j\right) + b_j + dp[S \cup \{j\}][\text{next}_t] \right\} $$ 其中: - $\sum_{i \in S} a_i$:$M1$累计加工时间 - $\max(t, \sum_{i \in S} a_i + a_j)$:作业$j$在$M2$的**实际开始时间**(需等$M1$完成且$M2$空闲) - $\text{next}_t = \max(t, \sum_{i \in S} a_i + a_j) + b_j$:$M2$的新空闲时间 2. **初始化** - $dp[0][0] = 0$(无作业时时间为0) - 其他状态初始化为无穷大 3. **计算顺序** 按作业数量递增顺序计算: ```python for S in all_subsets: # 遍历所有作业子集 for t in possible_M2_time: for j not in S: M1_time = sum(a_i for i in S) + a_j start_on_M2 = max(t, M1_time) new_t = start_on_M2 + b_j new_time = new_t + dp[S∪{j}][new_t] dp[S][t] = min(dp[S][t], new_time) ``` 4. **输出结果** $dp[\text{full set}][0]$即为最小总完成时间 #### 示例说明 假设3个作业:$J1(a=3,b=2), J2(a=1,b=4), J3(a=5,b=2)$ 1. 按Johnson法则排序:$N_1=\{J2\}, N_2=\{J1,J3\}$ → 最优顺序 $J2→J1→J3$ 2. 动态规划计算: - $M1$时间线:0-1($J2$), 1-4($J1$), 4-9($J3$) - $M2$时间线:1-5($J2$), 5-7($J1$), 9-11($J3$) - 总时间=11 #### 复杂度分析 - 时间:$O(n^2 \cdot 2^n)$(需遍历所有子集和$M2$时间点) - 空间:$O(n \cdot 2^n)$ > **实际应用建议**:当$n>20$时,优先使用Johnson法则(时间复杂度$O(n \log n)$),动态规划适用于小规模问题[^1][^2]。 --- ### 相关问题 1. Johnson法则在哪些情况下可能失效? 2. 如何用贪心算法近似求解大规模流水作业调度? 3. 动态规划状态定义中$t$的取值范围如何优化? 4. 流水作业调度与Job Shop调度的本质区别是什么?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值