BipartiteGraphMatching
Last-Modified: 2008-04-05 19:46:53
二部グラフのマッチングを行うプログラム。Graphvizで画像化できるdotファイルを作る。
/** * Time-stamp: <2008-03-15 13:22:07> */ import java.io.*; import java.util.*; public class BipartiteGraphMatching{ /* * 2部グラフのマッチング * グラフのデータ構造は頂点に対する接続枝の隣接リスト * (始点として接続している枝集合、終点として接続してる枝集合のリスト) * さらに、各枝についての始点・終点の情報もあわせて持つ * * 入力グラフフォーマット * 第一行目に点数、枝数 * 6 6 * 0 4 * 0 5 * 1 4 * 1 6 * 2 5 * 3 6 */ protected List> nodeList ;//順方向枝接続リスト protected List
> invNodeList ;//逆方向枝接続リスト protected boolean [] left , match;//2部グラフにおいてどちら側に属すか,マッチされているか protected List
edgeList;//枝の始点と終点 private int dotFileCount ; protected Deque deque; protected boolean [] visit; protected int [] prev ; //デバグ用に途中経過を表示するか public boolean debug = true ; //Graphviz形式のdotファイルを出力するか public boolean dotFile = true ; static final String graphvizHead = "digraph sample {\ngraph [ranksep = 2.0 ,rankdir = LR];\nnode [shape = circle];"; private BufferedReader in = null; private BufferedWriter out = null; /** * */ public BipartiteGraphMatching(){ in = new BufferedReader(new InputStreamReader(System.in)); } public void init(int nodeSize , int edgeSize) throws IOException,NumberFormatException{ if(debug) System.out.println("node size : "+nodeSize +", edge size :"+edgeSize); dotFileCount = 0 ; nodeList = new ArrayList >(nodeSize); for(int i = 0 ; i < nodeSize ; i++) nodeList.add(new ArrayList
()); invNodeList = new ArrayList >(nodeSize); for(int i = 0 ; i < nodeSize ; i++) invNodeList.add(new ArrayList
()); edgeList = new ArrayList (edgeSize); for(int i = 0 ; i < edgeSize ; i++) edgeList.add(new Integer [2]); left = new boolean [nodeSize]; match = new boolean [nodeSize]; } /** * 交互道(alternating path)を返す * @return 交互道を枝の列として含むArrayList、交互道がない場合はnull */ private List findAltPath(){ deque = new ArrayDeque (); visit = new boolean [nodeList.size()]; prev = new int [nodeList.size()]; List altPath = new ArrayList (); for(int i = 0 , n = prev.length ; i < n ; i++){ prev[i]=-1; } //左側の各点から幅優先探索を行い交互道を発見する for(int startNode = 0 , n = nodeList.size() ; startNode < n ; startNode++){ //マッチされているか、すでに訪れているか、右側にあるものは調べない if(match[startNode]||visit[startNode]||(!left[startNode])) continue; //幅優先探索を行う altPath = breadthFirstSearch(startNode); if(altPath != null) return altPath; } return null; } /** * 交互道を見つけるための幅優先探索(交互道が見つかれば探索終了) * @param startNode 幅優先探索の開始点 * @return 交互道を枝の列として含むArrayList、交互道がない場合はnull */ private List breadthFirstSearch(int startNode){ deque.push(startNode); visit[startNode]=true; while(!deque.isEmpty()){ int nowNode = deque.poll(); //nowNodeに接続している枝について for(int edge : nodeList.get(nowNode)){ //nowNodeに隣接点について int neighbor = edgeList.get(edge)[1]; //まだ訪れていないときのみ if(!visit[neighbor]){ prev[neighbor]=edge; visit[neighbor]=true; deque.add(neighbor); //交互道か(マッチされていない右側の点で終わっているか) if(!left[neighbor]&&!match[neighbor]){ return altPath(startNode,edge); } } } } return null; } /** * 幅優先探索終了時に呼び出され、幅優先探索の結果から * 交互道を構成する * @param startNode 幅優先探索の開始点 * @param edge 幅優先探索で訪れた最後の枝 * @return 交互道を枝の列として含むArrayList */ private List altPath(int startNode,int edge){ List path = new LinkedList (); path.add(edge); while(edgeList.get(edge)[0]!=startNode){ edge = prev[edgeList.get(edge)[0]]; path.add(edge); } return path; } /** * 交互道からマッチングを一つ増やす * @param 交互道(枝の列) */ private void matchingExchange(List altPath){ if(debug) dispPath(altPath); int lastEdge = altPath.get(0);//交互道の最後の枝 int firstEdge = altPath.get(altPath.size()-1);//交互道の最初の枝 match[edgeList.get(lastEdge)[1]] = true; match[edgeList.get(firstEdge)[0]]= true; for(int edgeIndex : altPath){ changeDirection(edgeIndex); } } /** * 枝の向きを逆向きに変える * * @param edgeIndex 向きを変える枝の番号 */ private void changeDirection(int edgeIndex){ int src = edgeList.get(edgeIndex)[0]; int dst = edgeList.get(edgeIndex)[1]; nodeList.get(src).remove(new Integer(edgeIndex)); nodeList.get(dst).add(new Integer(edgeIndex)); invNodeList.get(dst).remove(new Integer(edgeIndex)); invNodeList.get(src).add(new Integer(edgeIndex)); edgeList.get(edgeIndex)[0]=dst; edgeList.get(edgeIndex)[1]=src; } /** * 2部グラフの最大マッチングの1つを求める */ public void findMaxMatching() throws IOException{ List altPath = null; if(dotFile) toDotFile(toGraphviz(null,"Start Bipartite Graph Matching.")); while((altPath = findAltPath())!=null){ if(dotFile) toDotFile(toGraphviz(altPath,"Alternating-Path(Bold)")); matchingExchange(altPath); if(dotFile) toDotFile(toGraphviz(null,"Mathing(Red)")); if(debug) dispGraph(); } if(dotFile) toDotFile(toGraphviz(altPath,"Matching is complete.")); } /** * データの内容を表示する。デバグ用メソッド */ private void dispGraph(){ System.out.println("node size = "+nodeList.size() +" , edge size = "+edgeList.size()); //順方向枝接続リストの表示 System.out.println("node -> edges:"); int j = 0; for(List list : nodeList){ System.out.print("node["+(j++)+"]"); for(Integer i : list) System.out.print(" "+i); System.out.println(); } //逆方向枝接続リストの表示 System.out.println("node <- edges:"); j = 0; for(List list : invNodeList){ System.out.print("node["+(j++)+"]"); for(Integer i : list) System.out.print(" "+i); System.out.println(); } //枝の始点、終点の表示 System.out.println("edge:"); j = 0 ; for(Integer[] e : edgeList) System.out.println("edge ["+(j++)+"] ("+e[0]+","+e[1]+")"); //マッチング状況の表示 System.out.print("matching:"); for(boolean b : match) System.out.print(b+" "); System.out.println(); } /** * 交互道を表示する。デバグ用メソッド。 */ private void dispPath(List path){ System.out.print("alt path : "); for(int edge : path){ System.out.print("("+edgeList.get(edge)[0]+","+edgeList.get(edge)[1]+")"); } System.out.println(); } /** * 現在のグラフの状態をGraphviz形式で出力 * @param label グラフのラベル * @return String型で現在のグラフの状態 */ private String toGraphviz(List path,String label){ StringBuffer sb = new StringBuffer(graphvizHead +"\n"); if(path == null) path = new LinkedList (); sb.append("label = \""+label+"\";\n"); //ノードについて出力 マッチされているノードは赤 for(int i = 0 , n = nodeList.size() ; i < n ; i++){ sb.append(i+"["); if(match[i]) sb.append("style = filled , color = black , fillcolor = red ];\n"); else sb.append("color = black];\n"); } //エッジについて出力 マッチに使われているエッジは赤 for(int i = 0 , n = edgeList.size() ; i < n ; i++){ sb.append(edgeList.get(i)[0] +" -> "+edgeList.get(i)[1]); if(left[edgeList.get(i)[1]]) sb.append("[color = red , "); else sb.append("[color = black , "); if(path.contains(new Integer(i))) sb.append("style = bold];\n"); else sb.append("style = solid];\n"); } sb.append("{rank = same "); for(int i = 0 , n = nodeList.size() ; i < n ; i++){ if(left[i]) sb.append(";"+i); } sb.append("}\n{rank = same "); for(int i = 0 , n = nodeList.size() ; i < n ; i++){ if(!left[i]) sb.append(";"+i); } sb.append("}\n}"); return sb.toString() ; } /** * out<連番>.dot(例,out000.dot,out001.dot,...)というファイルに引数の文字列を出力 * 想定している使い方は * toDotFile(toGraphviz(path))等 * @param dot 出力する文字列 * @return 出力したファイルの連番 */ protected int toDotFile(String dot) throws IOException{ out = new BufferedWriter(new FileWriter("out"+String.format("%03d",dotFileCount)+".dot")); out.write(dot); out.close(); return dotFileCount++ ; } /** * 標準入力からデータを読み込みグラフのデータ構造を作る */ public void readGraph() throws IOException { int [] tmp ; tmp = readNums(); init(tmp[0],tmp[1]); for(int i = 0 , n = edgeList.size() ; i < n ; i++){ tmp = readNums(); int leftNode = tmp[0]; int rightNode = tmp[1]; //順方向の枝の接続リストを作る nodeList.get(leftNode).add(i); //逆方向の枝の接続リストを作る invNodeList.get(rightNode).add(i); edgeList.get(i)[0] = leftNode; edgeList.get(i)[1] = rightNode; left[leftNode] = true; left[rightNode] = false; } if(debug) dispGraph(); } /** * 一行読んでそこにある二つの数字を返す * @return 0番目に最初に読んだ数、1番目に次に読んだ数が格納されたint型配列 */ private int [] readNums() throws IOException{ String [] strs = in.readLine().split("\\s+"); int [] nums = new int [2]; nums[0]= Integer.parseInt(strs[0]); nums[1]= Integer.parseInt(strs[1]); return nums ; } public static void main(String []args){ try{ BipartiteGraphMatching bgm = new BipartiteGraphMatching(); bgm.readGraph(); bgm.findMaxMatching(); }catch (IOException e ){ e.printStackTrace(); } } }