0Pricing
Coding Interview Prep · レッスン

Course Schedule IとII

履修条件を有向グラフとしてモデル化し、トポロジカルソートを使ってすべての講座を修了できるかどうかと、その順序を求めます。

「Course Schedule IとII」はCoddyKit上の無料Coding Interview Prepレッスンです。 これはレッスン3/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはCoding Interview Prep学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 Coding Interview Prepコースには全4レッスンが含まれています。

問題の概要

Course Schedule I(LeetCode 207)では、n個のコースと、[a, b]という形式で「bをaより前に受講する必要がある」ことを表すprerequisitesのペアのリストが与えられ、すべてのコースを修了できるかを判定します。Course Schedule II(LeetCode 210)では、コースを受講する実際の順序を返し、不可能な場合は空の配列を返します。どちらも、前提条件をエッジとして表す有向グラフ上のトポロジカルソートに帰着できます。

グラフのモデル化

有向グラフを構築します。各前提条件のペア[a, b]に対して、b → aというエッジを追加します。「bをaより前に処理する必要がある」ため、bからaへつなげます。各コースの入次数を計算します。入次数が0のコースには前提条件がなく、すぐに受講できます。この問題が解けるのは、このグラフにサイクル(循環依存)が存在しない場合に限ります。

from collections import defaultdict

