【数据结构和算法设计】算法篇(7) 贪心法 多机调度、最优装载问题
7.7 求解最优装载问题
【问题描述】有 nnn 个集装箱要装上一艘载重量为 WWW 的轮船,其中集装箱 i (1≤i≤n)i\ (1\le i \le n)i (1≤i≤n) 的重量为 wiw_iwi 。不考虑集装箱的体积限制,现要选出尽可能多的集装箱装上轮船,使它们的重量之和不超过 WWW 。
【问题求解】5.3.1小节讨论了简单装载问题,采用回溯法选出尽可能少的集装箱个数。这里的最优解是选出尽可能多的集装箱个数,并采用贪心法求解。当重量限制为 WWW 时,wiw_iwi 越小、可装载的集装箱个数越多,所以采用「优先选取重量轻的集装箱装船」的贪心思路,如图7.5所示。
对 wiw_iwi 从小到大排序,得到 (w1,w2,…,wn)(w_1, w_2, \dots, w_n)(w1,w2,…,wn) 。设最优解向量为 x=(x1,x2,…,xn)x = (x_1, x_2, \dots, x_n)x=(x1,x2,…,xn) ,显然,x1=1x_1 = 1x1=1 ,则 x′=(x2,…,xn)x' = (x_2, \dots, x_n)x′=(x2,…,xn) 是装载问题 w′=(w2,…,wn),W′=W−w1w' = (w_2, \dots, w_n), W' = W - w_1w′=(w2,…,wn),W′=W−w1 的最优解,满足贪心选择、最优子结构性质。对应的完整程序如下:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 20; // 最多集装箱个数
// 问题表示
int n = 5, W = 10;
int w[] = {0, 5, 2, 6, 4, 3}; // 各集装箱重量,不用下标为0的元素
// 求解结果表示
int maxw = 0; // 存放最优解的总重量
int x[MAXN]; // 存放最优解向量
void solve() { // 求解最优装载问题
memset(x, 0, sizeof(x)); // 初始化解向量
sort(w + 1, w + n + 1); // w[1...n]递增排序
int restw = W; // 剩余重量
for (int i = 1; i <= n && w[i] <= restw; ++i) {
x[i] = 1; // 选择集装箱i
restw -= w[i]; // 减少剩余重量i
maxw += w[i]; // 累计装载总重量
}
}
int main() {
solve();
printf("最优方案\n");
for (int i = 1; i <= n; ++i)
if (x[i] == 1)
printf(" 选取重量为%d的集装箱\n", w[i]);
printf("总重量=%d\n", maxw);
return 0;
}
上述程序的执行结果如下:
【算法分析】算法的时间主要花费在排序上,时间复杂度为 O(nlog2n)O(n\log_2 n)O(nlog2n) 。
7.8 求解多机调度问题
【问题描述】设有 nnn 个独立的作业 {1,2,…,n}\{ 1, 2, \dots, n\}{1,2,…,n} ,由 mmm 台相同的机器 {1,2,…,m}\{ 1, 2, \dots, m\}{1,2,…,m} 进行加工处理,作业 iii 所需的处理时间为 ti (1≤i≤n)t_i\ (1 \le i \le n)ti (1≤i≤n) ,每个作业均可在任何一台机器上加工处理,但未完工前不允许中断,任何作业也不能拆分成更小的子作业。多机调度问题要求,给出一种作业调度方案,使所给的 nnn 个作业在尽可能短的时间内、由 mmm 台机器加工处理完成。
【问题求解】采用贪心思路,让最长处理时间的作业优先,即把处理时间最长的作业分配给最先空闲的机器。这样可以保证处理时间长的作业优先处理,从而在整体上获得尽可能短的处理时间。按照最长处理时间的作业优先的贪心策略:
- 当 m≥nm \ge nm≥n 时,只要将机器 iii 的 [0,ti)[0, t_i)[0,ti) 时间区间分配给作业 iii 即可;
- 当 m<nm < nm<n 时,首先将 nnn 个作业依其所需处理时间从大到小排序,然后依此顺序、将作业分配给空闲的处理机。
例如,有 n=7n = 7n=7 个独立的作业 (1,2,3,4,5,6,7)(1, 2, 3, 4, 5, 6, 7)(1,2,3,4,5,6,7) ,由 m=3m = 3m=3 台机器 (1,2,3)(1, 2, 3)(1,2,3) 处理,各机器所需的处理时间如表7.3所示。采用贪心法求解的过程如下:
- 777 个作业按处理时间递减排序,其结果如表7.4所示。
- 先将排序后的前三个作业分配给三台机器,此时机器的分配情况为 ({4},{2},{5})(\{4\}, \{ 2\}, \{ 5\})({4},{2},{5}) ,对应的总处理时间为 (16,14,6)(16, 14, 6)(16,14,6) 。接着分配余下的作业。
- 分配作业 666 :三台机器中机器 333 在时间 666 后最先空闲,将作业 666 分配给它,此时机器的分配情况为 ({4},{2},{5,6})( \{ 4\} , \{ 2\}, \{ 5, 6\})({4},{2},{5,6}) ,对应的总处理时间为 (16,14,6+5=11)(16, 14, 6 + 5 = 11)(16,14,6+5=11) 。
- 分配作业 333 :三台机器中机器 333 在时间 111111 后最先空闲,将作业 333 分配给它,此时机器的分配情况为 ({4},{2},{5,6,3})(\{ 4\}, \{ 2\}, \{ 5, 6, 3\})({4},{2},{5,6,3}) ,对应的总处理时间为 (16,14,11+4=15)(16, 14, 11 + 4 = 15)(16,14,11+4=15) 。
- 分配作业 777 :三台机器中机器 222 在时间 141414 后最先空闲,将作业 777 分配给它,此时机器的分配情况为 ({4},{2,7},{5,6,3})( \{4\}, \{ 2, 7\}, \{ 5, 6, 3\})({4},{2,7},{5,6,3}) ,对应的总处理时间为 (16,14+3=17,15)(16, 14+3=17, 15)(16,14+3=17,15) 。
- 分配作业 111 :三台机器中机器 333 在时间 151515 后最先空闲,将作业 111 分配给它,此时机器的分配情况为 ({4},{2,7},{5,6,3,1})( \{4\}, \{2, 7\}, \{ 5, 6, 3, 1\})({4},{2,7},{5,6,3,1}) ,对应的总处理时间为 (16,17,15+2=17)(16, 17, 15+2=17)(16,17,15+2=17) 。
由于每次需要求出最先空闲的机器,即求正在执行作业的 ttt 最小的机器,为此要采用小根堆,堆中的作业是正在执行的作业,最多 mmm 个作业。当某个机器执行的作业的 ttt 最小时,它最先出队,加上当前安排的作业 jjj 的执行时间,然后继续进队执行。
对应的完整求解程序如下:
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 100;
// 问题表示
int n = 7;
int m = 3;
struct NodeType { // 优先队列结点类型
int no; // 作业序号
int t; // 执行时间
int mno; // 机器序号
bool operator<(const NodeType &s) const {
return t > s.t; // 小根堆
}
};
NodeType A[] = {{1, 2}, {2, 14}, {3, 4}, {4, 16}, {5, 6}, {6, 5}, {7, 3}};
void solve() { // 求解多机调度
if (n <= m) {
printf("为每个作业分配一台机器\n");
return;
}
sort(A, A + n); // 按t递减排序
priority_queue<NodeType> pq; // 小根堆
for (int i = 0; i < m; ++i) { // 首先分配m个作业,每台机器一个作业
A[i].mno = i + 1; // 作业对应的机器编号
printf(" 给机器%d分配作业%d, 执行时间为%2d, 占用时间段: [%d, %d)\n",
A[i].mno, A[i].no, A[i].t, 0, A[i].t);
pq.push(A[i]);
}
for (int j = m; j < n; ++j) { // 分配余下的作业
NodeType e = pq.top(); pq.pop();
printf(" 给机器%d分配作业%d, 执行时间为%2d, 占用时间段: [%d, %d)\n",
e.mno, A[j].no, A[j].t, e.t, e.t + A[j].t);
e.t += A[j].t;
pq.push(e);
}
}
int main() {
printf("多机调度方案\n");
solve();
return 0;
}
程序的执行结果如下:
【算法分析】排序的时间复杂度为 O(nlogn)O(n\log n)O(nlogn) ,两次循环的时间分别是 m, (n−m)logmm,\ (n -m) \log mm, (n−m)logm ,所以本算法的时间复杂度为 O(nlogn)O(n\log n)O(nlogn) 。
多机调度是NP难问题,到目前为止还没有有效的算法,上述算法是采用贪心法求得的一个较好的近似解。可以通过一个反例,说明上述算法得到的不一定是最优解。例如,n=7,m=3n = 7, m = 3n=7,m=3 ,作业的执行时间 ={16,14,12,11,10,9,8}= \{16, 14, 12, 11, 10, 9, 8\}={16,14,12,11,10,9,8} 。
- 按照该算法得到的结果是 313131 ,对应的方案是:机器一执行时间长度为 16,916, 916,9 的作业(总时间 252525 ),机器二执行时间长度为 14,1014, 1014,10 的作业(总时间为 242424 ),机器三执行时间长度为 12,11,812, 11, 812,11,8 的作业(总时间为 313131 )。
- 然而一个最优解是 272727 ,对应的方案是:机器一执行时间长度为 16,1116, 1116,11 的作业(总时间 272727 ),机器二执行时间长度为 14,1214, 1214,12 的作业(总时间为 262626 ),机器三执行时间长度为 10,9,810, 9, 810,9,8 的作业(总时间为 272727 )。
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)