simanのブログ

ゆるふわプログラマー。競技プログラミングやってます。Ruby好き

Marathon Match 129 ChickenRun

問題

www.topcoder.com

方針

  • 農家を 2人 1グループにまとめる。奇数の場合は 1つだけ3人のグループになる。
  • 農家を移動させる
  • 移動後の盤面を評価して一番良い配置を決定する

農家のグループ分け

鶏の確保には最低でも 2人必要なので 2人組のグループを作成します。(1人でも確保出来るパターンは存在しますがそんなに多くありません)

f:id:simanman:20210923220118p:plain:w480

f:id:simanman:20210923220132p:plain:w480

奇数の場合は 1つだけ 3人のグループが出来ます。 各グループごとに追いかける鶏を決定し、その鶏を追いかけるようにします。

農家の移動

農家の移動ですが、各農家の移動先の候補は , , , , 待機 の 5つの候補が存在しており、各農家の移動の組み合わせを試そうとした場合 5 ** 農家の人数 のパターンが発生します。この膨大なパターンの中から最適な配置を見つけるために農家の移動方向を配列としてその移動方向を焼き鈍すことで最適な配置を求めました。

f:id:simanman:20210923213836p:plain:w480

遷移は任意の1箇所を選んで方向を変更するです

f:id:simanman:20210923213940p:plain:w480

これで農家が増えてもある程度有効なパターンを探索しやすくなります。

盤面の評価

盤面の評価は以下の項目を中心に確認を行いました。

  • 鶏の確保
  • 鶏が確保できるパターンかどうかの確認
  • 捕獲対象の鶏に接近できているかどうか
  • 捕獲対象の鶏の移動可能範囲を狭めているかどうか

鶏の確保に繋がる可能性が高いほどより高い評価になるようにしています。

鶏の確保

鶏と同じマスに到達した場合はスコアが増えるので評価としては一番高いです。場合によってはあえて捕まえないほうが良くなる場合もありますが、そこまで評価出来ませんでした。

捕まえないほうが良くなるケース

f:id:simanman:20210923222127p:plain:w480

二羽同時に捕まえられるようにするのがここでは最適解となります。

f:id:simanman:20210923222231p:plain:w480

確保できるパターン

鶏が確保できる状態というのは 鶏が移動不可 + 鶏の隣接マスに農家が存在する ことを指します。各鶏についてこの状態になっているかどうかを調べ、そのパターンに一致した場合は評価を高くします。主なパターンとしては以下のようなものがあります。

f:id:simanman:20210923223028p:plain:w600

確定パターンへの誘導

確保出来るパターンについては事前の状態として 鶏の移動候補が 1つ + 移動先に柵または農家が存在している という状態になっているので、この状態についても評価を行います。

f:id:simanman:20210923224255p:plain:w600

確率で確保出来るパターン

鶏の行動は行動する順番も方向もランダムに決定されるので、鶏の移動順や移動方向によっては確定パターンにつながる可能性があります。それらのパターンについても評価を行います。

鶏の移動順によっては確保が可能になるパターン。

f:id:simanman:20210923231041p:plain:w600

鶏の移動先によっては確保が可能になるパターン。

f:id:simanman:20210923231350p:plain:w600

対象の鶏に接近する

対象の鶏に対して接近します。接近する際には対象の鶏より先に到達出来るマスの数を多くするように移動します。(鶏のボロノイ領域が小さくなるように移動する)

まとめ

鶏を確保するための評価関数の中身が複雑になって意図しない挙動も出てしまったので評価関数の設計で失敗してしまった感じ。農家の移動に関しては全体をまとめてやるより特定の領域とかで絞ったほうがもっと効率的に配置を調べられた気もするけど実装する時間がなかったので諦めた。鶏の移動についてランダム性が高いのでちょっと後回しにしてたけど、もうちょっと細かく調査して評価に組み込めば良かったなと思う。

RECRUIT 日本橋ハーフマラソン 2021〜増刊号〜 参加記

問題

atcoder.jp

方針

  • 機械は連結を維持する、ただし序盤は非連結をある程度許容する
  • 野菜の「出現位置」「出現時間」を元にヒートマップを作成してそれを元に機械の配置を決めていく
  • 探索で使用しているパラメータを変化させながらシミュレーションを複数回実行して一番良い結果を得られたものを採用する

ヒートマップの作成

