Prim法

特にテーマもなくBlogを初めたので、しばらくはグラフ理論アルゴリズムに関するプログラムをF#を使って書いていくことにしてみようと思う。今回はPrim法で、入力データは前回と同じもの。グラフが与えられた時に、全ての点を通り、コストの総和が最小になるよう辺を選び取るアルゴリズムになります。

ところで、自分は、プログラムを作る際には、効率よりも「解けるプログラムを作ること」と、(自分にとって)理解しやすいこと、のほうを重視している。PerlRubyなんかのLightweightな言語がはやってるように、とりあえず「出来る」だけで有用な場面のほうが多い気がするし、効率を気にして時間をかけてプログラムに四苦八苦するよりも、とりあえず出来たほうがなんか楽しい、というのが大きい。

という(いい)わけで、Prim法の実装にあたってResizeArray使った「手抜きPriority Queue」を実装してみた。本来ならヒープを使ってPriority Queueを実装するほうが効率もよいし本格的だが、そこは後で置き換えれば良いので、Prim法を解くことから入ってみる。

#light "off"

open Microsoft.FSharp.Collections;;
//プリム法
let pe x = print_any x;print_endline "";;
let m = Int32.max_int;;
let nodenum = 6;;
//辺リスト
let adj = [|(0,1,5);(0,2,4);(0,3,2);(1,2,2);(1,4,6);(2,3,3);(2,5,2);(3,5,6);(4,5,4)|];;
//簡易プライオリティキュー
type easy_pq<'T> = class
    val private array : ResizeArray<'T>
    val private sorter : 'T -> 'T -> int
    new (sortfunc,(ar : 'T array)) = {array = new ResizeArray<'T>(ar);sorter=sortfunc}
    new (sortfunc) = {array = new ResizeArray<'T>();sorter=sortfunc}
    member x.push v = x.array.Add(v);ResizeArray.sort x.sorter x.array
    member x.pop _ = let r = x.array.[0] in x.array.Remove(r)|>ignore;r
    member x.show f = Seq.iter f x.array
    end;;

let mst = new ResizeArray<int>();;
let edges = new ResizeArray<int*int*int>();;
//MSTの初期値としてノード0を指定
mst.Add(0);;

//MSTへ一つノードを追加し、edgesへ追加した辺を格納する
let update mst edges adj=
    let sorter a b = let ((_,_,l),(_,_,r))=(a,b) in
        if l>r then 1 else if l<r then -1 else 0 in
    let pq = new easy_pq<int*int*int>(sorter) in
    //次の辺を見つけるためキューにデータを入れる
    for i in mst do
        Array.map (fun x ->
            let (f,t,c)=x in if f=i || t=i then pq.push x
            ) adj |> ignore
    done;
    //mstにないノードが見つかるまでキューからPOP
    let rec update_aux mst (edges:ResizeArray<int*int*int>) adj (pq:easy_pq<int*int*int>)=
        let nextedge = pq.pop() in
        let (f,t,c) = nextedge in
        if not (ResizeArray.exists ((=)t) mst) then begin mst.Add(t);edges.Add(nextedge) end
        else if not(ResizeArray.exists ((=)f) mst) then begin mst.Add(f);edges.Add(nextedge) end
        else update_aux mst edges adj pq in
    update_aux mst edges adj pq;;

//最初に1個入れているのでノード数-1MSTのアップデートを繰り返す
for i=0 to nodenum-2 do update mst edges adj done;;
ResizeArray.iter pe edges;;

出力結果は、辺のリスト。これは、前に作ったグラフを可視化するプログラムにそのまま入力出来るので、次のプログラムで最小全域木がどうなってるか確認出来る。なお、今回のupdate関数は副作用ありな関数になっている。

#light "off"

open Microsoft.FSharp.Collections;;
open Microsoft.FSharp.Core;;
open System;;
open System.Windows.Forms;;
open System.Drawing;;

//プリム法
let pe x = print_any x;print_endline "";;
let m = Int32.max_int;;
let nodenum = 6;;
//辺リスト
let adj = [|(0,1,5);(0,2,4);(0,3,2);(1,2,2);(1,4,6);(2,3,3);(2,5,2);(3,5,6);(4,5,4)|];;
//簡易プライオリティキュー
type easy_pq<'T> = class
    val private array : ResizeArray<'T>
    val private sorter : 'T -> 'T -> int
    new (sortfunc,(ar : 'T array)) = {array = new ResizeArray<'T>(ar);sorter=sortfunc}
    new (sortfunc) = {array = new ResizeArray<'T>();sorter=sortfunc}
    member x.push v = x.array.Add(v);ResizeArray.sort x.sorter x.array
    member x.pop _ = let r = x.array.[0] in x.array.Remove(r)|>ignore;r
    member x.show f = Seq.iter f x.array
    end;;

let mst = new ResizeArray<int>();;
let edges = new ResizeArray<int*int*int>();;
//MSTの初期値としてノード0を指定
mst.Add(0);;

//MSTへ一つノードを追加し、edgesへ追加した辺を格納する
let update mst edges adj=
    let sorter a b = let ((_,_,l),(_,_,r))=(a,b) in
        if l>r then 1 else if l<r then -1 else 0 in
    let pq = new easy_pq<int*int*int>(sorter) in
    //次の辺を見つけるためキューにデータを入れる
    for i in mst do
        Array.map (fun x ->
            let (f,t,c)=x in if f=i || t=i then pq.push x
            ) adj |> ignore
    done;
    //mstにないノードが見つかるまでキューからPOP
    let rec update_aux mst (edges:ResizeArray<int*int*int>) adj (pq:easy_pq<int*int*int>)=
        let nextedge = pq.pop() in
        let (f,t,c) = nextedge in
        if not (ResizeArray.exists ((=)t) mst) then begin mst.Add(t);edges.Add(nextedge) end
        else if not(ResizeArray.exists ((=)f) mst) then begin mst.Add(f);edges.Add(nextedge) end
        else update_aux mst edges adj pq in
    update_aux mst edges adj pq;;

//最初に1個入れているのでノード数-1MSTのアップデートを繰り返す
for i=0 to nodenum-2 do update mst edges adj done;;
ResizeArray.iter pe edges;;


//要素数と(始点、終点、コスト)の配列を受け取り、そのグラフを表示するクラス
type Graph = class inherit Form
	val pbox : PictureBox
	val pos : (float32*float32) array   //各ノードのx,y座標
	val circle_width : float32          //1つのノードの幅
	val circle_height : float32         //1つのノードの高さ
	new (pnum:int,adj) as this =
	    {pbox = new PictureBox();
	     pos=Array.init pnum (fun x -> (0.f,0.f));
	     circle_width = 50.f;
	     circle_height = 50.f;}
		then
		let (whole_width,whole_height)=(800,600) in
		this.Width <- whole_width;
		this.Height <- whole_height;
		let (screen_width,screen_height)=(this.ClientRectangle.Width,this.ClientRectangle.Height) in
		this.pbox.Dock <- DockStyle.Fill;
		this.Controls.Add(this.pbox);
		this.pbox.Image <- new Bitmap(screen_width,screen_height);
		//画面中央の計算
		let (cx,cy)=((Float32.of_int screen_width)/2.f,(Float32.of_int screen_height)/2.f) in
		//各ノードの座標計算
		for i=0 to pnum-1 do
		    let dist = 200.f in
		    let (dx,dy)=
		    (cx+dist*Float32.of_float(Math.Cos(2.*Math.PI/(Float.of_int pnum)*(Float.of_int i))),
		     cy+dist*Float32.of_float(Math.Sin(2.*Math.PI/(Float.of_int pnum)*(Float.of_int i)))) in
		     this.pos.[i]<-(dx,dy);
		done;
		//ノードを描く
		Array.mapi (fun i x -> this.circle (fst x) (snd x) (i.ToString())) this.pos |> ignore;
		//ノード間の接続とコストを描く
        for i in adj do
            let (s,e,c) = i in
            this.line (fst this.pos.[s]) (snd this.pos.[s]) (fst this.pos.[e]) (snd this.pos.[e]) (Float32.of_int c)
            done
		
    //円とラベルを指定座標に描く
    member this.circle (x:float32) (y:float32) label =
		use g = System.Drawing.Graphics.FromImage(this.pbox.Image) in
		let w,h = (this.circle_width,this.circle_height) in
		g.DrawEllipse(Drawing.Pens.Black,x,y,w,h);
		let font = new Font("MS UI Gothic",18.f) in
		let size = g.MeasureString(label,font,768,new StringFormat()) in
		g.DrawString(label,font,Brushes.Black,x+w/2.f-size.Width/2.f,y+h/2.f-size.Height/2.f);

    //線を指定座標から指定座標へ描き、間にコストを描く
    member this.line (sx:float32) (sy:float32) (ex:float32) (ey:float32) c=
        use g = System.Drawing.Graphics.FromImage(this.pbox.Image) in
        let (dx,dy) = (this.circle_width/2.f,this.circle_height/2.f) in
		let font = new Font("MS UI Gothic",12.f) in
		let (nsx,nsy,nex,ney) = (sx+dx,sy+dy,ex+dx,ey+dy) in
        g.DrawLine(Pens.Black,nsx,nsy,nex,ney);
        //線を1:2に内分する点にコストを描く
		g.DrawString(c.ToString(),font,Brushes.Black,(nsx+2.f*nex)/3.f,(nsy+2.f*ney)/3.f);
end;;


[<STAThread>]
do System.Windows.Forms.Application.Run(new Graph(6,edges));;