diff --git a/graph/network/diffusion.go b/graph/network/diffusion.go new file mode 100644 index 00000000..11b5832f --- /dev/null +++ b/graph/network/diffusion.go @@ -0,0 +1,212 @@ +// Copyright ©2017 The Gonum Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package network + +import ( + "math" + + "gonum.org/v1/gonum/graph" + "gonum.org/v1/gonum/mat" +) + +// Diffuse performs a heat diffusion across nodes of the undirected +// graph described by the given Laplacian using the initial heat distribution, +// h, according to the Laplacian with a diffusion time of t. +// The resulting heat distribution is returned, written into the map dst and +// returned, +// d = exp(-Lt)×h +// where L is the graph Laplacian. Indexing into h and dst is defined by the +// Laplacian Index field. If dst is nil, a new map is created. +// +// Nodes without corresponding entries in h are given an initial heat of zero, +// and entries in h without a corresponding node in the original graph are +// not altered when written to dst. +func Diffuse(dst, h map[int64]float64, by Laplacian, t float64) map[int64]float64 { + heat := make([]float64, len(by.Index)) + for id, i := range by.Index { + heat[i] = h[id] + } + v := mat.NewVecDense(len(heat), heat) + + var m, tl mat.Dense + tl.Scale(-t, by) + m.Exp(&tl) + v.MulVec(&m, v) + + if dst == nil { + dst = make(map[int64]float64) + } + for i, n := range heat { + dst[by.Nodes[i].ID()] = n + } + return dst +} + +// DiffuseToEquilibrium performs a heat diffusion across nodes of the +// graph described by the given Laplacian using the initial heat +// distribution, h, according to the Laplacian until the update function +// h_{n+1} = h_n - L×h_n +// results in a 2-norm update difference within tol, or iters updates have +// been made. +// The resulting heat distribution is returned as eq, written into the map dst, +// and a boolean indicating whether the equilibrium converged to within tol. +// Indexing into h and dst is defined by the Laplacian Index field. If dst +// is nil, a new map is created. +// +// Nodes without corresponding entries in h are given an initial heat of zero, +// and entries in h without a corresponding node in the original graph are +// not altered when written to dst. +func DiffuseToEquilibrium(dst, h map[int64]float64, by Laplacian, tol float64, iters int) (eq map[int64]float64, ok bool) { + heat := make([]float64, len(by.Index)) + for id, i := range by.Index { + heat[i] = h[id] + } + v := mat.NewVecDense(len(heat), heat) + + last := make([]float64, len(by.Index)) + for id, i := range by.Index { + last[i] = h[id] + } + lastV := mat.NewVecDense(len(last), last) + + var tmp mat.VecDense + for { + iters-- + if iters < 0 { + break + } + lastV, v = v, lastV + tmp.MulVec(by.Matrix, lastV) + v.SubVec(lastV, &tmp) + if normDiff(heat, last) < tol { + ok = true + break + } + } + + if dst == nil { + dst = make(map[int64]float64) + } + for i, n := range v.RawVector().Data { + dst[by.Nodes[i].ID()] = n + } + return dst, ok +} + +// Laplacian is a graph Laplacian matrix. +type Laplacian struct { + // Matrix holds the Laplacian matrix. + mat.Matrix + + // Nodes holds the input graph nodes. + Nodes []graph.Node + + // Index is a mapping from the graph + // node IDs to row and column indices. + Index map[int64]int +} + +// NewLaplacian returns a Laplacian matrix for the simple undirected graph g. +// The Laplacian is defined as D-A where D is a diagonal matrix holding the +// degree of each node and A is the graph adjacency matrix of the input graph. +// If g contains self edges, NewLaplacian will panic. +func NewLaplacian(g graph.Undirected) Laplacian { + nodes := g.Nodes() + indexOf := make(map[int64]int, len(nodes)) + for i, n := range nodes { + id := n.ID() + indexOf[id] = i + } + + l := mat.NewSymDense(len(nodes), nil) + for j, u := range nodes { + to := g.From(u) + l.SetSym(j, j, float64(len(to))) + uid := u.ID() + for _, v := range to { + vid := v.ID() + if uid == vid { + panic("network: self edge in graph") + } + if uid < vid { + l.SetSym(indexOf[vid], j, -1) + } + } + } + + return Laplacian{Matrix: l, Nodes: nodes, Index: indexOf} +} + +// NewSymNormLaplacian returns a symmetric normalized Laplacian matrix for the +// simple undirected graph g. +// The normalized Laplacian is defined as I-D^(-1/2)AD^(-1/2) where D is a +// diagonal matrix holding the degree of each node and A is the graph adjacency +// matrix of the input graph. +// If g contains self edges, NewSymNormLaplacian will panic. +func NewSymNormLaplacian(g graph.Undirected) Laplacian { + nodes := g.Nodes() + indexOf := make(map[int64]int, len(nodes)) + for i, n := range nodes { + id := n.ID() + indexOf[id] = i + } + + l := mat.NewSymDense(len(nodes), nil) + for j, u := range nodes { + to := g.From(u) + if len(to) == 0 { + continue + } + l.SetSym(j, j, 1) + uid := u.ID() + squdeg := math.Sqrt(float64(len(to))) + for _, v := range to { + vid := v.ID() + if uid == vid { + panic("network: self edge in graph") + } + if uid < vid { + l.SetSym(indexOf[vid], j, -1/(squdeg*math.Sqrt(float64(len(g.From(v)))))) + } + } + } + + return Laplacian{Matrix: l, Nodes: nodes, Index: indexOf} +} + +// NewRandomWalkLaplacian returns a damp-scaled random walk Laplacian matrix for +// the simple graph g. +// The random walk Laplacian is defined as I-D^(-1)A where D is a diagonal matrix +// holding the degree of each node and A is the graph adjacency matrix of the input +// graph. +// If g contains self edges, NewRandomWalkLaplacian will panic. +func NewRandomWalkLaplacian(g graph.Graph, damp float64) Laplacian { + nodes := g.Nodes() + indexOf := make(map[int64]int, len(nodes)) + for i, n := range nodes { + id := n.ID() + indexOf[id] = i + } + + l := mat.NewDense(len(nodes), len(nodes), nil) + for j, u := range nodes { + uid := u.ID() + to := g.From(u) + if len(to) == 0 { + continue + } + l.Set(j, j, 1-damp) + rudeg := (damp - 1) / float64(len(to)) + for _, v := range to { + vid := v.ID() + if uid == vid { + panic("network: self edge in graph") + } + l.Set(indexOf[vid], j, rudeg) + } + } + + return Laplacian{Matrix: l, Nodes: nodes, Index: indexOf} +} diff --git a/graph/network/diffusion_test.go b/graph/network/diffusion_test.go new file mode 100644 index 00000000..33d32fcf --- /dev/null +++ b/graph/network/diffusion_test.go @@ -0,0 +1,526 @@ +// Copyright ©2017 The Gonum Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package network + +import ( + "math" + "sort" + "testing" + + "gonum.org/v1/gonum/floats" + "gonum.org/v1/gonum/graph" + "gonum.org/v1/gonum/graph/internal/ordered" + "gonum.org/v1/gonum/graph/simple" + "gonum.org/v1/gonum/mat" +) + +var diffuseTests = []struct { + g []set + h map[int64]float64 + t float64 + + wantTol float64 + want map[bool]map[int64]float64 +}{ + { + g: grid(5), + h: map[int64]float64{0: 1}, + t: 0.1, + + wantTol: 1e-9, + want: map[bool]map[int64]float64{ + false: { + A: 0.826684055, B: 0.078548060, C: 0.003858840, D: 0.000127487, E: 0.000003233, + F: 0.078548060, G: 0.007463308, H: 0.000366651, I: 0.000012113, J: 0.000000307, + K: 0.003858840, L: 0.000366651, M: 0.000018012, N: 0.000000595, O: 0.000000015, + P: 0.000127487, Q: 0.000012113, R: 0.000000595, S: 0.000000020, T: 0.000000000, + U: 0.000003233, V: 0.000000307, W: 0.000000015, X: 0.000000000, Y: 0.000000000, + }, + true: { + A: 0.9063462486, B: 0.0369774705, C: 0.0006161414, D: 0.0000068453, E: 0.0000000699, + F: 0.0369774705, G: 0.0010670895, H: 0.0000148186, I: 0.0000001420, J: 0.0000000014, + K: 0.0006161414, L: 0.0000148186, M: 0.0000001852, N: 0.0000000016, O: 0.0000000000, + P: 0.0000068453, Q: 0.0000001420, R: 0.0000000016, S: 0.0000000000, T: 0.0000000000, + U: 0.0000000699, V: 0.0000000014, W: 0.0000000000, X: 0.0000000000, Y: 0.0000000000, + }, + }, + }, + { + g: grid(5), + h: map[int64]float64{0: 1}, + t: 1, + + wantTol: 1e-9, + want: map[bool]map[int64]float64{ + false: { + A: 0.2743435076, B: 0.1615920872, C: 0.0639346641, D: 0.0188054933, E: 0.0051023569, + F: 0.1615920872, G: 0.0951799548, H: 0.0376583937, I: 0.0110766934, J: 0.0030053582, + K: 0.0639346641, L: 0.0376583937, M: 0.0148997194, N: 0.0043825455, O: 0.0011890840, + P: 0.0188054933, Q: 0.0110766934, R: 0.0043825455, S: 0.0012890649, T: 0.0003497525, + U: 0.0051023569, V: 0.0030053582, W: 0.0011890840, X: 0.0003497525, Y: 0.0000948958, + }, + true: { + A: 0.4323917545, B: 0.1660487336, C: 0.0270298904, D: 0.0029720194, E: 0.0003007247, + F: 0.1660487336, G: 0.0463974679, H: 0.0063556078, I: 0.0006056850, J: 0.0000589574, + K: 0.0270298904, L: 0.0063556078, M: 0.0007860810, N: 0.0000691647, O: 0.0000065586, + P: 0.0029720194, Q: 0.0006056850, R: 0.0000691647, S: 0.0000057466, T: 0.0000005475, + U: 0.0003007247, V: 0.0000589574, W: 0.0000065586, X: 0.0000005475, Y: 0.0000000555, + }, + }, + }, + { + g: grid(5), + h: map[int64]float64{0: 1}, + t: 10, + + wantTol: 1e-9, + want: map[bool]map[int64]float64{ + false: { + A: 0.0432408511, B: 0.0425986522, C: 0.0415977802, D: 0.0405588482, E: 0.0399403788, + F: 0.0425986522, G: 0.0420083007, H: 0.0409532810, I: 0.0399982373, J: 0.0393463013, + K: 0.0415977802, L: 0.0409532810, M: 0.0400339958, N: 0.0389913353, O: 0.0384232854, + P: 0.0405588482, Q: 0.0399982373, R: 0.0389913353, S: 0.0380844049, T: 0.0374622025, + U: 0.0399403788, V: 0.0393463013, W: 0.0384232854, X: 0.0374622025, Y: 0.0368918429, + }, + true: { + A: 0.0532814862, B: 0.0594280160, C: 0.0462076361, D: 0.0330529557, E: 0.0211688130, + F: 0.0594280160, G: 0.0612529898, H: 0.0462850376, I: 0.0319891593, J: 0.0213123519, + K: 0.0462076361, L: 0.0462850376, M: 0.0340410963, N: 0.0229646704, O: 0.0152763556, + P: 0.0330529557, Q: 0.0319891593, R: 0.0229646704, S: 0.0153031853, T: 0.0103681461, + U: 0.0211688130, V: 0.0213123519, W: 0.0152763556, X: 0.0103681461, Y: 0.0068893147, + }, + }, + }, + { + g: grid(5), + h: func() map[int64]float64 { + m := make(map[int64]float64, 25) + for i := int64(A); i <= Y; i++ { + m[i] = 1 + } + return m + }(), + t: 0.01, // FIXME(kortschak): Low t used due to instability in mat.Exp. + + wantTol: 1e-1, // FIXME(kortschak): High tolerance used due to instability in mat.Exp. + want: map[bool]map[int64]float64{ + false: { + A: 1, B: 1, C: 1, D: 1, E: 1, + F: 1, G: 1, H: 1, I: 1, J: 1, + K: 1, L: 1, M: 1, N: 1, O: 1, + P: 1, Q: 1, R: 1, S: 1, T: 1, + U: 1, V: 1, W: 1, X: 1, Y: 1, + }, + true: { + // Output from the python implementation associated with doi:10.1371/journal.pcbi.1005598. + A: 0.98264450473308107, B: 1.002568278028513, C: 0.9958911385307706, D: 1.002568278028513, E: 0.98264450473308107, + F: 1.002568278028513, G: 1.0075291695232433, H: 1.0038067383118021, I: 1.0075291695232433, J: 1.002568278028513, + K: 0.9958911385307706, L: 1.0038067383118021, M: 1.0001850837547184, N: 1.0038067383118021, O: 0.9958911385307706, + P: 1.002568278028513, Q: 1.0075291695232433, R: 1.0038067383118021, S: 1.0075291695232433, T: 1.002568278028513, + U: 0.98264450473308107, V: 1.002568278028513, W: 0.9958911385307706, X: 1.002568278028513, Y: 0.98264450473308107, + }, + }, + }, + { + g: []set{ + A: linksTo(B, C), + B: linksTo(D), + C: nil, + D: nil, + E: linksTo(F), + F: nil, + }, + h: map[int64]float64{A: 1, E: 10}, + t: 0.1, + + wantTol: 1e-9, + want: map[bool]map[int64]float64{ + false: { + A: 0.8270754166, B: 0.0822899600, C: 0.0863904410, D: 0.0042441824, E: 9.0936537654, F: 0.9063462346, + }, + true: { + A: 0.9082331512, B: 0.0453361743, C: 0.0640616812, D: 0.0016012085, E: 9.0936537654, F: 0.9063462346, + }, + }, + }, +} + +func TestDiffuse(t *testing.T) { + for i, test := range diffuseTests { + g := simple.NewUndirectedGraph() + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) + } + for v := range e { + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) + } + } + for j, lfn := range []func(g graph.Undirected) Laplacian{NewLaplacian, NewSymNormLaplacian} { + normalize := j == 1 + var wantTemp float64 + for _, v := range test.h { + wantTemp += v + } + got := Diffuse(nil, test.h, lfn(g), test.t) + prec := 1 - int(math.Log10(test.wantTol)) + for n := range test.g { + if !floats.EqualWithinAbsOrRel(got[int64(n)], test.want[normalize][int64(n)], test.wantTol, test.wantTol) { + t.Errorf("unexpected Diffuse result for test %d with normalize=%t:\ngot: %v\nwant:%v", + i, normalize, orderedFloats(got, prec), orderedFloats(test.want[normalize], prec)) + break + } + } + + if j == 1 { + continue + } + + var gotTemp float64 + for _, v := range got { + gotTemp += v + } + gotTemp /= float64(len(got)) + wantTemp /= float64(len(got)) + if !floats.EqualWithinAbsOrRel(gotTemp, wantTemp, test.wantTol, test.wantTol) { + t.Errorf("unexpected total heat for test %d with normalize=%t: got:%v want:%v", + i, normalize, gotTemp, wantTemp) + } + } + } +} + +var randomWalkLaplacianTests = []struct { + g []set + damp float64 + + want *mat.Dense +}{ + { + g: []set{ + A: linksTo(B, C), + B: linksTo(C), + C: nil, + }, + + want: mat.NewDense(3, 3, []float64{ + 1, 0, 0, + -0.5, 1, 0, + -0.5, -1, 0, + }), + }, + { + g: []set{ + A: linksTo(B, C), + B: linksTo(C), + C: nil, + }, + damp: 0.85, + + want: mat.NewDense(3, 3, []float64{ + 0.15, 0, 0, + -0.075, 0.15, 0, + -0.075, -0.15, 0, + }), + }, + { + g: []set{ + A: linksTo(B), + B: linksTo(C), + C: linksTo(A), + }, + damp: 0.85, + + want: mat.NewDense(3, 3, []float64{ + 0.15, 0, -0.15, + -0.15, 0.15, 0, + 0, -0.15, 0.15, + }), + }, + { + // Example graph from http://en.wikipedia.org/wiki/File:PageRanks-Example.svg 16:17, 8 July 2009 + g: []set{ + A: nil, + B: linksTo(C), + C: linksTo(B), + D: linksTo(A, B), + E: linksTo(D, B, F), + F: linksTo(B, E), + G: linksTo(B, E), + H: linksTo(B, E), + I: linksTo(B, E), + J: linksTo(E), + K: linksTo(E), + }, + + want: mat.NewDense(11, 11, []float64{ + 0, 0, 0, -0.5, 0, 0, 0, 0, 0, 0, 0, + 0, 1, -1, -0.5, -1. / 3., -0.5, -0.5, -0.5, -0.5, 0, 0, + 0, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 1, -1. / 3., 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 1, -0.5, -0.5, -0.5, -0.5, -1, -1, + 0, 0, 0, 0, -1. / 3., 1, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, + }), + }, +} + +func TestRandomWalkLaplacian(t *testing.T) { + const tol = 1e-14 + for i, test := range randomWalkLaplacianTests { + g := simple.NewDirectedGraph() + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) + } + for v := range e { + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) + } + } + l := NewRandomWalkLaplacian(g, test.damp) + _, c := l.Dims() + for j := 0; j < c; j++ { + if got := mat.Sum(l.Matrix.(*mat.Dense).ColView(j)); !floats.EqualWithinAbsOrRel(got, 0, tol, tol) { + t.Errorf("unexpected column sum for test %d, column %d: got:%v want:0", i, j, got) + } + } + l = NewRandomWalkLaplacian(sortedNodeGraph{g}, test.damp) + if !mat.EqualApprox(l, test.want, tol) { + t.Errorf("unexpected result for test %d:\ngot:\n% .2v\nwant:\n% .2v", + i, mat.Formatted(l), mat.Formatted(test.want)) + } + } +} + +type sortedNodeGraph struct { + graph.Graph +} + +func (g sortedNodeGraph) Nodes() []graph.Node { + n := g.Graph.Nodes() + sort.Sort(ordered.ByID(n)) + return n +} + +var diffuseToEquilibriumTests = []struct { + g []set + builder builder + h map[int64]float64 + damp float64 + tol float64 + iter int + + want map[int64]float64 + wantOK bool +}{ + { + g: grid(5), + builder: simple.NewUndirectedGraph(), + h: map[int64]float64{0: 1}, + damp: 0.85, + tol: 1e-6, + iter: 1e4, + + want: map[int64]float64{ + A: 0.025000, B: 0.037500, C: 0.037500, D: 0.037500, E: 0.025000, + F: 0.037500, G: 0.050000, H: 0.050000, I: 0.050000, J: 0.037500, + K: 0.037500, L: 0.050000, M: 0.050000, N: 0.050000, O: 0.037500, + P: 0.037500, Q: 0.050000, R: 0.050000, S: 0.050000, T: 0.037500, + U: 0.025000, V: 0.037500, W: 0.037500, X: 0.037500, Y: 0.025000, + }, + wantOK: true, + }, + { + // Example graph from http://en.wikipedia.org/wiki/File:PageRanks-Example.svg 16:17, 8 July 2009 + g: []set{ + A: nil, + B: linksTo(C), + C: linksTo(B), + D: linksTo(A, B), + E: linksTo(D, B, F), + F: linksTo(B, E), + G: linksTo(B, E), + H: linksTo(B, E), + I: linksTo(B, E), + J: linksTo(E), + K: linksTo(E), + }, + builder: simple.NewDirectedGraph(), + h: map[int64]float64{ + A: 1. / 11., + B: 1. / 11., + C: 1. / 11., + D: 1. / 11., + E: 1. / 11., + F: 1. / 11., + G: 1. / 11., + H: 1. / 11., + I: 1. / 11., + J: 1. / 11., + K: 1. / 11., + }, + damp: 0.85, + tol: 1e-6, + iter: 1e4, + + // This does not look like Page Rank because we do not + // do the random node hops. An alternative Laplacian + // value that does do that would replicate PageRank. This + // is left as an excercise for the reader. + want: map[int64]float64{ + A: 0.227273, + B: 0.386364, + C: 0.386364, + D: 0.000000, + E: 0.000000, + F: 0.000000, + G: 0.000000, + H: 0.000000, + I: 0.000000, + J: 0.000000, + K: 0.000000, + }, + wantOK: true, + }, + { + g: []set{ + A: linksTo(B, C), + B: linksTo(D, C), + C: nil, + D: nil, + E: linksTo(F), + F: nil, + }, + builder: simple.NewDirectedGraph(), + h: map[int64]float64{A: 1, E: -10}, + tol: 1e-6, + iter: 3, + + want: map[int64]float64{ + A: 0, B: 0, C: 0.75, D: 0.25, E: 0, F: -10, + }, + wantOK: true, + }, + { + g: []set{ + A: linksTo(B, C), + B: linksTo(D, C), + C: nil, + D: nil, + E: linksTo(F), + F: nil, + }, + builder: simple.NewUndirectedGraph(), + h: map[int64]float64{A: 1, E: -10}, + damp: 0.85, + tol: 1e-6, + iter: 1e4, + + want: map[int64]float64{ + A: 0.25, B: 0.375, C: 0.25, D: 0.125, E: -5, F: -5, + }, + wantOK: true, + }, + { + g: []set{ + A: linksTo(B), + B: linksTo(C), + C: nil, + }, + builder: simple.NewUndirectedGraph(), + h: map[int64]float64{B: 1}, + iter: 1, + tol: 1e-6, + want: map[int64]float64{ + A: 0.5, B: 0, C: 0.5, + }, + wantOK: false, + }, + { + g: []set{ + A: linksTo(B), + B: linksTo(C), + C: nil, + }, + builder: simple.NewUndirectedGraph(), + h: map[int64]float64{B: 1}, + iter: 2, + tol: 1e-6, + want: map[int64]float64{ + A: 0, B: 1, C: 0, + }, + wantOK: false, + }, +} + +func TestDiffuseToEquilibrium(t *testing.T) { + for i, test := range diffuseToEquilibriumTests { + g := test.builder + for u, e := range test.g { + // Add nodes that are not defined by an edge. + if !g.Has(simple.Node(u)) { + g.AddNode(simple.Node(u)) + } + for v := range e { + g.SetEdge(simple.Edge{F: simple.Node(u), T: simple.Node(v)}) + } + } + var wantTemp float64 + for _, v := range test.h { + wantTemp += v + } + got, ok := DiffuseToEquilibrium(nil, test.h, NewRandomWalkLaplacian(g, test.damp), test.tol*test.tol, test.iter) + if ok != test.wantOK { + t.Errorf("unexpected success value for test %d: got:%t want:%t", i, ok, test.wantOK) + } + prec := -int(math.Log10(test.tol)) + for n := range test.g { + if !floats.EqualWithinAbsOrRel(got[int64(n)], test.want[int64(n)], test.tol, test.tol) { + t.Errorf("unexpected DiffuseToEquilibrium result for test %d:\ngot: %v\nwant:%v", + i, orderedFloats(got, prec), orderedFloats(test.want, prec)) + break + } + } + + var gotTemp float64 + for _, v := range got { + gotTemp += v + } + gotTemp /= float64(len(got)) + wantTemp /= float64(len(got)) + if !floats.EqualWithinAbsOrRel(gotTemp, wantTemp, test.tol, test.tol) { + t.Errorf("unexpected total heat for test %d: got:%v want:%v", + i, gotTemp, wantTemp) + } + } +} + +type builder interface { + graph.Graph + graph.Builder +} + +func grid(d int) []set { + dim := int64(d) + s := make([]set, dim*dim) + for i := range s { + s[i] = make(set) + } + for i := int64(0); i < dim*dim; i++ { + if i%dim != 0 { + s[i][i-1] = struct{}{} + } + if i/dim != 0 { + s[i][i-dim] = struct{}{} + } + } + return s +} diff --git a/graph/network/network_test.go b/graph/network/network_test.go index c9954075..ddd0d838 100644 --- a/graph/network/network_test.go +++ b/graph/network/network_test.go @@ -16,6 +16,21 @@ const ( I J K + L + M + N + O + P + Q + R + S + T + U + V + W + X + Y + Z ) // set is an integer set.