fc2ブログ
ヽ|∵|ゝ(Fantom) の 開発blog? ホーム »二分探索
このページの記事一覧

【C#】ソート済み List を動的に作る拡張メソッド  


 今回は二分探索によるリスト処理の高速化できる拡張メソッドを作ってみよう。

 普段、何気なく List に値を Add したり、Remove したりしてはいないだろうか?
 また検索というと、標準で使える List.ContainsList.FindList.FindIndex 等や、Linq の FistOrDefault などを使う機会も多いだろう。
 しかし、これらは逐次探索(O(n))のため、要素数に比例して、検索速度は遅くなる。特に1000を超えるデータを頻繁に扱うとなると、かなり実行速度に影響が出る。

 そのような場合、ケースにもよるのだが、List に追加する要素の順番は必要ないとき、または ID のように昇順で並んでいる方が便利なとき等、常に List をソート済みにしておくと、二分探索(BinaraySearch)が使えるようになる。
 二分探索のオーダーは O(log n) のため、要素が多くなっても、検索速度はそれほど落ちない。

 もし、ただの1回のみのソートで良いのなら、全て要素を追加した後に、単純に List.Sort や Linq の OrderBy を使う方が良いだろう。しかし、頻繁にデータを追加したり、削除したりする場合は、動的にソート済みリストを作っていく拡張メソッドを作っておくと便利だ。



●動的にソート済みリストに要素を追加、削除していく拡張メソッド
using System.Collections.Generic;

public static class Extentions //※名前は任意
{
/// <summary>
/// 昇順で要素を追加する
/// 2021/02/02 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-392.html
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list">対象リスト</param>
/// <param name="value">追加する要素</param>
/// <returns>true = 新規追加 / false = 既存あり(重複で追加)</returns>
public static bool AddSorted<T>(this List<T> list, T value)
{
int idx = list.BinarySearch(value);
if (idx < 0)
{
list.Insert(~idx, value);
return true;
}
list.Insert(idx, value);
return false;
}

/// <summary>
/// 昇順で要素をユニーク追加する
/// 2021/02/02 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-392.html
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list">対象リスト</param>
/// <param name="value">追加する要素</param>
/// <returns>true = 新規追加 / false = 既存あり(追加しない)</returns>
public static bool AddSortedUniq<T>(this List<T> list, T value)
{
int idx = list.BinarySearch(value);
if (idx < 0)
{
list.Insert(~idx, value);
return true;
}
return false;
}

/// <summary>
/// 昇順のリストから要素を削除
/// 2021/02/02 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-392.html
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="list">対象リスト</param>
/// <param name="value">削除する要素</param>
/// <returns>true = 削除した / false = 既存なし(削除失敗)</returns>
public static bool RemoveSorted<T>(this List<T> list, T value)
{
int idx = list.BinarySearch(value);
if (idx >= 0)
{
list.RemoveAt(idx);
return true;
}
return false;
}
}

●メインコード例
using System;
using System.Collections.Generic;

string[] array =
{
"Apple", "Banana", "Candy", "Donuts", "Egg", "Fish", "Candy", "Egg"
};

array.Shuffle(); //※Shuffle は以前の記事を参照
Console.WriteLine(array.Dump()); //※Dump は以前の記事を参照

var list1 = new List(array.Length);
var list2 = new List(array.Length);
foreach (var item in array)
{
list1.AddSorted(item); //昇順で追加
list2.AddSortedUniq(item); //昇順でユニーク追加
}

Console.WriteLine("list1 : " + list1.Dump()); //※Dump は以前の記事を参照
Console.WriteLine("list2 : " + list2.Dump()); //※Dump は以前の記事を参照

//昇順リストから削除
list1.RemoveSorted("Candy");
list1.RemoveSorted("Egg");
Console.WriteLine("list1 (Removed) : " + list1.Dump()); //※Dump は以前の記事を参照
Console.WriteLine("list2 (Removed) : " + list2.Dump()); //※Dump は以前の記事を参照

//二分探索
int idx1 = list1.BinarySearch("Candy");
Console.WriteLine($"idx1 = {idx1}");
int idx2 = list1.BinarySearch("Egg");
Console.WriteLine($"idx2 = {idx2}");

[Banana, Egg, Apple, Candy, Egg, Donuts, Fish, Candy]
list1 : [Apple, Banana, Candy, Candy, Donuts, Egg, Egg, Fish]
list2 : [Apple, Banana, Candy, Donuts, Egg, Fish]
list1 (Removed) : [Apple, Banana, Candy, Donuts, Egg, Fish]
list2 (Removed) : [Apple, Banana, Candy, Donuts, Egg, Fish]
idx1 = 2
idx2 = 4

 DumpShuffle の拡張メソッドは以前に作ったものだ。

