LoginSignup
1

More than 3 years have passed since last update.

posted at

updated at

グラフデータベースNeo4jでダイクストラ

この記事について

Neo4jを使ったグラフアルゴリズムの実装を紹介してみようという計画「グラフアルゴリズム入門 - javaとNeo4jで学ぶ -」の一部です(どこまで続くか・・・)

サンプルコードはこちらにおいた。

最短経路探索

幅優先・深さ優先探索は、頂点と辺のつながりの情報だけが与えられていたが、辺に移動コストの数値が定義されているときに、スタートするノードからゴールするノードまでの総コストが最小になるような経路と総コストを求める探索を最短経路探索と呼ぶ。最短経路探索には、有名なダイクストラのアルゴリズムがあり、ここではそれをNeo4jで実装してみる。コストはNeo4jのプロパティで表現することができる。プロパティグラフを容易に扱えるNeo4jにとって非常に役に立ち、わかりやすい例題である。

ダイクストラ法

ダイクストラ法の簡単な解説とアルゴリズムは、こちらにあります(Qiitaは実装例は多いけど解説は意外と少ない)。スタートノードに隣接するノードを探し、その中で最もコストが小さいものは、そのノードへのコストの最小値として確定することができる。それを繰り返していき、コストの最小値を次々と確定させて、最終的にゴールのノードのコストが確定したら終了したらよい。下図にダイクストラ法の実行のイメージを示す。ここでは品川から久里浜に至る経路を探索している。左のコストテーブルが、コストが確定していく順番のイメージである。
ダイクストラ.png

自前でダイクストラ

実装例を下に示す。このプロシージャは、スタートとゴールのノードidをもらって、その最短経路のパスと総コストを求める。返却値は必ず一つで、複数同じコストのパスがあるケースには対応していない(最初に見つかったパスが返却される)。ダイクストラ法をナイーブに実装するとO(N^2)の計算量になるが、ノードをコストの小さい順に並べるプライオリティキューを利用することによって、計算量を減らすことができる。下のプログラムでは、ノードとコストの対応とノードのコストが確定済みかをクラスNodeInfoを利用して管理している。よって、プライオリティキューにはNodeInfoのオブジェクトを入れている。その順序はNodeInfoのcostの比較によって並べ替えられている(比較関数がラムダ式で定義されている)。NodeInfoの情報は、ノードと対応をつけてmapで管理している。また、パスを得るためのノードとその一つ手前の関連の対応を管理するマップも幅優先探索などと同様に使っている。コストを表すプロパティは、変数cost_propertyに定義している。ここでは"cost"という名前を使っている。データベース上にこのプロパティがあることが前提とされる。この文字列をプロシージャのパラメータに入れるほうが、柔軟になるが、その改造は容易である。

    // sample8_1: djkstra
    @Procedure(value = "example.sample8_1")
    @Description("sample8_1: djkstra")
    public Stream<Output> sample8_1( @Name("from_id") Long from_id, @Name("to_id") Long to_id )
    {
        Node from_nd = db.getNodeById(from_id);
        Node to_nd = db.getNodeById(to_id);
        // Priority Queue by cost property value
        PriorityQueue<NodeInfo> pq = new PriorityQueue<>((n1,n2)->Double.compare(n1.cost,n2.cost));
        // map for keeping node and cost
        Map<Node, NodeInfo> nodes = new HashMap<>();
        // map for keeping Node and parent relationship
        Map<Node, Relationship> parent = new HashMap<>();

        // current node(info)
        NodeInfo cur_ni = new NodeInfo(from_nd, 0.0);
        pq.add(cur_ni);
        nodes.put(from_nd, cur_ni);

        // if to_node's cost is fixed, exit
        NodeInfo to_ni = null;
        while(to_ni == null || to_ni.done != true){
            // if queue is empty, no route exit
            if(cur_ni == null){
                return new ArrayList<Output>().stream();
            }
            // top node of queue's cost is fixed
            cur_ni = pq.poll();
            cur_ni.done = true;
            // get adjacent nodes and add them to queue
            // Property for cost: type must be double
            String cost_property = "cost";
            Iterable<Relationship> rels = cur_ni.nd.getRelationships();
            for(Relationship rel: rels){
                // get adjacent nodes and their costs
                Node o_nd = rel.getOtherNode(cur_ni.nd);
                double cost_rel = (double)rel.getProperty(cost_property);
                // check whether the node was found 
                NodeInfo next_ni = nodes.get(o_nd);
                // not found -> 1st appearance of the node, add it to map
                if(next_ni == null){
                    next_ni = new NodeInfo(o_nd, cur_ni.cost+cost_rel);
                    pq.add(next_ni);
                    nodes.put(o_nd, next_ni);
                    parent.put(o_nd, rel);
                }
                // found but cost is't fixed and has lower cost -> overwrite cost
                else if(next_ni.done == false){
                    if(next_ni.cost > cur_ni.cost+cost_rel){
                        next_ni.cost = cur_ni.cost+cost_rel;
                        parent.put(o_nd, rel);
                    }
                }
                // found and cost was fixed -> do nothing
            }   
            // get to_node from map
            to_ni = nodes.get(to_nd);
        }
        // output construction
        Output o = new Output();
        o.path = getPath(from_nd, to_nd, parent);
        o.cost = to_ni.cost;
        // list for result
        List<Output> o_l = new ArrayList<>();
        o_l.add(o);
        return o_l.stream();
    }

    // Node and cost to it 
    public class NodeInfo {
        public Node nd; // Neo4j Node
        public double cost; // cost summation from start node
        public boolean done; // flag for cost fixed

        // constructor
        public NodeInfo(Node nd, double cost) {
            super();
            this.nd = nd;
            this.cost = cost;
            done = false;
        }
    }

幅優先探索のときと同じサンプルでこのプログラムを実行してみよう。以下のサンプルである。
スクリーンショット 2019-09-14 21.16.44.png

ちなみに関連のコストは以下のようになっている。
スクリーンショット 2019-09-23 18.39.13.png

A(idは0)からF(idは5)へ至る最短経路を求める実行結果は、以下の通りである。
スクリーンショット 2019-09-23 18.41.15.png
スクリーンショット 2019-09-23 18.41.57.png
(幅優先などと同じようにすべてのノードへの最短経路を求める例題も追加するかもしれない。最短経路木が同様に書ける)

組み込み関数でダイクストラ

Neo4jには組み込みでダイクストラ法は実装されている。それを利用する方法は、こちらにある。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
1