[php]トポロジカルソート
トポロジカルソートって知っていますか?
最近では平成19年のソフトウェア開発技術者の午後問題で取り上げられました。
Googleで探しても他のソートよりは明らかに数が少ないですね。
おそらく、トポロジカルソートという名前のほかにも呼び方が複数あるのと、
そもそもソートの分類の中では異質なのかもしれません。
本題に入る前にグラフの種類について説明します。
ここでいうグラフというのは、Excelなどのウィザードで作成するグラフではなく、
グラフ理論で取り扱われる点集合と辺集合の組み合わせのことです。
下図(図1)のように図示されるのが一般的です。

図1. グラフの例
グラフにはいくつかの種類があります。
わかりやすいのが無向グラフ(undirected graph)と有向グラフ(directed graph)です。
その名のとおり、無向グラフは各辺に方向がついていないグラフ、有向グラフは各辺に方向がついているグラフです。
先ほどの図にはそれぞれの例が描かれています。
トポロジカルソートで扱うのはDAG(Directed Acyclic Graph : 有向非循環グラフ)です。
DAGは有向グラフの一種で、閉路を持たない(循環する部分を持たない)ものを指しています。
次の図2は循環のあるグラフの例で、今回のトポロジカルソートには用いることができないものです。

図2. 循環のあるグラフの例
さてさて本題。トポロジカルソートというのは、グラフの頂点に連番をつけていくのですが、
矢印の向きと大小関係が一致するようにするというものです。
頂点につけた番号は位相的順序とかトポロジカルソート値と呼びます。
図3、図4はトポロジカルソート値を割り当てたグラフの例です。

図3. トポロジカルソート値の例1

