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();
}
}
}