require 'more_math/ranking_common' module MoreMath class Permutation include Enumerable include Comparable include RankingCommon # Creates a new Permutation instance of size # (and ranked with rank). def initialize(size, rank = 0) @size, self.rank = size, rank @last = factorial(size) - 1 end # Creates a new Permutation instance from the Array # indices, that should consist of a permutation of Fixnums # in the range of 0 and indices.size - 1. This is # for example the result of a call to the Permutation#value method. def self.from_value(indices) indices = indices.clone obj = new(indices.size) obj.instance_eval do self.rank = rank_indices(indices) end obj end # Creates a new Permutation instance from the Array of Arrays # cycles. This is for example the result of a # call to the Permutation#cycles method . def self.from_cycles(cycles, max = 0) indices = Array.new(max) cycles.each do |cycle| cycle.empty? and next for i in 0...cycle.size indices[ cycle[i - 1] ] = cycle[i] end end indices.each_with_index { |r, i| r or indices[i] = i } from_value(indices) end # A permutation instance of size collection.size is created with # collection as the default Permutation#project data object. A # collection should respond to size, [], and []=. The Permutation # instance will default to rank 0 if none is given. def self.for(collection, rank = 0) perm = new(collection.size, rank) perm.instance_variable_set(:@collection, collection) perm end # Shortcut to generate the identity permutation that has # Permutation#size n. def self.identity(n) new(n) end # Returns the identity permutation that has the same Permutation#size as this # instance. def identity self.class.new(size) end # Builds a permutation that maps a into b. # Both arguments must be the same length and must contain the same # elements. If these arrays contain duplicate elements, the solution # will not be unique. def self.for_mapping(a, b) a.size == b.size or raise ArgumentError, "Initial and final lists must be the same length" lookup = Hash.new { |h, k| h[k] = [] } a.size.times { |i| lookup[a[i]] <<= i } value = Array.new(b.size) do |i| e = b[i] lookup[e].pop or raise ArgumentError, "no corresponding element for #{e.inspect}" end Permutation.from_value value end # Computes the nth power (ie the nth repeated permutation) of this instance. # Negative powers are taken to be powers of the inverse. def power(n) if n.respond_to?(:to_int) n = n.to_int else raise TypeError, "#{n.inspect} cannot be converted to an integer" end if n >= 0 (1..n).inject(identity) { |p, e| p * self } else # negative powers are taken to be powers of the inverse -power(-n) end end alias ** power # Assigns m to the rank attribute of this Permutation # instance. That implies that the indices produced by a call to the # Permutation#value method of this instance is the permutation ranked with # this new rank. def rank=(m) @rank = m % factorial(size) end # Returns the indices in the range of 0 to Permutation#size - 1 # of this permutation that is ranked with Permutation#rank. # # Example: # perm = Permutation.new(6, 312) # # => # # perm.value # # => [2, 4, 0, 1, 3, 5] def value unrank_indices(@rank) end # Returns the projection of this instance's Permutation#value # into the data object that should respond to # the #[] method. If this Permutation inbstance was created # with Permutation.for the collection used to create # it is used as a data object. # # Example: # perm = Permutation.new(6, 312) # # => # # perm.project("abcdef") # # => "ceabdf" def project(data = (@collection if defined? @collection)) data or raise ArgumentError, "a collection is required to project" raise ArgumentError, "data size is != #{size}!" if data.size != size projection = data.clone value.each_with_index { |i, j| projection[j] = data[i] } projection end # Compares to Permutation instances according to their Permutation#size # and the Permutation#rank. # # The mixed in methods from the Comparable module rely on this method. def <=>(other) size <=> other.size.zero? || rank <=> other.rank end # Returns true if this Permutation instance and the other have the same # value, that is both Permutation instances have the same Permutation#size # and the same Permutation#rank. def eql?(other) self.class == other.class && size == other.size && rank == other.rank end alias == eql? # Computes a unique hash value for this Permutation instance. def hash size.hash ^ rank.hash end # Switchtes this Permutation instance to the inverted permutation. # (See Permutation#compose for an example.) def invert! indices = unrank_indices(rank) inverted = Array.new(size) for i in 0...size inverted[indices[i]] = i end self.rank = rank_indices(inverted) self end # Returns the inverted Permutation of this Permutation instance. # (See Permutation#compose for an example.) def invert clone.invert! end alias -@ invert # Compose this Permutation instance and the other to # a new Permutation. Note that a permutation # composed with it's inverted permutation yields # the identity permutation, the permutation with rank 0. # # Example: # p1 = Permutation.new(5, 42) # # => # # p2 = p1.invert # # => # # p1.compose(p2) # => # # Or a little nicer to look at: # p1 * -p1 # # => # def compose(other) size == other.size or raise ArgumentError, "permutations of unequal sizes cannot be composed!" indices = value composed = other.value.map { |i| indices[i] } klon = clone klon.rank = rank_indices(composed) klon end alias * compose # Returns the cycles representation of this Permutation instance. # The return value of this method can be used to create a # new Permutation instance with the Permutation.from_cycles method. # # Example: # perm = Permutation.new(7, 23) # # => # # perm.cycles # # => [[3, 6], [4, 5]] def cycles perm = value result = [[]] seen = {} current = nil loop do current or current = perm.find { |x| !seen[x] } break unless current if seen[current] current = nil result << [] else seen[current] = true result[-1] << current current = perm[current] end end result.pop result.select { |c| c.size > 1 }.map do |c| min_index = c.index(c.min) c[min_index..-1] + c[0...min_index] end end # Returns the signum of this Permutation instance. # It's -1 if this permutation is odd and 1 if it's # an even permutation. # # A permutation is odd if it can be represented by an odd number of # transpositions (cycles of length 2), or even if it can be represented of # an even number of transpositions. def signum s = 1 cycles.each do |c| c.size % 2 == 0 and s *= -1 end s end alias sgn signum # Returns true if this permutation is even, false otherwise. def even? signum == 1 end # Returns true if this permutation is odd, false otherwise. def odd? signum == -1 end private @@fcache = [ 1 ] def factorial(n) @@fcache.size.upto(n) { |i| @@fcache[i] = i * @@fcache[i - 1] } @@fcache[n] end # Rank the indices of the permutation +p+. Beware that this method may # change its argument +p+. def rank_indices(p) result = 0 for i in 0...size result += p[i] * factorial(size - i - 1) for j in (i + 1)...size p[j] -= 1 if p[j] > p[i] end end result end # Unrank the rank +m+, that is create a permutation of the appropriate size # and rank as an array of indices and return it. def unrank_indices(m) result = Array.new(size, 0) for i in 0...size f = factorial(i) x = m % (f * (i + 1)) m -= x x /= f result[size - i - 1] = x x -= 1 for j in (size - i)...size result[j] += 1 if result[j] > x end end result end end end