두 대의 기계에서 작업 스케줄링하기¶
이 문제는 두 대의 기계에서 $n$개의 작업을 처리하는 최적의 스케줄을 찾는 문제입니다. 모든 항목은 먼저 첫 번째 기계에서 처리된 후, 이어서 두 번째 기계에서 처리되어야 합니다. $i$번째 작업은 첫 번째 기계에서 $a_i$의 시간이, 두 번째 기계에서 $b_i$의 시간이 걸립니다. 각 기계는 한 번에 하나의 작업만 처리할 수 있습니다.
최종 처리 시간이 가능한 한 최소가 되도록 하는 작업의 최적 순서를 찾고자 합니다.
여기서 다루는 이 풀이는 Johnson의 규칙이라고 불립니다(S. M. Johnson의 이름을 따서 명명되었습니다).
기계가 두 대보다 많으면 이 문제는 NP-완전이 된다는 점에 주목할 만합니다.
알고리즘의 구성¶
먼저, 첫 번째 기계와 두 번째 기계의 작업 순서가 일치해야 한다고 가정할 수 있다는 점에 주목하십시오. 실제로, 두 번째 기계의 작업은 첫 번째 기계에서 처리된 후에야 사용 가능해지며, 두 번째 기계에서 사용 가능한 작업이 여러 개 있다면 처리 시간은 순서와 관계없이 그 작업들의 $b_i$의 합과 같습니다. 따라서 작업을 첫 번째 기계에 보낸 것과 같은 순서로 두 번째 기계에 보내는 것은 이점만 있습니다.
작업의 순서가 입력 순서 $1, 2, \dots, n$과 일치한다고 가정해 봅시다.
작업 $i$를 처리하기 직전 두 번째 기계의 유휴 시간을 $x_i$로 표기합니다. 우리의 목표는 총 유휴 시간을 최소화하는 것입니다:
첫 번째 작업에 대해서는 $x_1 = a_1$입니다. 두 번째 작업은 시각 $a_1 + a_2$에 기계로 보내지고, 두 번째 기계는 $x_1 + b_1$에 비게 되므로, $x_2 = \max\left((a_1 + a_2) - (x_1 + b_1), 0\right)$입니다. 일반적으로 다음 방정식을 얻습니다:
이제 총 유휴 시간 $F(x)$를 계산할 수 있습니다. 이는 다음과 같은 형태를 가진다고 주장할 수 있습니다.
여기서
이는 귀납법을 이용하여 쉽게 확인할 수 있습니다.
이제 순열 방법을 사용합니다: 인접한 두 작업 $j$와 $j+1$을 교환하고, 이것이 총 유휴 시간을 어떻게 변화시키는지 살펴보겠습니다.
$K_i$ 식의 형태로부터 $K_j$와 $K_{j+1}$만 변한다는 것이 분명하며, 이들의 새 값을 $K_j'$와 $K_{j+1}'$로 표기합니다.
작업 $j$와 $j+1$의 이러한 교환이 총 유휴 시간을 증가시켰다면, 다음이 성립해야 합니다:
(두 작업을 교환해도 아무런 영향이 없을 수도 있습니다. 위 조건은 충분조건일 뿐, 필요조건은 아닙니다.)
부등식의 양변에서 $\sum_{i=1}^{j+1} a_i - \sum_{i=1}^{j-1} b_i$를 제거하면 다음을 얻습니다:
그리고 음의 부호를 없애면 다음과 같습니다:
따라서 비교자를 얻었습니다: 이를 기준으로 작업을 정렬하면, 어떤 두 작업을 교환해도 최종 시간이 개선되지 않는 최적의 작업 순서를 얻습니다.
하지만 비교 함수를 다른 관점에서 보면 정렬을 더 단순화할 수 있습니다. 비교 함수는 다음과 같이 해석할 수 있습니다. 네 개의 시간 $(a_j, a_{j+1}, b_j, b_{j+1})$이 있을 때 그중 최솟값이 첫 번째 기계에 해당하는 시간이라면, 해당 작업을 먼저 수행해야 합니다. 최솟값이 두 번째 기계의 시간이라면 해당 작업은 나중에 수행해야 합니다. 따라서 작업을 $\min(a_i, b_i)$ 기준으로 정렬할 수 있으며, 현재 작업의 첫 번째 기계에서의 처리 시간이 두 번째 기계에서의 처리 시간보다 작으면 이 작업은 남은 모든 작업보다 먼저 수행해야 하고, 그렇지 않으면 남은 모든 작업보다 나중에 수행해야 합니다.
어느 방식이든 Johnson의 규칙에 따라 작업을 정렬하여 문제를 풀 수 있으며, 그 결과 $O(n \log n)$의 시간 복잡도를 얻습니다.
구현¶
여기서는 앞서 설명한 알고리즘의 두 번째 변형을 구현합니다.
struct Job {
int a, b, idx;
bool operator<(Job o) const {
return min(a, b) < min(o.a, o.b);
}
};
vector<Job> johnsons_rule(vector<Job> jobs) {
sort(jobs.begin(), jobs.end());
vector<Job> a, b;
for (Job j : jobs) {
if (j.a < j.b)
a.push_back(j);
else
b.push_back(j);
}
a.insert(a.end(), b.rbegin(), b.rend());
return a;
}
pair<int, int> finish_times(vector<Job> const& jobs) {
int t1 = 0, t2 = 0;
for (Job j : jobs) {
t1 += j.a;
t2 = max(t2, t1) + j.b;
}
return make_pair(t1, t2);
}
각 작업에 대한 모든 정보는 구조체에 저장됩니다. 첫 번째 함수는 모든 작업을 정렬하고 최적의 스케줄을 계산합니다. 두 번째 함수는 주어진 스케줄에 대해 두 기계의 종료 시간을 계산합니다.