エレベータのシミュレータ


日本語のみ(Japanese only)


これは、Oh!Xに掲載された記事をほぼそのまま再現したものです。
iOS版には合わせてませんので、X-BASIC/68用ソースと見比べながらお読みください。
ただし、雑誌には分割掲載されたりストがありましたが、それは1つにまとめています。
また、iOS版にはダイレクトモードはないので、同様の手法はとれません。




皆さんもエレベータがこなくてイライラしたことはあると思います。今月から2カ月にわたって
「賢い(?)エレベータ」を作ってみましよう。実現方法としてリスト処理を使うなど、
X-BASIC自体よりアルゴリズム重点の講座。結構、難しいかも?


泉、怒る



シミュレーションはコンピュータの応用分野でも最もポピュラーなもののひとつです。高層建築物や
橋を建設するときは、台風などの強風や地震でどのように揺れるかを計算して、揺れのエネルギーを全体で
うまく分散吸収できるように設計されています。自動車もそうです。どんなに強力なエンジンを搭載
しても、剛性の足りないプアなシャシではそのパワーを吸収しきれず、安定した走りは望めません。
より剛いシャシをより軽く作るため、最近では自動車のボディ強度を調べる構造解析のためコンピュータ
が活躍しています。実物を作ることなく、パラメータを変えるだけで強度を知ることができるコンピュータは、
ますます利用されるようになっていくでしょう(ちなみに、東京タワーは計算尺を使って建てられたそうです。合掌)。

私は祝一平氏と違って、万物に不満を持っているわけでも、そこらじゅうのものに悪態をつくわけで
もありません。温厚な人間なのです(うひよひよ)。しかしそんな私でも腹に据え兼ねるモノはあるもの
で、編集室のある九段某ビルのエレベータには毎度頭にきているのです。オンボロなのではありません。
中にはジュータンまでしいてあります。ナリはなかなか立派なものなのです。ところが「頭が悪い」!
2基あるのですが、運が悪いと尋常じゃなく待たされ、いっそのこと非常階段で上がってやろうかという気
にもなってしまう、そんなヤツなのです。

ある日のことです。私は原稿を持って編集室に行く途中でした。玄関を入ってエレベータを見ると、
1基が1階、1基が4階に止まっています。ラッキーと上のボタンを押した途端、エレベータは動き始
めました。誰かが2階でボタンを押したのです。2階から乗り込んだ誰かさんは、3階に向かいます。
そしてタイミングよく3階で別の人が乗り込んだのでしょう。もう一度2階で止まり、やっと私の待つ
1階へと到着しました。この間もう1基のエレベータは4階で止まったまま……。こんな暴挙が許されていいのか!

もっとも、エレベータの気持ちもわからなくはないのです。きっと省エネ第一なのでしょう。2階に
向かったエレベータは次に1階に降りるかもしれない。3階に向かったエレベータも次は1階に降りる
かもしれない。それならわざわざ4階のエレベータまで動員し、エネルギーを浪費することはあるまい。
そんなことを考えているに違いありません。

なっとら~ん!

2階で誰かがボタンを押したときに、その人が上に行くのか下に行くのかはすぐにわかるはずです。
上に行くのなら、さっさと空きのエレベータよこさんかい。くっそー!プログラマ出てこい!しばきまわしたる。


エレベータを作る