図4. トポロジカルソート値の例2
どのようにしてこのトポロジカルソート値を求めるかと言うと、一言で言えば再帰的に求めます。
深さ優先探索とかそういう用語も関係してきますが、解説が面倒なので今回は省略します。
というかもう解説がだいぶ面倒なのでソースコード出しちゃいます。
-
/**
-
* topologically sort graph
-
*
-
* @author uechoco <uechoco @gmail.com>
-
* @version 1.0.0
-
* @since PHP 5.2.1
-
*/
-
class TopologicalSort
-
{
-
/**
-
* label-list of nodes
-
*
-
* @var array
-
* @access private
-
*/
-
var $nodeLabels;
-
-
/**
-
* adjacent matrix of nodes
-
*
-
* @var array
-
* @access private
-
*/
-
var $adjacent;
-
-
/**
-
* index of topological values
-
*
-
* @var array
-
* @access private
-
*/
-
var $result;
-
-
/**
-
* number of results
-
*
-
* @var integer
-
* @access private
-
*/
-
var $k;
-
-
/**
-
* execute "topological sort"
-
*
-
* @param string
-
* @access public
-
* @return boolean
-
*/
-
function execute($fileName)
-
{
-
$this->initialize();
-
-
$num = $this->readDAG($fileName);
-
-
if ($num == 0)
-
{
-
echo "{$fileName} is invalid data.";
-
return false;
-
}
-
-
for ($i = 0; $i <$num; $i++)
-
{
-
if ($this->isNeverVisited($i))
-
{
-
if (!$this->visit($i, $num))
-
{
-
echo 'A cyclic link is found.';
-
return false;
-
}
-
}
-
}
-
return true;
-
}
-
-
/**
-
* initialize class fields
-
*
-
* @access private
-
* @return void
-
*/
-
function initialize()
-
{
-
for ($i = 0; $i <NMAX; $i++)
-
{
-
$this->nodeLabels[$i] = '';
-
$this->result[$i] = NO_NODE;
-
$this->visited[$i] = NEVER;
-
for ($j = 0; $j <NMAX; $j++)
-
{
-
$this->adjacent[$i][$j] = false;
-
}
-
}
-
-
$this->k = 0;
-
}
-
-
/**
-
* return true if the index have never visited, otherwise false
-
*
-
* @param integer
-
* @access private
-
* @return void
-
*/
-
function isNeverVisited($i)
-
{
-
return ($this->visited[$i] == NEVER);
-
}
-
-
/**
-
* get number of nodes
-
*
-
* @access private
-
* @return integer
-
*/
-
function getNodeCount()
-
{
-
$n = 0;
-
-
// search the valid nodes in the label-list of nodes, and count these
-
while ($n <NMAX && $this->nodeLabels[$n] != '')
-
{
-
$n++;
-
}
-
return $n;
-
}
-
-
/**
-
* register label of node
-
*
-
* @param string label of node
-
* @access private
-
* @return integer
-
*/
-
function registerNodeLabel($nodeLabel)
-
{
-
if (
-
$nodeLabel == '')
-
{
-
return 0;
-
}
-
-
// search the label in the label-list of nodes
-
$i = 0;
-
while ($i <NMAX && $this->nodeLabels[$i] != '')
-
{
-
// return the index if the label already exists
-
if ($this->nodeLabels[$i] == $nodeLabel)
-
{
-
return $i;
-
}
-
$i++;
-
}
-
if (
-
$i>= NMAX)
-
{
-
return 0;
-
}
-
-
// a new node label
-
$this->nodeLabels[$i] = $nodeLabel;
-
return $i;
-
}
-
-
/**
-
* read DAG data
-
*
-
* @param string
-
* @access private
-
* @return integer
-
*/
-
function readDAG($fileName)
-
{
-
// open and read the file
-
if ($lines === false)
-
{
-
return 0;
-
}
-
-
// parse each line
-
foreach($lines as $line)
-
{
-
$nodeLabelFrom = '';
-
$nodeLabelTo = '';
-
if (
-
{
-
$nodeLabelFrom = $matches[1];
-
$nodeLabelTo = $matches[2];
-
}
-
else
-
{
-
return 0;
-
}
-
if (
-
$nodeLabelFrom == '' || $nodeLabelTo == '')
-
{
-
return 0;
-
}
-
-
// register nodes
-
$i = $this->registerNodeLabel($nodeLabelFrom);
-
$j = $this->registerNodeLabel($nodeLabelTo);
-
if (
-
$i <0 || $i>= NMAX || $j <0 || $j>= NMAX)
-
{
-
return 0;
-
}
-
-
$this->adjacent[$i][$j] = true;
-
}
-
return $this->getNodeCount();
-
}
-
-
/**
-
* check visited
-
*
-
* @param integer
-
* @param integer
-
* @access private
-
* @return boolean
-
*/
-
function visit($i, $n)
-
{
-
if ($n <0 || $n>= NMAX || $i <0 || $i>= $n)
-
{
-
return false;
-
}
-
-
$this->visited[$i] = JUST;
-
for (
-
$j = 0; $j <$n; $j++)
-
{
-
if ($this->adjacent[$j][$i])
-
{
-
$v = $this->visited[$j];
-
if (($v == JUST) || (($v == NEVER) && !$this->visit($j, $n)))
-
{
-
return false;
-
}
-
}
-
}
-
-
$this->visited[$i] = ONCE;
-
$this->result[$this->k] = $i;
-
$this->k++;
-
return true;
-
}
-
-
/**
-
* toArray
-
*
-
* @access public
-
* @return string
-
*/
-
function toArray()
-
{
-
if ($this->k <= 0)
-
{
-
return;
-
}
-
-
for ($i = 0; $i <$this->k; $i++)
-
{
-
$data[] = $this->nodeLabels[$this->result[$i]];
-
}
-
return $data;
-
}
-
-
/**
-
* toString
-
*
-
* @param string
-
* @access public
-
* @return string
-
*/
-
function toString($glue = ', ')
-
{
-
}
-
}
-
-
-
$ts = new TopologicalSort();
-
if ($ts->execute('./data/ts01.dat'))
-
{
-
echo $ts->toString();
-
}
というわけで、こんな感じです。なんとなく英語でコメント書いたんですが、
正直、英語で書き始めなければと後悔しています。
各フィールドをざっくり説明します。
- nodeLabels
- 頂点のラベルを保持しておくための配列です。データ内の出現順に追加されます。
- adjacent
- どの頂点からどの頂点への接続があるのかを保持しておくための隣接行列です。
行列自体はn次の正方行列です。2次元配列を用いることで、行列を再現しています。 - visited
- 走査中の頂点の状態を保持しておくための配列です。頂点の個数だけ使用します。
頂点の状態は、次の3つがあります。- NEVER
- まだ走査で訪れていない頂点です。
- JUST
- 現在の走査で訪れている頂点です。
- ONCE
- 既に走査で訪れたことのある頂点です。
- result
- トポロジカルソート値が確定した頂点のインデックス(添字)を保持しておくための配列です。
頂点の個数だけ使用します。トポロジカルソート値が確定した頂点から順に追加されます。
初期状態では、NO_NODEという定数で初期化されています。 - k
- トポロジカルソート値が確定している頂点の個数です。
このプログラムのアルゴリズムの中心は、visit()メソッドです。
自分への接続がある場合は再帰的にトポロジカルソート値を求め、
循環接続がある場合はfalseを返してメソッドを抜けようとします。
■参考Web文献
Spaghetti Source - トポロジカルソート
http://www.prefield.com/algorithm/graph/topological_sort.html
情報処理推進機構:情報処理技術者試験センター:問題冊子・配点割合・解答例・採点講評(2007、平成19年)
http://www.jitec.jp/1_04hanni_sukiru/mondai_kaitou_2007h19.html#19aki