くろたんく雑記帳

日常とか、わんちゃんとか、機械学習とか、競プロとか、

MENU

Python3で実装 問題解決力を鍛える!アルゴリズムとデータ構造【第7章】設計技法(5):貪欲法

「問題解決力を鍛える!アルゴリズムとデータ構造」の章末問題をPython3で実装していくシリーズ。

大枠まとめ記事は、ここに。 blacktanktop.hatenablog.com

前回の記事は、【第6章】設計技法(4):二分探索法

blacktanktop.hatenablog.com

今回は【第7章】設計技法(5):貪欲法について扱う

問題解決力を鍛える!アルゴリズムとデータ構造 (KS情報科学専門書)

問題解決力を鍛える!アルゴリズムとデータ構造 (KS情報科学専門書)

  • 作者:大槻 兼資
  • 発売日: 2020/10/02
  • メディア: 単行本(ソフトカバー)


内容

  • Python3でやっている。
  • 簡単に自分の理解を整理する用。
  • そこまで厳密ではない可能性が高い。
  • 記事を書いたら、書評の方に記事を載せていく。
  • コード等はGitHubにおいた。 github.com

【第7章】設計技法(5):貪欲法の概要

いろんな選択肢から、逐次的に実行していくことを考えるときに、次のステップのことだけを考えて(その先のことは考えずに)そのときの最善手を繰り返すという方法。

  • コイン問題
  • 区間スケジュール

などが挙げられて、

貪欲法のポイント

  • 単調増加など、単調性が構造的にみられる(ことが多い)。
    • ソートすれば貪欲でいけるなということがままある。
  • 問題設定によっては必ずしも最適解にならない。
    • 貪欲法によって最適解が求められる理由を考えられることが重要。(少なくとも例外が思いつかないにしても感覚的にこう考えれば、次ステップのことだけ考えれば十分だなと思えるなど)

章末問題

7.1 有名問題
  • {\displaystyle n}個の整数列{\displaystyle a}からいくつかと{\displaystyle n}個の整数列{\displaystyle b}からいくつかを選んでペアを作る。
  • ただし、各ペア{\displaystyle a_i \lt b_j}を満たす必要がある。
  • この時、最大で何ペア作れますか?

ちょっと問題を把握し切れていない・・・例が欲しい。

7.2 2D Plane 2N Points

出典は、この問題。

atcoder.jp 詳細は出典をみてもらうとして、ポイントは、

青い点で、最もx座標が小さいものにおいて、赤の点と仲良しペアになれる点が存在したら、
そのような点のうち最も y 座標が大きいものと仲良しペアする。

これが最適解かということが重要であるので、考えてみる。

  • 手元にあるx座標が最も小さい青い点で、条件に合う赤の点の候補があるが、ペアになっていない場合。
    • 候補のうち最もy座標が小さい赤の点(r1)をペアにする。
    • 仮に、別の青い点と上記がペアになっているなら解消して組み換える。
    • ペア数は少なくとも変わらないか、増えることになる。
  • 手元にあるx座標が最も小さい青い点(b1)で、条件に合う赤の点の候補ではあるが、最もy座標が小さい赤い点ではないもの(r2)とペアになっている場合。
    • r1がどれともペアじゃなければ、b1-r1と組み換える。
    • r1が別の点b2とペアだった場合もb1-r1, b2-r2と組み換える。
      • b2はr1とペアになっていたのならば、 r1よりy座標が小さいr2とはペアに必ずなれる。(x座標もb2はb1よりも大きいので)

実装の流れは、

  • 赤点の配列{\displaystyle red}{\displaystyle b}つまり、{\displaystyle y}座標を基準に昇順にソートする。
  • 青点の配列{\displaystyle blue}{\displaystyle c}つまり、{\displaystyle x}座標を基準に降順にソートする。
  • {\displaystyle blue}をforで回しながら、条件に合う{\displaystyle a}を見つけたら、その{\displaystyle a}をペアにしたよっていう辞書に書き込む。
  • {\displaystyle a}のforをbreakする。

実装ができたが、これが{\displaystyle O(nlogn)}になっているのか? これは{\displaystyle O(n^2)}だよなー・・・わからん。
誰か教えて欲しいOrz

入力例は

5
0 0
7 3
2 2
4 8
1 6
8 5
6 9
5 4
9 1
3 7
def main():
    n = int(input())
    r = sorted([list(map(int, input().split())) for i in range(n)], \
        key=lambda x: x[1], reverse=True)
    b = sorted([list(map(int, input().split())) for i in range(n)])    
    done = dict()
    cnt = 0
    print('最大数の仲良しペアの例は')
    for bx, by in b:
        for rx, ry in r:
            if (rx, ry) in done:
                continue
            if rx < bx and ry < by:
                print('a:', rx, ry, 'b:', bx, by)
                done[(rx, ry)] = True
                cnt += 1
                break
    print(cnt)
if __name__ == '__main__':
    main()
7.3 Megalomania

問題の出典は以下。 atcoder.jp 概要は、

  • {\displaystyle n}個の仕事があり、{\displaystyle i}番目の仕事は、{\displaystyle d_i}かかる。
  • 締め切りは{\displaystyle t_i}で同時に複数の仕事はできない。
  • 時刻0から始めて、仕事が全て終わるかどうかを判定する。
  • ただし計算量が{\displaystyle O(Nlog(N))}で。

締め切りが早い順に、こなしていって、終わらなかったら無理でしょ。 ということで。手順としては、以下のように。

  • {\displaystyle [[d_0, t_0], [d_1, t_1], [d_2, t_2]...[d_{n-1}, t_{n-1}]]}こんな感じで持つことになるので、{\displaystyle t_i}を基準にソートする。
  • その時点での仕事にかかった総合計時間({\displaystyle d\_sum})とその仕事の締め切り時刻({\displaystyle t})を比較して、総合計時間が大きくなったら、無理。
  • 無事、総合計時間({\displaystyle d\_sum})がその仕事の締め切り時刻({\displaystyle t})を上回ることなく全てが終われば、可能。

入力は以下のような形を想定

5
2 4
1 9
1 8
4 9
3 12
def main():
    n = int(input())
    dt = [list(map(int, input().split())) for _ in range(n)]
    # O(nlogn)
    dt_s = sorted(dt, key=lambda x: x[1])
    print('bを基準にソート')
    print(dt_s)
    d_sum = 0
    # O(n)
    for d, t in dt_s:
        d_sum += d
        print('仕事時間の合計:', d_sum, 'この仕事の締め切り:', t)
        if d_sum > t:
            print('No')
            exit()
    print('Yes')
if __name__ == '__main__':
    main()

まとめ

  • 貪欲は未来のことは考えず、すぐ次のステップのことだけ考えて今の最善を繰り返し行うことなので比較的イメージしやすい。
  • 貪欲が必ずしも最善とは限らないことがあるので注意(今の選択が未来の選択に影響してしまう場合など)

終わりに

貪欲は比較的やりやすいが、前提が間違っていることもあって、その場合は、ミスるので貪欲でいけそうかどうかはしっかり考えてコードを書き始めるのがいい。

問題解決力を鍛える!アルゴリズムとデータ構造 (KS情報科学専門書)

問題解決力を鍛える!アルゴリズムとデータ構造 (KS情報科学専門書)

  • 作者:大槻 兼資
  • 発売日: 2020/10/02
  • メディア: 単行本(ソフトカバー)