rbgraph 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 79cc2737a16f085557a84112f66bf9b0f8478254
4
- data.tar.gz: 966a3c89e50ce68437217197c00ba20c39f0a3be
3
+ metadata.gz: 8284a63b040d1fb014f4ca839ef4e66ca3d8fc19
4
+ data.tar.gz: b31b8d5cd254f6b9d46320c05bea0ebfa3b13369
5
5
  SHA512:
6
- metadata.gz: 03c0337048a02c661405d874cd03a2bf8b46f154874a3df2a65d977c14451298e976254d5ef3a29bf822fa2c48765438ba899278d2a57f84be0369e8f34133c1
7
- data.tar.gz: 249567dfd34825a58dd5733b7dbfc0d38649d4fc262b73bf1f3b5207e57992af312dace4c70dcfc2dd66326d86f7911309519240a1ebf0124dd48ee62e662fab
6
+ metadata.gz: 94efb880fb5e11ab8efa02e5df9da6f13cf8daca8f4ccbd4d452e8aa48731b6f699944f131c22a19f1ff5cc7230a062050bbbdc11af3d7111a32f160af50d207
7
+ data.tar.gz: cd62f60bb91d2bc65421653c8abea219b5e3298ec28d200e41f214edcea4018c51b7c7e5fa0491a7bb0d39d1c0c6160be6e19e08870039b66dc35a6aa0479ffd
data/README.md CHANGED
@@ -11,7 +11,107 @@ or add it to your Gemfile
11
11
 
12
12
  ```gem 'rbgraph'```
13
13
 
14
- ## Example
14
+ ## Basic objects
15
+
16
+ ####*Graph*:
17
+ Graphs can be undirected or directed.
18
+ ```ruby
19
+ graph = Rbgraph::UndirectedGraph.new
20
+ graph = Rbgraph::DirectedGraph.new
21
+ ```
22
+ Use ```graph.directed?``` to figure out if a graph is directed or not.
23
+ The graph object has the ```nodes``` and ```edges``` properties which are ruby hashes, the keys being the ids of each object respectively.
24
+
25
+ ####*Nodes*:
26
+ Every node should have an already set ```id``` property upon initialization.
27
+ Nodes also carry data, in the data attribute which is a ruby hash.
28
+ Usually you will not need initialize a node directly, but rather just add one with the desired properties in the graph.
29
+ ```ruby
30
+ graph = Rbgraph::UndirectedGraph.new
31
+ # graph.add_node!(node_id, node_data, &block)
32
+ # Add a node with id = 5 and attached data t = [0, 4, 19].
33
+ node = graph.add_node!(5, {t: [0, 4, 19]})
34
+ ```
35
+ By default if a node with the same id already exists, then ```add_node!``` does nothing unless given a block.
36
+ ```ruby
37
+ # Add another node with id = 5 and attached data t = [1, 5, 20].
38
+ node = graph.add_node!(5, {t: [1, 5, 20]}) # does nothing
39
+ node = graph.add_node!(5, {t: [1, 5, 20]}) do |graph, existing_node, new_node_data|
40
+ existing_node.data[:t] += new_node_data[:t]
41
+ end
42
+ node.data[:t] # => [0, 4, 19, 1, 5, 20]
43
+ graph.nodes[5].data[:t] # => [0, 4, 19, 1, 5, 20]
44
+ ```
45
+ Nodes have the following properties:
46
+ * id
47
+ * data
48
+ * neighbors (respects directional/undirectional)
49
+ * edges (will contain both incoming and outgoing edges)
50
+ * graph
51
+
52
+ and the following methods:
53
+
54
+ * out_degree
55
+ * outgoing_edges
56
+ * in_degree
57
+ * incoming_edges
58
+ * parent (works only in directed graph - return the node that has only one edge towards this one if any)
59
+ * ancestors (works only in directed graph - returns the list of parents for this node up to the root if the graph is not cyclic)
60
+ * root (works only in directed graph - return the root of this node if the graph is not cyclic)
61
+
62
+
63
+ ####*Edges*:
64
+ Edges connect nodes with either a one-way (directional graph) or two-way (undirectional graph) link.
65
+ Edges have an internally computed id, which is a string representation of the ids of the nodes they connect.
66
+ Each node can also have a ```weight``` attribute, and a ```kind``` attribute, as well as carry additional data as a ruby hash in its ```data``` attribute.
67
+ Again you do not want to instantiate edges directly, rather you would just add them to the graph.
68
+ ```ruby
69
+ graph = Rbgraph::UndirectedGraph.new
70
+ # graph.add_edge!(node1, node2, weight = 1, kind = nil, edge_data = {}, &block)
71
+ # Add an edge connecting nodes with id 2 and 3
72
+ edge1 = graph.add_edge!({id: 2}, {id: 3})
73
+ edge1.weight # => 1
74
+ # Add an edge connecting nodes with id 1 and 4, weight = 3, kind = "friendship" and data = {created_at: <some_date>}
75
+ edge2 = graph.add_edge!({id: 1}, {id: 4}, 3, "friendship", {created_at: <some_date>})
76
+ edge1.weight # => 3
77
+ ```
78
+ You can add an edge even if the nodes that will be connected do not yet exist in the graph.
79
+ They will be added automatically when you call ```add_edge!```.
80
+ When adding an edge that already exists, i.e. connects the same nodes and is of the same kind, then the existing edge increases its weight by the amount present in the new edge, unless you specify a block to provide custom behavior.
81
+ ```ruby
82
+ edge1 = graph.add_edge!({id: 2}, {id: 3})
83
+ edge1.weight # => 2
84
+ edge1 = graph.add_edge!({id: 2}, {id: 3}, 0)
85
+ edge1.weight # => 2
86
+ edge1 = graph.add_edge!({id: 2}, {id: 3}, 1, nil, {some: :data}) do |graph, existing_edge, new_edge|
87
+ existing_edge.data.merge!(new_edge.data)
88
+ end
89
+ edge1.weight # => 2 (weight doesn't change when you provide a block it is your responsibility to increase it if you want)
90
+ edge1.data # => {some: :data}
91
+
92
+ edge3 = graph.add_edge!({id: 2}, {id: 3}, 1, "something")
93
+ edge3.id != edge1.id # => true
94
+ ```
95
+
96
+ Edges have the following properties:
97
+ * id
98
+ * data
99
+ * node1
100
+ * node2
101
+ * weight
102
+ * kind
103
+ * graph
104
+
105
+ and the following methods:
106
+
107
+ * has_node?(some_node)
108
+ * other_node(some_node)
109
+ * different_node(some_node)
110
+ * out_for?(some_node)
111
+ * in_for?(some_node)
112
+
113
+
114
+ ## Examples
15
115
 