さて今回はX-BASICを使ってシミュレーションプログラムに挑戦してみましょう。題材に取り上げ
るのはエレベータです。もちろん、この選択には個人的な遺恨のようなものは「まったく」ありません。
怨恨の線は却下してください。最初から数基のエレベータを駆使して効率よく乗客をさばいていくとい
うのは難しいので、今回は1基のエレベータを動かせるようになるところまで考えてみることにします。
エレベータは人を乗せたり降ろしたりしながら各階を上下しています。この様子をX-BASICで書けば以下のようになるでしょう。
while 1
getOff() /* 人を降ろす
getOn() /* 人を乗せる
moveElevator() /* エレベータを動かす
endwhile
ここではエレベータがそれぞれの階に到着するごとに「降りる人はいないか」「乗る人はいないか」を判定
することにしてあります。エレベータは1階ずつ動きながら、乗客をチェックしてループを続けるのです。
もちろん、降りる人がいるかどうかを先に判定します。それがマナーというものです。

ここで問題となるのは、人の乗り降りをデータにする方法です。簡単なのは配列を使う方法でしょう。
仮にこのエレベータがあるビルを7階建てだとします。7×7の大きさの配列を用意し、「7階に向かう人が3階で待っている」という状態を、
floor(3,7)=1
のようにして表現するのです。7階に向かう人が2人いるなら、
floor(3,7)=2
となります。この方法は簡単ですのでこれまでの知識で十分皆さんにもプログラムできるはずです。
しかし、今回はX-BASICに用意されているデータ表現を使って新しいデータ表現を作ってみるという
意味も兼ねて、別の方法に挑戦してみましょう。


「リスト」というデータ表現



いま、3階で7階に向かう人が待っているとします。この状態を、
floor(3)=[7]
と表すことにします。ここに6階に向かう人がやってきました。この人はエレベータ待ちに加わります。
その結果、
floor(3)=[6 7]
となりました。次に2階に向かう人と1階に向かう人がやってきます。
floor(3)=[1 2 6 7]
です。ちょうど上に向かうエレベータがやってきたので3階から上に行く人はエレベータに乗り込み、エレベータを待つ人は、
floor(3)=[1 2]
になりました。続いて下に向かうエレベータが到着し、3階で待つ人は、
floor(3)=[]
となる、このようにデータを好きなように追加・削除できるデータの表現方法を使ってみることにしましょう。
リスト1です。

もちろんX-BASICにはこのようなデータを表現する方法はありません。しかし、リスト1に用意した
関数を使うことによって、このようなデータを扱うことができるようになるのです。最近この連載を
読み始めた方にはちょっと難しいかもしれませんので、今回はこれらの関数を使うことだけに焦点を絞って
いきたいと思います。

リスト処理ブログラムの実行



ではプログラムを入力し、実行してみましょう。プログラムリスト1のAを1~6行に入力し、
Bを101~176行に入力すると仮の行番号と対応させやすく簡単でいいでしょう。入力後、
renum
を実行すれば、いつもの見慣れた10行から始まるプログラムになります。

ではまずリストを作ってみましょう。runしたあとダイレクトモードで、
int x
としてリストを入れる変数を宣言しておきます。
x=EMPTY
でxに空のリストをセットしたら、
printList(x)
としてみましょう。[]が表示されましたね。
x=addData(1,x)
で空のリストに1という要素を加えてみます。
printList(x)
で[1]と表示されましたか?続いて、
for i=2 to 5:x:addData(i,x):next
ではどうでしょう。これはまずxに2をつけ加え、次に3,次に4,5とつけ加えていきます。
xを表示してどのようなリストになったか確認してみてください。

私が用意したconcat関数は、2つのリストを強制的につなぎ合わせるものでした。そこで2つのリストをつなぎ合わせた
新しいリストを作ってくれる関数を書いてみることにしましょう。appendと名づけることにします。プログラムリスト2です。

append関数は、lst1、lst2の2つのリストをパラメータとして受け取ります。もしlst1がEMPTYなら
2つのリストをつないだ結果はlst2になりますね。そこで4行で、lst2を返して終了します。lst1がEMPTY
でないならぱ、6桁でtmpという変数にlst1の先頭要素を除いたリストとlst2をappendした結果を
入れ、7行でその先頭にlst1の最初の要素をつけ加えて返します。そう、第6回でやった再帰を使っているのです。
このプログラムは、「lst1とlst2のappendとは、tail(lst1)とlst2をappendし、その先頭にhead(lst1)をつけ加えたものだ」
と考えて作ってあります。
プログラムリスト2をプログラムリスト1につけ加え、runしたらダイレクトモードでappend関数を
使ってみましょう。まず変数xに[1 2 3]を、yに[4 5 6]をセットしてください。変数zを宣言し、
z=append(x,y)
を実行してみてください。本当につながったかどうか確かめてみましょう。
printList(z)
で[1 2 3 4 5 6]というリストが表示されましたか?続いてxとyも調べてみましょう。xは[1 2 3]のまま、
yは[4 5 6]のままですね。このようにappendはパラメータとして与えられたリストを壊すことなく
2つのリストをつないでくれる関数なのです。このほか、リストを逆順に並べ替えてくれる関数や、
リストの要素数が奇数か偶数かを調べる関数など、いろいろ作って遊ぶことができます。挑戦してみてください。
append関数は今回のシミュレーションでは使いませんので、実験が終わったら削除してしまってかまいません。


エレベ一夕の乗客を作る



ではエレベータシミュレーションを始めることにしましょう。まず最初に作るのは乗客のルーチンです。
シミュレーションを行うこのビルは7階建てだということにしましよう。乗客は決まった階に現れる
わけではなく、ランダムに登場します。まずはこの「ランダム」、というのをやっつけることにします。
X-BASICにはrndという関数が用意されています。これは「乱数」を発生させる関数です。乱数とは
その名のとおり、決まった順序ではなくランダムに並んだ数のことです。rnd関数が実行されるたびに、
X-BASICは、デタラメに並んでいる数をひとつずつ順番に返します。返すのは0以上1未満の実数です。
実験してみましょう。
for i=1 to 10:print rnd():next
どうですか、デタラメな数が10個表示されたでしょう。このrnd関数を使って、ランダムに各階に到着
する乗客を表現します。rnd関数が返すのは0以上1未満の数でしたから、それを7倍すれば0以上7未満
の実数を作ることができますね。そしてint関数を使ってその実数の小数部分を取り払ってやれば、0~6
のランダムな整数を作り出すことができます。これに1を加えると、1~7のランダムな数を作り出すこと
ができるというわけです。やってみましょう。
for i=1 to 10
print int(rnd()*7)+1
next
ランダムな1~7の数が表示されましたね。これで乗客が現れる階をデタラメに決めることができるようになりました。

次に乗客が向かう階を決めます。3階から3階へ向かうなどということはあり得ませんので、到着した階と
違う階がでるまで階をランダムに発生させ続ければ目的の階も決めることができます。次のようになるでしょう。


int ff /* floor from
int ft /* floor to
ff=int(md()*7)+1
repeat
ft=int(md()*7)+1
until ff<>ft
まず到着階を決め、到着階と異なる目的階が出るまでrepeat~untilでループするわけです。

このようにして発生させた乗客をどのように保持しておくかですが、各階ごとにリストを作りaddData関数で
つけ加えていくことにします。
int floor(7)
int i
for i=1 to 7
floor(i)=EMPTY
next
として各階の乗客を入れるためのリストを作り、空リストで初期化しておきます。次に上の方法で乗客を発生させ、
floor(ff)=addData(ff,floor(ff))
とすればOKです。では方針が固まったところで、プログラムリスト3を見てください。

まずはAの部分です。大域変数を追加しています。ここではfloor配列を2次元の配列としました。
floor(n,0)
にはn階から下に向かう人を、 floor(n,1) には上に向かう人を入れることにします。こうしておけば、上に向かうエレベータがn階に到着したとき、
floor(n,1)のリストをエレベータの乗客リストに追加するだけで乗客を乗せることができますからね。

次のlocArvl(locationofarrivals)という変数は、各階で待っている乗客のリストを表示するx座標です。
表示位置を変更したくなったとき、この変数に代入している値を変えるだけで簡単に対処できるのでこうしてあります。

4行目からはメインルーチンの変更です。5行が新しく加わった行で、各階を初期化するinit関数を呼び出しています。

Bには初期化を受け持つinit関数、乗客の到着を処理するarrival関数、そして各階で待つ乗客を表示する
display関数が入っています。

init関数はfloor(n,0)とfloor(n,1)を空リストにし、最後に各階の状態を表示して終了します。

arrival関数の基本は先程説明したとおりです。15行は、乱数が0.3以下だったときだけ乗客が到着した
ことにするために入れてあります。エレベータが1階動くごとにどこかの階に乗客が到着するという事態を
回避するためです。乗客の到着頻度を大きくしたければ、ここをいじればOKです。21行のif文は、
到着階と目的階の大小によって処理を分けているところです。floor(n,0)にセットするかどうかを
決め、ついでに画面表示も書き換えています。実行時の画面写真を見ていただければおわかりのように、
各階の様子は7階を画面一番上にしてそれぞれ3行使って表示してあります。画面の一番上は第0行です
から、7から到着階を引きその結果を3倍すれば7階の乗客を画面一番上に、6階の乗客を第3行に表示する
ことができますね。これをやっているのが23行と28行です。
print chr$(5)
というのはコントロールキーを押しながらEを押すのと同じ働きをします。これで表示行をクリアして
おいてからprintList関数で侍っている乗客を表示します。

最後のdisplay関数は簡単でしょう。画面をクリアしてから、上に向かう乗客、下に向かう乗客、ただの
改行を順次7階から1階まで表示するだけです。

●プログラムの実行

プログラムリスト3に従ってプログラムを追加・変更したらrunしてみてください。7~1までが第0桁目に
表示され、後ろにズラリと[]が表示されていますね。これが最初の状態で、いま各階には乗客
が誰もいないということを表しています。
arrival()
とダイレクトモードで入力してみましょう。画面表示が変わりましたか?だめなら何度かトライしてみてください。
乗客は目的階を要素とするリストで表示されます。乗客が現れたら、上に向かう乗客がn階の上側に、
下に向かう乗客が下側にちゃんと表示されているかどうかを確認してみてください。
大丈夫ですね。では、
int i
for i=1 to 100:arrival():next
とダイレクトモードで実行してみましょう。各階の様子が次々と変わり、乗客がどんどんやってきます。


乗客の乗り降りのシミュレート



各階にエレベータ待ちの行列ができたところで、次に乗り降りの部分を作りましょう。そのためには、
まずエレベータを作らなければなりません。プログラムリスト4を見てください。Aの部分がエレベータを表す大域変数です。
elevator変数はエレベータがいま何階にいるのかを保持します。passはパッセンジャー、
つまり乗客を保持するリストです。変数を宣言と同時に初期化する際には、変数を代入する
ことができません。定数でなければならないのです。
本来なら、
int pass=EMPTY
と書きたいところなのですが、ここは涙を飲んで-1で初期化してあります。そしてvector変数はエレベータの移動の向きを表します。
1なら上へ,-1なら下へ、0ならエレベータは止まっているという意味にしています。

エレベータを動かす



いよいよエレベータを動かすことにしましょう。実行写真にあるように、エレベータは階を表す番号
と乗客リストの間に、Down,Stop、Upの3つの状態を表示して表現することにします。それぞれ下に
向かっている、止まっている、上に向かっているという意味です。ではプログラムリスト6を見ていきましょう。

全体を見張る関数



最後にこの世界を統御する関数を導入します。その名もsimulation関数(プログラムリスト7)です。
まずメインルーチンを、このsimulation関数を呼び出すように変更します。続いてinit関数にちょっと
した追加をします。8~12行はキーを押すまで乱数を発生させ続けています。何回かチェックをして
いるうちにお気づきになったかと思いますが、runすると毎回必ず同じ順番で乱数が発生します。そこで、
キーが押されるまで乱数を発生させ続けることによって、それをメチャクチャにしてやろうというのです。

新しくinkey$(0)というのが使ってありますね。inkey$はカーソルが点滅してキー入力を促し、何か
キーを押すまで待っています。これに対してinkey$(0)ではカーソルが現れず、キーが押されるまで
待つこともありません。実行と同時に現在押されているキーを返してくれます。なにも押されていなければ
""となります。これを使って「キーが押されていない間ループする」という処理を実現しています。
13行では新たにmoveElevator関数が呼び出されています。これはエレベータを動かすためではなく、
エレベータと乗客を表示するために追加しました。16行からがsimulation関数です。moveElevator関数
の説明のところで示したものとほとんど同じです。25,26行が追加されているのはwhile~endwhileループ
が1秒間で1回実行されるようにしたかったからです。time$はX-BASICに用意されているシステム変数で、
現在の時刻を保持しています。
print time$
を実行すると、現在の時刻を表示させることもできます。これをtmという文字型の変数に代入しておき、
26行でtmとtime$が同じ間ループしています。1秒以内に必ずtm≠time$と意りますから、26行のループ
を抜けプログラムの続きが実行されるという仕組みです。

プログラムの入力、変更が済んだら実行してみましょう。getOn,getOff関数内のwait関係のコメント
をはずして実行されるようにしたらrunです。どうですか、それらしく動いているでしょう。しばらく
実行の様子を見守ってみましょう。


エレベータの改造(またはデバッグ)



実行していると、たまに妙な動作をすることがあります。妙な動作はたとえば次のような状態のとき
に起きます。上に向かっているエレベータに6階で降りる人が乗っています。6階には3階に降りる人
が待っており、7階にも下に降りる人が待っています。

エレベータが6階に到着して乗っていた人が降りると、あろうことか、エレベータは6階で待っていた人
を乗せて下に降りていってしまうのです。7階で待っている人は待ちぼうけです。なにが原因なのでしょう。

答えはgetOff関数にあります。乗っている人がすべて降りてしまうと、vectorは0になりエレベータは
止まってしまいます。次にdirection関数が実行された時点でvectorは0ですから、エレベータは自分の
いる階を調べ、乗客を乗せて…。もうおわかりですね。エレベータの進行方向の階で人が待っている
ときは、乗客がすべて降りてしまったからといってvectorを0にしてはいけなかったのです。もちろん、
「こういうエレベータなんだ!」といいはることもできますが、万人の不評を買うこと請け合いです。
プログラムリスト4のB、19~21行にあるif文をプログラムリスト8のif文に置き換えてみてください。
プログラムリスト8は、エレベータが上に向かっているときに上の階でエレベータを待っている人
がいれば、あるいはエレベータが到着した階に上に向かう人がいれば、乗客がいなくなってもvectorを
そのままにしておくようにプログラムしてあります。下に向かっているときも同様です。

さて、これで期待どおりに動くエレベータが1基できあがりました。waitとtime$の効果で、なかなか
本物っぽい動きを見せてくれます。試しにプログラムリスト7の26行をコメントにしてみてください。
この世のものとは思えないほどのスピードでエレベータが動くはずです。高速にシミュレートできると
いうのも、コンピュータシミュレーションの大きな魅力のひとつです。あすの天気があさってにならないと
わからないようではシミュレーションなどしても実用になりませんからね。

来月はエレベータの数を増やし、「エレベータのアルゴリズム」を考えてみるつもりです。人にやさしい
エレベータをいっしょに考えてみましょう。




今月は「賢いエレベータ作り」の後編。エレベータが2台になっただけではなく、速度も1.5倍ほど
速くなりました。さらに待ち時間を折れ線グラフで表示してくれるという至れり尽くせりのサービス。
これでイライラも解消されるかも?

先月はコンピュータシミュレーションの例題としてエレベータを作ってみました。各階に到着する乗客を
エレベータが黙々と運ぶ様子は、見ているだけで楽しいものです。さて、今月はエレベータの数を増やし、
効率的なエレベータの運用を考えてみることにしましょう。


エレベータの数を増やす



ではさっそくエレベータの数を増やす作業にとりかかりましょう。これは実に簡単です。先月号では
エレベータは、
int elevator /* エレベータのいる階
int vector /* 動いている方向
int pass /* 乗客
int wait /* 乗り降り時の待ち
という4つの変数で表現されていました。これを複数台分用意すればいいだけです。配列で表現することにしましょう。
int Elevator(1) /* エレベータのいる階
int Vector(1) /* 動いている方向
int Pass(1) /* 乗客
int Wait(1) /* 乗り降り時の待ち
これで終わりです。あとは、プログラム中で4つの変数を使っているところを適宜書き換えていけば
OK。エレベータを表す配列名が大文字になっている理由は、書き換え後のプログラムリストを見れば
わかっていただけるでしょう。最後のページに掲載しましたので、どのように書き換えてあるのかちょっと
のぞいてみましょう。プログラムリスト3です。

1550行にはエレベータに乗っている人を降ろす関数getOffがあります。今月号のgetOff関数は、乗客を
降ろすのはどちらのエレベータなのかを引数として受け取るようになっています。行末に「変更」と
コメントしてありますね。次に1580~1610行はプログラムを追加した部分です。先月使ったelevator,
vector,passという変数を宣言し、 Elevator,Vector,Pass配列から該当エレベータの分をコピーしています。
これによって、1620行以降のプログラムは先月のままで、今月の拡張に対応することができるわけです。
getOff関数の中で変更を加えられた変数は、1860,1870行で元に戻しています。1850行はWait関係の変更です。
小さな変更ですので、ここではわざわざwait変数を宣言せずに対応しています。行末に
「変更」とコメントしてありますね。

このように「追加」「変更」のコメントのある場所を順次書き換えていけば、先月号のプログラムリスト
を入力しである方は簡単に今月号のものを手に入れることができます。関数の並び方がお手元のものとは
違うかもしれませんが、 それはそのままでOKです。関数ごとに修正を加えていってください。
エレベータがどのように動くのかはsimulation関数を見ればすぐにわかります。3130~3210行のfor~nextループで、
エレベータの0番と1番を交互に動かしているだけです。

先月は関数ごとにプログラムリストを掲載し、次第にプログラムができあがっていく様子をお見せしました。
あとで全部まとめてみると300行以上ものプログラム。皆さんはこれを入力していたわけです。
すごいですね。


エレベータのアルゴリズム



プログラムの修正が終わったらさっそくrunしましょう。どうですか、とりあえずこれで動きますね。
ではエレベータの動きを追いながら、どのように動かせばいいのかを考えてみることにしましょう。

最初エレベータは止まっています。ある階に人が到着すると、エレベータ0が動き出します。このとき
同時にエレベータ1も動き始めるのに気づかれると思います。先月、エレベータを1台だけ動かした
のと同じアルゴリズムを使っているのですから当然です。上の階に向かう人が乗っていたり、上の階で
人が待っているとエレベータは上へ動くのです。もう1台のエレベータがなにをやっているのか、ここ
では全く考えていません。結果、誰も乗っていないエレベータが(運が悪いと) 7階まで動いていくこと
になります。これは大いに改善する余地があります。こんな無駄なことはないでしょう。

これに関して注意してほしいのは、プログラムリスト3の2350~2420行です。先月のプログラムでは、
エレベータが7階に到着した場合には必ず7階に向かう人がエレベータに乗っているか、 あるいは7階
で人が待っていました。どちらにしてもエレベータは7階でいったん停止しますから、階を越えて
エレベータが動き続けようとすることなどなかったのです。しかし、プログラムリスト3ではこのような
事態が発生してしまいます。そこでこれらの追加によってエレベータが屋上に出てしまわないよう、地下
にもぐってしまわないよう、修正しているのです。

プログラムリスト1はこの無駄を排除するために作った「方向決定関数」です。direction1と命名しました。
試行錯誤の結果、direction関数に簡単な追加を施すことでうまく解決できました。先月と同じように、
適当な行番号から(たとえば4000行から)4001 4002とひとつずつ行番号を増やしながら入力
すると見比べやすく簡単でいいでしょう。頭に仮に付けてある行番号は省いて入力してください。

では無駄排除の方法を説明していきましょう。3行ではdownward、upwardの2つの変数が宣言してあります。
これはもう一方のエレベータが上に向かっているか、下に向かっているかを示すフラグです。
エレベータAより上の階にエレベータBがいて、 しかもエレベータBが上に向かって動いているときに
upwardフラグ、が1になります。downwardフラグはこの逆です。これらのフラグをセットしているのが
7~15行のfor~nextループです。続く17行からのif文は、エレベータAが止まっているとき、 つまり
vectorが0のときにはその階に人がいるかどうか、上か下の階に待っている人がいるかどうかを調べて
動く方向を決定します。これはdirection1関数と同じです。次にエレベータAが止まっている階に人がいない
場合の方向決定ですが、 ここに少々細工をしてやります。

upwardが1のとき、つまりエレベータBがエレベータAより上の階にいて上に動いているときには、
エレベータAは「下に待っている人がいるかどうか」を調べます。もしエレベータBが下の階にいて
下に動いているときには「上の階で、人が待っているかどうか」だけを調べます(22~25行)。他方の
エレベータが上に動いているなら、エレベータAより上の階て待っていて上に向かう人はエレベータBに乗って
しまったあとである可能性が高いし、またエレベータAより上の階で下に向かう人はエレベータBが7階まで
行ったあと降りてきて拾う可能性が高いと判断したからです。

次の26~30行では、 downwardが0のとき、つまりエレベータBが下にいて下に向かっていないときと、
upwardが0のときのエレベータAの動きを決めています。downwardが0のときというのは、 エレベータB
がエレベータAより下の階にいて上に向かっているか、エレベータBがエレベータAより上の階にいる場合
です。このとき下の階で待っている人がいればエレベータAは下に迎えに行きます。upwardが0
の場合にはエレベータAは上に迎えに行きます。

これらの変更・追加の結果、エレベータは次のような動作をするようになります。最初2台のエレベータ
は1階に止まっています。上の階に乗客が到着すると1台のエレベータが動きだします。upwardが
1になりますからもう1台は17~30行のどの条件にもマッチせず止まったままになります。動いている
エレベータが来客を乗せたり降ろしたりしながら7階に到着し、下向きに動きだしたときもう1台の
エレベータは28行のif文が成立して動き始めます。あるいは1階に乗客が到着した場合にももう1台は
動き始めます。7階で待っているひとりの人を乗せるため2台のエレベータが動き出すという間抜けな
事態がこれで回避されるという寸法です。

では新しいdirection関数を試してみることにしましょう。プログラムリスト3に続いて、 4000行から
ら4001、4002と行番号を増やしながら4049行まで入力します。入力が終わったらsimulation関数に変更
を加えます。プログラムリスト3の、
3150 direction(i)
を、
3150 direction1(i)
に変更してください。
実行結果はどうですか? かなり無駄が省かれたことと思います。もちろんまだ満足のいくものでは
ありませんが、 direction関数と比べると随分洗練された動きをしています。きて、乗客の待ち時間のほうは
どうなっているのでしょうか。新しいdirectin1関数によって短くなったのでしょうか。次にこれを
調べることにしましょう。


待ち時間を計る



先月、乗客を保持するために新しくリストというデータ構造を導入しました。それが実際にどのような形で
実現されているのかはブラックボックスとして、 リストを操作することだけに主眼をおいて説明
しました。ここでリストというデータ構造がどのようにして内部で実現されているのかを説明しておきたい
と思います。このデータ構造に少し変更を加えるだけで簡単に待ち時間を計ることができるからです。

リストは伸縮自在な配列のようなものです。簡単にするため、整数しか保持できないようにしたことは
先月説明したとおりです。このリスト構造はdataという配列によって実現されています。実現方法は
図1です。

data配列は2次元の配列です。横方向に2つの箱が並んだものが1000個分用意されています。
data配列の左側の箱data(i,0)には乗客を入れます。そして右側の箱data(i,1)には「次のデータを
入れてある配列の添字」が入っています。データの続きを表しているわけです。図1は[1 2 3]という
リストを意味しています。最後の部分であるdata(i+2,1)には-1、つまりEMPTYを入れ、続くデータがない
という印にします。head、tailの2つの関数がなにをやっているのか調べてみてください。

問題の待ち時間ですが、横に2つの箱が並んでるdata配列を変更し、箱の数を3つにすれば解決できます。
最後の箱には乗客がエレベータを待ち始めた時刻を入れておきます。エレベータに乗ったとき
にそのときの時刻から待ち始めの時刻を引いてやれば、どれだけの時間エレベータを待っていたのかが
わかるという仕組みです。時刻ですが、これはtimerという変数を用意し、これを増やしていくことで表現
することにしましょう。

プログラムリスト2を見てください。Aは大域変数の変更と追加です。data配列の大きさを変更し、
3~7行の変数を追加します。passesはエレベータに乗った乗客の総数、longestは一番長く待った人の
待ち時間、 averageは待ち時間の平均値を入れる変数です。lastX,lastYは待ち時間の平均値をグラフ
で表示するために用意しました。前回グラフを描いた座標を入れます。Aの1行はプログラムリスト3
の10行の変更です。3~7行は、プログラムリスト3の113~117行部分に入力してください。大域変数
はプログラム先頭になければならないからです。

プログラムリスト2のBは関数の変更し待ち時間計測用の新しい関数を追加しています。init関数は
グラフイツク画面の初期化を追加しました。arrival関数は乗客が到着したときに、 setTime関数を
呼び出して現在の時刻をセットする部分の追加です。getOn関数では乗客を乗せるときに、待ち時間の平均を
計算するためgetAverage関数を呼び出すようにしてあります。

simulation関数ではwhile~endwhileのループを1回まわるごとにtimerひとつ増やすように変更
を加えました。また無限ループをtimerが768より小さい間だけループするように書き換え、グラフが画面
右端まで表示されたら実行を終了するようにしてあります。86行をコメントにしたのはグラフをさっさと
描かせたかったからです。これまでのように1秒のウエイトを入れたければコメントをはずしてください。
これらの変更は小さいので、「3205行」のように中途半端な行番号を使うことで簡単に修正できるでしょう。

94行からは時間計測用の新しい関数です。4000行以降にはdirection1関数を入力してありますので、
これらは5094行から1行ずつ入力していくといいでしょう。setTime関数は、引数として受け取ったリストの
先頭要素に現在の時刻を付け加えます。逆にgetTime関数は引数として受け取ったリストの先頭
要素から時刻を取り出します。試してみましょう。プログラムリスト2を先の指示にしたがって入力
したらrunしてください。「キーを押してください」と表示されたらBREAKキーで実行を中断します。
ここで
timer=300
x=addData(1,-1) /* xに[1]をセット
setTime(x)
とすると、リストxの先頭要素に現在の時刻300がセットされます。これは1階に向かう人が時刻300にやってきたことを意味します。
timer= 35
x=addData(2,x) /* x = [2 1]
setTime(x)
なら2階に向かう人に時刻350がセットされます。確かめてみましょう。
print getTime(x) /* 2階に行く人
なら350が表示されますし、
print getTime(tail(x)) /* 1階に行く人
なら300が表示されます。
最後にgetAverage関数です。この関数はリストを引数に受け取り、 先頭要素から順にエレベータ待ち
を始めた時刻を取り出しながら、平均待ち時間と最長待ち時間の計算を行います。104~110行で計算が
終わったら、112行で画面に表示し113行でグラフを描いています。


より賢いエレベータにするには



実行してみた結果はいかがでしたか。最初は待ち時間平均の振動が大きいのですが、時間がたつに
したがって収束してくる様子がわかると思います。2つのdirection関数の性能を比較したければ次のように
するといいでしょう。まず、direction関数を呼び出すようにsimulation関数を変更してグラフを描かせます。
次にdirection1関数を呼び出すようにsimulation関数を変更し、init関数のscreen命令をコメントにして
実行するのです。screen命令をコメントにするとグラフィック画面がクリアされませんので、
2つのdirection関数をグラフで比較できます。

グラフは画面左下を原点として表示されますので下に描かれるグラフのほうが待ち時間が短いことを
意味します。direction1関数の性能はどうでしょうか。乱数の発生条件を同じにするため「キーを押してください」と
表示される前に1発キーを叩いておけば万全です。

direction1関数はまだまだ改良の余地があります。エレベータAが4階にいて上に向かっており、
エレべータBは1階で止まっているとします。2階あるいは3階に上に向かう人がやって来た場合、
エレベータBはすぐに動き始めるべきです。現在はエレベータAが止まるか1階に上に行く人がこない限り
エレベータBは動きません。この改良だけで待ち時間平均はもう少し短くなるはずです。自分と相手の
エレベータの聞の階で人が待っているかどうかを調べる関数が必要になりますが、これは簡単でしょう。
あるいはもっと徹底的に効率化を図ることもできるはずです。このシミュレーションでは現在エレベータが
何階にいるのかを、待っている人が知ることができるタイプのものを想定しましたが、これを待っている
人から隠せば好き放題のことができるのです。4階から上に行こうとしている人を無視して、
5階から下に行こうとしている人を迎えにエレベータを動かすこともできます。4階で待っている人は
自分が無視されたことを知る方法はないのですから、そのほうが効率がいいと判断したら実行することに
なんの障害もないのです。実際シミュレーション画面を見ていると、こいつら全部無視でここにダイレクトに
エレベータを動かせば効率がいいなぁと思うことがしばしばあります。

ひとつ徹底的に効率化を図ったdirection関数を作ってみたのですが、これがもう、嵐のようなif文の
塊になってしまいました。まるでエキスパートシステムです。ちょっと大きいので発表は止めにします
が、皆さんも自分なりの効率のいいdirection関数を考え試してみてください。いいものができたら、
編集部気付てや私宛に送ってくださいね。できのいい作品はこの連載の中で紹介したいと思います。

来月はちょっと息抜き。ゲームに挑戦してみましょう。

リストにおけるデータ削除
[1 2 3]というリストから2を削除する場合、
data(i,1) = i+2
とすればよい。本文の図1を見ながら考えてみていただきたい。注意しなければならないのは、
このときdata(i+1,n) は誰からもつながらない宙ぶらりんデータとなってしまうことだ。
先月摩訶不思議な作用でゴミが発生するといったが、これがゴミの正体である。
このような方法でデータ削除を続けると宙ぶらりんデータが次第に増えていき、
最後にはdata配列すべてがゴミになってしまうことになる。

これを避けるため本プログラムでは次のような方法を採用している。initList関数は
プログラム起動時にdata配列をひとつの長いリストにする関数である。この長いリス
トを自由リストといい、その先頭は変数dataBaseに保持されている。addData関数に
よって新しいリストを作る必要が生じた場合、 addData関数はnewData関数を呼び出し
て自由リストの先頭からデータを入れる箱をひとつ切り出してもらう。結果自由リストは
ひとつ減ることになり、 dataBaseの値も更新される。

逆にリストから要素を削除する場合には、delData関数はgc関数を呼び出して削除した
箱を自由リストに戻すという作業を行う。この結果自由リストはひとつ増えることになる。
要素の削除には必ずdelData関数を使うように先月指示したのは、このゴミ回収
作業を行うためである。

newData、gcの2つの関数の働きにより、シミュレーション実行中に1000人を超える
人がエレベータに殺到しない限り、自由リストが枯渇することはない。


お約束など


  1. 転載は禁止です。
  2. このプログラムを使って発生したのいかなる問題にも、当方は関知しません。
  3. 感想などありましたらお寄せください。
Oh!X 1990/4および5より抜粋。