第64回 プログラミングについて『迷路を通ろう!(その1)』

迷路といえば、映画の『ラビリンス』とか、人間が歩けるような大きな迷路などを思い浮かべるかたも多いのではないでしょうか。『ラビリンス』はデビッドボーイがなかなか良くて、わくわくして見たものです。そこで、という訳では全然ないのですが、今回からは迷路を考えてみましょう。
プログラムのコンテストには迷路を作るプログラムが課題になることもあります。どれだけ高速に複雑な迷路を作ることができるかを競うのですが、なかなか優れたものがあるようです。しかしながら今回からは、迷路そのものを作ることではなく、迷路を通ることを本題としていきます。
プリント基板設計用のCADでは迷路を通ることを主目的としたプログラムがあります。コンピュータを扱っていてプリント基板を見たことのない方は多分いないと思いますので詳しい説明はしませんが、プリント基板上の部品と部品を接続するには、パターンという銅箔の線で行ないます。このパターンを自動的に生成しようというのが、自動配線プログラムです。
自動配線といっても、ロボットが半田ゴテを持って銅箔を半田付けするのではなく、どのように配線をするかを計算するものです。要するに、基板上の部品の接続すべき端子と端子が迷路の出発点と終着点になり、接続に関係のない端子や他のパターンなどが障害物になる訳です。
迷路を探索する方法には、『線分探索法』と『迷路探索法』の2種類が基本的なアルゴリズムとして古くから考えられています。現在では、自動配線プログラムはかなり進歩していて、配線の効率が良くなっていますが、基本はこの2つのアルゴリズムを使用しています。まず下の図を見て下さい。
・・・・・・・・・T
XXXX・・・X・・
・・・・・・・X・・
・・・・・・・X・・
・・XXXX・・・・
・・・・SX・・・・
・・X・・X・・XX
・・X・・X・・・・
・・X・・・・・・・
・・XXXXXX・・
この図で、・は障害物のない点で、Xは障害物、Sは出発地点、Tは目標地点と解釈して下さい。
線分探索法というのは、最初にSの地点から横に線を引きます。次にその線から縦に線を引きます。再びその縦の線から横に線を引いていきます。これを繰返していき、線がTに到達したところで、探索を終了して、経路を決定します。
・・・・・・・・・T ・・・・・・・・・T ・・・・・・・・・T
XXXX・・・X・・ XXXX・・・X・・ XXXX・・・X・・
・・・・・・・X・・ ||・・・・・X・・ ++ーーーーーX・・
・・・・・・・X・・ ||・・・・・X・・ ++ーーーーーX・・
・・XXXX・・・・ → ||XXXX・・・・ → ||XXXX・・・・
ーーーーSX・・・・ ++ー+SX・・・・ ++ー+SX・・・・
・・X・・X・・XX ||X|・X・・XX ||X+ーX・・XX
・・X・・X・・・・ ||X|・X・・・・ ||X+ーX・・・・
・・X・・・・・・・ ||X|・・・・・・ ||X+ーーーーーー
・・XXXXXX・・ ||XXXXXX・・ ||XXXXXX・・
↓
・・・・・・+ーーT ーーーー+++ーーT ・・・・|||・・T
XXXX・・|X・・ XXXX|||X・・ XXXX|||X・・
・+ーーーー+X・・ ++ーー+++X・・ ++ーー+++X・・
・|・・・・・X・・ ++ーー+++X・・ ++ーー+++X・・
・|XXXX・・・・ ← ||XXXX++ーー ← ||XXXX||・・
・+ーーSX・・・・ ++ー+SX++ーー ++ー+SX||・・
・・X・・X・・XX ||X+ーX||XX ||X+ーX||XX
・・X・・X・・・・ ||X+ーX|||| ||X+ーX||||
・・X・・・・・・・ ||X+ーー++++ ||X+ーー++++
・・XXXXXX・・ ||XXXXXX|| ||XXXXXX||
次に迷路探索法は、出発点Sから縦または横に1ステップで移動できる点に印 1 を、次に印 1 から1ステップで移動出来るところに印 2 を付けます。これを繰り返し、Tに到達したところで探索を終了して、経路を決定します。
・・・・・・・・・T ・・・・・・・・・T ・・・・・・・・・T
XXXX・・・X・・ XXXX・・・X・・ XXXX・・・X・・
・・・・・・・X・・ ・・・・・・・X・・ ・・・・・・・X・・
・・・・・・・X・・ ・・・・・・・X・・ ・・・・・・・X・・
・・XXXX・・・・ → ・・XXXX・・・・ → ・・XXXX・・・・
・・・1 SX・・・・ ・・2 1 SX・・・・ ・3 2 1 SX・・・・
・・X・1 X・・XX ・・X2 1 X・・XX ・・X2 1 X・・XX
・・X・・X・・・・ ・・X・2 X・・・・ ・・X3 2 X・・・・
・・X・・・・・・・ ・・X・・・・・・・ ・・X・3 ・・・・・
・・XXXXXX・・ ・・XXXXXX・・ ・・XXXXXX・・
↓
151413121112131415T ・・・・・・・・・T
XXXX101112X1415 XXXX・・・X・・
7 6 7 8 9 1011X1314 ・・・・・・・X・・
6 5 6 7 8 9 10X1213 ・・・・・・・X・・
5 4 XXXX9 101112 ← 途中省略 ← ・4 XXXX・・・・
4 3 2 1 SX8 9 1011 4 3 2 1 SX・・・・
5 4 X2 1 X7 8 XX ・4 X2 1 X・・XX
6 5 X3 2 X6 7 8 9 ・・X3 2 X・・・・
7 6 X4 3 4 5 6 7 8 ・・X4 3 4 ・・・・
8 7 XXXXXX8 9 ・・XXXXXX・・
↓
・・・・・・+ーーT
XXXX・・|X・・
・+ーーーー+X・・
・|・・・・・X・・
・|XXXX・・・・
・+ーーSX・・・・
・・X・・X・・XX
・・X・・X・・・・
・・X・・・・・・・
・・XXXXXX・・
線分探索法でも、迷路探索法でも同じことが言えるのですが、探索を終了した後の経路の決定において複数の経路が考えられるのですが、通常はできる限り短い経路で折れ曲がりの少ないように決定するようにします。
今回は、迷路探索法によるプログラムを考えていきます。上の迷路探索法の図をみていて気が付くことは、出発点から順に1、2、3、4 .... と番号を振っていったとき到着点が遠い時には、その数値がとんでもなく大きくなっていく可能性があります。当然のことながら、マトリクスを作って探索を行ないますので、1つの点に使用するバイト数が2バイトとか4バイトになってしまうとメモリーを大量に消費してしまうことになります。
やはり世の中には頭のいい人がいるもので、2ビットあれば実現できる方法というものがあるのです。実際にはプログラムを作るときには2ビットでは面倒なので、1バイトにしますが、次のように数値を振っていくようにするのです。これは2ステップ以内で移動できる点に印1を、次にその印1から2ステップ以内で移動できる点に印2を、再び印2から2ステップ以内で移動できる点に印1を付けていくようにします。
・・・・・・・・・T ・・・・・・・・・T ・・・・・・・・・T
XXXX・・・X・・ XXXX・・・X・・ XXXX・・・X・・
・・・・・・・X・・ ・・・・・・・X・・ ・1・・・・・X・・
・・・・・・・X・・ ・・・・・・・X・・ 111・・・・X・・
・・XXXX・・・・ → ・2XXXX・・・・ → 12XXXX・・・・
・・11SX・・・・ 2211SX・・・・ 2211SX・・・・
・・X11X・・XX ・2X11X・・XX 12X11X・・XX
・・X・1X・・・・ ・・X21X・・・・ 11X21X1・・・
・・X・・・・・・・ ・・X222・・・・ ・1X22211・・
・・XXXXXX・・ ・・XXXXXX・・ ・・XXXXXX・・
↓
・・・222・・・T ・・・・・・・・・T ・・・・・・・・・T
XXXX122X・・ XXXX1・・X・・ XXXX・・・X・・
2122112X・・ 212211・X・・ 2122・・・X・・
1112211X2・ 1112211X・・ 11122・・X・・
12XXXX1122 ← 12XXXX11・・ ← 12XXXX・・・・
2211SX2112 2211SX211・ 2211SX2・・・
12X11X22XX 12X11X22XX 12X11X22XX
11X21X1221 11X21X1221 11X21X122・
21X2221122 21X2221122 21X2221122
22XXXXXX21 22XXXXXX21 22XXXXXX2・
↓
・1122211・T 211222112T ・・・・・・+ーーT
XXXX122X1・ XXXX122X12 XXXX・・|X・・
2122112X11 2122112X11 ・+ーーーー+X・・
1112211X21 1112211X21 ・|・・・・・X・・
12XXXX1122 → 12XXXX1122 → ・|XXXX・・・・
2211SX2112 2211SX2112 ・+ーーSX・・・・
12X11X22XX 12X11X22XX ・・X・・X・・XX
11X21X1221 11X21X1221 ・・X・・X・・・・
21X2221122 21X2221122 ・・X・・・・・・・
22XXXXXX21 22XXXXXX21 ・・XXXXXX・・
探索が終了したら、到達点から逆に2、2、1、1か1、1、2、2の順に出発点に辿っていき経路を決定します。私が最初にこのアルゴリズムを見たのは15年以上も前のことですが、この2ステップずつということに気が付いた人は(誰が考えたのかは、私は知りません)よっぽど頭を悩ましたのだろうと思いつつも、目からウロコが落ちたような気がしたものです。
さてそれでは、実際にプログラムを考えていきましょう。今回使用する関数は46回目の芋虫を作ったときのものを流用していますので参考にして下さい。全体のプログラムの流れは大きく、
(1)画面の消去、ランダム関数の初期化。
(2)迷路の作成と表示。
(3)経路の探索と決定。
の順です。
まずは、メイン関数の部分。
/* ヘッダーファイルのインクルード */
#include <stdio.h>
#include <sys\timeb.h>
#include <time.h>
#include <stdlib.h>
#include <malloc.h>
/* 動的なメモリの確保/解放を行なうマクロ */
#define NEW(type,element) (type *)malloc(sizeof(type)*(element))
#define DELETE(addr) free(addr)
/* マトリクス(迷路)のサイズ */
#define XSIZE 78
#define YSIZE 22
/* マトリクスの各要素に設定する値 */
#define ROUT1 1 /* 経路探索時の印1に使用 */
#define ROUT2 2 /* 経路探索時の印2に使用 */
#define FIXED 3 /* 決定した経路に使用 */
#define DOOR1 4 /* 入口 */
#define DOOR2 5 /* 出口 */
#define WALL 6 /* 壁(障害物)*/
/* マトリクス */
char matrix[XSIZE+1][YSIZE+1];
/* 経路探索時に使用します */
struct rout_data_strct{
int x,y;
struct rout_data_strct *prev,*next;
};
struct rout_data_strct *rout_data=NULL;
main()
{
clear_screen(); /* 画面の消去 */
initialize_random(); /* ランダム関数の初期化 */
set_frame(); /* マトリクスの周辺に壁を設定する */
set_door(); /* 入口、出口の設定 */
make_maze(); /* 迷路の作成 */
redraw_matrix(); /* 迷路の再描画 */
routing(); /* 経路の探索、決定 */
move_cursor(1,YSIZE+1);
}
ここまでは特に難しいところはありません。次に画面を消去する関数 clear_screen()。この関数は46回目に説明してありますので、リストのみを記載します。
clear_screen()
{
printf("%c[2J",0x1b);
fflush(stdout);
}
次はランダム関数の初期化 initialize_random()。
initialize_random()
{
struct _timeb t;
_ftime(&t);
srand(t.millitm);
}
ランダムな値を返す関数 rand() は初期値が同じときには返してくる値は常に同じ値を同じ順番になりますので、プログラムの起動時に時間のパラメータを使用して初期値を変えるようにするために、このように時間をOSから受け取っています。
マトリクス(迷路)の周辺に壁を設定する関数 set_frame() は次のものです。
set_frame()
{
int x,y;
for(x=1;x<=XSIZE;x++){
for(y=1;y<=YSIZE;y++){
matrix[x][y]=0;
}
}
for(x=1;x<=XSIZE;x++){
matrix[x][1 ]=WALL;
matrix[x][YSIZE]=WALL;
}
for(y=1;y<=YSIZE;y++){
matrix[1 ][y]=WALL;
matrix[XSIZE][y]=WALL;
}
}
ループがいっぱいありますが、要するにマトリクスの周辺の要素に値 WALL を代入しています。ただちょっと気にしておいて欲しいのは、後々のプログラムを簡単にするためにマトリクスの0番の要素は使用していないことです。
だいぶ長くなってしまったので、続きはまた次回にしましょう。