stamina 0.3.0
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.
- data/.gemtest +0 -0
- data/CHANGELOG.md +22 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +33 -0
- data/LICENCE.md +22 -0
- data/Manifest.txt +16 -0
- data/README.md +78 -0
- data/Rakefile +23 -0
- data/bin/adl2dot +12 -0
- data/bin/classify +12 -0
- data/bin/redblue +12 -0
- data/bin/rpni +12 -0
- data/example/adl/automaton.adl +49 -0
- data/example/adl/sample.adl +53 -0
- data/example/basic/characteristic_sample.adl +32 -0
- data/example/basic/target.adl +9 -0
- data/example/competition/31_test.adl +1500 -0
- data/example/competition/31_training.adl +1759 -0
- data/lib/stamina.rb +19 -0
- data/lib/stamina/adl.rb +298 -0
- data/lib/stamina/automaton.rb +1237 -0
- data/lib/stamina/automaton/walking.rb +336 -0
- data/lib/stamina/classifier.rb +37 -0
- data/lib/stamina/command/adl2dot_command.rb +73 -0
- data/lib/stamina/command/classify_command.rb +57 -0
- data/lib/stamina/command/redblue_command.rb +58 -0
- data/lib/stamina/command/rpni_command.rb +58 -0
- data/lib/stamina/command/stamina_command.rb +79 -0
- data/lib/stamina/errors.rb +20 -0
- data/lib/stamina/induction/commons.rb +170 -0
- data/lib/stamina/induction/redblue.rb +264 -0
- data/lib/stamina/induction/rpni.rb +188 -0
- data/lib/stamina/induction/union_find.rb +377 -0
- data/lib/stamina/input_string.rb +123 -0
- data/lib/stamina/loader.rb +0 -0
- data/lib/stamina/markable.rb +42 -0
- data/lib/stamina/sample.rb +190 -0
- data/lib/stamina/version.rb +14 -0
- data/stamina.gemspec +190 -0
- data/stamina.noespec +35 -0
- data/tasks/debug_mail.rake +78 -0
- data/tasks/debug_mail.txt +13 -0
- data/tasks/gem.rake +68 -0
- data/tasks/spec_test.rake +79 -0
- data/tasks/unit_test.rake +77 -0
- data/tasks/yard.rake +51 -0
- data/test/stamina/adl_test.rb +491 -0
- data/test/stamina/automaton_additional_test.rb +190 -0
- data/test/stamina/automaton_classifier_test.rb +155 -0
- data/test/stamina/automaton_test.rb +1092 -0
- data/test/stamina/automaton_to_dot_test.rb +64 -0
- data/test/stamina/automaton_walking_test.rb +206 -0
- data/test/stamina/exit.rb +3 -0
- data/test/stamina/induction/induction_test.rb +70 -0
- data/test/stamina/induction/redblue_mergesamestatebug_expected.adl +19 -0
- data/test/stamina/induction/redblue_mergesamestatebug_pta.dot +64 -0
- data/test/stamina/induction/redblue_mergesamestatebug_sample.adl +9 -0
- data/test/stamina/induction/redblue_test.rb +83 -0
- data/test/stamina/induction/redblue_universal_expected.adl +4 -0
- data/test/stamina/induction/redblue_universal_sample.adl +5 -0
- data/test/stamina/induction/rpni_inria_expected.adl +7 -0
- data/test/stamina/induction/rpni_inria_sample.adl +9 -0
- data/test/stamina/induction/rpni_test.rb +129 -0
- data/test/stamina/induction/rpni_test_pta.dot +22 -0
- data/test/stamina/induction/rpni_universal_expected.adl +4 -0
- data/test/stamina/induction/rpni_universal_sample.adl +4 -0
- data/test/stamina/induction/union_find_test.rb +124 -0
- data/test/stamina/input_string_test.rb +323 -0
- data/test/stamina/markable_test.rb +70 -0
- data/test/stamina/randdfa.adl +66 -0
- data/test/stamina/sample.adl +4 -0
- data/test/stamina/sample_classify_test.rb +149 -0
- data/test/stamina/sample_test.rb +218 -0
- data/test/stamina/small_dfa.dot +16 -0
- data/test/stamina/small_dfa.gif +0 -0
- data/test/stamina/small_nfa.dot +18 -0
- data/test/stamina/small_nfa.gif +0 -0
- data/test/stamina/stamina_test.rb +69 -0
- data/test/test_all.rb +7 -0
- metadata +279 -0
@@ -0,0 +1,188 @@
|
|
1
|
+
module Stamina
|
2
|
+
module Induction
|
3
|
+
|
4
|
+
#
|
5
|
+
# Implementation of the standard Regular Positive and Negative Induction (RPNI)
|
6
|
+
# algorithm. From a given sample, containing positive and negative strings, RPNI
|
7
|
+
# computes the smallest deterministic automaton compatible with the sample.
|
8
|
+
#
|
9
|
+
# See J. Oncina and P. Garcia, Infering Regular Languages in Polynomial Update
|
10
|
+
# Time, In N. Perez de la Blanca, A. Sanfeliu and E. Vidal, editors, Pattern
|
11
|
+
# Recognition and Image Analysis, volume 1 of Series in Machines Perception and
|
12
|
+
# Artificial Intelligence, pages 49-61, World Scientific, 1992.
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
# # sample typically comes from an ADL file
|
16
|
+
# sample = Stamina::ADL.parse_sample_file('sample.adl')
|
17
|
+
#
|
18
|
+
# # let RPNI build the smallest dfa
|
19
|
+
# dfa = Stamina::Induction::RPNI.execute(sample, {:verbose => true})
|
20
|
+
#
|
21
|
+
# Remarks:
|
22
|
+
# - Constructor and instance methods of this class are public but not intended
|
23
|
+
# to be used directly. They are left public for testing purposes only.
|
24
|
+
# - This class intensively uses the Stamina::Induction::UnionFind class and
|
25
|
+
# methods defined in the Stamina::Induction::Commons module which are worth
|
26
|
+
# reading to understand the algorithm implementation.
|
27
|
+
#
|
28
|
+
class RPNI
|
29
|
+
include Stamina::Induction::Commons
|
30
|
+
|
31
|
+
# Union-find data structure used internally
|
32
|
+
attr_reader :ufds
|
33
|
+
|
34
|
+
# Additional options of the algorithm
|
35
|
+
attr_reader :options
|
36
|
+
|
37
|
+
# Creates an algorithm instance with given options.
|
38
|
+
def initialize(options={})
|
39
|
+
@options = options
|
40
|
+
end
|
41
|
+
|
42
|
+
#
|
43
|
+
# Merges a state of rank j with a state of lower rank i. This merge method
|
44
|
+
# includes merging for determinization.
|
45
|
+
#
|
46
|
+
# Preconditions:
|
47
|
+
# - States denoted by i and j are expected leader states (non merged ones)
|
48
|
+
# - States denoted by i and j are expected to be different
|
49
|
+
#
|
50
|
+
# Postconditions:
|
51
|
+
# - Union find is refined, states i and j having been merged, as well as all
|
52
|
+
# state pairs that need to be merged to ensure the deterministic property
|
53
|
+
# of the quotient automaton.
|
54
|
+
# - If the resulting quotient automaton is consistent with the negative sample,
|
55
|
+
# this method returns true and the refined union-find correctly encodes the
|
56
|
+
# quotient automaton. Otherwise, the method returns false and the union-find
|
57
|
+
# information must be considered inaccurate.
|
58
|
+
#
|
59
|
+
def merge_and_determinize(i, j)
|
60
|
+
# Make the union (keep additional merges to be performed in determinization)
|
61
|
+
# and recompute the user data attached to the new state group (new_data)
|
62
|
+
determinization = []
|
63
|
+
@ufds.union(i, j) do |d1, d2|
|
64
|
+
new_data = merge_user_data(d1, d2, determinization)
|
65
|
+
return false unless new_data
|
66
|
+
new_data
|
67
|
+
end
|
68
|
+
|
69
|
+
# Merge for determinization
|
70
|
+
determinization.each do |pair|
|
71
|
+
# we take the leader states of the pair to merge
|
72
|
+
pair = pair.collect{|i| @ufds.find(i)}
|
73
|
+
# do nothing if already the same leader state
|
74
|
+
next if pair[0]==pair[1]
|
75
|
+
# otherwise recurse or fail
|
76
|
+
return false unless merge_and_determinize(pair[0], pair[1])
|
77
|
+
end
|
78
|
+
|
79
|
+
# Everything seems ok!
|
80
|
+
true
|
81
|
+
end
|
82
|
+
|
83
|
+
#
|
84
|
+
# Makes a complete merge (including determinization), or simply do nothing if
|
85
|
+
# it leads accepting a negative string.
|
86
|
+
#
|
87
|
+
# Preconditions:
|
88
|
+
# - States denoted by i and j are expected leader states (non merged ones)
|
89
|
+
# - States denoted by i and j are expected to be different
|
90
|
+
#
|
91
|
+
# Postconditions:
|
92
|
+
# - Union find is refined, states i and j having been merged, as well as all
|
93
|
+
# state pairs that need to be merged to ensure the deterministic property
|
94
|
+
# of the quotient automaton.
|
95
|
+
# - If the resulting quotient automaton is consistent with the negative sample,
|
96
|
+
# this method returns true and the refined union-find correctly encodes the
|
97
|
+
# quotient automaton. Otherwise, the union find has not been changed.
|
98
|
+
#
|
99
|
+
def successfull_merge_or_nothing(i,j)
|
100
|
+
# try a merge and determinize inside a transaction on the ufds
|
101
|
+
@ufds.transactional do
|
102
|
+
merge_and_determinize(i, j)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
#
|
107
|
+
# Main method of the algorithm. Refines the union find passed as first argument
|
108
|
+
# by merging well chosen state pairs. Returns the refined union find.
|
109
|
+
#
|
110
|
+
# Preconditions:
|
111
|
+
# - The union find _ufds_ is correctly initialized (contains :initial, :accepting,
|
112
|
+
# and :error boolean flags as well as a :delta sub hash)
|
113
|
+
#
|
114
|
+
# Postconditions:
|
115
|
+
# - The union find has been refined. It encodes a quotient automaton (of the PTA
|
116
|
+
# it comes from) such that all positive and negative strings of the underlying
|
117
|
+
# sample are correctly classified by it.
|
118
|
+
#
|
119
|
+
def main(ufds)
|
120
|
+
@ufds = ufds
|
121
|
+
puts "Starting RPNI (#{@ufds.size} states)" if @options[:verbose]
|
122
|
+
# First loop, iterating all PTA states
|
123
|
+
(1...@ufds.size).each do |i|
|
124
|
+
# we ignore those that have been previously merged
|
125
|
+
next if @ufds.slave?(i)
|
126
|
+
# second loop: states of lower rank, with ignore
|
127
|
+
(0...i).each do |j|
|
128
|
+
next if @ufds.slave?(j)
|
129
|
+
# try to merge this pair, including determinization
|
130
|
+
# simply break the loop if it works!
|
131
|
+
success = successfull_merge_or_nothing(i,j)
|
132
|
+
if success
|
133
|
+
puts "#{i} and #{j} successfully merged" if @options[:verbose]
|
134
|
+
break
|
135
|
+
end
|
136
|
+
end # j loop
|
137
|
+
end # i loop
|
138
|
+
@ufds
|
139
|
+
end
|
140
|
+
|
141
|
+
#
|
142
|
+
# Build the smallest DFA compatible with the sample given as input.
|
143
|
+
#
|
144
|
+
# Preconditions:
|
145
|
+
# - The sample is consistent (does not contains the same string both labeled as
|
146
|
+
# positive and negative) and contains at least one string.
|
147
|
+
#
|
148
|
+
# Postconditions:
|
149
|
+
# - The returned DFA is the smallest DFA that correctly labels the learning sample
|
150
|
+
# given as input.
|
151
|
+
#
|
152
|
+
# Remarks:
|
153
|
+
# - This instance version of RPNI.execute is not intended to be used directly and
|
154
|
+
# is mainly provided for testing purposes. Please use the class variant of this
|
155
|
+
# method if possible.
|
156
|
+
#
|
157
|
+
def execute(sample)
|
158
|
+
# create union-find
|
159
|
+
puts "Creating PTA and UnionFind structure" if @options[:verbose]
|
160
|
+
ufds = sample2ufds(sample)
|
161
|
+
# refine it
|
162
|
+
ufds = main(ufds)
|
163
|
+
# compute and return quotient automaton
|
164
|
+
ufds2dfa(ufds)
|
165
|
+
end
|
166
|
+
|
167
|
+
#
|
168
|
+
# Build the smallest DFA compatible with the sample given as input.
|
169
|
+
#
|
170
|
+
# Options (the _options_ hash):
|
171
|
+
# - :verbose can be set to true to trace algorithm execution on standard output.
|
172
|
+
#
|
173
|
+
# Preconditions:
|
174
|
+
# - The sample is consistent (does not contains the same string both labeled as
|
175
|
+
# positive and negative) and contains at least one string.
|
176
|
+
#
|
177
|
+
# Postconditions:
|
178
|
+
# - The returned DFA is the smallest DFA that correctly labels the learning sample
|
179
|
+
# given as input.
|
180
|
+
#
|
181
|
+
def self.execute(sample, options={})
|
182
|
+
RPNI.new(options).execute(sample)
|
183
|
+
end
|
184
|
+
|
185
|
+
end # class RPNI
|
186
|
+
|
187
|
+
end # module Induction
|
188
|
+
end # module Stamina
|
@@ -0,0 +1,377 @@
|
|
1
|
+
module Stamina
|
2
|
+
module Induction
|
3
|
+
|
4
|
+
#
|
5
|
+
# Implements an UnionFind data structure dedicated to state merging induction algorithms.
|
6
|
+
# For this purpose, this union-find handles mergeable user data as well as transactional
|
7
|
+
# support. See Stamina::Induction::Commons about the usage of this class (and mergeable
|
8
|
+
# user data in particular) by induction algorithms.
|
9
|
+
#
|
10
|
+
# == Example (probably easier than a long explanation)
|
11
|
+
#
|
12
|
+
# # create a union-find for 10 elements
|
13
|
+
# ufds = Stamina::Induction::UnionFind.new(10) do |index|
|
14
|
+
# # each element will be associated with a hash with data of interest:
|
15
|
+
# # smallest element, greatest element and concatenation of names
|
16
|
+
# {:smallest => index, :greatest => index, :names => index.to_s}
|
17
|
+
# end
|
18
|
+
#
|
19
|
+
# # each element is its own leader
|
20
|
+
# puts (0...10).all?{|s| ufds.leader?(s)} -> true
|
21
|
+
#
|
22
|
+
# # and their respective group number are the element indices themselve
|
23
|
+
# puts ufds.to_a -> [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
|
24
|
+
#
|
25
|
+
# # now, let merge 4 with 0
|
26
|
+
# ufds.union(0, 4) do |d0, d4|
|
27
|
+
# {:smallest => d0[:smallest] < d4[:smallest] ? d0[:smallest] : d4[:smallest],
|
28
|
+
# :greatest => d0[:smallest] > d4[:smallest] ? d0[:smallest] : d4[:smallest],
|
29
|
+
# :names => d0[:names] + " " + d4[:names]}
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# # let see what happens on group numbers
|
33
|
+
# puts ufds.to_a -> [0, 1, 2, 3, 0, 5, 6, 7, 8, 9]
|
34
|
+
#
|
35
|
+
# # let now have a look on mergeable_data of the group of 0 (same result for 4)
|
36
|
+
# puts ufds.mergeable_data(0).inspect -> {:smallest => 0, :greatest => 4, :names => "0 4"}
|
37
|
+
#
|
38
|
+
# == Basic Union Find API
|
39
|
+
#
|
40
|
+
# A UnionFind data structure typically allows encoding a partition of elements (a
|
41
|
+
# partition is a collection of disjoint sets - aka a collection of groups). Basically,
|
42
|
+
# this class represents elements by successive indices (from 0 to size, the later being
|
43
|
+
# excluded). The partitioning information is kept in a array, associating a group number
|
44
|
+
# to each element. This group number is simply the index of the least element in the
|
45
|
+
# group (which means that group numbers are not necessarily consecutive). For example,
|
46
|
+
# the following arrays maps to the associated partitions:
|
47
|
+
#
|
48
|
+
# [0, 1, 2, 3, 4, 5] -> {{0}, {1}, {2}, {3}, {4}}
|
49
|
+
# [0, 0, 0, 0, 0, 0] -> {{0, 1, 2, 3, 4, 5}}
|
50
|
+
# [0, 1, 1, 0, 4, 4] -> {{0, 3}, {1, 2}, {5, 5}}
|
51
|
+
#
|
52
|
+
# The API of this basic union-find data structure is composed of the following
|
53
|
+
# methods:
|
54
|
+
# - new(size) (class method): builds an initial partition information over _size_
|
55
|
+
# elements. This initial partition keeps each element in its own group.
|
56
|
+
# - find(i): returns the group number of the i-th element
|
57
|
+
# - union(i, j): merge the group of the i-th element with the group of the j-th
|
58
|
+
# element. Note that i and j are elements, NOT group numbers.
|
59
|
+
#
|
60
|
+
# As we use least elements as group numbers, it is also interesting to know if a
|
61
|
+
# given element is that least element (aka leader element of the group) or not:
|
62
|
+
#
|
63
|
+
# - leader?(i): returns true if i is the group number of the i-th element, false
|
64
|
+
# otherwise. In other words, returns true if find(i)==i
|
65
|
+
# - slave?(i): the negation of leader?(i).
|
66
|
+
#
|
67
|
+
# == Handling User Data
|
68
|
+
#
|
69
|
+
# Even if this class represents elements by indices, it also allows keeping user
|
70
|
+
# data attached to each group. For this:
|
71
|
+
#
|
72
|
+
# - an initial user data is attached to each element at construction time by
|
73
|
+
# yielding a block (passing the element index as first argument and expecting
|
74
|
+
# user data as block return value).
|
75
|
+
# - the union(i, j) method allows a block to be given. It passes user data of i's
|
76
|
+
# and j's groups as arguments and expects the block to compute and return the
|
77
|
+
# merged user data for the new group.
|
78
|
+
# - mergeable_data(i) returns the current user data associated to the group of
|
79
|
+
# the i-th element.
|
80
|
+
# - mergeable_datas returns an array with user data attached to each group.
|
81
|
+
#
|
82
|
+
# Please note that user data are considered immutable values, and should never be
|
83
|
+
# changed... Only new ones can be created at union time. To ensures this good usage,
|
84
|
+
# user data are freezed by this class at creation time and union time.
|
85
|
+
#
|
86
|
+
# == Transactional support
|
87
|
+
#
|
88
|
+
# The main aim of this UnionFind is to make the implementation induction algorithms
|
89
|
+
# Stamina::Induction::RPNI and Stamina::Induction::RedBlue (sufficiently) efficient,
|
90
|
+
# simple and readable. These algorithms rely on a try-and-error strategy are must be
|
91
|
+
# able to revert the changes they have made during their last try. The transaction
|
92
|
+
# support implemented by this data structure helps them achieving this goal. For this
|
93
|
+
# we provide the following methods:
|
94
|
+
#
|
95
|
+
# - save_point: ensures that the internal state of the UnionFind can be restored if
|
96
|
+
# rollback is invoked later.
|
97
|
+
# - commit: informs the UnionFind that changes that have been made since the last
|
98
|
+
# invocation of save_point will not be reconsidered.
|
99
|
+
# - rollback: restores the internal state of the UnionFind that has been saved when
|
100
|
+
# save_point has been called.
|
101
|
+
#
|
102
|
+
# Please note that this class does not support sub-transactions.
|
103
|
+
#
|
104
|
+
class UnionFind
|
105
|
+
|
106
|
+
#
|
107
|
+
# An element of the union find, keeping the index of its leader element as well as
|
108
|
+
# mergeable user data. This class is not intended to be used by external users of the
|
109
|
+
# UnionFind data structure.
|
110
|
+
#
|
111
|
+
class Node
|
112
|
+
|
113
|
+
# Index of the parent element (on the way to the leader)
|
114
|
+
attr_accessor :parent
|
115
|
+
|
116
|
+
# Attached user data
|
117
|
+
attr_accessor :data
|
118
|
+
|
119
|
+
#
|
120
|
+
# Creates a default Node instance with a specific parent index and attached
|
121
|
+
# user data.
|
122
|
+
#
|
123
|
+
def initialize(parent, data)
|
124
|
+
@parent = parent
|
125
|
+
@data = data
|
126
|
+
end
|
127
|
+
|
128
|
+
#
|
129
|
+
# Duplicates this node, ensuring that future changes will not affect the copy.
|
130
|
+
# Please note that the user data itself is not duplicated and is not expected
|
131
|
+
# to change. This property (not changing user data) is respected by the RPNI
|
132
|
+
# and RedBlue classes as implemented in this library.
|
133
|
+
#
|
134
|
+
def dup
|
135
|
+
Node.new(@parent, @data)
|
136
|
+
end
|
137
|
+
|
138
|
+
end # class Node
|
139
|
+
|
140
|
+
#
|
141
|
+
# Number of elements in this union find
|
142
|
+
#
|
143
|
+
attr_reader :size
|
144
|
+
|
145
|
+
#
|
146
|
+
# (protected) Accessor on elements array, provided for duplication
|
147
|
+
#
|
148
|
+
attr_writer :elements
|
149
|
+
|
150
|
+
#
|
151
|
+
# Creates a default union find of a given size. Each element is initially in its own
|
152
|
+
# group. User data attached to each group is obtained by yielding a block, passing
|
153
|
+
# element index as first argument.
|
154
|
+
#
|
155
|
+
# Precondition:
|
156
|
+
# - size is expected to be strictly positive
|
157
|
+
#
|
158
|
+
def initialize(size)
|
159
|
+
@size = size
|
160
|
+
@elements = (0...size).collect do |i|
|
161
|
+
Node.new(i, block_given? ? yield(i).freeze : nil)
|
162
|
+
end
|
163
|
+
@changed = nil
|
164
|
+
end
|
165
|
+
|
166
|
+
# Union Find API ###########################################################
|
167
|
+
|
168
|
+
#
|
169
|
+
# Finds the group number of the i-th element (the group number is the least
|
170
|
+
# element of the group, aka _leader_).
|
171
|
+
#
|
172
|
+
# Preconditions:
|
173
|
+
# - i is a valid element: 0 <= i < size
|
174
|
+
#
|
175
|
+
# Postconditions:
|
176
|
+
# - returned value _found_ is such that <code>find(found)==found</code>
|
177
|
+
# - the union find data structure is not modified (no compression implemented).
|
178
|
+
#
|
179
|
+
def find(i)
|
180
|
+
while @elements[i].parent != i
|
181
|
+
i = @elements[i].parent
|
182
|
+
end
|
183
|
+
i
|
184
|
+
end
|
185
|
+
|
186
|
+
#
|
187
|
+
# Merges groups of the i-th element and j-th element, yielding a block to compute
|
188
|
+
# the merging of user data attached to their respective groups before merging.
|
189
|
+
#
|
190
|
+
# Preconditions:
|
191
|
+
# - This method allows i and j not to be leaders, but any element.
|
192
|
+
# - i and j are expected to be valid elements (0 <= i <= size, same for j)
|
193
|
+
#
|
194
|
+
# Postconditions:
|
195
|
+
# - groups of i and j have been merged. All elements of the two subgroups have
|
196
|
+
# the group number defined as <code>min(find(i),find(j))</code> (before
|
197
|
+
# merging)
|
198
|
+
# - if a block is provided, the user data attached to the new group is computed by
|
199
|
+
# yielding the block, passing mergable_data(i) and mergable_data(j) as arguments.
|
200
|
+
# The block is ecpected to return the merged data that will be kept for the new
|
201
|
+
# group.
|
202
|
+
# - If a transaction is pending, all required information is saved to restore
|
203
|
+
# the union-find structure if the transaction is rollbacked later.
|
204
|
+
#
|
205
|
+
def union(i, j)
|
206
|
+
i, j = find(i), find(j)
|
207
|
+
reversed = false
|
208
|
+
i, j, reversed = j, i, true if j<i
|
209
|
+
|
210
|
+
# Save i and j if in transaction and not already saved
|
211
|
+
if @changed
|
212
|
+
@changed[i] = @elements[i].dup unless @changed.has_key?(i)
|
213
|
+
@changed[j] = @elements[j].dup unless @changed.has_key?(j)
|
214
|
+
end
|
215
|
+
|
216
|
+
# Make the changes now
|
217
|
+
@elements[j].parent = i
|
218
|
+
if block_given?
|
219
|
+
d1, d2 = @elements[i].data, @elements[j].data
|
220
|
+
d1, d2 = d2, d1 if reversed
|
221
|
+
@elements[i].data = yield(d1, d2).freeze
|
222
|
+
else
|
223
|
+
nil
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
#
|
228
|
+
# Checks if an element is the leader of its group.
|
229
|
+
#
|
230
|
+
# Preconditions:
|
231
|
+
# - i is a valid element: 0 <= i < size
|
232
|
+
#
|
233
|
+
# Postconditions:
|
234
|
+
# - true if find(i)==i, false otherwise.
|
235
|
+
#
|
236
|
+
def leader?(i)
|
237
|
+
@elements[i].parent==i
|
238
|
+
end
|
239
|
+
|
240
|
+
#
|
241
|
+
# Checks if an element is a slave in its group (negation of leader?).
|
242
|
+
#
|
243
|
+
# Preconditions:
|
244
|
+
# - i is a valid element: 0 <= i < size
|
245
|
+
#
|
246
|
+
# Postconditions:
|
247
|
+
# - false if find(i)==i, true otherwise.
|
248
|
+
#
|
249
|
+
def slave?(i)
|
250
|
+
@elements[i].parent != i
|
251
|
+
end
|
252
|
+
|
253
|
+
# UserData API #############################################################
|
254
|
+
|
255
|
+
#
|
256
|
+
# Returns the mergeable data of each group in an array. No order of the
|
257
|
+
# groups is ensured by this method.
|
258
|
+
#
|
259
|
+
def mergeable_datas
|
260
|
+
indices = (0...size).select {|i| leader?(i)}
|
261
|
+
indices.collect{|i| @elements[i].data}
|
262
|
+
end
|
263
|
+
|
264
|
+
#
|
265
|
+
# Returns the mergeable data attached to the group of the i-th element.
|
266
|
+
#
|
267
|
+
# Preconditions:
|
268
|
+
# - This method allows i not to be leader, but any element.
|
269
|
+
# - i is a valid element: 0 <= i < size
|
270
|
+
#
|
271
|
+
def mergeable_data(i)
|
272
|
+
@elements[find(i)].data
|
273
|
+
end
|
274
|
+
|
275
|
+
# Transactional API ########################################################
|
276
|
+
|
277
|
+
#
|
278
|
+
# Makes a save point now. Internally ensures that future changes will be
|
279
|
+
# tracked and that a later rollback will restore the union find to the
|
280
|
+
# internal state it had before this call. This method should not be called
|
281
|
+
# if a transaction is already pending.
|
282
|
+
#
|
283
|
+
def save_point
|
284
|
+
@changed = {}
|
285
|
+
end
|
286
|
+
|
287
|
+
#
|
288
|
+
# Terminates the pending transaction by commiting all changes that have been
|
289
|
+
# done since the last save_point call. This method should not be called if no
|
290
|
+
# transaction is pending.
|
291
|
+
#
|
292
|
+
def commit
|
293
|
+
@changed = nil
|
294
|
+
end
|
295
|
+
|
296
|
+
#
|
297
|
+
# Rollbacks all changes that have been done since the last save_point call.
|
298
|
+
# This method will certainly fail if no transaction is pending.
|
299
|
+
#
|
300
|
+
def rollback
|
301
|
+
@changed.each_pair do |index, node|
|
302
|
+
@elements[index] = node
|
303
|
+
end
|
304
|
+
@changed = nil
|
305
|
+
end
|
306
|
+
|
307
|
+
#
|
308
|
+
# Makes a save point, yields the block. If it returns false or nil, rollbacks
|
309
|
+
# the transaction otherwise commits it. This method is a nice shortcut for
|
310
|
+
# the following piece of code
|
311
|
+
#
|
312
|
+
# ufds.save_point
|
313
|
+
# if try_something
|
314
|
+
# ufds.commit
|
315
|
+
# else
|
316
|
+
# ufds.rollback
|
317
|
+
# end
|
318
|
+
#
|
319
|
+
# which can also be expressed as:
|
320
|
+
#
|
321
|
+
# ufds.transactional do
|
322
|
+
# try_something
|
323
|
+
# end
|
324
|
+
#
|
325
|
+
# This method returns the value returned by the block
|
326
|
+
#
|
327
|
+
def transactional
|
328
|
+
save_point
|
329
|
+
returned = yield
|
330
|
+
if returned.nil? or returned == false
|
331
|
+
rollback
|
332
|
+
else
|
333
|
+
commit
|
334
|
+
end
|
335
|
+
returned
|
336
|
+
end
|
337
|
+
|
338
|
+
# Common utilities #########################################################
|
339
|
+
|
340
|
+
#
|
341
|
+
# Duplicates this data-structure, ensuring that no change on self or on the
|
342
|
+
# copy is shared. Please note that user datas themselve are not duplicated as
|
343
|
+
# they are considered immutable values (and freezed at construction and union).
|
344
|
+
#
|
345
|
+
def dup
|
346
|
+
copy = UnionFind.new(size)
|
347
|
+
copy.elements = @elements.collect{|e| e.dup}
|
348
|
+
copy
|
349
|
+
end
|
350
|
+
|
351
|
+
#
|
352
|
+
# Returns the partitioning information as as array with the group number of
|
353
|
+
# each element.
|
354
|
+
#
|
355
|
+
def to_a
|
356
|
+
(0...size).collect{|i| find(i)}
|
357
|
+
end
|
358
|
+
|
359
|
+
#
|
360
|
+
# Returns a string representation of this union find information.
|
361
|
+
#
|
362
|
+
def to_s
|
363
|
+
@elements.to_s
|
364
|
+
end
|
365
|
+
|
366
|
+
#
|
367
|
+
# Returns a string representation of this union find information.
|
368
|
+
#
|
369
|
+
def inspect
|
370
|
+
@elements.to_s
|
371
|
+
end
|
372
|
+
|
373
|
+
protected :elements=
|
374
|
+
end # class UnionFind
|
375
|
+
|
376
|
+
end # module Induction
|
377
|
+
end # module Stamina
|