def build_graph(n, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * n
    for a, b in prerequisites:  # b must come before a
        graph[b].append(a)
        in_degree[a] += 1
    return graph, in_degree

graph, ind = build_graph(4, [[1,0],[2,0],[3,1],[3,2]])
print('In-degrees:', ind)   # [0, 1, 1, 2]
print('Graph edges:', dict(graph))

Course Schedule I:Kahn法による解法

Kahn法を使用します。処理したコース数がnと等しければ、すべてのコースを修了できます。そうでなければ、循環依存によって修了できません。

from collections import deque, defaultdict

def canFinish(numCourses, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * numCourses
    for a, b in prerequisites:
        graph[b].append(a)
        in_degree[a] += 1
    
    queue = deque(i for i in range(numCourses) if in_degree[i] == 0)
    count = 0
    
    while queue:
        course = queue.popleft()
        count += 1
        for nxt in graph[course]:
            in_degree[nxt] -= 1
            if in_degree[nxt] == 0:
                queue.append(nxt)
    
    return count == numCourses

print(canFinish(2, [[1,0]]))        # True
print(canFinish(2, [[1,0],[0,1]])) # False

Course Schedule II:順序を返す

Course Schedule Iと同様ですが、処理したコースの順序を集めます。すべてのコースが含まれていればその順序を返し、そうでなければ空のリストを返します。

from collections import deque, defaultdict

def findOrder(numCourses, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * numCourses
    for a, b in prerequisites:
        graph[b].append(a)
        in_degree[a] += 1
    
    queue = deque(i for i in range(numCourses) if in_degree[i] == 0)
    order = []
    
    while queue:
        course = queue.popleft()
        order.append(course)
        for nxt in graph[course]:
            in_degree[nxt] -= 1
            if in_degree[nxt] == 0:
                queue.append(nxt)
    
    return order if len(order) == numCourses else []

print(findOrder(4, [[1,0],[2,0],[3,1],[3,2]]))

DFSを使ったCourse Schedule

DFSによるサイクル検出を使う別の方法もあります。コースには3つの状態があります。未訪問 (0)、処理中 (1)、完了 (2)です。DFS中に処理中のコースへ到達した場合、サイクルが存在します。この方法はKahn法と機能的には同等ですが、再帰的なDFSを使用します。

from collections import defaultdict

def canFinish_dfs(numCourses, prerequisites):
    graph = defaultdict(list)
    for a, b in prerequisites:
        graph[b].append(a)
    
    # 0=unvisited, 1=in-progress, 2=done
    state = [0] * numCourses
    
    def has_cycle(course):
        if state[course] == 1: return True  # back edge
        if state[course] == 2: return False # already cleared
        state[course] = 1
        for nxt in graph[course]:
            if has_cycle(nxt):
                return True
        state[course] = 2
        return False
    
    return not any(has_cycle(i) for i in range(numCourses))

print(canFinish_dfs(2, [[1,0]]))        # True
print(canFinish_dfs(2, [[1,0],[0,1]])) # False

エッジの向きが重要な理由

よくある間違いは、エッジの向きを逆にすることです。前提条件が[a, b]で「bをaより前に処理する」という意味なら、a → bではなくb → aというエッジを追加します。エッジの向きは依存関係の流れを反映しなければなりません。つまり、矢印は最初に処理するものから、それに依存するものへ向けます。向きを間違えると、サイクル検出と順序が逆になり、複数の依存関係がある問題で誤った結果になります。

Course Schedule III:貪欲法による変形問題

Course Schedule III(LeetCode 630)は別の問題です。コースには所要時間と締切があり、受講するコース数を最大化します。これは最大ヒープを使った貪欲法で解きます。常に締切が最も遅いコースを先に選び、コースを追加して締切を超えた場合は、これまでに選んだコースの中で最も長いもののほうが長ければ、それを置き換えます。これはトポロジカルソートではなく貪欲法の問題であり、問題文を注意深く読むことの重要性を示しています。

孤立ノードの扱い

前提条件も依存先もないコースは孤立ノードです。入次数が0で、出ていくエッジもありません。Kahn法ではこれらを正しく処理できます。すぐにキューへ追加され、処理されます。prerequisitesのリストに登場しないコースも含め、0からn - 1までのすべてのノードの入次数を0で初期化してください。初期化しないと、それらのコースが処理対象から漏れてしまいます。

# Example: 4 courses, but only courses 0 and 1 have a prerequisite relationship
# Courses 2 and 3 are isolated - they should appear in the output
from collections import deque, defaultdict

def findOrder_isolated(numCourses, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * numCourses  # initialise ALL nodes
    for a, b in prerequisites:
        graph[b].append(a)
        in_degree[a] += 1
    queue = deque(i for i in range(numCourses) if in_degree[i] == 0)
    order = []
    while queue:
        c = queue.popleft(); order.append(c)
        for nxt in graph[c]:
            in_degree[nxt] -= 1
            if in_degree[nxt] == 0: queue.append(nxt)
    return order if len(order) == numCourses else []

print(findOrder_isolated(4, [[1,0]]))  # [0,1,2,3] or [2,3,0,1] etc.

コースを並行して修了する時間

Parallel Courses IIでは、1学期に最大k個のコースを受講でき、前提条件を守らなければならない場合に、すべてのコースを受講するための最小学期数を求めます。これには、レベルごとに処理するKahn法と、k個を選択する制約に対するビットマスクDPが必要です。トポロジカルソートとビットマスクDPを組み合わせた、非常に難しい問題です。

面接でのコミュニケーション戦略

面接でCourse Schedule系の問題に直面したら、次の手順で進めます。(1) すぐにトポロジカルソート/サイクル検出問題だと識別します。(2) エッジがどちら向きに進むかを明確にして、グラフをモデル化します。(3) 簡潔さを重視するならKahn法(BFS)、慣れている方法を使うならDFSを選択します。(4) サイクルがある場合を明示的に処理します。(5) 時間計算量O(V+E)に言及します。この体系的なアプローチによって、問題を組織的に解決する能力を示せます。

包括的なテスト

さまざまな入力に対して両方の解法をテストし、正しさを検証します。Kahn法は複数の有効な順序を柔軟に扱えます。Course Schedule IIの答えとしては、どの有効なトポロジカル順序でも問題ありません。

from collections import deque, defaultdict

def findOrder(numCourses, prerequisites):
    graph = defaultdict(list)
    in_degree = [0] * numCourses
    for a, b in prerequisites:
        graph[b].append(a)
        in_degree[a] += 1
    queue = deque(i for i in range(numCourses) if in_degree[i] == 0)
    order = []
    while queue:
        c = queue.popleft(); order.append(c)
        for nxt in graph[c]:
            in_degree[nxt] -= 1
            if in_degree[nxt] == 0: queue.append(nxt)
    return order if len(order) == numCourses else []

print(findOrder(1, []))                    # [0]
print(findOrder(2, [[0,1]]))              # [1, 0]
print(findOrder(3, [[1,0],[2,1]]))        # [0, 1, 2]
print(findOrder(3, [[1,0],[0,1]]))        # [] cycle

理解度チェック

このレッスンで扱った Data Structures & Algorithms — Coding Interview Prep の概念について理解度を確認します。

レッスンのまとめ

このレッスンでは、Course Schedule IとIIはいずれも、前提条件[ a, b ]に対してエッジb → aを使うトポロジカルソートで解けること、Course Schedule Iではlen(order) == nを確認するだけなのに対し、Course Schedule IIでは順序そのものを返すこと、そして3つの状態を使ったDFSベースのサイクル検出は、Kahn法によるBFSの有効な代替手段であることを学びました。次は、強連結成分を求めるKosaraju法を学びます。

よくある質問

「Course Schedule IとII」レッスンは無料ですか?

はい。「Course Schedule IとII」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、Coding Interview Prepコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 Coding Interview Prepコースには全4レッスンが含まれています。

「Course Schedule IとII」で何を学びますか?

履修条件を有向グラフとしてモデル化し、トポロジカルソートを使ってすべての講座を修了できるかどうかと、その順序を求めます。 ブラウザで直接実行するハンズオンコードでCoding Interview Prepを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

Coding Interview Prepを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのCoding Interview Prepは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン3/4です。

「Course Schedule IとII」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このCoding Interview Prepレッスンでコードを書いて実行できますか?

はい。すべてのCoding Interview Prepレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. Kahnのアルゴリズム:BFSトポロジカルソート
  2. DFSの後順によるトポロジカルソート
  3. Course Schedule IとII
  4. Kosarajuによる強連結成分
← Coding Interview Prepに戻る