このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
鉄道や道路の所要時間がわかっている時、ある都市から別の都市への最短の経 路を求める問題を考えましょう。 これはカーナビゲーションシステムや鉄道の乗り換え案内などに応用できる問 題です。 例えば次のような状況を考えましょう。 東京電機大学東京千住キャンパスから新宿に行くのに複数の経路が考えられます。 ここでは次の経路を考えてみましょう。
これらの関係を行列で表すことを考えます。 行と列をそれぞれの地点に対応させ、各成分にはおおよその所要時間を入れま す。また直接行けない地点間は∞(無限大)で表します。 但し、C言語で直接は∞無限大を扱えませんので、十分に大きな数を使います。 ここでは、H に大きな値を define しておいて使うことにします。 また、 路線図などからこの行列を作ることもそこそこ複雑なので、各路線の停車駅と 駅間の所要時間から、この行列を求めることを考えます。
入力はつぎのように配列の塊で与えます。 乗り換えの移動や待ち時間は考慮してませんし、駅間の時間は多少実際と異 なっているかも知れません。
#define NUM 16
char *eki[NUM]={"電機大","北千住","西日暮里","日暮里",
"上野","新御徒町","秋葉原",
"お茶の水","新御茶ノ水","小川町","森下","銀座","原宿",
"明治神宮前","新宿西口","新宿"
};
int length[NUM][NUM]={
{0,3,H,H,H,H,H,H,H,H,H,H,H,H,H,H},//電機大
{3,0,6,8,9,8,H,H,H,H,H,H,H,H,H,H},//北千住
{H,6,0,2,H,H,H,H,8,H,H,H,H,H,H,H},//西日暮里
{H,8,2,0,4,H,H,H,H,H,H,H,H,H,H,H},//日暮里
{H,9,H,4,0,H,H,H,H,H,H,13,H,H,H,H},//上野
{H,8,H,H,H,0,2,H,H,H,6,H,H,H,19,H},//新御徒町
{H,H,H,H,4,2,0,2,H,H,H,13,30,H,H,H},//秋葉原
{H,H,H,H,H,H,2,0,5,5,H,H,H,H,H,16},//お茶の水
{H,H,8,H,H,H,H,5,0,5,H,H,H,17,H,H},//新御茶ノ水
{H,H,H,H,H,H,H,5,5,0,6,H,H,H,H,17},//小川町
{H,H,H,H,H,6,H,H,H,5,0,H,H,H,H,H},//森下
{H,H,H,H,H,H,13,H,H,H,H,0,H,H,H,17},//銀座
{H,H,H,H,H,H,H,H,H,H,H,H,0,5,H,4},//原宿
{H,H,H,H,H,H,H,H,17,H,H,H,5,0,H,H},//明治神宮前
{H,H,H,H,H,18,H,H,H,H,H,H,H,H,0,5}//新宿西口
{H,H,16,H,H,H,H,16,H,11,H,17,4,H,5,0}//新宿
};
なお、簡単のために直接行けても H で表しているところもあります。 一方、実際の運用では乗換え時間は重要ですが、簡単のためにお茶の水周辺以 外は無視します。
電機大 | 北千住 | 西日暮里 | 日暮里 | 上野 | 新御徒町 | 秋葉原 | お茶の水 | 新御茶ノ水 | 小川町 | 森下 | 銀座 | 原宿 | 明治神宮前 | 新宿西口 | 新宿 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
電機大 | - | 3 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
北千住 | 3 | - | 6 | 8 | 9 | 8 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
西日暮里 | ∞ | 6 | - | 2 | ∞ | ∞ | ∞ | ∞ | 8 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 16 |
日暮里 | ∞ | 8 | 2 | - | 4 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
上野 | ∞ | 9 | ∞ | 4 | - | ∞ | 4 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ |
新御徒町 | ∞ | 8 | ∞ | ∞ | ∞ | - | 2 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 18 | ∞ |
秋葉原 | ∞ | ∞ | ∞ | ∞ | 4 | 2 | - | 2 | ∞ | ∞ | ∞ | 13 | 30 | ∞ | ∞ | ∞ |
お茶の水 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 2 | - | 5 | 5 | ∞ | ∞ | ∞ | ∞ | ∞ | 16 |
新御茶ノ水 | ∞ | ∞ | 8 | ∞ | ∞ | ∞ | ∞ | 5 | - | 5 | ∞ | ∞ | ∞ | 17 | ∞ | ∞ |
小川町 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 5 | 5 | - | 6 | ∞ | ∞ | ∞ | ∞ | 17 |
森下 | ∞ | ∞ | ∞ | ∞ | ∞ | 6 | ∞ | ∞ | ∞ | 6 | - | ∞ | ∞ | ∞ | ∞ | ∞ |
銀座 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 13 | ∞ | ∞ | ∞ | ∞ | - | ∞ | ∞ | ∞ | 17 |
原宿 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 30 | ∞ | ∞ | ∞ | ∞ | ∞ | - | 5 | ∞ | 4 |
明治神宮前 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | 17 | ∞ | ∞ | ∞ | 5 | - | ∞ | ∞ |
新宿西口 | ∞ | ∞ | ∞ | ∞ | ∞ | 18 | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | ∞ | - | 5 |
新宿 | ∞ | ∞ | 16 | ∞ | ∞ | ∞ | ∞ | 16 | ∞ | 17 | ∞ | 17 | 4 | ∞ | 5 | - |
このような状況でどの経路が最短かを計算させるプログラムを作りましょう。 ここで紹介する方法はダイクストラ法と呼ばれるものです。 この方法では電機大からすべての駅への所要時間が求まります。
ダイクストラ法のアイディアは次のようなものです。 駅の集合のうち、最短経路が求まっているような部分集合を考えます。 この部分集合に含まれる駅は、将来この部分集合の要素が増えても経路が更 新されたりしません。 この前提で、部分集合に入っていない駅のうち一番近い駅を新たに部分集合 の駅として加えます。
さて、このアイディアにおいて、出発地点は最初から部分集合に入っている ものとします。 また、部分集合に入っていない駅までの距離の計算は、出発駅からの距離で、 部分集合からたどり着ける頂点の距離を計算します。 なお、ここで、距離のもつ重要な性質の三角不等式の性質を次の様に利用します。
したがって、ダイクストラのアルゴリズムをまとめると、次のようになりま す。
これをC言語で実現するため、次の様にします。
if(flag[j])
で判断できるようにした
作成したプログラムは以下のようになります。
#include <stdio.h>
#define H 9999
#include "kitasenju.h"
int dist[NUM],flag[NUM],path[NUM];
void initArray(){
int i;
for(i=0;i<NUM;i++){
dist[i]=H;
flag[i]=1;
}
}
int argmin(void){
int index=-1;
int j;
int min=H;
for(j=0;j<NUM;j++){
if(flag[j] && dist[j]<min){
index=j;
min=dist[j];
}
}
return index;
}
void recalc(int index){
int j;
for(j=0;j<NUM;j++){
if((dist[index]+length[index][j])<dist[j]){
dist[j]=dist[index]+length[index][j];
path[j]=index;
}
}
}
void printEki(void){
int j;
int index;
for(j=0;j<NUM;j++){
printf("%s %d: ",eki[j],dist[j]);
index=j;
do{
printf("-%d- %s ",length[index][path[index]],eki[path[index]]);
index=path[index];
}while(index!=0);
printf("\n");
}
}
int main(void){
int i;
int index;
initArray();
dist[0]=0;
for(i=0;i<NUM;i++){
index = argmin();
if(index <0) {
fprintf(stderr,"The graph is disconnected.\n");
return -1;
}
flag[index]=0;
recalc(index);
}
printEki();
return 0;
}
ここで、 path[] 変数には、電機大から来る場合に直前に経由した駅を入れて います。 そうすると、最短路が見つかった場合、目的地から順に電機大まで最短経路をた どることができます。
旧電機大から渋谷までの経路を求めるためのヘッダファイルを示します
#define NUM 10
char *eki[NUM]={"電機大","新お茶の水","大手町","神保町","お茶の水",
"神田","表参道","代々木","原宿","渋谷"};
int length[NUM][NUM]={{0,5,13,15,10,10,H,H,H,H},
{5,0,3,H,H,H,H,H,H,H},
{13,3,0,3,H,H,12,H,H,H},
{15,H,3,0,H,H,11,H,H,H},
{10,H,H,H,0,3,H,13,H,H},
{10,H,H,H,3,0,H,H,H,25},
{H,H,12,11,H,H,0,H,3,3},
{H,H,H,H,13,H,H,0,3,H},
{H,H,H,H,H,H,3,3,0,2},
{H,H,H,H,H,25,3,H,2,0}
};
経路配列のヘッダファイルをチェックするプログラムを示します。
#include <stdio.h>
#define H 9999
#include "kitasenju.h"
int main(void){
int i,j;
for(i=0;i<NUM; i++){
for(j=0; j<NUM; j++){
if(length[i][j]!=length[j][i]){
printf("%s -%d-> %s, but %s -%d-> %s\n",
eki[i],length[i][j],eki[j],
eki[j],length[j][i],eki[i]);
}
}
}
return 0;
}
この講義の主な目的は、情報処理の基礎であるデータの数え上げや整列を制限 なく行えるようになることと、汎用のプログラミングテクニックとしての状態 遷移図の取り扱いです。 そのために、以下のことを学びました。
大量のデータを扱うには、あらかじめ容量を決めなければいけない配列は使え ません。そこで使用したのが線形リストというデータ構造です。 これはメモリをあらかじめ確保するのではなく、必要に応じて必要な量だけメ モリを取得し、データを順に格納するものです。
C 言語で実現するには次のような、データとポインタを格納するような構造体 を作り、 malloc 関数でメモリ上に生成してはひとつひとつつなげて使いまし た。
struct node {
int data;
struct node *next;
};
線形リストは大量のデータを取り扱う仕組みのうち、もっとも簡単な構造をし てます。従って、検索などはデータ量に比例した時間かかります。 一方で、データの追加や削除は容易にできます。
なお、C++ では STL(Standard Template Library)に deque や list というテ ンプレートが存在し、また、 Java では java.util.ArrayList が存在します。 どちらも C 言語のようにポインタの操作やメモリの操作無しに、頂点を付け 加えたり削除したりできます。
数の決まっているものを順序に従って整列させるには、配列のソートを行いま す。 しかし、数の決まってないものを整列させるには、順序木を使います。 順序木は、二分木において、頂点のデータが左の子よりも大きく、右の子より 小さいものを言います。 このような木に対して、データを格納する場合、根の頂点から順にデータを比 較していくとデータを格納すべき場所を捜し出すことができます。 一方、木の左から順にデータを取り出すことで、いつでも整列した状態でデー タを取り出すことができます。 C 言語ではこの順序木を使うのに、自らの構造を指すことができるポインタを 二つ持つ構造体を使用しました。
struct node {
int data;
struct node *left,*right;
};
なお、値の整列は情報処理の基礎であり、多くのプログラミング言語で簡単に 利用できるようになっています(但し、内部では順序木を利用している)。 C++ では STL にある map や multimap で利用 できます。 また、Java 言語では java.util.TreeMap を使います。 しかし、大量のデータの入出力が発生する場合、効率良くデータをやりとりす るために、外部データベースの利用を考えた方が良い場合があります。
プログラム開発において、与えられた問題を分解し、元の問題と同じ構造を発 見できれば、再帰という手法を用いてプログラムを書くことができます。
例えば、与えられたデータを整列する場合、整列された列はどの部分列を見て も整列されています。よってデータ列を半分にすれば、大きい列と小さい列に 分割できます。 従って、データ列を大きいものと小さいものとに半分に分割するような関数を 設計し、半分になった列それぞれに対して、同様な処理を繰り返すようにする と最終的にデータを整列できます(Quick Sort)。
このように、データの列に何らかの規則がありデータを分割しても成り立つ場 合、関数の中でデータを分割しながら同じ関数を呼び出すようにすると、シン プルなプログラムで複雑なデータを取り扱うことができます。 再帰は、このほか線形リストや順序木の処理などにも活用できます。
プログラム開発において、状態の解析は重要です。 状態遷移図を書くことで、状態の移り変わりに応じたプログラムの作成が容易 になります。
一般のフローチャートと似ていますが内容は大きく異なります。 フローチャートでは一つの図形がプログラム一行を表すことがあり、 また、入出力の仕様や、変数の値、関数呼び出しなど、プログラムを作成する 上で必要な情報のほとんど全て(さすがにファイルレイアウトまでは記入でき ませんが)を詰め込めます。 これはある意味一つのプログラム言語となっていて、別のプログラム言語で記 述するための共通言語としての役割を担うことになります。 但し、フローチャートはプログラミングスタイルを強要することができないの で、読み易く保守し易くプログラムを開発すると言う近年のプログラム開発か らは使いづらいツールになっています。
一方、状態遷移図に記入できるのは入力と状態と状態の移り変わり、それに加 えて出力や開始、終了状態だけです。 このような限定条件のもとで図を書くことにより、一つの状態は必ずしもプロ グラム一行に対応するわけではなく、プログラム内のまとまった一ブロックを 指すようになります。