16
116
  ```ruby
17
117
  graph = Rbgraph::UndirectedGraph.new()
@@ -103,5 +203,26 @@ t = Rbgraph::Traverser::BfsTraverser.new(graph)
103
203
  c = t.connected_components(respect_direction: false)
104
204
  ```
105
205
 
206
+ Version 0.5.0+
207
+
208
+ You can now request a path between two nodes in the graph.
209
+ ```ruby
210
+ graph = Rbgraph::UndirectedGraph.new()
211
+ ... # add nodes and edges
212
+ t = Rbgraph::Traverser::BfsTraverser.new(graph)
213
+ a = graph.nodes[1]
214
+ b = graph.nodes[2]
215
+ path = t.bfs_between_a_and_b(a, b)
216
+ ```
217
+ or you can use a directed graph and choose to respect the direction of the edges while search for a path between a and b or not.
218
+ ```ruby
219
+ graph = Rbgraph::DirectedGraph.new()
220
+ ... # add nodes and edges
221
+ t = Rbgraph::Traverser::BfsTraverser.new(graph)
222
+ a = graph.nodes[1]
223
+ b = graph.nodes[2]
224
+ path = t.bfs_between_a_and_b(a, b, respect_direction: false)
225
+ ```
226
+
106
227
  ### Disclaimer
107
228
  This project is written on a need to use basis for inclusion to other projects I'm working on for now, so completion is not an immediate goal.
@@ -4,52 +4,99 @@ module Rbgraph
4
4
  class BfsTraverser
5
5
 
6
6
  attr_accessor :graph
7
- attr_accessor :connected_subgraphs
8
- attr_accessor :visited_nodes
9
- attr_accessor :unvisited_nodes
10
- attr_accessor :queue
11
7
 
12
8
  def initialize(graph)
13
9
  self.graph = graph
14
- self.connected_subgraphs = []
15
- self.unvisited_nodes = Set.new [*graph.nodes.values]
16
10
  end
17
11
 
18
12
  def connected_components(options = {})
