3

Here are two related SO questions 1 2 that helped me formulate my preliminary solution.

The reason for wanting to do this is to feed permutations by edit distance into a Damerau-Levenshtein NFA; the number of permutations grows fast, so it's a good idea to delay (N-C) cycle N permutations candidates until (N-C) iterations of the NFA.

I've only studied engineering math up to Differential Equations and Discrete Mathematics, so I lack the foundation to approach this task from a formal perspective. If anyone can provide reference materials to help me understand this problem properly, I would appreciate that!

Through brief empirical analysis, I've noticed that I can generate the swaps for all C cycle N permutations with this procedure:

  1. Generate all 2-combinations of N elements (combs)
  2. Subdivide combs into arrays where the smallest element of each 2-combination is the same (ncombs)
  3. Generate the cartesian products of the (N-C)-combinations of ncombs (pcombs)
  4. Sum pcombs to get a list of the swaps that will generate all C cycle N permutations (swaps)

The code is here.

My Python is a bit rusty, so helpful advice about the code is appreciated (I have the feeling that lines 17, 20, and 21 should be combined. I'm not sure if I should be making lists of the results of itertools.(combinations|product). I don't know why line 10 can't be ncombs += ... instead of ncombs.append(...)).

My primary question is how to solve this question properly. I did the rounds on my own due diligence by finding a solution, but I am sure there's a better way. I've also only verified my solution for N=3 and N=4, is it really correct?

The ideal solution would be functionally identical to heap's algorithm, except it would generate the permutations in decreasing cycle order (by the minimum number of swaps to generate the permutation, increasing).

okovko
  • 1,851
  • 14
  • 27
  • Not sure I follow. What does "by decreasing cycles" mean? – גלעד ברקן Jul 31 '19 at 03:45
  • @גלעדברקן Check out the code, run it, and look at the output. Should be apparent. – okovko Jul 31 '19 at 06:47
  • I did. Results were not apparent so I asked the question. – גלעד ברקן Jul 31 '19 at 08:56
  • @גלעדברקן Read [this](http://mathworld.wolfram.com/PermutationCycle.html), and also look at the SO questions I linked. – okovko Jul 31 '19 at 09:00
  • That seems to explain permutation cycles. I could not find the word "decrease"/ "decreasing" there, although there was a list of permutations ordered by "lowest canonical" order. – גלעד ברקן Jul 31 '19 at 09:13
  • @גלעדברקן Well in the code/output you can see that the output is per cycles (ctrl-f "cycles: "). You could generate the cycles in decreasing order (from N to 1) with this implementation, but it's far from optimal (which matters a lot for a permutation algorithm). What would be preferable is a solution like Heap's permutation algorithm, where each permutation is generated from the previous permutation, such that all N cycle permutations are generated first, then the N-1, and so on. – okovko Aug 02 '19 at 02:07
  • Do you mean you'd like to generate all permutations with N cycles first, followed by all permutations with (N-1) cycles, then all permutations with (N-2) cycles...etc.? What about the order within each cycle-cardinality group of permutations, does that order matter as well? – גלעד ברקן Aug 02 '19 at 02:13
  • @גלעדברקן Yes, that is exactly what I mean. More specifically: there is some operation that takes an N cycle permutation and generates a unique N cycle permutation (every permutation will be generated) for all but one case (for a given N), where it instead generates an N-1 cycle permutation. I don't know about and am not interested in the order within the cycle-cardinality group of permutations, but it sounds like an interesting topic for further research. – okovko Aug 02 '19 at 02:17
  • Forgot one more thing - what about the distribution of cardinalities between the cycles (e.g., (1) (2 3 4) vs (1 2) (3 4)). Is any ordering there important? – גלעד ברקן Aug 02 '19 at 02:34
  • @גלעדברקן No, no difference between (1)(2 3 4) and (1 2)(3 4). Merely the count of cycles. It so happens that size less cycles gives the minimum swaps necessary to generate the permutation from the sorted form, and that is what I'm directly interested in. – okovko Aug 02 '19 at 02:36

1 Answers1

1

This is far from Heap's efficiency, but it does produce only the necessary cycle combinations restricted by the desired number of cycles, k, in the permutation. We use the partitions of k to create all combinations of cycles for each partition. Enumerating the actual permutations is just a cartesian product of applying each cycle n-1 times, where n is the cycle length.

Recursive Python 3 code:

from math import ceil

def partitions(N, K, high=float('inf')):
  if K == 1:
    return [[N]]
  result = []
  low = ceil(N / K)
  high = min(high, N-K+1)
  for k in range(high, low - 1, -1):
    for sfx in partitions(N-k, K - 1, k):
      result.append([k] + sfx)
  return result

print("partitions(10, 3):\n%s\n" % partitions(10, 3))

def combs(ns, subs):
  def g(i, _subs):
    if i == len(ns):
      return [tuple(tuple(x) for x in _subs)]

    res = []
    cardinalities = set()

    def h(j):
      temp = [x[:] for x in _subs]
      temp[j].append(ns[i])
      res.extend(g(i + 1, temp))

    for j in range(len(subs)):
      if not _subs[j] and not subs[j] in cardinalities:
        h(j)
        cardinalities.add(subs[j])

      elif _subs[j] and len(_subs[j]) < subs[j]:
        h(j)

    return res

  _subs = [[] for x in subs]

  return g(0, _subs)

A = [1,2,3,4]
ns = [2, 2]
print("combs(%s, %s):\n%s\n" % (A, ns, combs(A, ns)))
A = [0,1,2,3,4,5,6,7,8,9,10,11]
ns = [3, 3, 3, 3]
print("num combs(%s, %s):\n%s\n" % (A, ns, len(combs(A, ns))))

def apply_cycle(A, cycle):
  n = len(cycle)
  last = A[ cycle[n-1] ]
  for i in range(n-1, 0, -1):
    A[ cycle[i] ] = A[ cycle[i-1] ]
  A[ cycle[0] ] = last

def permutations_by_cycle_count(n, num_cycles):
  arr = [x for x in range(n)]

  cycle_combs = []
  for partition in partitions(n, num_cycles):
    cycle_combs.extend(combs(arr, partition))

  result = {}

  def f(A, cycle_comb, i):
    if i == len(cycle_comb):
      result[cycle_comb].append(A)
      return

    if len(cycle_comb[i]) == 1:
      f(A[:], cycle_comb, i+1)

    for k in range(1, len(cycle_comb[i])):
      apply_cycle(A, cycle_comb[i])
      f(A[:], cycle_comb, i+1)

    apply_cycle(A, cycle_comb[i])

  for cycle_comb in cycle_combs:
    result[cycle_comb] = []
    f(arr, cycle_comb, 0)

  return result

result = permutations_by_cycle_count(4, 2)

print("permutations_by_cycle_count(4, 2):\n")

for e in result:
  print("%s: %s\n" % (e, result[e]))

Output:

partitions(10, 3):
[[8, 1, 1], [7, 2, 1], [6, 3, 1], [6, 2, 2], [5, 4, 1], [5, 3, 2], [4, 4, 2], [4, 3, 3]]

# These are the cycle combinations
combs([1, 2, 3, 4], [2, 2]):
[((1, 2), (3, 4)), ((1, 3), (2, 4)), ((1, 4), (2, 3))]

num combs([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11], [3, 3, 3, 3]):
15400

permutations_by_cycle_count(4, 2):

((0, 1, 2), (3,)): [[2, 0, 1, 3], [1, 2, 0, 3]]

((0, 1, 3), (2,)): [[3, 0, 2, 1], [1, 3, 2, 0]]

((0, 2, 3), (1,)): [[3, 1, 0, 2], [2, 1, 3, 0]]

((1, 2, 3), (0,)): [[0, 3, 1, 2], [0, 2, 3, 1]]

((0, 1), (2, 3)): [[1, 0, 3, 2]]

((0, 2), (1, 3)): [[2, 3, 0, 1]]

((0, 3), (1, 2)): [[3, 2, 1, 0]]
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
  • Kind of hard to follow your code. Would be much easier to read if you used a functional programming style instead of ad hoc implementations of conceptual ideas like the generation of combinations and cartesian products. Particularly I don't understand why you didn't use `product` and `combinations` from `itertools`, which would significantly lessen the LOC burden on the reader of your solution. And closures incur an intellectual burden. – okovko Aug 12 '19 at 02:31
  • @okovko I would be happy to consider the itertools methods if you could help explain. I do not know how to use them to create all divisions of a set over a custom partition (list of cardinalities) as my `combs` function does. And I don't know how to use them to apply a function as a modifier inside a conceptual cartesian product as my `f` function is doing when it applies the permutation cycles one by one. – גלעד ברקן Aug 12 '19 at 03:05
  • [Here](https://repl.it/repls/WingedOccasionalFields). That's how to get `combs` using a simple non-recursive approach using standard functions. – okovko Aug 12 '19 at 07:46
  • @okovko `cycles` in the [repl you linked to](https://repl.it/repls/WingedOccasionalFields) seems to contain both `[(4,5), (3,), (1, 2)]` and `[(1, 2), (3,), (4, 5)]`, which I would consider duplicates. – גלעד ברקן Aug 12 '19 at 14:31
  • [Fixed](https://repl.it/repls/SociableHauntingSystemsanalysis). There was p! repeated counting for (n, p) partitions. Took some investigation, but I did find an algorithm to generate combinations with groups (as opposed to combinations of combinations). – okovko Aug 18 '19 at 13:08
  • Of course, it was always possible to simply filter the duplicates, which would still be shorter, simpler, faster, and more legible than your solution. But you piqued my curiosity about generating combinations with groups directly, so I solved it directly. – okovko Aug 18 '19 at 13:17
  • @okovko I'm not sure what you're showing me there. I see groupings of [1..7] into parts of sizes 1, 3 and 3; where the part with only one element seems to always have the number 1 in it. My `combs` function takes a list of elements and a list of cardinalities, and returns all unique ways of dividing the list of elements into groups with the listed cardinalities, where element order does not matter. `combs([1...7], [1,3,3])` would return significantly more than 10 groupings. – גלעד ברקן Aug 18 '19 at 13:22
  • I'm still chewing on how to generate the partitions elegantly. With that taken care of, and with `combinations_with_groups`, the direct solution is significantly improved, but only if cycles with specific cardinalities are desired (not my original question, but still interesting). My original solution is still preferable. However, the optimal solution is based on taking successive inversions of cycles to generate them one at a time, directly analogous to heap's algorithm. Or something like that, haven't really taken the time on that yet. – okovko Aug 18 '19 at 13:25
  • Yeah, there's 70. Needs a special case for (1, p) partitions, but working for n > 1, which is the tricky case. – okovko Aug 18 '19 at 13:30
  • The same link gives 70 solutions now, just added an if else branch (in other words, it is an alternative implementation of your `combs`). Begging for a generator implementation, though. I'll probably get around to it later. And there should definitely be a simpler way to generate cycles from prods. – okovko Aug 18 '19 at 13:40
  • @okovko I'm getting inconsistent results with your function (see the result lengths here: http://ideone.com/FUnIEP). Am I using it right? – גלעד ברקן Aug 18 '19 at 17:28
  • Yes, the (n, 1) case is broken (simple fix), and so is the (1, p > 1) case (not so simple to fix). – okovko Aug 19 '19 at 02:16
  • @okovko I'm not sure what `(n, 1)` and `(1, p)` mean in terms of the parameters passed to the function but I also got inconsistent results with parameters that did not include a `1`, such as `combinations_with_partitions([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], ((2, 2), (3, 2)))` – גלעד ברקן Aug 19 '19 at 08:23
  • Yes, I wrote some tests and I'm seeing a lot of other broken cases. Fixed (n > 1, p = 1) and (n = 1, p > 1), but now I see that (n > 1, p > 1) only works when it is the final item. So ((2,1), (3,2)) and ((3,1),(2,2)) both work, but ((2,2), (3,2)) breaks. This is getting way more fun than I intended.. – okovko Aug 19 '19 at 10:23