探索の際に「どの位置に機械を配置するか」「どの野菜を回収するか」を決定するためにシミュレーション実行前に作成を行う。 作成方法については実際の出現時間を変更して早めに野菜が出現しているような状態を表現しました。

例えば出現時間が 7 ~ 9 の野菜に対して 4 ~ 9 のように出現時間を早めています。

f:id:simanman:20210913161657p:plain:w400

f:id:simanman:20210913193639p:plain:w400

こうすることで、まだ出現していない野菜に対しても事前に機械を先置き出来るようになります。

収穫する野菜の決定

収穫する野菜について

  • 野菜の価値
  • 周辺のヒートマップの値の合計値
  • 野菜消失までの時間
  • 野菜までの距離

の 4つの要素から決定しました。 コードとして色々なパラメータが絡んでいるので説明し辛いのですが、基本的には「価値 / 距離」をベースにそこから消失までの残り時間を見て、収穫を後回しにしても良いかどうかを評価値に含んでいる感じです。

収穫する野菜までの経路作成

収穫する野菜を決定したら、その野菜をスタート地点としてダイクストラでヒートマップの価値が高い順に探索していき経路を作成しました、ただし経路自体は最短経路になるようにしています。(スタートからの距離のコストを高くしている)

機械の移動

経路を作成後、機械の移動を行います。移動の際には連結を崩さないように移動しても連結が崩れない機械を探索します。この部分については特に難しいことはしておらず、素直にその機械を実際に消して連結状態が維持されているかどうかを再帰探索で調べています。

f:id:simanman:20210913165519p:plain:w400

移動可能な機械を列挙する

f:id:simanman:20210913165624p:plain:w400

列挙後、移動可能な機械の中でヒートマップの値が一番小さいものを移動させます。(ヒートマップの値が高い場合はその後そこに野菜が来る可能性があるのでなるべくその位置に機械を配置するようにする)

f:id:simanman:20210913165858p:plain:w400

この作業を目標の野菜に到達するまで繰り返します。

例外

移動に関して、目標の野菜までの距離が現在の機械の数よりも大きい場合は連結を解除して野菜を収穫しに行きます。

f:id:simanman:20210913170437p:plain:w400

f:id:simanman:20210913170648p:plain:w400

f:id:simanman:20210913170543p:plain:w400

シミュレーションを複数回実行

このシミュレーションを時間制限いっぱい、探索に使用しているパラメータを変化させながらシミュレーションを複数回実行して、その中で一番良い結果が得られたものを採用しています。主に変化させたパラメータとしては以下になります。

  • 購入する機械の数
  • 消失までの残り時間の重み
  • 周辺のヒートマップの重み
  • ヒートマップ作成時の野菜の出現時間をどれだけ早めるか

動作状況

実際どんな感じで動いているかのキャプチャです

f:id:simanman:20210913173851g:plain

My approach of MM126 Slider

It was a matter of efficiently dropping the blocks into the holes.

https://www.topcoder.com/challenges/82eb883d-85e9-442f-b0eb-91631d3d2a8f

方針

First, create an operation array in the order of dropping the base block.

There are several steps involved in creating an operation array.

Improving efficiency of 1-move drop

The first step is to list the blocks that can be dropped in one move, and find the order in which they are dropped by SA.

Since the operation of moving a block affects the operations before and after it, when the order of operations is changed, it is necessary to check each time whether the current operations is valid or not, and this cost is so large that it is difficult to do it.

However, if the order of the moves is reduced to [position of the hole, direction of block acquisition], there is no dependency between the previous and next move, so there is no need to check the feasibility of the move.

f:id:simanman:20210506191317p:plain:w300

For example, if you are in the state shown in the image above

[[Hole 1, LEFT], [Hole 1, LEFT], [Hole 2, UP], [Hole 2, RIGHT], [Hole 1, DOWN], [Hole 2, RIGHT]]

It's like creating a sequence of operations like If you limit yourself to at least one move drop, this will give you a nearly optimal procedure.

However, when I dropped the 1 value block, I shifted it to the side instead of dropping it if it looked good, because shifting it to the side might increase the number of blocks for 2-move operations.

f:id:simanman:20210506212051p:plain:w250

Greedily select operations.

Although 1-move operations are given priority, there are cases where the latter is actually more efficient than "1-move operations twice to drop blocks 2 and 3", such as "2-move operations to drop block 9". For this reason, we chose the one that seemed best while also running the process of greedily deciding the operation in parallel.

It greedily chooses an operation, but if the best operation is more than three moves, it runs a search to see if it can manipulate the board so that it becomes a two-move operation.