Dump の拡張メソッド
Shuffle の拡張メソッド

 どのように追加されているかに注目して欲しい。常に昇順に並び替えたリストと同義となるため、追加するたびソート(List.Sort)する必要はもちろん無い。
 取得するときは List.BinarySearch が使えるので、ちょっとやそっと要素数が増えた所で、検索速度が落ちることはない(実際に 100000 [10万] くらいの要素数があっても、検索速度は速い)。

 また、この例ではコンストラクタで new List(int capacity) を使っているが、これは予め格納できるサイズを確保しておくコンストラクタだ。色々なコードを見ていても、ほとんどが引数なし new List() で使われているが、実はこれはとても重要だ。と言うのは、List の様な動的に増やせるコレクションというのは、現在格納できるサイズを超えると、メモリの再割当てをし(サイズ拡大)、コピーする動作を頻繁に繰り返すからだ。要素が多くなると、負荷がかかるのは言うまでもない。はじめからわかっている、または想定できるなら、capacity を指定した方が、要素を追加するとき、無駄な処理が走らない(Dictionary なども同じ)。

 以下は Unity 上で、毎フレーム逐次検索(List.ContainsList.Find)していた箇所を、二分探索探索に置き換えただけのもので、パフォーマンスを比較してみたものだ。
 青い部分はスクリプトの負荷で、激減していることが分かる。実際にほんの数行置き換えただけなのだが、圧倒的なパフォーマンスを出すことができることさえある(この時は約5倍以上になった)。



 ここでは、List.BinarySearch しか使ってないが、以前に紹介した LowerBounds, UpperBounds も併用して使うと良いだろう。

【C#】 LowerBound, UpperBound (二分探索)

 二分探索は上手く使えば、かなりのパフォーマンス改善になる場合があることも少なくない。開発現場では簡潔だからか Linq が使われることが多い気がするが、実はとても遅く、GCを発生させ易いので、利用には注意が必要だ(要は使い処である)。

Unity のパフォーマンスに関する推奨事項 - コストのかかる操作を避ける
LINQとfor, ListとArray(配列)での実行速度を比較してみる
【Unity】CPUプロファイラでパフォーマンスを改善する - GCの発生要因を減らす






(関連記事)
【C#】配列やListなどの中身(要素)を見る拡張メソッド Dump
【Unity】【C#】配列・リストのシャッフル
【C#】二分探索の実装(範囲インデクス指定実装)
【C#】 LowerBound, UpperBound (二分探索)
【Ruby】二分探索, lower_bound, upper_bound
【Unity】【C#】LINQとfor, ListとArray(配列)での実行速度を比較してみる
【C#】連想配列(Dictionary)を値 or キーでソート
【C#】2次元配列(ジャグ配列・多次元配列)のソート
【C#】多次元配列とジャグ配列(2次元配列)のサイズ(長さ)、相互変換など


関連記事
スポンサーサイト



category: C#

thread: プログラミング

janre: コンピュータ

tag: C#ライブラリ  配列操作  二分探索 
tb: 0   cm: --

