C++で解くコーススケジュールIV ― 前提条件クエリの判定方法
問題概要
受講できるコースが全部で n 個あり、各コースには 0 から n-1 までの番号が割り当てられています。
一部のコースには直接の前提条件が存在します。たとえば、コース 0 を受講する前にコース 1 を受講しておく必要がある場合、この関係はペア [1,0] として表現されます。
ここで、コース数 n、直接の前提条件ペアのリスト、そしてクエリペアのリストが与えられます。
各クエリ queries[i] に対して、「コース queries[i][0] はコース queries[i][1] の前提条件であるか」を判定してください。最終的に、すべてのクエリへの回答をブーリアン値のリストとして返します。
注意すべき点として、前提条件の関係は推移的です。つまり、コース a がコース b の前提条件であり、さらにコース b がコース c の前提条件であるならば、コース a はコース c にとっても前提条件となります。
たとえば、入力が n = 3、prerequisites = [[1,2],[1,0],[2,0]]、queries = [[1,0],[1,2]] のとき、出力は [true, true] となります。
解法の考え方
この問題はトポロジカルソート(幅優先探索)を用いて効率的に解けます。基本的なアイデアは次のとおりです。
- 各コースについて、「そのコースを受講するために必要なすべての先行コース」を集合として記録します。
- トポロジカル順にノードを処理しながら、隣接ノードへ祖先(先行コース)の情報を伝播させていきます。
- この前計算を行っておけば、各クエリには集合の検索だけで即座に回答できます。
アルゴリズムの手順
- 定数 N := 110 を定義します。
- 結果を格納する配列 ret を用意します。
- 各ノードの入次数を管理するマップ in を定義します。
- v の各要素 it について、次を行います。
- graph[it[0]] の末尾に it[1] を追加します。
- in[it[1]] を 1 増やします。
- キュー q を定義します。
- i := 0 から i < n の範囲で i を 1 ずつ増やしながら、in[i] が 0 のノード i を q に挿入します。
- q が空になるまで、レベルごとに幅優先探索を繰り返します。
- sz := q のサイズ とし、sz が 0 になるまで次を繰り返します。
- node := q の先頭要素 を取り出し、キューから削除します。
- graph[node] の各要素 it について:
- in[it] を 1 減らします。
- c[node] 内の全要素 x を c[it] に挿入します。
- node 自身も c[it] に挿入します。
- in[it] が 0 になったら、it を q に挿入します。
- sz := q のサイズ とし、sz が 0 になるまで次を繰り返します。
- x の各要素 it について、c[it[1]] における it[0] の存在確認(count の結果)を ret の末尾に追加します。
- ret を返します。
C++実装例
理解を深めるために、以下の実装を見てみましょう。
#include <bits/stdc++.h>
using namespace std;
void print_vector(vector<bool> v){
cout << "[";
for(int i = 0; i<v.size(); i++){
cout << v[i] << ", ";
}
cout << "]"<<endl;
}
const int N = 110;
class Solution {
public:
vector <int> graph[N];
map <int, set <int>> c;
vector<bool> checkIfPrerequisite(int n, vector<vector<int>>& v, vector<vector<int>>& x) {
vector<bool> ret;
map<int, int> in;
for (auto& it : v) {
graph[it[0]].push_back(it[1]);
in[it[1]]++;
}
queue<int> q;
for (int i = 0; i < n; i++) {
if (in[i] == 0)
q.push(i);
}
map<int, int> idx;
for (int lvl = 1; !q.empty(); lvl++) {
int sz = q.size();
while (sz--) {
int node = q.front();
q.pop();
for (auto& it : graph[node]) {
in[it]--;
for (auto& x : c[node])
c[it].insert(x);
c[it].insert(node);
if (in[it] == 0) {
q.push(it);
}
}
}
}
for (auto& it : x) {
ret.push_back(c[it[1]].count(it[0]));
}
return ret;
}
};
main(){
Solution ob;
vector<vector<int>> prerequisites = {{1,2},{1,0},{2,0}}, queries = {{1,0},{1,2}};
print_vector(ob.checkIfPrerequisite(3, prerequisites, queries));
}
入力
3, {{1,2},{1,0},{2,0}}, {{1,0},{1,2}}出力
[1, 1]
まとめ
この手法では、各コースの祖先(前提条件)集合を事前に構築しておくため、クエリごとにグラフを再探索する必要がありません。計算量は集合のマージを含めておおよそ O(n³) オーダー、必要なメモリは O(n²) であり、n ≤ 100 程度の制約下では十分高速に動作します。前提条件が推移的である点を正しく扱えるのが、このアプローチの大きな強みです。
-
C++で解くジョブスケジュールの最小難易度問題
問題概要d日間でタスクのリストをスケジューリングすることを考えます。タスクには依存関係があり、i番目のタスクに取り掛かるためには、0 <= j < i を満たすすべてのタスク j を先に完了させておく必要があります。さらに、毎日最低1つはタスクを完了させなければなりません。スケジュール全体の難易度は、d日間の各日の難易度の合計として定義され、ある日の難易度は、その日に完了したタスクの中で最も高い難易度の値となります。ここで、整数型配列 taskDifficulty と整数 d が与えられます。i番目のタスクの難易度は taskDifficulty[i] です。スケジュール全体の難易
-
C++で解く長方形エリアII ― 座標圧縮と走査線法による被覆面積の計算
問題概要 軸に平行な長方形のリストが与えられるものとします。各 rectangle[i] = {x1, y1, x2, y2} において、(x1, y1) は i 番目の長方形の左下隅の座標、(x2, y2) は右上隅の座標を表します。 求めたいのは、平面上でこれらすべての長方形が覆っている領域の合計面積です。答えは非常に大きな値になる可能性があるため、109 + 7 で割った余りを返すことになっています。 たとえば、入力が次のような場合を考えてみましょう。 このとき、出力は 6 となります。 解法の方針:座標圧縮+走査線(スイープライン) この問題は、座標圧縮(座標の離散化)と走査線法(