f:id:simanman:20210506195519p:plain:w250

For example, in this situation, it would take three moves to drop the 7 block, but by sliding the block one move upper, it can be dropped in two moves.

f:id:simanman:20210506195802p:plain:w250

If you only look at this move, the result is the same: "drop the block of 7 in 3 moves" in both cases, but now that you can drop the block in 2 moves, the efficiency of dropping other blocks will also increase, resulting in a higher score.

f:id:simanman:20210506200531p:plain:w250

To check for the existence of such a board manipulation move, we check the orthogonal lines of "hole" and "block" and see if we can place a block in one square around the point.

f:id:simanman:20210506201025p:plain:w300

We have also included a process to remove the block of 1 to prevent "the block of 1 is blocking the 2-move operation".

f:id:simanman:20210506201441p:plain:w250

f:id:simanman:20210506201544p:plain:w250

Calculate the expected value of the value by getting the block

For overall optimization, the score including several moves ahead is more important than the maximum value at that timing, so I roughly calculated the value and added it to the evaluation.

f:id:simanman:20210506202542p:plain:w250

In the case of this image, processing it as [1, 9] would result in a higher score than processing it as [7, 1]. The evaluation was determined by the following code for the block that exists behind.

value += block_value / d 
d += random_value  # random_value is randomly set to a value between [1.0, 3.0] that is randomly determined before the search.

I adopted it because it scored better than [sum of blocks] / [number of moves required], but I honestly don't know why.

Treat operations as events.

The final sequence of operations will be handled as an array of events, with "drop the block into the hole" as the delimiter. So

1 2 S R
4 6 S U
8 6 S U
6 5 M R
6 6 S U
0 2 M D

Instead of a flat operation array like

[
  [1 2 S R],
  [4 6 S U],
  [
    8 6 S U,
    6 5 M R,
    6 6 S U,
    0 2 M D
  ]
]

It's like having an array of operations grouped in units of "drop blocks".

f:id:simanman:20210506205209p:plain:w400

Change the order of operation of events.

Move and manipulate the array of events obtained at the end as long as the overall operation is not broken. If the move raises the overall score, the replacement is adopted. (Hill Climbing)

The way to do this is to skip the application of the event to be replaced, and continue applying the event until the subsequent event fails, and then calculate the score of "if this event is inserted at this timing" each time and insert it at the timing with the highest value.

