yargraph 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.document +5 -0
- data/.rspec +1 -0
- data/Gemfile +14 -0
- data/LICENSE.txt +20 -0
- data/README.md +18 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/yargraph.rb +454 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/yargraph_spec.rb +228 -0
- data/yargraph.gemspec +63 -0
- metadata +126 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7eaedaa62ff94b250c68ca0dfe9a4127e390b5d8
|
4
|
+
data.tar.gz: 757a7b6dfd85305d6df76963bd2ebc0d2bf863bc
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d9a65b8e7038068406bf52ec4b248814c9d13dfffef5de3ba5cffceff5b2c644442e568dd13fd664a2f48f8610d3878823cfbd889c54446c7944bbbc84c217f1
|
7
|
+
data.tar.gz: d0e755409c82c0ad7cbb4d7528b6d39beb2a857c8966e72f83879a83a19c2cfff38b9e9e120f21ad940e31205176d3c3cd67094e313322a68e81d0638ab75a94
|
data/.document
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
gem 'ds'
|
6
|
+
|
7
|
+
# Add dependencies to develop your gem here.
|
8
|
+
# Include everything needed to run rake, tests, features, etc.
|
9
|
+
group :development do
|
10
|
+
gem "rspec", ">= 2.8.0"
|
11
|
+
gem "rdoc", ">= 3.12"
|
12
|
+
gem "bundler", ">= 1.0"
|
13
|
+
gem "jeweler", ">= 1.8.7"
|
14
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2014 Ben J. Woodcroft
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# Yargraph
|
2
|
+
|
3
|
+
Yet another Ruby graphing library. Implements some [graph](http://en.wikipedia.org/wiki/Graph_theory)/vertex/edge related algorithms. Currently operates only on undirected graphs.
|
4
|
+
|
5
|
+
* find all [Hamiltonian cycles](http://en.wikipedia.org/wiki/Hamiltonian_cycle) in a graph using an exponential time algorithm (`hamiltonian_cycles`, dynamic programming method of Bellman, Held, and Karp).
|
6
|
+
* find edges that are a part of all Hamiltonian cycles (```edges_in_all_hamiltonian_cycles```, requires exponential time so may be _very_ slow)
|
7
|
+
* find only some edges that are a part of all Hamiltonian cycles (```some_edges_in_all_hamiltonian_cycles```, faster but may not find all edges)
|
8
|
+
|
9
|
+
Soon to be implemented:
|
10
|
+
* finding [bridges](http://en.wikipedia.org/wiki/Bridge_%28graph_theory%29) (```bridges```, requires linear time using Schmidt's [chain decompositions method](http://dx.doi.org/10.1016%2Fj.ipl.2013.01.016))
|
11
|
+
* determining [3-edge-connectivity](http://en.wikipedia.org/wiki/K-edge-connected_graph) and if 3-edge-connected (but not 4- or more), determine pairs of edges whose removal disconnects the graph (```three_edge_connected?```, ```three_edge_connections```, algorithm runs in O(n^2))
|
12
|
+
|
13
|
+
Contributions are most welcome.
|
14
|
+
|
15
|
+
## Copyright
|
16
|
+
Copyright (c) 2014 Ben J. Woodcroft. See LICENSE.txt for
|
17
|
+
further details.
|
18
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
begin
|
6
|
+
Bundler.setup(:default, :development)
|
7
|
+
rescue Bundler::BundlerError => e
|
8
|
+
$stderr.puts e.message
|
9
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
10
|
+
exit e.status_code
|
11
|
+
end
|
12
|
+
require 'rake'
|
13
|
+
|
14
|
+
require 'jeweler'
|
15
|
+
Jeweler::Tasks.new do |gem|
|
16
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
17
|
+
gem.name = "yargraph"
|
18
|
+
gem.homepage = "http://github.com/wwood/yargraph"
|
19
|
+
gem.license = "MIT"
|
20
|
+
gem.summary = %Q{Pure Ruby graph algorithms}
|
21
|
+
gem.description = %Q{Pure Ruby graph algorithms, particularly e.g. Hamiltonian cycles}
|
22
|
+
gem.email = "donttrustben near gmail.com"
|
23
|
+
gem.authors = ["Ben J. Woodcroft"]
|
24
|
+
# dependencies defined in Gemfile
|
25
|
+
end
|
26
|
+
Jeweler::RubygemsDotOrgTasks.new
|
27
|
+
|
28
|
+
require 'rspec/core'
|
29
|
+
require 'rspec/core/rake_task'
|
30
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
31
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
32
|
+
end
|
33
|
+
|
34
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
35
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
36
|
+
spec.rcov = true
|
37
|
+
end
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'rdoc/task'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
44
|
+
|
45
|
+
rdoc.rdoc_dir = 'rdoc'
|
46
|
+
rdoc.title = "yargraph #{version}"
|
47
|
+
rdoc.rdoc_files.include('README*')
|
48
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
49
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
data/lib/yargraph.rb
ADDED
@@ -0,0 +1,454 @@
|
|
1
|
+
require 'set'
|
2
|
+
require 'ds'
|
3
|
+
|
4
|
+
module Yargraph
|
5
|
+
class OperationalLimitReachedException < Exception; end
|
6
|
+
|
7
|
+
class UndirectedGraph
|
8
|
+
# vertices is a Set
|
9
|
+
attr_accessor :vertices
|
10
|
+
|
11
|
+
# edges is an EdgeSet
|
12
|
+
attr_accessor :edges
|
13
|
+
# Contained within is a Hash of vertex1 => Set: vertex2, vertex3, ..
|
14
|
+
# If vertex1 => Set: vertex2, then vertex2 => Set: vertex1, ..
|
15
|
+
class EdgeSet
|
16
|
+
include Enumerable
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
@edges = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
def add_edge(v1, v2)
|
23
|
+
@edges[v1] ||= Set.new
|
24
|
+
@edges[v2] ||= Set.new
|
25
|
+
@edges[v1] << v2
|
26
|
+
@edges[v2] << v1
|
27
|
+
end
|
28
|
+
|
29
|
+
# Return an array of neighbours
|
30
|
+
def neighbours(v)
|
31
|
+
e = @edges[v]
|
32
|
+
return [] if e.nil?
|
33
|
+
return e.to_a
|
34
|
+
end
|
35
|
+
|
36
|
+
# Return a Set of vertices that are neighbours of the given
|
37
|
+
# vertex, or an empty Set if there are none
|
38
|
+
def [](vertex)
|
39
|
+
e = @edges[vertex]
|
40
|
+
if e
|
41
|
+
return e
|
42
|
+
else
|
43
|
+
return Set.new
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def each
|
48
|
+
already_seen_vertices = Set.new
|
49
|
+
@edges.each do |v1, neighbours|
|
50
|
+
neighbours.each do |v2|
|
51
|
+
yield v1, v2 unless already_seen_vertices.include?(v2)
|
52
|
+
end
|
53
|
+
already_seen_vertices << v1
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def length
|
58
|
+
count = 0
|
59
|
+
each do
|
60
|
+
count += 1
|
61
|
+
end
|
62
|
+
return count
|
63
|
+
end
|
64
|
+
|
65
|
+
# Is there an edge between v1 and v2?
|
66
|
+
def edge?(v1,v2)
|
67
|
+
e = @edges[v1]
|
68
|
+
return false if e.nil?
|
69
|
+
return e.include?(v2)
|
70
|
+
end
|
71
|
+
|
72
|
+
def empty?
|
73
|
+
@edges.each do |v, neighbours|
|
74
|
+
return false unless neighbours.empty?
|
75
|
+
end
|
76
|
+
return true
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
|
81
|
+
def initialize
|
82
|
+
@vertices = Set.new
|
83
|
+
@edges = EdgeSet.new
|
84
|
+
end
|
85
|
+
|
86
|
+
# Add an edge between two vertices, adding the vertices in the
|
87
|
+
# edge to the graph if they aren't already contained within it.
|
88
|
+
def add_edge(vertex1, vertex2)
|
89
|
+
@vertices << vertex1
|
90
|
+
@vertices << vertex2
|
91
|
+
@edges.add_edge vertex1, vertex2
|
92
|
+
return
|
93
|
+
end
|
94
|
+
|
95
|
+
# Add a vertex to the graph
|
96
|
+
def add_vertex(vertex)
|
97
|
+
@vertices << vertex
|
98
|
+
end
|
99
|
+
|
100
|
+
# Return an Enumerable collection of vertices that
|
101
|
+
# are directly connected to the given vertex
|
102
|
+
def neighbours(vertex)
|
103
|
+
@edges.neighbours(vertex)
|
104
|
+
end
|
105
|
+
|
106
|
+
def delete_edge(v1,v2)
|
107
|
+
@edges[v1].delete v2
|
108
|
+
@edges[v2].delete v1
|
109
|
+
end
|
110
|
+
|
111
|
+
# Yield a pair of vertices for each edge
|
112
|
+
def each_edge
|
113
|
+
@edges.each do |v1, v2|
|
114
|
+
yield v1, v2
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def edge?(v1,v2)
|
119
|
+
@edges.edge?(v1,v2)
|
120
|
+
end
|
121
|
+
|
122
|
+
def copy
|
123
|
+
another = UndirectedGraph.new
|
124
|
+
@vertices.each do |v|
|
125
|
+
another.add_vertex v
|
126
|
+
end
|
127
|
+
each_edge do |v1, v2|
|
128
|
+
another.add_edge v1, v2
|
129
|
+
end
|
130
|
+
return another
|
131
|
+
end
|
132
|
+
|
133
|
+
# Run depth first search, returning an array of Hamiltonian paths.
|
134
|
+
# Or, if a block is given, yield each Hamiltonian path that comes
|
135
|
+
# along (in no defined order), and don't return the array (to potentially
|
136
|
+
# save RAM).
|
137
|
+
#
|
138
|
+
# The operational limit is used to make sure this algorithm doesn't
|
139
|
+
# get out of hand - only this many 'operations' are used when traversing
|
140
|
+
# the graph, as in #hamiltonian_paths_brute_force. When nil, there is no operational
|
141
|
+
# limit
|
142
|
+
def hamiltonian_cycles_brute_force(operational_limit=nil)
|
143
|
+
stack = DS::Stack.new
|
144
|
+
return [] if @vertices.empty?
|
145
|
+
|
146
|
+
origin_vertex = @vertices.to_a[0]
|
147
|
+
hamiltonians = []
|
148
|
+
num_operations = 0
|
149
|
+
|
150
|
+
path = Path.new
|
151
|
+
path << origin_vertex
|
152
|
+
stack.push path
|
153
|
+
while path = stack.pop
|
154
|
+
last_vertex = path[path.length-1]
|
155
|
+
if last_vertex == origin_vertex and path.length > 1
|
156
|
+
# Cycle of some sort detected. Is it Hamiltonian?
|
157
|
+
if path.length == vertices.length + 1
|
158
|
+
# Found a Hamiltonian path. Yield or save it for later
|
159
|
+
hpath = path.copy[0...(path.length-1)]
|
160
|
+
if block_given?
|
161
|
+
yield hpath
|
162
|
+
else
|
163
|
+
hamiltonians << hpath
|
164
|
+
end
|
165
|
+
else
|
166
|
+
# non-Hamiltonian path found. Ignore
|
167
|
+
end
|
168
|
+
|
169
|
+
elsif path.find_index(last_vertex) != path.length - 1
|
170
|
+
# Found a loop, go no further
|
171
|
+
|
172
|
+
else
|
173
|
+
# No loop, just another regular thing.
|
174
|
+
neighbours(last_vertex).each do |neighbour|
|
175
|
+
unless operational_limit.nil?
|
176
|
+
num_operations += 1
|
177
|
+
if num_operations > operational_limit
|
178
|
+
raise OperationalLimitReachedException
|
179
|
+
end
|
180
|
+
end
|
181
|
+
new_path = Path.new(path.copy+[neighbour])
|
182
|
+
stack.push new_path
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
return hamiltonians
|
188
|
+
end
|
189
|
+
|
190
|
+
# Use dynamic programming to find all the Hamiltonian cycles in this graph
|
191
|
+
def hamiltonian_cycles_dynamic_programming(operational_limit=nil)
|
192
|
+
stack = DS::Stack.new
|
193
|
+
return [] if @vertices.empty?
|
194
|
+
|
195
|
+
origin_vertex = @vertices.to_a[0]
|
196
|
+
hamiltonians = []
|
197
|
+
num_operations = 0
|
198
|
+
|
199
|
+
# This hash keeps track of subproblems that have already been
|
200
|
+
# solved. ie is there a path through vertices that ends in the
|
201
|
+
# endpoint
|
202
|
+
# Hash of [vertex_set,endpoint] => Array of Path objects.
|
203
|
+
# If no path is found, then the key is false
|
204
|
+
# The endpoint is not stored in the vertex set to make the programming
|
205
|
+
# easier.
|
206
|
+
dp_cache = {}
|
207
|
+
|
208
|
+
# First problem is the whole problem. We get the Hamiltonian paths,
|
209
|
+
# and then after reject those paths that are not cycles.
|
210
|
+
initial_vertex_set = Set.new(@vertices.reject{|v| v==origin_vertex})
|
211
|
+
initial_problem = [initial_vertex_set, origin_vertex]
|
212
|
+
stack.push initial_problem
|
213
|
+
|
214
|
+
while next_problem = stack.pop
|
215
|
+
vertices = next_problem[0]
|
216
|
+
destination = next_problem[1]
|
217
|
+
|
218
|
+
if dp_cache[next_problem]
|
219
|
+
# No need to do anything - problem already solved
|
220
|
+
|
221
|
+
elsif vertices.empty?
|
222
|
+
# The bottom of the problem. Only return a path
|
223
|
+
# if there is an edge between the destination and the origin
|
224
|
+
# node
|
225
|
+
if edge?(destination, origin_vertex)
|
226
|
+
path = Path.new [destination]
|
227
|
+
dp_cache[next_problem] = [path]
|
228
|
+
else
|
229
|
+
# Reached dead end
|
230
|
+
dp_cache[next_problem] = false
|
231
|
+
end
|
232
|
+
|
233
|
+
else
|
234
|
+
# This is an unsolved problem and there are at least 2 vertices in the vertex set.
|
235
|
+
# Work out which vertices in the set are neighbours
|
236
|
+
neighs = Set.new neighbours(destination)
|
237
|
+
possibilities = neighs.intersection(vertices)
|
238
|
+
if possibilities.length > 0
|
239
|
+
# There is still the possibility to go further into this unsolved problem
|
240
|
+
subproblems_unsolved = []
|
241
|
+
subproblems = []
|
242
|
+
|
243
|
+
possibilities.each do |new_destination|
|
244
|
+
new_vertex_set = Set.new(vertices.to_a.reject{|v| v==new_destination})
|
245
|
+
subproblem = [new_vertex_set, new_destination]
|
246
|
+
|
247
|
+
subproblems.push subproblem
|
248
|
+
if !dp_cache.key?(subproblem)
|
249
|
+
subproblems_unsolved.push subproblem
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# if solved all the subproblems, then we can make a decision about this problem
|
254
|
+
if subproblems_unsolved.empty?
|
255
|
+
answers = []
|
256
|
+
subproblems.each do |problem|
|
257
|
+
paths = dp_cache[problem]
|
258
|
+
if paths == false
|
259
|
+
# Nothing to see here
|
260
|
+
else
|
261
|
+
# Add the found sub-paths to the set of answers
|
262
|
+
paths.each do |path|
|
263
|
+
answers.push Path.new(path+[destination])
|
264
|
+
end
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
if answers.empty?
|
269
|
+
# No paths have been found here
|
270
|
+
dp_cache[next_problem] = false
|
271
|
+
else
|
272
|
+
dp_cache[next_problem] = answers
|
273
|
+
end
|
274
|
+
else
|
275
|
+
# More problems to be solved before a decision can be made
|
276
|
+
stack.push next_problem #We have only delayed solving this problem, need to keep going in the future
|
277
|
+
subproblems_unsolved.each do |prob|
|
278
|
+
unless operational_limit.nil?
|
279
|
+
num_operations += 1
|
280
|
+
raise OperationalLimitReachedException if num_operations > operational_limit
|
281
|
+
end
|
282
|
+
stack.push prob
|
283
|
+
end
|
284
|
+
end
|
285
|
+
|
286
|
+
else
|
287
|
+
# No neighbours in the set, so reached a dead end, can go no further
|
288
|
+
dp_cache[next_problem] = false
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
if block_given?
|
294
|
+
dp_cache[initial_problem].each do |hpath|
|
295
|
+
yield hpath
|
296
|
+
end
|
297
|
+
return
|
298
|
+
else
|
299
|
+
return dp_cache[initial_problem]
|
300
|
+
end
|
301
|
+
end
|
302
|
+
alias_method :hamiltonian_cycles, :hamiltonian_cycles_dynamic_programming
|
303
|
+
|
304
|
+
|
305
|
+
# Return an array of edges (edges being an array of 2 vertices)
|
306
|
+
# that correspond to edges that are found in all Hamiltonian paths.
|
307
|
+
# This method might be quite slow because it requires finding all Hamiltonian
|
308
|
+
# paths, which implies solving the (NP-complete) Hamiltonian path problem.
|
309
|
+
#
|
310
|
+
# There is probably no polynomial time way to implement this method anyway, see
|
311
|
+
# http://cstheory.stackexchange.com/questions/20413/is-there-an-efficient-algorithm-for-finding-edges-that-are-part-of-all-hamiltoni
|
312
|
+
#
|
313
|
+
# The operational limit is used to make sure this algorithm doesn't
|
314
|
+
# get out of hand - only this many 'operations' are used when traversing
|
315
|
+
# the graph, as in #hamiltonian_cycles_brute_force
|
316
|
+
def edges_in_all_hamiltonian_cycles(operational_limit=nil)
|
317
|
+
hedges = nil
|
318
|
+
hamiltonian_cycles do |path|
|
319
|
+
# Convert the path to a hash v1->v2, v2->v3. Can't have collisions because the path is Hamiltonian
|
320
|
+
edge_hash = {}
|
321
|
+
path.each_with_index do |v, i|
|
322
|
+
unless i == path.length-1
|
323
|
+
edge_hash[v] = path[i+1]
|
324
|
+
end
|
325
|
+
end
|
326
|
+
edge_hash[path[path.length-1]] = path[0] #Add the final wrap around edge
|
327
|
+
|
328
|
+
if hedges.nil?
|
329
|
+
# First Hpath found
|
330
|
+
hedges = edge_hash
|
331
|
+
else
|
332
|
+
# Use a process of elimination, removing all edges that
|
333
|
+
# aren't in hedges or this new Hpath
|
334
|
+
hedges.select! do |v1, v2|
|
335
|
+
edge_hash[v1] == v2 or edge_hash[v2] = v1
|
336
|
+
end
|
337
|
+
# If no edges fit the bill, then we are done
|
338
|
+
return [] if hedges.empty?
|
339
|
+
end
|
340
|
+
end
|
341
|
+
return [] if hedges.nil? #no Hpaths found in the graph
|
342
|
+
return hedges.to_a
|
343
|
+
end
|
344
|
+
|
345
|
+
# If #edges_in_all_hamiltonian_cycles is too slow, the method
|
346
|
+
# here is faster, but is not guaranteed to find every edge that is
|
347
|
+
# part of Hamiltonian cycles. This method proceeds under the assumption
|
348
|
+
# that the graph has at least 1 Hamiltonian cycle, but may stumble
|
349
|
+
# across evidence to the contrary.
|
350
|
+
#
|
351
|
+
# Returns an instance of EdgeSearchResult where #edges_in_all are those
|
352
|
+
# edges that are in all hamiltonian cycles, and #edges_in_none are those
|
353
|
+
# edges that are not in any hamiltonian cycles. While
|
354
|
+
def some_edges_in_all_hamiltonian_cycles
|
355
|
+
stack = DS::Stack.new
|
356
|
+
result = EdgeSearchResult.new
|
357
|
+
|
358
|
+
# As we are deleting edges, make a deep copy to start with
|
359
|
+
g = copy
|
360
|
+
|
361
|
+
# Fill up the stack, in reverse to ease testing
|
362
|
+
g.vertices.to_a.reverse.each do |v|
|
363
|
+
stack.push v
|
364
|
+
end
|
365
|
+
|
366
|
+
while v = stack.pop
|
367
|
+
all_neighbours = g.neighbours(v)
|
368
|
+
ham_neighbours = result.hamiltonian_neighbours(v)
|
369
|
+
# p v
|
370
|
+
# p all_neighbours
|
371
|
+
# p ham_neighbours
|
372
|
+
|
373
|
+
# If a vertex contains 1 or 0 total neighbours, then the graph cannot contain
|
374
|
+
# any hamcycles (in contrast, degree 1 doesn't preclude hampaths).
|
375
|
+
if all_neighbours.length < 2
|
376
|
+
result.contains_hamcycle = false
|
377
|
+
|
378
|
+
elsif all_neighbours.length == 2
|
379
|
+
# If a vertex has degree 2 then both edges must be a part of the hamcycle
|
380
|
+
all_neighbours.each do |n|
|
381
|
+
unless result.edges_in_all.edge?(v,n)
|
382
|
+
result.edges_in_all.add_edge(v,n)
|
383
|
+
stack.push n #now need to re-evalute the neighbour, as its neighbourhood is changed
|
384
|
+
end
|
385
|
+
|
386
|
+
# if an edge be and must not be in all hamcycles, then the graph is not Hamiltonian.
|
387
|
+
# Are there any concrete examples of this? Possibly.
|
388
|
+
if result.edges_in_all[v].include?(n) and result.edges_in_none[v].include?(n)
|
389
|
+
result.contains_hamcycle = false
|
390
|
+
end
|
391
|
+
end
|
392
|
+
|
393
|
+
elsif ham_neighbours.length >= 2
|
394
|
+
# There cannot be any further hamcycle edges from this vertex, so the rest of the edges
|
395
|
+
# cannot be a part of _any_ hamcycle
|
396
|
+
all_neighbours.each do |n|
|
397
|
+
next if ham_neighbours.include?(n)
|
398
|
+
|
399
|
+
result.edges_in_none.add_edge(v,n)
|
400
|
+
g.delete_edge(v,n)
|
401
|
+
stack.push n #reconsider the neighbour
|
402
|
+
end
|
403
|
+
|
404
|
+
else
|
405
|
+
# Anything else that can be done cheaply?
|
406
|
+
# Maybe edges that create non-Hamiltonian cycles when only considering edges
|
407
|
+
# that are in all Hamiltonian cycles -> these cannot be in any hamcycle
|
408
|
+
|
409
|
+
end
|
410
|
+
#p stack
|
411
|
+
end
|
412
|
+
#p result
|
413
|
+
|
414
|
+
return result
|
415
|
+
end
|
416
|
+
|
417
|
+
class EdgeSearchResult
|
418
|
+
# EdgeSets of edges that are contained in, or not contained in all Hamiltonian cycles.
|
419
|
+
attr_accessor :edges_in_all, :edges_in_none
|
420
|
+
|
421
|
+
# True, false, or dunno (nil), does the graph contain one or more Hamiltonian cycles?
|
422
|
+
attr_accessor :contains_hamcycle
|
423
|
+
|
424
|
+
def initialize
|
425
|
+
@edges_in_all = EdgeSet.new
|
426
|
+
@edges_in_none = EdgeSet.new
|
427
|
+
end
|
428
|
+
|
429
|
+
# Return an Set of neighbours that must be next or previous in a hamiltonian cycle (& path?)
|
430
|
+
def hamiltonian_neighbours(vertex)
|
431
|
+
@edges_in_all[vertex]
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
class Path < Array
|
436
|
+
def copy
|
437
|
+
Path.new(self)
|
438
|
+
end
|
439
|
+
end
|
440
|
+
|
441
|
+
# # Return a "chain decomposition" as defined by Jens M. Schmidt, "A simple
|
442
|
+
# # test on 2-vertex- and 2-edge-connectivity"
|
443
|
+
# def chain_decomposition
|
444
|
+
# end
|
445
|
+
#
|
446
|
+
# class ChainDecomposition
|
447
|
+
# # A Hash of edge objects (arrays of objects)objects. Each element of the array
|
448
|
+
# # is
|
449
|
+
# attr_accessor :edges_to_chainsets
|
450
|
+
#
|
451
|
+
# def number_of_
|
452
|
+
# end
|
453
|
+
end
|
454
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
2
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
3
|
+
require 'rspec'
|
4
|
+
require 'yargraph'
|
5
|
+
|
6
|
+
# Requires supporting files with custom matchers and macros, etc,
|
7
|
+
# in ./support/ and its subdirectories.
|
8
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each {|f| require f}
|
9
|
+
|
10
|
+
RSpec.configure do |config|
|
11
|
+
|
12
|
+
end
|
@@ -0,0 +1,228 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
class GraphTesting
|
4
|
+
def self.generate_undirected(edges)
|
5
|
+
g = Yargraph::UndirectedGraph.new
|
6
|
+
edges.each do |edge|
|
7
|
+
g.add_edge edge[0], edge[1]
|
8
|
+
end
|
9
|
+
return g
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.sorted_edges(edges)
|
13
|
+
edges.collect{|edge| edge.sort}.sort
|
14
|
+
end
|
15
|
+
|
16
|
+
# return an array of cycles the same as the original
|
17
|
+
# set, except that they have been rotated until so the min element
|
18
|
+
# is the first element
|
19
|
+
def self.sort_cycles(cycles)
|
20
|
+
cycles.collect do |cycle|
|
21
|
+
[cycle, cycle.reverse].collect do |cyc|
|
22
|
+
m = cyc.min
|
23
|
+
i = cyc.find_index(m)
|
24
|
+
cyc.rotate(i)
|
25
|
+
end.sort[0]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
CYCLE_FINDING_METHODS = [
|
31
|
+
:hamiltonian_cycles_dynamic_programming,
|
32
|
+
:hamiltonian_cycles_brute_force,
|
33
|
+
]
|
34
|
+
|
35
|
+
describe "Yargraph" do
|
36
|
+
it 'should do neighbours' do
|
37
|
+
g = GraphTesting.generate_undirected([
|
38
|
+
[0,1],
|
39
|
+
[1,2],
|
40
|
+
[2,0]
|
41
|
+
])
|
42
|
+
g.neighbours(0).sort.should == [1,2]
|
43
|
+
g.neighbours(1).sort.should == [0,2]
|
44
|
+
|
45
|
+
g = GraphTesting.generate_undirected([
|
46
|
+
[0,1],
|
47
|
+
[1,2],
|
48
|
+
])
|
49
|
+
g.neighbours(2).sort.should == [1]
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should find hamiltonian cycles 1" do
|
53
|
+
g = GraphTesting.generate_undirected([
|
54
|
+
[0,1],
|
55
|
+
[1,2],
|
56
|
+
[2,0]
|
57
|
+
])
|
58
|
+
CYCLE_FINDING_METHODS.each do |method|
|
59
|
+
cycles = GraphTesting.sort_cycles(g.send(method))
|
60
|
+
cycles.should == GraphTesting.sort_cycles([
|
61
|
+
[1,2,0],
|
62
|
+
[2,1,0],
|
63
|
+
])
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should find hamiltonian cycles 2" do
|
68
|
+
g = GraphTesting.generate_undirected([
|
69
|
+
[1,2],
|
70
|
+
[1,3],
|
71
|
+
[2,4],
|
72
|
+
[2,3],
|
73
|
+
[3,6],
|
74
|
+
[4,5],
|
75
|
+
[4,6],
|
76
|
+
[5,6],
|
77
|
+
])
|
78
|
+
paths = [
|
79
|
+
[2,4,5,6,3,1],
|
80
|
+
]
|
81
|
+
revpaths = paths.collect do |path|
|
82
|
+
(path[1..path.length]+[path[0]]).reverse
|
83
|
+
end
|
84
|
+
|
85
|
+
CYCLE_FINDING_METHODS.each do |method|
|
86
|
+
GraphTesting.sort_cycles(g.send(method)).should ==
|
87
|
+
GraphTesting.sort_cycles(paths+revpaths)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'should operated within limits' do
|
92
|
+
g = GraphTesting.generate_undirected([
|
93
|
+
[1,2],
|
94
|
+
[1,3],
|
95
|
+
[2,4],
|
96
|
+
[2,3],
|
97
|
+
[3,6],
|
98
|
+
[4,5],
|
99
|
+
[4,6],
|
100
|
+
[5,6],
|
101
|
+
])
|
102
|
+
CYCLE_FINDING_METHODS.each do |method|
|
103
|
+
expect {
|
104
|
+
g.send(method, 4)
|
105
|
+
}.to raise_error(Yargraph::OperationalLimitReachedException)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'should find edges in all hamiltonian cycles' do
|
110
|
+
g = GraphTesting.generate_undirected([
|
111
|
+
[1,2],
|
112
|
+
[1,3],
|
113
|
+
[2,4],
|
114
|
+
[2,3],
|
115
|
+
[3,6],
|
116
|
+
[4,5],
|
117
|
+
[4,6],
|
118
|
+
[5,6],
|
119
|
+
])
|
120
|
+
GraphTesting.sorted_edges(g.edges_in_all_hamiltonian_cycles).should ==
|
121
|
+
GraphTesting.sorted_edges([
|
122
|
+
[1,2],
|
123
|
+
[2,4],
|
124
|
+
[4,5],
|
125
|
+
[5,6],
|
126
|
+
[6,3],
|
127
|
+
[3,1],
|
128
|
+
])
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should find some all-hamiltonian edges first' do
|
132
|
+
g = GraphTesting.generate_undirected([
|
133
|
+
[0,1],
|
134
|
+
[1,2],
|
135
|
+
[2,0]
|
136
|
+
])
|
137
|
+
edgeset_results = g.some_edges_in_all_hamiltonian_cycles
|
138
|
+
edgeset_results.contains_hamcycle.should == nil
|
139
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_all.to_a).should ==
|
140
|
+
GraphTesting.sorted_edges([
|
141
|
+
[0,1],
|
142
|
+
[1,2],
|
143
|
+
[2,0],
|
144
|
+
])
|
145
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_none.to_a).should == []
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'some all-hamiltonian edges should say when it falsifies the assumption' do
|
149
|
+
g = GraphTesting.generate_undirected([
|
150
|
+
[0,1],
|
151
|
+
[1,2],
|
152
|
+
])
|
153
|
+
edgeset_results = g.some_edges_in_all_hamiltonian_cycles
|
154
|
+
edgeset_results.contains_hamcycle.should == false
|
155
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_all.to_a).should ==
|
156
|
+
GraphTesting.sorted_edges([
|
157
|
+
[0,1],
|
158
|
+
[1,2],
|
159
|
+
])
|
160
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_none.to_a).should == []
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
it 'some all-hamiltonian edges should not choose all edges when not all are right' do
|
165
|
+
g = GraphTesting.generate_undirected([
|
166
|
+
[0,1],
|
167
|
+
[1,2],
|
168
|
+
[2,3],
|
169
|
+
[3,0],
|
170
|
+
|
171
|
+
[0,2],
|
172
|
+
[1,3],
|
173
|
+
]) #This graph has hamiltonian paths but no edges are in every hamiltonian cycle
|
174
|
+
edgeset_results = g.some_edges_in_all_hamiltonian_cycles
|
175
|
+
edgeset_results.contains_hamcycle.should == nil
|
176
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_all.to_a).should == []
|
177
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_none.to_a).should == []
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'some all-hamiltonian edges should choose none when none are right' do
|
181
|
+
g = GraphTesting.generate_undirected([
|
182
|
+
[0,1],
|
183
|
+
[1,2],
|
184
|
+
[2,3],
|
185
|
+
[3,0],
|
186
|
+
|
187
|
+
[0,2],
|
188
|
+
[1,3],
|
189
|
+
]) #This graph has hamiltonian paths but no edges are in every hamiltonian cycle
|
190
|
+
edgeset_results = g.some_edges_in_all_hamiltonian_cycles
|
191
|
+
edgeset_results.contains_hamcycle.should == nil
|
192
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_all.to_a).should == []
|
193
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_none.to_a).should == []
|
194
|
+
end
|
195
|
+
|
196
|
+
it 'some all-hamiltonian edges should iterate properly' do
|
197
|
+
g = GraphTesting.generate_undirected([
|
198
|
+
[0,1],
|
199
|
+
[1,2],
|
200
|
+
|
201
|
+
[3,4],
|
202
|
+
[4,5],
|
203
|
+
|
204
|
+
[0,3],
|
205
|
+
[1,4],
|
206
|
+
[2,5],
|
207
|
+
]) #This graph requires removal of edges to discover more h_edges
|
208
|
+
edgeset_results = g.some_edges_in_all_hamiltonian_cycles
|
209
|
+
edgeset_results.contains_hamcycle.should == nil
|
210
|
+
|
211
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_all.to_a).should ==
|
212
|
+
GraphTesting.sorted_edges([
|
213
|
+
[0,1],
|
214
|
+
[1,2],
|
215
|
+
|
216
|
+
[3,4],
|
217
|
+
[4,5],
|
218
|
+
|
219
|
+
[0,3],
|
220
|
+
#[1,4],
|
221
|
+
[2,5],
|
222
|
+
])
|
223
|
+
GraphTesting.sorted_edges(edgeset_results.edges_in_none.to_a).should ==
|
224
|
+
GraphTesting.sorted_edges([
|
225
|
+
[1,4],
|
226
|
+
])
|
227
|
+
end
|
228
|
+
end
|
data/yargraph.gemspec
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
# stub: yargraph 0.0.1 ruby lib
|
6
|
+
|
7
|
+
Gem::Specification.new do |s|
|
8
|
+
s.name = "yargraph"
|
9
|
+
s.version = "0.0.1"
|
10
|
+
|
11
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
|
+
s.require_paths = ["lib"]
|
13
|
+
s.authors = ["Ben J. Woodcroft"]
|
14
|
+
s.date = "2014-05-30"
|
15
|
+
s.description = "Pure Ruby graph algorithms, particularly e.g. Hamiltonian cycles"
|
16
|
+
s.email = "donttrustben near gmail.com"
|
17
|
+
s.extra_rdoc_files = [
|
18
|
+
"LICENSE.txt",
|
19
|
+
"README.md"
|
20
|
+
]
|
21
|
+
s.files = [
|
22
|
+
".document",
|
23
|
+
".rspec",
|
24
|
+
"Gemfile",
|
25
|
+
"LICENSE.txt",
|
26
|
+
"README.md",
|
27
|
+
"Rakefile",
|
28
|
+
"VERSION",
|
29
|
+
"lib/yargraph.rb",
|
30
|
+
"spec/spec_helper.rb",
|
31
|
+
"spec/yargraph_spec.rb",
|
32
|
+
"yargraph.gemspec"
|
33
|
+
]
|
34
|
+
s.homepage = "http://github.com/wwood/yargraph"
|
35
|
+
s.licenses = ["MIT"]
|
36
|
+
s.rubygems_version = "2.2.2"
|
37
|
+
s.summary = "Pure Ruby graph algorithms"
|
38
|
+
|
39
|
+
if s.respond_to? :specification_version then
|
40
|
+
s.specification_version = 4
|
41
|
+
|
42
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
43
|
+
s.add_runtime_dependency(%q<ds>, [">= 0"])
|
44
|
+
s.add_development_dependency(%q<rspec>, [">= 2.8.0"])
|
45
|
+
s.add_development_dependency(%q<rdoc>, [">= 3.12"])
|
46
|
+
s.add_development_dependency(%q<bundler>, [">= 1.0"])
|
47
|
+
s.add_development_dependency(%q<jeweler>, [">= 1.8.7"])
|
48
|
+
else
|
49
|
+
s.add_dependency(%q<ds>, [">= 0"])
|
50
|
+
s.add_dependency(%q<rspec>, [">= 2.8.0"])
|
51
|
+
s.add_dependency(%q<rdoc>, [">= 3.12"])
|
52
|
+
s.add_dependency(%q<bundler>, [">= 1.0"])
|
53
|
+
s.add_dependency(%q<jeweler>, [">= 1.8.7"])
|
54
|
+
end
|
55
|
+
else
|
56
|
+
s.add_dependency(%q<ds>, [">= 0"])
|
57
|
+
s.add_dependency(%q<rspec>, [">= 2.8.0"])
|
58
|
+
s.add_dependency(%q<rdoc>, [">= 3.12"])
|
59
|
+
s.add_dependency(%q<bundler>, [">= 1.0"])
|
60
|
+
s.add_dependency(%q<jeweler>, [">= 1.8.7"])
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: yargraph
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ben J. Woodcroft
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-05-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: ds
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 2.8.0
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 2.8.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rdoc
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.12'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.12'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: bundler
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: jeweler
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 1.8.7
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 1.8.7
|
83
|
+
description: Pure Ruby graph algorithms, particularly e.g. Hamiltonian cycles
|
84
|
+
email: donttrustben near gmail.com
|
85
|
+
executables: []
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files:
|
88
|
+
- LICENSE.txt
|
89
|
+
- README.md
|
90
|
+
files:
|
91
|
+
- ".document"
|
92
|
+
- ".rspec"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- VERSION
|
98
|
+
- lib/yargraph.rb
|
99
|
+
- spec/spec_helper.rb
|
100
|
+
- spec/yargraph_spec.rb
|
101
|
+
- yargraph.gemspec
|
102
|
+
homepage: http://github.com/wwood/yargraph
|
103
|
+
licenses:
|
104
|
+
- MIT
|
105
|
+
metadata: {}
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
require_paths:
|
109
|
+
- lib
|
110
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
116
|
+
requirements:
|
117
|
+
- - ">="
|
118
|
+
- !ruby/object:Gem::Version
|
119
|
+
version: '0'
|
120
|
+
requirements: []
|
121
|
+
rubyforge_project:
|
122
|
+
rubygems_version: 2.2.2
|
123
|
+
signing_key:
|
124
|
+
specification_version: 4
|
125
|
+
summary: Pure Ruby graph algorithms
|
126
|
+
test_files: []
|