19
- self.visited_nodes = Set.new
20
- self.queue = Queue.new
13
+ @connected_subgraphs = []
14
+ @unvisited_nodes = Set.new [*graph.nodes.values]
15
+ @visited_nodes = Set.new
16
+ @queue = Queue.new
21
17
 
22
- while !unvisited_nodes.empty? do
23
- root = unvisited_nodes.to_a.first
24
- unvisited_nodes.delete(unvisited_nodes.to_a.first)
25
- self.connected_subgraphs << bfs_from_root(root, options)
18
+ while !@unvisited_nodes.empty? do
19
+ root = @unvisited_nodes.to_a.first
20
+ @unvisited_nodes.delete(@unvisited_nodes.to_a.first)
21
+ @connected_subgraphs << bfs_from_root(root, options.merge(sticky: true))
26
22
  end
27
- connected_subgraphs
23
+ @connected_subgraphs
28
24
  end
29
25
 
30
26
  def bfs_from_root(root, options = {})
27
+ if !options[:sticky]
28
+ @unvisited_nodes = Set.new [*graph.nodes.values]
29
+ @visited_nodes = Set.new
30
+ @queue = Queue.new
31
+ end
32
+
33
+ clear_backtracking_info
34
+
31
35
  subgraph = graph.class.new
32
- visited_nodes.add(root)
33
- queue.enq(root)
34
- while !queue.empty? do
35
- t = queue.deq
36
- unvisited_nodes.delete(t)
37
- yield(t) if block_given? # do sth on current node
36
+ @visited_nodes.add(root)
37
+ @queue.enq(root)
38
+ while !@queue.empty? do
39
+ t = @queue.deq
40
+ @unvisited_nodes.delete(t)
41
+ yield_value = yield(t) if block_given? # do sth on current node
42
+ break if yield_value == :break
38
43
  subgraph.nodes[t.id] ||= t
39
44
  t.edges.each do |eid, edge|
40
45
  next if graph.directed? && !edge.out_for?(t) unless options[:respect_direction] == false
41
46
  neighbor = edge.other_node(t)
42
47
  subgraph.edges[eid] ||= edge
43
48
  subgraph.nodes[neighbor.id] ||= neighbor
44
- if !visited_nodes.include?(neighbor)
45
- visited_nodes.add(neighbor)
46
- queue.enq(neighbor)
49
+ if !@visited_nodes.include?(neighbor)
50
+ @visited_nodes.add(neighbor)
51
+ @queue.enq(neighbor)
52
+ neighbor.data[:__bfs_prev_node] ||= t
53
+ @__bfs_prev_node_dirty = true
47
54
  end
48
55
  end
49
56
  end
50
57
  subgraph
51
58
  end
52
59
 
60
+ def bfs_between_a_and_b(a, b, options = {})
61
+ if graph.nodes[a.id].nil? || graph.nodes[b.id].nil?
62
+ return nil
63
+ end
64
+
65
+ if a.id == b.id # a is the same as b
66
+ return [a]
67
+ end
68
+
69
+ if !a.neighbors[b.id].nil? # a and b are just one step away
70
+ return [a, b]
71
+ end
72
+
73
+ bfs_from_root(a, options) do |current_node|
74
+ :break if current_node.id == b.id # destination node found! return ':break' in the block to stop traversing!
75
+ end
76
+
77
+ if b.data[:__bfs_prev_node].nil?
78
+ return [] # no path found
79
+ end
80
+
81
+ path = [b]
82
+
83
+ while path[-1].id != a.id do
84
+ path << path[-1].data[:__bfs_prev_node]
85
+ end
86
+
87
+ path.reverse
88
+ end
89
+
90
+ private
91
+
92
+ def clear_backtracking_info
93
+ if @__bfs_prev_node_dirty
94
+ graph.nodes.each do |node_id, node|
95
+ node.data[:__bfs_prev_node] = nil
96
+ end
97
+ end
98
+ end
99
+
53
100
  end
54
101
 
55
102
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rbgraph
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - George Lamprianidis
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-14 00:00:00.000000000 Z
11
+ date: 2018-07-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -86,7 +86,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
86
86
  version: '0'
87
87
  requirements: []
88
88
  rubyforge_project:
89
- rubygems_version: 2.5.1
89
+ rubygems_version: 2.6.14
90
90
  signing_key:
91
91
  specification_version: 4
92
92
  summary: Ruby graphs!