Try this both from the front and from the back, and if you can't update the score, you're done. (In the case of backward, it's like doing the reverse operation for each event from the board after all events have been applied.

This is the end of the process.

Event binding process

For certain events, the dependencies are so strong that they cannot be replaced properly, so we try to reduce the number of targets for the reordering process by grouping events to some extent.

f:id:simanman:20210506205626p:plain:w300

For example, in the image, the operation of inserting blocks in the order [7, 6, 9] is almost immutable, so we combined these events as one event instead of three. By grouping the events together, the variation in score that occurs in one replacement process becomes larger, and the range of improvement becomes larger, resulting in a higher score.

To determine whether or not to combine events, we used to judge whether or not the y-axis or x-axis of the coordinates of the first operation was equal. (If they are equal, the same decision is made.)

Repeat the process until the time limit is reached.

The rest of the process (procedure creation -> procedure replacement) will continue until the time limit is reached. Before execution, change the values of various parameters so that a different sequence of operations can be taken each time.

Impressions

I think the difference was in the greed to determine the operating procedure, and I realized once again that I lacked the ability to evaluate the board.

MM126 Slider 参加記

ブロックを効率よく穴に落とす問題でした

https://www.topcoder.com/challenges/82eb883d-85e9-442f-b0eb-91631d3d2a8f

方針

まずベースとなるブロックを落とす順番の操作配列を作成します。

操作配列の作成についてはいくつかステップを踏んでいます

1手落としの効率化

最初は 1手で落とせるブロックを列挙して落とす順番を焼きなましで求めます。

ブロックを動かす操作というのは前後の操作に影響を与えてしまうため、操作の順序を変更した際に現時点での操作が有効かどうかを毎回調べる必要が発生して、このコストがとても大きいので焼き鈍すのは難しいです。

ただ、1手落としの順番に関しては [穴の位置、ブロック取得する方向] の情報に落とし込めば前後の依存関係がなくなるため、実行可能性を調べる必要がなくなり十分高速な処理が可能になります。

f:id:simanman:20210506191317p:plain:w300

例えば上記の画像の状態であれば

[[穴1, 左], [穴1, 左], [穴2, 上], [穴2, 右], [穴1, 下], [穴2, 右]]

のような操作配列を作る感じです。少なくとも 1手落としに限定すればこれでほぼほぼ最適な手順が手に入ります。

ただ、 1のブロックを落とす際には横にずらすことで 2手操作のブロックが増える可能性があるので、良さそうであれば落とすのではなくずらしていました。

f:id:simanman:20210506212051p:plain:w250

貪欲に操作を決定

1手操作を優先的に実行はしますが、実際には「1手操作を 2回行って 1, 2 のブロックを落とす」より「2 手操作を行って 9 のブロックを落とす」のような後者のほうがスコア効率がよくなるケースが存在します。そのため、並行で貪欲に操作を決定する処理も走らせながら良さそうなものを選択しました。

貪欲に操作を決定しますが、一番良い操作が 3手以上の場合は 2手操作になるように盤面を操作できないかどうかを探索する処理を走らせます。

f:id:simanman:20210506195519p:plain:w250

例えばこの状況だと 3手で 7のブロックが落とせますが、1つ上のブロックを上にスライドさせることによって 2手で落とすことが可能となります。

f:id:simanman:20210506195802p:plain:w250

この手だけを見ればどっちも「3 手で 7のブロックを落とす」という結果は同じなのですが、2 手で落とせることになったことで他のブロックの落とす効率も上がるので結果的にスコアが出るようになります。

f:id:simanman:20210506200531p:plain:w250

このような盤面操作の手が存在するかどうかは「穴」と「ブロック」のラインが直交している部分を調べて、その周辺 1マスにブロックが置けるかどうかをチェックしています。

f:id:simanman:20210506201025p:plain:w300

他にも「1 のブロックが邪魔して 2手操作ができないパターン」の対策で 1のブロックを除去する処理を入れています。

f:id:simanman:20210506201441p:plain:w250

f:id:simanman:20210506201544p:plain:w250

ブロックを取得することによる値の期待値を計算

全体の最適化のためには、そのタイミングでの最大値より数手先を含めたスコアのほうが重要なので、ざっくりと値を計算して評価に加えていました。

f:id:simanman:20210506202542p:plain:w250

この画像のケースだと [7, 1] と処理するより [1, 9] と処理したほうがスコアが高くなる。 評価は後ろに存在するブロックに対して下記のコードで決定していました。

value += block_value / d 
d += random_value  # random_value は探索前にランダムに決定される値 [1.0, 3.0] の間でランダムに設定

[ブロックの総和] / [必要な手数] よりはスコアが出たので採用しましたが、正直理由はわかってないです。

操作をイベントとして扱う

最終的に得られた操作列は「ブロックを穴に落下させる」を区切りとしてイベントの配列して扱います。なので

1 2 S R
4 6 S U
8 6 S U
6 5 M R
6 6 S U
0 2 M D

のようなフラットな操作配列ではなくて

[
  [1 2 S R],
  [4 6 S U],
  [
    8 6 S U,
    6 5 M R,
    6 6 S U,
    0 2 M D
  ]
]

「ブロックを落とす」という単位でまとめられた操作配列を持つ感じになります。

f:id:simanman:20210506205209p:plain:w400

イベントの操作順番の変更

最後に得られたイベントの配列を全体の操作が壊れない範囲で移動操作します。移動によって全体のスコアが上がった場合はその入れ替えを採用します。(山登り)

方法としては入れ替え対象のイベントの適用をスキップして、その後のイベントの適用が失敗するまで適用を続けて、その都度「このタイミングでこのイベントを入れた場合」のスコアを計算して一番値が大きいタイミングの位置に挿入します。

これを前からと後ろからの両方試して、スコアの更新が出来なくなったら終了です。(後ろからの場合はすべてのイベントの適用後の盤面から各イベントの逆操作を行っていく感じです)

ここまでが一連の処理になります。

イベントの結合処理

特定のイベントに関しては依存関係が強くてまともに入れ替えができないので、ある程度イベントをまとめることで順番の変更処理の対象を減らすようにしています。

f:id:simanman:20210506205626p:plain:w300

例えば画像のような状態では [7, 6, 9] と順番よくブロックを入れるという操作はほぼほぼ不動なのでこれらは 3つイベントとしてではなく 1つのイベントとしてまとめています。イベントをまとめることによって 1回の入れ替え処理で発生するスコアの変動が大きくなり改善幅も大きくなって結果的にスコアが上昇しました。

イベントをまとめるかどうかについては「初手の操作の座標の y軸 or x軸 が等しいかどうか」で判定を行っていました。(等しいと同一判定)

時間制限まで処理を繰り返し行う。

あとはこの処理(手順作成 -> 手順入れ替え)を時間制限が来るまで回し続けます。実行前は様々なパラメータの値を変更して毎回違う操作列が取れるようにします。

感想

貪欲に操作手順を決めるところで差がついてしまったと思うので、盤面評価力が足りてないなと改めて痛感しました。

MM 123 Jewels 参加録

参加してました。最初から最後まで順位表の不具合で自分の正しい順位がよくわからなかった。(恐らく 4, 5 位あたり)

https://www.topcoder.com/challenges/b846362a-218e-4a3d-b86c-3d38f472e199

方針

スコアの計算式が「ラインスコア * コンボ数」で計算されており、ラインスコアは (x - 2) ^ 2 で指数的に増加するので、なるべく長いラインを複数消すのが重要。

長さ N のライン消しパターンの作成

自分は盤面サイズごとに長さ N のラインを消すためのパターンを用意して、それを利用して連鎖を組んでました。

N = 8

f:id:simanman:20210206050404p:plain:w300

N = 16

f:id:simanman:20210206050429p:plain:w400

また、何列同時消しを行うかによってブロックを追加しています。(下の画像は 3列同時消しを行う場合)

f:id:simanman:20210208211844p:plain:w400

連鎖型の作成

インスコアを稼いだ後はコンボを稼ぐことで高スコアにつながるので、連鎖系も各盤面ごとに用意しました。

N = 8

f:id:simanman:20210206050539p:plain:w300

パターンの組み合わせで盤面構築

スコア取得のパターンと連鎖のパターンを組み合わせて、作成した盤面の構築を行います。 スコアのパターンについては、同時消しの数によってブロックを追加で上積みしています。

f:id:simanman:20210206051149p:plain:w500

同時消しの数が増えるほどスコアも増えますが、現状の盤面の色状況によっては作成出来ない盤面を構築することになるので、その場合は構築できる盤面の中で最大の同時消しが出来るような盤面の構築を行っています。

自分の場合ほとんどケースで「同時消しの数: 1」が構築可能になるようにパターンを作成しました。一番作りにくいケースが「N: 8, C: 10」でしたが、このケースでもほとんど問題にならないようにしてます。(どうしても作れない場合は 1手貪欲に探索して適当にブロックを消してました)

構築した盤面の配色

盤面を構築した後は各ブロックの配色を決めていきます。自分は「現状の盤面から最小手数で達成出来る盤面」を選択するように配色を行いました。配色に関しては焼きなましで配色パターンを探索しています。

発火処理

配色が完了したら、後は実際に目的の盤面までの手順を作成して発火します。パターンは決め打ちだったので発火箇所も決め打ちです。 後は「盤面構築 -> 発火」の繰り返しをゲームが終了するまで繰り返します。

感想

スコア計算的に「Nライン同時消し + 連鎖」にたどり着かないとそもそもスタートラインに立てない感じだったので、早めに気づけたのは良かった。後半は「暴発を防ぐ」ための戦略を色々考えていたが、解決策を最後まで実装出来なかったのでそこが反省点。

「これ焼きなましだ!」みたいな感じの問題でなければ良問という風潮がある(偏見)

HACK TO THE FUTURE 2021 予選参加録

問題

atcoder.jp

方針

  • 1 - カードをすべて回収する。
    • 全部のカードを持っていたほうが後にカードを再配置する際に自由が効きそう。
  • 2 - 集めたカードを再度配置し直す。
    • 後で 0 から 99 のカードを集めるときの移動コストが小さくなるようにする。
  • 3 - 0 から 99 のカードを回収する。

この方針だと以下の状況で移動が発生します。

  • カードを全回収するとき
  • カードを再配置するとき
  • 0 から 99 のカードを回収するとき

最後のパートについては回収する順番が固定なので、回収する順番については考えなくて良いです。 1つ目と 2つ目については連動していて、山札がスタック構造なので最初に回収した順番の逆順でカードの再配置を行います。 なので、全体の移動回数は以下の 2つの要素で決まります。

  • 初期配置のカードの回収の順番
  • カードの再配置場所

これらを焼きなましを行って最適化を行いました。(それぞれを独立ではなく同時に)

遷移は以下の 5つです。

  • 回収順番の 2点swap
  • 回収順番を 1点だけ入れ替え(insert)
  • 回収順番を 2-opt
  • 再配置場所の 2点swap
  • 1つカード選んで再配置場所をランダムに変更

最終スコアは 168440 で予選 3位でした。(後で試してみたら回収順番の遷移は 2-opt だけのほうがスコア良かったです 168531

過去の順位が 192149 だったので正直かなり高順位で驚いてます。(毎回フィーリングで解いてるので安定感が無いのはそう)

コンテスト終盤、方針変更するには時間がないし、テストケースが 50個なのをいいことに 1時間ぐらいひどいパラメータ調整をしてた気がする。(要反省)

Ruby で赤黒木を実装してみた

この記事は Okinawa.rb Advent Calendar 2019 の 15日目の記事です。

結論

実装しました。

https://gist.github.com/siman-man/a8ed58effeaaf59855deac490574760c

二分探索木

前々から赤黒木を実装しようと思っていたのですがずっと放置していたので、今回実装してみました。

まず比較用に二分探索木から実装します。

https://gist.github.com/siman-man/dc79878e5a8d93c73299c1145458a820

二分探索木ではデータの検索、挿入、削除といった操作が O(log n) で可能となるデータ構造です。

探索木の各ノードは以下の条件を満たします。

あるノード x において、y が左側に存在するノードであるならば「x.value >= y.value」、右側であれば「x.value <= y.value」を満たす。

この条件が満たされていることで、値の検索が下記のようなコードで可能となり、その実行時間は O(log n) となります。

  def find(value)
    z = @root

    while z != NUL && z.value != value
      if value < z.value
        z = z.left
      else
        z = z.right
      end
    end

    z
  end

データの挿入時についても、値の挿入先を探すのに上記と似たような処理を実行するのですが、入力されるデータによっては実行時間が O(n) となってしまいます。

例として 1000 件のデータをランダムに入れるコードを書いてみます。

require 'benchmark'

nums = (1..1000).to_a.shuffle

time = Benchmark.measure { nums.each { |n| bst.insert(n) } }

puts time

結果

$ ruby binary_search_tree.rb
  0.006067   0.000049   0.006116 (  0.006128)

ここでランダムだった部分を取り除きます。

  require 'benchmark'

- nums = (1..1000).to_a.shuffle
+ nums = (1..1000).to_a

  time = Benchmark.measure { nums.each { |n| bst.insert(n) } }

  puts time

すると実行時間が遅くなることが確認出来ます。

$ ruby binary_search_tree.rb
  0.192344   0.000213   0.192557 (  0.192703)

このように特定の入力によって操作の速度が低下してしまうのを回避できるのが「平衡二分探索木」であり、その一つに赤黒木があります。

赤黒木

赤黒木では先程の二分探索木の各ノードに色情報が付与されます。ノードの色は「赤」か「黒」です。赤黒木では以下の制約によって木の高さの差が2倍を超えることが無いことが保証されています。

  1. 各ノードは赤または黒色
  2. ルートノードの色は黒色
  3. すべての葉は黒色
  4. あるノードが赤色であればその子ノードは黒色
  5. あるノードからその子孫に対する経路について、その経路に含まれる黒色のノードの数が同数である。

実際に実装してみたのがこちらになります。

https://gist.github.com/siman-man/a8ed58effeaaf59855deac490574760c

実際に速度を測ってみます。

require 'benchmark'

rbt = RedBlackTree.new

nums = (1..1000).to_a

puts Benchmark.measure { nums.shuffle.each { |n| rbt.insert(n) } }
puts Benchmark.measure { nums.each { |n| rbt.insert(n) } }

結果

$ ruby red-black-tree.rb
  0.005910   0.000016   0.005926 (  0.005923)
  0.005540   0.000009   0.005549 (  0.005549)

このように偏った入力についても実行時間が遅くならないことが確認出来ました。

おわりに

前々から実装しようと思っていたので実装できる良い機会でした。(思った以上に実装量が多かった)

本当は定数名に NUL じゃなくて NIL を使おうと思ったのですが ruby 側で定義されていたのでそれは避けました(deprecated みたいなので将来的に無くなるとは思いますが)

今後も色々とアルゴリズムの実装をしていきたいと思います。(競プロ用のライブラリ整備)

参考文献

実装と詳細については「アルゴリズムイントロダクション 第3版」を参考にしました。