【C#】二分探索の実装(範囲インデクス指定実装)  


 ただの実装用メモ。元々標準ライブラリで BinarySearch は実装されているので(配列も同様)、利用するだけなら、自分で実装する必要はない。ちょっと改造して、検索を高速化したいときにアルゴリズムだけ利用することもあるので(Java の標準ライブラリはすぐ実装見れるが、C# の標準ライブラリってメタデータだけだったりするので)、確認用に書いておいた。

●インデクスで範囲指定の二分探索の実装
using System;
using System.Collections.Generic;

/// <summary>
/// インデクスで範囲指定の二分探索
/// 2019/10/11 Fantom
/// http://fantom1x.blog130.fc2.com/blog-entry-338.html
/// </summary>
/// <typeparam name="T">比較する値の型</typeparam>
/// <param name="arr">対象の配列(※ソート済みであること)</param>
/// <param name="start">開始インデクス [inclusive]</param>
/// <param name="end">終了インデクス [exclusive]</param>
/// <param name="value">検索する値</param>
/// <param name="comparer">比較関数(インターフェイス)</param>
/// <returns>0 以上のとき見つかったインデクス / 0 より小さいとき ~index (ビット反転)で挿入位置になる</returns>
public static int BinarySearch<T>(this List<T> list, int start, int end, T value, IComparer<T> comparer)
{
int low = start;
int high = end - 1;
int mid;
while (low <= high)
{
mid = ((high - low) >> 1) + low;
if (comparer.Compare(list[mid], value) < 0)
{
low = mid + 1;
}
else if (comparer.Compare(list[mid], value) > 0)
{
high = mid - 1;
}
else
{
return mid;
}
}
return -(low + 1);
}

//引数省略のオーバーロード
public static int BinarySearch<T>(this List<T> list, int start, int end, T value)
{
return BinarySearch(list, start, end, value, Comparer<T>.Default);
}

 実際には以前に書いた、LowerBound, UpperBound(配列で実装してるが、List でも同じ)と BinarySearch を使い分けると便利だったりする。大きな違いは検索結果のインデクスの値で、例えば BinarySearch では同値が並んでいるとどのインデクスになるかは不定だが、LowerBound を使うと同値の最初のインデクス、UpperBound を使うと同値の最後の次のインデクスとなる(「検索する値の個数を求める」の例がわかり易いと思う)。

 私の場合、一部のプロパティを二分探索で検索するために(例えば常にインクリメントする ID 番号など)、上記の実装を少し改造して使うことも多い。大量にデータが増えたとき、二分探索はその効果を発揮する(よく FindIndexFirstOrDefault で実装されている場合も多いが、逐次検索なので大量のデータには向かない)。LowerBound, UpperBound と共に使えば、かなりの高速化ができるので、色々やってみると良いだろう。





(関連記事)
【C#】 LowerBound, UpperBound (二分探索)
【Ruby】二分探索, lower_bound, upper_bound
【Java】lower_bound(), upper_bound() 関数を作る
【Java】二分探索を汎用的に使えるようにする


関連記事

category: C#

thread: プログラミング

janre: コンピュータ

tag: C#ライブラリ  二分探索 
tb: 0   cm: --

【C#】 LowerBound, UpperBound (二分探索)  


 私は時折、頭の体操代わりにプロコン問題を解いたりしているのだが、使う言語はその時々で変えてるので、関数によっては無いと不便に思うものがある。LowerBound, UpperBound はその代表例かな。ものによっては BinarySearch でも同じ結果を得られるものもあるが、やはりあった方が良いので改めて書いておくことにした。内容的には JavaRuby 版と同じ。使い所を覚えれば、簡潔かつ高速に処理できるので、二分探索系のアルゴリズムは知っておいても損はないだろう。




■C# 版 LowerBound, UpperBound

using System;
using System.Collections.Generic;

/// <summary>
/// 指定した値以上の先頭のインデクスを返す (二分探索)
/// 2017/08/04 Fantom
/// http://fantom1x.blog130.fc2.com//blog-entry-256.html
/// </summary>
/// <typeparam name="T">比較する値の型</typeparam>
/// <param name="arr">対象の配列(※ソート済みであること)</param>
/// <param name="start">開始インデクス [inclusive]</param>
/// <param name="end">終了インデクス [exclusive]</param>
/// <param name="value">検索する値</param>
/// <param name="comparer">比較関数(インターフェイス)</param>
/// <returns>指定した値以上の先頭のインデクス</returns>
public static int LowerBound<T>(T[] arr, int start, int end, T value, IComparer<T> comparer)
{
int low = start;
int high = end;
int mid;
while (low < high)
{
mid = ((high - low) >> 1) + low;
if (comparer.Compare(arr[mid], value) < 0)
low = mid + 1;
else
high = mid;
}
return low;
}

//引数省略のオーバーロード
public static int LowerBound<T>(T[] arr, T value) where T : IComparable
{
return LowerBound(arr, 0, arr.Length, value, Comparer<T>.Default);
}

/// <summary>
/// 指定した値より大きい先頭のインデクスを返す (二分探索)
/// 2017/08/04 Fantom
/// http://fantom1x.blog130.fc2.com//blog-entry-256.html
/// </summary>
/// <typeparam name="T">比較する値の型</typeparam>
/// <param name="arr">対象の配列(※ソート済みであること)</param>
/// <param name="start">開始インデクス [inclusive]</param>
/// <param name="end">終了インデクス [exclusive]</param>
/// <param name="value">検索する値</param>
/// <param name="comparer">比較関数(インターフェイス)</param>
/// <returns>指定した値より大きい先頭のインデクス</returns>
public static int UpperBound<T>(T[] arr, int start, int end, T value, IComparer<T> comparer)
{
int low = start;
int high = end;
int mid;
while (low < high)
{
mid = ((high - low) >> 1) + low;
if (comparer.Compare(arr[mid], value) <= 0)
low = mid + 1;
else
high = mid;
}
return low;
}

//引数省略のオーバーロード
public static int UpperBound<T>(T[] arr, T value)
{
return UpperBound(arr, 0, arr.Length, value, Comparer<T>.Default);
}

 ついでに引数省略のオーバーロードも書いてあるが、実際にはもう何パターンかあった方が便利かも知れない(全要素+IComparer指定 / 範囲指定+IComparer省略など)。もし予め検索の範囲が絞りこめるなら、更に高速に処理できるので範囲指定はオススメだ。プロコン問題やリアルタイム処理が必要な高負荷アプリケーションなどでは積極的に使っていこう。

 ちなみに返ってくるインデクスは LowerBound は「指定した値以上の先頭のインデクス」で、UpperBound は「指定した値より大きい先頭のインデクス」になる。BinarySearch の場合は、値が見つかった場合はそのインデクスが返り、見つからないとき負の値となり、ビット反転することで「指定した値より大きい先頭のインデクス」が返る(つまり毎回正負を調べる必要がある)。完全一致の有無を調べるには BinarySearch の方が便利だが、一番近い値のインデクス見つけるなら LowerBound や UpperBound の方が便利だ。使い分けると良いだろう。

 以降は簡単な使用例なので、既に知っているなら読み飛ばしても良いだろう。



■いくつかの値の範囲で何番目の要素かを求める

 例えば以下のような表があり、「ある風速に対して風力を求める」問題があったとする。実際にこの問題は AtCoder の練習問題の一部を抜粋したものだが、色々な人の解答例を見ていると(AtCoderyukicoder などは他の人の解答を見れるので勉強になる)、逐次検索または if 文で頭から値を比較しているものが圧倒的に多い。しかしこの表のように単調増加なものなら、二分検索が使えると考えて良い。この場合は UpperBound を使うと簡単だ。

風力風速
風力00.0m⁄s 以上 0.2m⁄s 以下
風力10.3m⁄s 以上 1.5m⁄s 以下
風力21.6m⁄s 以上 3.3m⁄s 以下
風力33.4m⁄s 以上 5.4m⁄s 以下
風力45.5m⁄s 以上 7.9m⁄s 以下
風力58.0m⁄s 以上 10.7m⁄s 以下
風力610.8m⁄s 以上 13.8m⁄s 以下
風力713.9m⁄s 以上 17.1m⁄s 以下
風力817.2m⁄s 以上 20.7m⁄s 以下
風力920.8m⁄s 以上 24.4m⁄s 以下
風力1024.5m⁄s 以上 28.4m⁄s 以下
風力1128.5m⁄s 以上 32.6m⁄s 以下
風力1232.7m⁄s 以上

●UpperBound を使っての解答例
//※・・・関数などは略・・・

//表の各要素の開始の風速を配列にしたもの
double[] WindPower = {
0.0, 0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.8, 24.5, 28.5, 32.7
};

double ms = 15.3; //※与えられた風速とする(m/s)
int wp = UpperBound(WindPower, ms) - 1;
Console.WriteLine("風速" + wp);

風速7

 実はこの問題の場合は BinarySearch でも事足りる。しかし BinarySearch の場合、値が見つかった時と見つからなかったときで処理を分岐する必要があり、冗長となることも多い。

●Array.BinarySearchを使っての解答例
//※・・・関数などは略・・・

//表の各要素の開始の風速を配列にしたもの
double[] WindPower = {
0.0, 0.3, 1.6, 3.4, 5.5, 8.0, 10.8, 13.9, 17.2, 20.8, 24.5, 28.5, 32.7
};

double ms = 15.3; //※与えられた風速とする(m/s)
int wp = Array.BinarySearch(WindPower, ms);
Console.WriteLine("風速" + (wp < 0 ? ~wp - 1 : wp)); //見つからなかったときビット反転(~)するとUpperBoundと同じになる

風速7

 実際にはこの問題くらいの要素数なら逐次検索でも構わないのだが、要素数が何千とあったり、60fpsで動作するゲームなどでフレーム毎で使用する必要などあった場合、検索回数を大幅に減らせるので効果はてきめんとなる。



■検索する値の個数を求める

 次によくあるパターンだが、ある数が連続して並んでいるとき、その範囲や個数を調べる方法などもある。例えば以下のような表があったとき、「2~4 の値を持つインデクスの範囲とその要素数を求めたい」としよう。一番初めに思い浮かぶアルゴリズムは先頭から検索していき、2 が出てきた所で開始インデクスを記録、4 より上の値が出てきた所で終了インデクスを記録するといった感じだろうか。開始と終了の差を取ればその個数もわかる。しかし、ここでは LowerBound, UpperBound を使ってみよう。

Index[0][1][2][3][4][5][6][7][8][9][10][11][12]
Value0011122344455

//※・・・関数などは略・・・

int[] arr = {0, 0, 1, 1, 1, 2, 2, 3, 4, 4, 4, 5, 5}; //表
int low = 2; //検索する最小値
int high = 4; //検索する最大値
int from = LowerBound(arr, low); //検索する最小値で最初のインデクス
int to = UpperBound(arr, high); //検索する最大値を超えた最初のインデクス
Console.WriteLine("from = " + from + ", to = " + to + ", count = " + (to - from));

from = 5, to = 11, count = 6

 この例も要素数が少ない場合は逐次検索でもそれほど大差ないが、何千・何万という要素数でなら、圧倒的な速さとなる。



 私の見たところ、実際の開発現場でのコードでも逐次検索は多用されている感じがあるので、データが単調増加ないし単調減少なら二分探索を取り入れると良いと思う。確実に高速化できるし、もしデータを順に並べて良いものなら、予めリアルタイムにソート済みデータを作っていくアルゴリズムを組んでおけば、何かと便利なことが多い(Ruby 版だが「ソート済み配列を作る」方法もある[※C# なら List にすると良い])。

 この辺りは慣れだと思うが、常に検索回数を減らせるような工夫を忘れないようにすれば、paizaCodeIQ などの上級者問題(A, S ランク)も解けるようになる(タイムアウトしなくなる)。挑戦してみると良いだろう。





(関連記事)
【C#】二分探索の実装(範囲インデクス指定実装)
【Ruby】二分探索, lower_bound, upper_bound
【Java】lower_bound(), upper_bound() 関数を作る
【Java】二分探索を汎用的に使えるようにする


関連記事

category: C#

thread: プログラミング

janre: コンピュータ

tag: C#ライブラリ  配列操作  二分探索 
tb: 0   cm: --

【Ruby】二分探索, lower_bound, upper_bound  


 Ruby2.3 以降には見つけた値のインデクスを返す Array.bsearch_index() があるのだが、自作した方が使い勝手が良いことも多いので、Java 版をそのまま移植してみた。


なお、組み込み関数の場合は以下のようになる。

●Ruby の bsearch_index で lower_bound, upper_bound 的な動作 (Ruby 2.3以降)
     #[0][1][2][3][4][5][6][7][8][9[10[11[12[13[14]
arr = [1, 1, 3, 3, 3, 4, 5, 5, 6, 6, 6, 6, 7, 8, 8];
v = 3;
lower = arr.bsearch_index{|x| x >= v} #lower_bound 的(ただし、見つからない=nil) (Ruby 2.3以降)
upper = arr.bsearch_index{|x| x > v} #upper_bound 的(ただし、見つからない=nil) (Ruby 2.3以降)
print "lower = #{lower}\n"
print "upper = #{upper}\n"

lower = 2
upper = 5

 値の意味は lower_bound 的な方は「指定した値以上の先頭の要素を返す」ことになるので、配列 arr 内で v = 3 が続く値の先頭インデクス = 2 を、upper_bound 的な方は「指定した値より大きい先頭の要素を返す」ことになるので、v = 3 が続く値より後ろのインデクス = 5 という意味になる。

 ちなみに Array.bsearch() の方は要素の値そのものを返す。今回の関数と組み込み関数との大きな違いは、値が見つからないとき nil (組み込み関数)か、近いインデクスを返す(今回の関数)かという感じだ。実は見つからないとき値を返すようにしておくと、色々利用できるのでわざわざ Java から(元はC++) 移植したわけだ。

●二分探索:binary_search, lower_bound, upper_bound (オープンクラス版)
#オープンクラス版
class Array
#二分探索(同値連続はインデクス不定となる)
#見つからないとき=負の値となり、~index で挿入位置になる
def binary_search(value, from = 0, to = self.size)
low = from
high = to - 1
while low <= high
mid = (low + high) / 2
if self[mid] < value
low = mid + 1
elsif self[mid] > value
high = mid - 1
else
return mid
end
end
-(low + 1) #見つからない(~index で挿入位置)
end

#指定した値以上の先頭のインデクスを返す
def lower_bound(value, from = 0, to = self.size)
low = from
high = to
while low < high
mid = (low + high) / 2
if self[mid] < value
low = mid + 1
else
high = mid
end
end
low
end

#指定した値より大きい先頭のインデクスを返す
def upper_bound(value, from = 0, to = self.size)
low = from
high = to
while low < high
mid = (low + high) / 2
if self[mid] <= value
low = mid + 1
else
high = mid
end
end
low
end

end


#メインでは...
#[0][1][2][3][4][5][6][7][8][9[10[11[12[13[14]
arr = [1, 1, 3, 3, 3, 4, 5, 5, 6, 6, 6, 6, 7, 8, 8];
v = 3; #検索する値
print "binary_search = #{arr.binary_search(v)}\n"
print "lower_bound = #{arr.lower_bound(v)}\n"
print "upper_bound = #{arr.upper_bound(v)}\n"

print "bsearch_index(lower) = #{arr.bsearch_index{|x| x >= v}}\n" #lower_bound 的(ただし、見つからない=nil) (Ruby 2.3)
print "bsearch_index(upper) = #{arr.bsearch_index{|x| x > v}}\n" #upper_bound 的(ただし、見つからない=nil) (Ruby 2.3)

binary_search = 3
lower_bound = 2
upper_bound = 5
bsearch_index(lower) = 2
bsearch_index(upper) = 5

 意味は組み込み関数の例とだいたい同じになる。違いは値が見つからないときの戻値だ。例えば検索する値を v = 2 とすると、組み込み関数の場合 nil となるが、lower_bound()、upper_bound() は 2 となる。binary_search() の場合は -3 という値が返るが、この値はビット反転すると(~index = ~(-3))2 となる。つまり、binary_search() の場合、同値が連続しているとき、どのインデクスになるかは不定だが、負の値が返ってきたとき見つからなかったことがわかるので、処理分岐がしやすい利点がある。

 また、Ruby には関数のオーバーロードがないので、検索範囲を決める引数(from = 開始インデクス、to = 終了インデクス[このインデクスは含まない] はデフォルト値を設定してある(デフォは配列の最初から最後まで)。検索するインデクス範囲がわかっているなら、絞り込めば更に高速に動作する。余談だが、範囲を決める引数を関数に持たせる場合、終了インデクスは「含まない」としておくと、差分計算や連続検索などしやすくなる(標準ライブラリ等もだいたいそうなっている)。

 あと lower_bound() も upper_bound() も要素の最大より上の値を検索すれば、配列の長さと同じ値が返ってくる(例えば、v = 9 とすれば、どちらもインデクス = 15 が返る)。これらは値が見つからないとき、配列の最後に値を追加するのに使える。この方法を使えば、リアルタイムにソート済み配列を作ることも可能だ。試しにやってみよう。

●二分探索でソート済み配列を作る
arr = [3, 5, 6, 4, 2, 0, 8, 7, 1, 9]
sorted = []

arr.each{|e|
sorted.insert(sorted.upper_bound(e), e)

# sorted.insert(sorted.lower_bound(e), e)

# idx = sorted.binary_search(e)
# if idx < 0
# idx = ~idx
# end
# sorted.insert(idx, e)
}

p arr
p sorted

[3, 5, 6, 4, 2, 0, 8, 7, 1, 9]
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

 この場合は binary_search(), lower_bound(), upper_bound() どれを使っても同じになる。しかし実際には同値がある場合、binary_search() は同値の中ではそのどこかに、lower_bound() の場合は同値の先頭に、upper_bound() の場合は同値の一番最後(=検索値の次の値の先頭)に挿入されることになる。追加順序が必要な場合は使い分けた方が良いだろう。もちろんこの例の場合は最後にソート(Array.sort())でも良いが、リアルタイムに追加・削除を繰り返す場合は、何度もソートするより負荷が軽くなる。

 もう1つ、lower_bound() と upper_bound() を上手く使う例として、指定範囲内の値の個数を求めてみよう。

●lower_bound() と upper_bound() で検索範囲内の値の個数を求める
arr = [0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 6, 6, 6, 7, 8, 9, 9]

lo, hi = 1, 3 #検索する値の範囲
fr = arr.lower_bound(lo) #開始インデクスを表す
to = arr.upper_bound(hi) #終了インデクスを表す[このインデクスは含まない]

p arr
print "fr = #{fr}, to = #{to}, cnt = #{to - fr}\n" #差分が個数
p arr.slice(fr, to - fr) #範囲を抜き出し

[0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 6, 6, 6, 7, 8, 9, 9]
fr = 3, to = 17, cnt = 14
[1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3]

 この例は配列の中から、検索する値 = 1~3 のインデクス範囲(3~17 [17は含まない])を取得し、差分から個数を求めている。Array.slice() はおまけでその範囲を抜き出して表示しているだけだ。もちろんこのくらいの数なら逐次検索でもそれほど差がないかも知れないが、要素数が多くなると効果はてきめんとなる。

 最後にオープンクラスで使いたくない人のためにも通常のメソッドでも書いておこう。内容は全く同じになるので、好きな方を使えば良い。

●二分探索:binary_search, lower_bound, upper_bound (通常メソッド版)
#二分探索(同値連続はインデクス不定となる)
#見つからないとき=負の値となり、~index で挿入位置になる
def binary_search(arr, value, from = 0, to = arr.size)
low = from
high = to - 1
while low <= high
mid = (low + high) / 2
if arr[mid] < value
low = mid + 1
elsif arr[mid] > value
high = mid - 1
else
return mid
end
end
-(low + 1) #見つからない(~index で挿入位置)
end

#指定した値以上の先頭のインデクスを返す
def lower_bound(arr, value, from = 0, to = arr.size)
low = from
high = to
while low < high
mid = (low + high) / 2
if arr[mid] < value
low = mid + 1
else
high = mid
end
end
low
end

#指定した値より大きい先頭のインデクスを返す
def upper_bound(arr, value, from = 0, to = arr.size)
low = from
high = to
while low < high
mid = (low + high) / 2
if arr[mid] <= value
low = mid + 1
else
high = mid
end
end
low
end


#メインでは...
#[0][1][2][3][4][5][6][7][8][9[10[11[12[13[14]
arr = [1, 1, 3, 3, 3, 4, 5, 5, 6, 6, 6, 6, 7, 8, 8];
v = 3; #検索する値

print "binary_search = #{binary_search(arr, v)}\n"
print "lower_bound = #{lower_bound(arr, v)}\n"
print "upper_bound = #{upper_bound(arr, v)}\n"

s, e = 3, 6 #検索する値の範囲
fr = lower_bound(arr, s)
to = upper_bound(arr, e)
print "fr = #{fr}, to = #{to}, cnt = #{to - fr}\n"

binary_search = 3
lower_bound = 2
upper_bound = 5
fr = 2, to = 12, cnt = 10


 オンラインジャッジの問題でも上級レベルになるほど、逐次検索では計算量が間に合わずタイムアウトしやすくなる。CodeIQ でも paiza でもランク A, S(★★★★)問題には必要となるシーンも多いので、使える場合は積極的に使ってみると良いだろう(要するに単純な昇順・降順データ・累積和・フィボナッチのような増加数列には使った方が安全)。標準ライブラリにあるものでも、その実装に近いものを知っていれば柔軟に対応できるようになるので、色々改造して使うと良いだろう。


(関連記事)
【Java】lower_bound(), upper_bound() 関数を作る
【Java】二分探索を汎用的に使えるようにする


■参考になる書籍


関連記事

category: Ruby

thread: プログラミング

janre: コンピュータ

tag: 配列操作  二分探索 
tb: 0   cm: --

【Java】二分探索を汎用的に使えるようにする  


 色々な問題を解いていると、二分探索で簡単に解けるものもあるのだが、毎回同じコードを書くのは少しメンドイね。なので探索部分と条件判別部分を分け、探索絞り込み方向だけを返すインターフェイスを作って、二分探索のコードを使いまわす方法をやってみよう。条件だけ変えて、色々な値を二分探索でテストしてみたいときにも良いだろう。

 まずは、毎回入れ替える条件部分のコードを独立させるためのインターフェイスを作る。値を1つ受け取り、真偽値を返すだけのシンプルなものだ。

●二分探索の条件関数用インターフェイス
public interface ICondition {
boolean condition(double x);
}

※JDK1.8 以降なら、インタフェース「DoublePredicate」の「test()」メソッドを使うのも良い。


 次に、二分探索の本体を作る。関数は実数と整数用で2つ用意した。特に実数は条件によっては無限ループしかねないので、単純回数分のループとなっている。実数演算はどうしても誤差が出るのだが、精度としてはこれで十分だろう。

●値の二分探索関数(実数, 整数用の2つ)
/**<h1>条件にあった値を二分探索で求める(実数)</h1>
* <p>条件判別関数は、等しいかより値を大きくとるとき = true となる関数を作る。</p>
* @param low : 探索範囲下限
* @param high : 探索範囲上限
* @param c : 条件判別関数インターフェイス
* @return<b>double</b> : 探索で見つけた値
*/
public static final double binarySearch(double low, double high, final ICondition c) {
double mid = 0;
for (int i = 0; i < 100; i++) { //無限ループ対策
mid = (high - low) / 2 + low; //(low + high) / 2 (オーバーフロー対策)
if (c.condition(mid)) {
low = mid;
} else {
high = mid;
}
}
return low;
}


/**<h1>条件にあった値を二分探索で求める(整数)</h1>
* <p>条件判別関数は、等しいかより値を大きくとるとき = true となる関数を作る。</p>
* @param low : 探索範囲下限
* @param high : 探索範囲上限
* @param c : 条件判別関数インターフェイス
* @return<b>int</b> : 探索で見つけた値
*/
public static final int binarySearch(int low, int high, final ICondition c) {
int mid = 0;
while (high - low > 1) {
mid = ((high - low) >>> 1) + low; //(low + high) / 2 (オーバーフロー対策)
if (c.condition(mid)) {
low = mid;
} else {
high = mid;
}
}
return low;
}

※ IConditionインターフェイスはそれぞれ型によって、IntPredicate, LongPredicate, DoublePredicate (JDK1.8 以降) などに置き換えても良い(もちろん、引数の異なった複数の関数を書く必要があるが…)。


 インターフェイスを噛ませて作る関数「boolean condition(double x)」の戻値は、「解がもっと大きな値をとる場合 true、解がもっと小さな値をとる場合 false」となるように作る。これは二分探索の探索方向(絞り込む範囲)を定めるためのものだ。これは図解でのイメージと、実際の問題を例にあげて理解してみよう。


●二分探索は mid を基準に、探索範囲を半分にしながら、x を絞り込む

●問題例1
 長さのリストが A であるような N 本の紐があります。これらの紐を切って、同じ長さの紐を K 本つくるときの最長の長さを求めなさい。(参考) アリ本 P.129

●解答例1 (実数での二分探索)
public class BinarySearchTest {
public static void main(String[] args) throws Exception {
new BinarySearchTest();
}

double[] A; //長さのリスト
int N; //リストの要素数
int K; //作る本数

public BinarySearchTest() {
A = new double[]{8.02, 7.43, 4.57, 5.39};
N = A.length;
K = 11;

double ans = binarySearch(0.0, 1e5, new ICondition() {
//・長さ x で K より多く切れるとき → もっと x を長くできる = true
//・長さ x で K より少なく切れるとき → もっと x を短くする = false
@Override
public boolean condition(double x) {
int cnt = 0;
for (int i = 0; i < N; i++) {
cnt += (int)(A[i] / x); //作れる本数
}
return (cnt >= K);
}
});

System.out.println(ans); //2.005
}

//※ binarySearch() 関数は省略
}

2.005

 上の例では、条件判別の関数「boolean condition(double x)」を匿名クラスで作っているが、メインコードのクラスに implements して使っても良い。

●解答例1b (インターフェイスを implements して使う)
public class BinarySearchTest implements ICondition {
public static void main(String[] args) throws Exception {
new BinarySearchTest();
}

double[] A; //長さのリスト
int N; //リストの要素数
int K; //作る本数

public BinarySearchTest() {
A = new double[]{8.02, 7.43, 4.57, 5.39};
N = A.length;
K = 11;

double ans = binarySearch(0.0, 1e5, this);
System.out.println(ans); //2.005
}

//・長さ x で K より多く切れるとき → もっと x を長くできる = true
//・長さ x で K より少なく切れるとき → もっと x を短くする = false
@Override
public boolean condition(double x) {
int cnt = 0;
for (int i = 0; i < N; i++) {
cnt += (int)(A[i] / x); //作れる本数
}
return (cnt >= K);
}

//※ binarySearch() 関数は省略
}

2.005


 方程式の解などでも構わない。ただし式が単調増加(単調減少)のものに限る。

●問題例2
 x >= 0 のとき、x3 + x2 + x = 1 の解 x を求めなさい。(参考) ヒョウ本 P.253 (解答例はない)

●解答例2 (方程式の解を求める二分探索)
double ans = binarySearch(0.0, 1.0, new ICondition() {
//・x = 0 とすると、f(x) = 0、x = 1 とすると、f(x) = 3 から、x = 0.0~1.0 となる。
//・f(x) の解が 1 以下のとき x はもっと大きい = true
//・f(x) の解が 1 より上のとき x はもっと小さい = false
@Override
public boolean condition(double x) {
return (x * x * x + x * x + x <= 1.0);
}
});

System.out.println(ans); //0.5436890126920764

//※ メインコード, binarySearch() 関数は省略

0.5436890126920764


 もう少し複雑な問題もやってみよう。

●問題例3
 自動車ローンは固定利率で計算されます。ローンの残高は表示価格から始まり、各月において、月々の利子が残高に加わります。月々の利率は年間利率の 1/12 です。そして、支払い分が残高から引かれます。ローン初期残高(表示価格)を price、月々の支払額を monthlyPayment、ローン回数を loanTerm とするとき、年間利率(%) を求めなさい。(参考) ヒョウ本 P.257

●解答例3 (ローン利率を求める二分探索)
public class BinarySearchTest {
public static void main(String[] args) throws Exception {
new BinarySearchTest();
}

double price; //ローン初期残高
double monthlyPayment; //月々の支払額
int loanTerm; //ローン回数

public BinarySearchTest() {
price = 2000;
monthlyPayment = 510;
loanTerm = 4;

double ans = binarySearch(0.0, 100.0, new ICondition() {
//・残高がマイナスになる → 利率を大きくする = true
//・残高がプラスになる → 利率を小さくする = false
@Override
public boolean condition(double x) {
double balance = price;
for (int i = 0; i < loanTerm; i++) {
balance *= x / 1200.0 + 1.0; //月々の利率
balance -= monthlyPayment;
}
return (balance <= 0);
}
});

System.out.println(ans); //9.56205462458368 (%) (※誤差あり)
}

//※ binarySearch() 関数は省略
}

9.56205462458368


 最後に整数の二分探索の関数も使ってみよう。

●問題例4
 直線上に並ぶ、位置リストが A であるような N 個の小屋があります。M 頭の牛を、他の小屋とできるだけ離れるように入れるとき、最も近い2頭の間隔を最大化しなさい。(参考) アリ本 P.131

●解答例4 (整数での二分探索)
public class BinarySearchTest {
public static void main(String[] args) throws Exception {
new BinarySearchTest();
}

int[] A; //小屋の位置リスト
int N; //リストの要素数
int M; //配置する牛の数

public BinarySearchTest() {
A = new int[]{1, 3, 4, 6, 7, 10, 12, 15, 18, 20, 22, 23, 25, 27};
N = A.length;
M = 4;

int ans = binarySearch(0, A[N - 1], new ICondition() {
//・間隔 x で M 頭の牛が全て収まるとき → もっと x を大きくできる = true
//・間隔 x で M 頭の牛が収まらないとき → もっと x を小さくする = false
@Override
public boolean condition(double x) {
int now = 0;
for (int i = 1; i < M; i++) {
int next = now + 1;
while (next < N && A[next] - A[now] < x) {
next++;
}
if (next == N) {
return false; //M 頭まで配置できない
}
now = next;
}
return true; //M 頭配置できる
}
});

System.out.println(ans); //8
}

//※ binarySearch() 関数は省略
}

8


 二分探索は絞り込む方向だけ間違えなければ、だいたい近い答えが出る。もし明らかにおかしい値が出たら、とりあえず不等号を反転してみると良い(上限または下限値が出たら、方向間違いと疑ってみる)。あとは探索対象が単調増加であることを確かめよう。上手くいけば、全探索より何倍も速くなるので、考慮してみる価値はあるだろう。





(関連記事)
【Java】lower_bound(), upper_bound() 関数を作る


関連記事

category: Java

thread: プログラミング

janre: コンピュータ

tag: 二分探索  アルゴリズム  練習問題 
tb: 0   cm: --


プロフィール

Social

検索フォーム

全記事一覧

カテゴリ

ユーザータグ

最新記事

リンク

PR

▲ Pagetop