D - プリンターの割り当て / Printer Assignment Editorial by admin
gpt-5.5-high(計算量の説明が誤り)概要
各プリンターは「処理できるページ数の上限」と「1件あたりの処理時間」を持ちます。
すべての依頼を処理し終える最小時刻を、二分探索と貪欲な判定で求めます。
考察
時刻 \(x\) までに、プリンター \(i\) が処理できる依頼数は
\( \left\lfloor \frac{x}{T_i} \right\rfloor \)
件です。
つまり、時刻 \(x\) までにすべての依頼を処理できるかどうかは、
- プリンター \(i\) は最大 \(W_i\) ページまでの依頼を処理できる
- そのような処理枠を \(\left\lfloor \frac{x}{T_i} \right\rfloor\) 個持っている
と考えることができます。
例えば、プリンターの能力が
| プリンター | \(W_i\) | \(T_i\) |
|---|---|---|
| 1 | 10 | 3 |
| 2 | 5 | 2 |
で、\(x = 6\) のとき、
- プリンター 1 は \(\lfloor 6/3 \rfloor = 2\) 件処理可能
- プリンター 2 は \(\lfloor 6/2 \rfloor = 3\) 件処理可能
です。
したがって、
- 最大 10 ページまで処理できる枠が 2 個
- 最大 5 ページまで処理できる枠が 3 個
ある、とみなせます。
ここで依頼をページ数の大きい順に並べます。
ページ数が大きい依頼ほど、処理できるプリンターが限られるため、先に考えるべきです。
例えば、依頼が
\(9, 7, 4\)
だったとします。
最初の 2 件、つまり \(9, 7\) ページの依頼は、少なくとも \(7\) ページ以上を処理できるプリンターの枠で処理する必要があります。
一般に、依頼を降順に並べて
\(P_1 \geq P_2 \geq \cdots \geq P_M\)
としたとき、先頭 \(k\) 件の依頼はすべて \(P_k\) ページ以上の依頼です。
したがって、時刻 \(x\) までに処理可能であるためには、
「\(W_i \geq P_k\) を満たすプリンターたちの処理可能件数の合計」が \(k\) 以上
である必要があります。
この条件をすべての \(k\) について満たせば、処理可能です。
素朴に各依頼をどのプリンターに割り当てるかを全探索すると、組み合わせが膨大で到底間に合いません。
また、各時刻について実際にスケジューリングをシミュレーションするのも複雑です。
そこで、
- 答えの時刻を二分探索する
- その時刻で処理可能かを高速に判定する
という方針を取ります。
時刻 \(x\) で処理可能なら、それより大きい時刻でも必ず処理可能です。
この単調性があるため、二分探索が使えます。
アルゴリズム
まず、すべての依頼をページ数の降順にソートします。
また、プリンターも \(W_i\) の降順にソートします。
判定関数 feasible(x) では、時刻 \(x\) までにすべての依頼を処理できるかを調べます。
手順は以下の通りです。
- 依頼をページ数の大きい順に見る
- 現在見ている依頼のページ数を \(p\) とする
- \(W_i \geq p\) を満たすプリンターを追加する
- 追加したプリンターについて、時刻 \(x\) までに処理可能な件数 \(\left\lfloor x / T_i \right\rfloor\) を合計する
- ここまで見た依頼数より、処理可能件数の合計が少なければ不可能
コード中では、処理可能件数の合計を cap としています。
while (idx < N && printers[idx].first >= p) {
ll add = x / printers[idx].second;
if (add > M) add = M;
cap = min<ll>(M, cap + add);
idx++;
}
プリンターは \(W_i\) の降順に並んでいるので、各プリンターは高々 1 回だけ追加されます。
そのため、1 回の判定は \(O(N + M)\) で行えます。
あとは答えを二分探索します。
まず、どのプリンターでも処理できない依頼がある場合、つまり
\( \max P_j > \max W_i \)
なら答えは \(-1\) です。
そうでない場合、最大ページ数の依頼を処理できるプリンターのうち、最も速いものを使えば、全依頼を処理できます。
その処理時間を minT とすると、答えは高々
\(minT \times M\)
です。
これを二分探索の上限にします。
ll ng = 0;
ll ok = minT * M;
ng: 処理不可能な時刻ok: 処理可能な時刻
として二分探索します。
while (ok - ng > 1) {
ll mid = (ng + ok) / 2;
if (feasible(mid)) ok = mid;
else ng = mid;
}
最終的に ok が答えになります。
計算量
- 時間計算量: \(O((N + M) \log (minT \times M))\)
- 空間計算量: \(O(N + M)\)
実装のポイント
プリンターごとの処理可能件数
\( \left\lfloor \frac{x}{T_i} \right\rfloor \)
をすべて足し合わせると、非常に大きな値になり、オーバーフローする可能性があります。
しかし、必要なのは最大でも依頼数 \(M\) 件までなので、コードでは cap を \(M\) で打ち切っています。
if (add > M) add = M;
cap = min<ll>(M, cap + add);
また、依頼を降順、プリンターも \(W_i\) の降順にソートしておくことで、判定関数内でプリンターを一度ずつ見るだけで済みます。
ソースコード
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int N, M;
cin >> N >> M;
vector<pair<ll, ll>> printers(N);
ll maxW = 0;
for (int i = 0; i < N; i++) {
ll W, T;
cin >> W >> T;
printers[i] = {W, T};
maxW = max(maxW, W);
}
vector<ll> jobs(M);
for (int i = 0; i < M; i++) cin >> jobs[i];
sort(jobs.rbegin(), jobs.rend());
if (jobs[0] > maxW) {
cout << -1 << '\n';
return 0;
}
sort(printers.begin(), printers.end(), [](const auto& a, const auto& b) {
if (a.first != b.first) return a.first > b.first;
return a.second < b.second;
});
ll minT = (ll)4e18;
for (auto [W, T] : printers) {
if (W >= jobs[0]) minT = min(minT, T);
}
auto feasible = [&](ll x) -> bool {
ll cap = 0;
int idx = 0;
for (int j = 0; j < M; j++) {
ll p = jobs[j];
while (idx < N && printers[idx].first >= p) {
ll add = x / printers[idx].second;
if (add > M) add = M;
cap = min<ll>(M, cap + add);
idx++;
}
if (cap < j + 1) return false;
}
return true;
};
ll ng = 0;
ll ok = minT * M;
while (ok - ng > 1) {
ll mid = (ng + ok) / 2;
if (feasible(mid)) ok = mid;
else ng = mid;
}
cout << ok << '\n';
return 0;
}
この解説は gpt-5.5-high によって生成されました。
posted:
last update: