たくあんポリポリ

勉強したことを載せていきます。最近、技術系の記事はZennに書いています。(https://zenn.dev/chittai)

【C#】区間スケジューリング問題における解法の説明と実装について

f:id:c_taquna:20200115005201p:plain

キーエンス プログラミング コンテスト 2020区間スケジューリング問題がでたのですが、覚えていれば実装できる問題だったので、改めて整理してみたいと思います。

はじめに

本記事のテーマ

区間スケジューリング問題とは何か、どう実装すればよいか説明します。後半で今回のネタ元となるキーエンス プログラミングコンテストの問題を例題として説明します。

本題

区間スケジューリング問題とは

区間スケジューリング問題とは 貪欲法に分類される解法の一つです。貪欲法の定義は、蟻本(プログラミングコンテストチャレンジブック)にのっている定義を引用させていただくと

貪欲法とは”1つのルールに従って、貪欲的に「その場での最善」を選択することを繰り返す”というアルゴリズム設計技法です

となります。それを踏まえた上で考えていきます。

区間スケジューリング問題の特徴としては

  • 数直線上で区間をもつタスクが与えられる
  • その各タスクがかぶらないように選択し、選択するタスク数を最大化する

という点です。タスクと表現していますが距離のある棒でもいいですし、手の長さでも構いません。区間として見れるものであれば置き換えてください。

タスクの選択方法

貪欲法なので、ルールを決める必要があります。このルールをどう選ぶかが問題です。そして、そのルールは

  • 選べるタスクの中で、終了時間が最も早いものを選ぶことを繰り返す

となります。今回のキーエンスプログラミングコンテストを例題に説明します。

キーエンス プログラミング コンテスト 2020 B問題 Robot Arms

問題へのリンク

atcoder.jp

問題の概要

  • 数直線上のXiにロボットiが設置されている
  • 数直線上の正負に長さLi腕を伸ばすことができる(Xi-Li と Xi+Liまで伸ばせる)
  • 腕が動く範囲が共通部分を持たないようにロボットを取り除ける
  • できる限りロボットを残すように選択したい

解法の説明

まず、これがどの様にして区間スケジューリング問題であるかを判断するかです。先程あげた区間スケジューリングの特徴は下記です。

  1. 数直線上で区間をもつタスクが与えられる
  2. その各タスクがかぶらないように選択し、選択するタスク数を最大化する

1に関しては、腕の長さを区間と見ることができます。2に関してはまさに問題文がそのとおりになっています。なので、今回はすごくオーソドックスな区間スケジューリング方法であると判断できます。

なので下記ルールに則って確認してみます。対象はAtCoderの問題の例題1を使用します。

  • 選べるタスクの中で、終了時間が最も早いものを選ぶことを繰り返す

終了時間が最も早いものから選択対象となるので、Xi - Li と Xi + Li を計算し、 Xi + Liにてソートします。 f:id:c_taquna:20200119170752j:plain:w600

そして、ソートした後の要素を順に見ていきます。 f:id:c_taquna:20200119170820j:plain:w600 まず( -2, 6)は最初の要素なので取ることができます。次の要素は開始が最初の要素にかぶっているので取れません。その次はどこにもかぶってないので取ることができます。非常に簡単な例ですが、この様にしていくことで残すロボットを最大化できます。これが区間スケジューリングです。

実装

実装の全体へのリンクを載せます。 github.com

下記実装が、ソートする部分です。

long N = long.Parse(Console.ReadLine());
Tuple<long, long>[] t = new Tuple<long, long>[N];
for (int i = 0; i < N; i++)
{
    long[] input = Console.ReadLine().Split().Select(long.Parse).ToArray();
    t[i] = Tuple.Create(input[0] - input[1], input[0] + input[1]);
}
t = t.OrderBy(x => x.Item2).ToArray();

下記実装が区間スケジューリングの部分です。

long res = 0; 
long end = int.MinValue;
for (int i = 0; i < N; i++)
{
    if (end <= t[i].Item1)
    {
        res++;
        end = t[i].Item2;
    }
}
Console.WriteLine(res);