Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to check if two permutations are symmetric?

Tags:

Given two permutations A and B of L different elements, L is even, let's call these permutations "symmetric" (for a lack of a better term), if there exist n and m, m > n such as (in python notation):

 - A[n:m] == B[L-m:L-n]  - B[n:m] == A[L-m:L-n]  - all other elements are in place 

Informally, consider

A = 0 1 2 3 4 5 6 7 

Take any slice of it, for example 1 2. It starts at the second index and its length is 2. Now take a slice symmetric to it: it ends at the penultimate index and is 2 chars long too, so it's 5 6. Swapping these slices gives

B = 0 5 6 3 4 1 2 7 

Now, A and B are "symmetric" in the above sense (n=1, m=3). On the other hand

A = 0 1 2 3 4 5 6 7 B = 1 0 2 3 4 5 7 6 

are not "symmetric" (no n,m with above properties exist).

How can I write an algorithm in python that finds if two given permutations (=lists) are "symmetric" and if yes, find the n and m? For simplicity, let's consider only even L (because the odd case can be trivially reduced to the even one by eliminating the middle fixed element) and assume correct inputs (set(A)==set(B), len(set(A))==len(A)).

(I have no problem bruteforcing all possible symmetries, but looking for something smarter and faster than that).

Fun fact: the number of symmetric permutations for the given L is a Triangular number.

I use this code to test out your answers.

Bounty update: many excellent answers here. @Jared Goguen's solution appears to be the fastest.

Final timings:

testing 0123456789 L= 10     test_alexis ok in 15.4252s     test_evgeny_kluev_A ok in 30.3875s     test_evgeny_kluev_B ok in 27.1382s     test_evgeny_kluev_C ok in 14.8131s     test_ian ok in 26.8318s     test_jared_goguen ok in 10.0999s     test_jason_herbburn ok in 21.3870s     test_tom_karzes ok in 27.9769s 
like image 492
georg Avatar asked Feb 22 '16 15:02

georg


People also ask

What is a symmetric permutation?

Permutation symmetry is such a discrete symmetry arising as the mathematical basis underlying the statistical behaviour of ensembles of certain types of indistinguishable quantum particle (e.g., fermions and bosons).

How do you know if a list is symmetric?

“A list is symmetric if the first row is the same as the first column, the second row is the same as the second column and so on.

How many even permutations does S4 have?

(4) The 4-cycles are (1 2 3 4), (1 2 4 3), (1 3 2 4), (1 3 4 2), (1 4 2 3), (1 4 3 2). There are six. (5) There are no 5-cycles! (6) We have found 20 permutations of 24 total permutations in S4.

How many cycles in S4?

In what follows, products of cycles are assumed to be disjoint. (a) The possible cycle types of elements in S4 are: identity, 2-cycle, 3-cycle, 4- cycle, a product of two 2-cycles. These have orders 1, 2, 3, 4, 2 respectively, so the possible orders of elements in S4 are 1, 2, 3, 4.


2 Answers

Here is the working solution for the question:

def isSymmetric(A, B):     L = len(A) #assume equivalent to len(B), modifying this would be as simple as checking if len(A) != len(B), return []     la = L//2 # half-list length     Al = A[:la]     Ar = A[la:]     Bl = B[:la]     Br = B[la:]     for i in range(la):         lai = la - i #just to reduce the number of computation we need to perform         for j in range(1, lai + 1):             k = lai - j #same here, reduce computation             if Al[i] != Br[k] or Ar[k] != Bl[i]: #the key for efficient computation is here: do not proceed unnecessarily                  continue             n = i #written only for the sake of clarity. i is n, and we can use i directly             m = i + j             if A[n:m] == B[L-m:L-n] and B[n:m] == A[L-m:L-n]: #possibly symmetric                 if A[0:n] == B[0:n] and A[m:L-m] == B[m:L-m] and A[L-n:] == B[L-n:]:                     return [n, m]     return [] 

As you have mentioned, though the idea looks simple, but it is actually quite a tricky one. Once we see the patterns, however, the implementation is straight-forward.

The central idea of the solution is this single line:

if Al[i] != Br[k] or Ar[k] != Bl[i]: #the key for efficient computation is here: do not proceed unnecessarily 

All other lines are just either direct code translation from the problem statement or optimization made for more efficient computation.


There are few steps involved in order to find the solution:

Firstly, we need to split the each both list Aand list B into two half-lists (called Al, Ar, Bl, and Br). Each half-list would contain half of the members of the original lists:

Al = A[:la] Ar = A[la:] Bl = B[:la] Br = B[la:] 

Secondly, to make the evaluation efficient, the goal here is to find what I would call pivot index to decide whether a position in the list (index) is worth evaluated or not to check if the lists are symmetric. This pivot index is the central idea to find an efficient solution. So I would try to elaborate it quite a bit:

Consider the left half part of the A list, suppose you have a member like this:

Al = [al1, al2, al3, al4, al5, al6] 

We can imagine that there is a corresponding index list for the mentioned list like this

Al  = [al1, al2, al3, al4, al5, al6] iAl = [0,   1,   2,   3,   4,   5  ] #corresponding index list, added for explanation purpose 

(Note: the reason why I mention of imagining a corresponding index list is for ease of explanation purposes)

Likewise, we can imagine that the other three lists may have similar index lists. Let's name them iAr, iBl, and iBr respectively and they are all having identical members with iAl.

It is the index of the lists which would really matter for us to look into - in order to solve the problem.


Here is what I mean: suppose we have two parameters:

  1. index (let's give a variable name i to it, and I would use symbol ^ for current i)
  2. length (let's give a variable name j to it, and I would use symbol == to visually represent its length value)

for each evaluation of the index element in iAl - then each evaluation would mean:

Given an index value i and length value of j in iAl, do something to determine if it is worth to check for symmetric qualifications starting from that index and with that length (Hence the name pivot index come).

Now, let's take example of one evaluation when i = 0 and j = 1. The evaluation can be illustrated as follow:

iAl = [0, 1, 2, 3, 4, 5]        ^ <-- now evaluate this index (i) = 0        == <-- now this has length (j) of 1 

In order for those index i and length j to be worth evaluated further, then the counterpart iBr must have the same item value with the same length but on different index (let's name it index k)

iBr = [0, 1, 2, 3, 4, 5]                       ^ <-- must compare the value in this index to what is pointed by iAl                       == <-- must evaluate with the same length = 1 

For example, for the above case, this is a possible "symmetric" permutation just for the two lists Al-Br (we will consider the other two lists Ar-Bl later):

Al = [0, x, x, x, x, x] #x means don't care for now Br = [x, x, x, x, x, 0] 

At this moment, it is good to note that

It won't worth evaluating further if even the above condition is not true

And this is where you get the algorithm to be more efficient; that is, by selectively evaluating only the few possible cases among all possible cases. And how to find the few possible cases?

By trying to find relationship between indexes and lengths of the four lists. That is, for a given index i and length j in a list (say Al), what must be the index k in the counterpart list (in the case is Br). Length for the counterpart list need not be found because it is the same as in the list (that is j).

Having know that, let's now proceed further to see if we can see more patterns in the evaluation process.


Consider now the effect of length (j). For example, if we are to evaluate from index 0, but the length is 2 then the counterpart list would need to have different index k evaluated than when the length is 1

iAl = [0, 1, 2, 3, 4, 5]        ^ <-- now evaluate this index (i) = 0        ===== <-- now this has length (j) of 2  iBr = [0, 1, 2, 3, 4, 5]                    ^ <-- must compare the value in this index to what is pointed by iAl                    ===== <-- must evaluate with the same length = 2 

Or, for the illustration above, what really matters fox i = 0 and y = 2 is something like this:

# when i = 0 and y = 2 Al = [0, y, x, x, x, x] #x means don't care for now Br = [x, x, x, x, 0, y] #y means to be checked later 

Take a look that the above pattern is a bit different from when i = 0 and y = 1 - the index position for 0 value in the example is shifted:

# when i = 0 and y = 1, k = 5 Al = [0, x, x, x, x, x] #x means don't care for now Br = [x, x, x, x, x, 0]  # when i = 0 and y = 2, k = 4 Al = [0, y, x, x, x, x] #x means don't care for now Br = [x, x, x, x, 0, y] #y means to be checked later 

Thus, length shifts where the index of the counterpart list must be checked. In the first case, when i = 0 and y = 1, then the k = 5. But in the second case, when i = 0 and y = 1, then the k = 4. Thus we found the pivot indexes relationship when we change the length j for a fixed index i (in this case being 0) unto the counterpart list index k.


Now, consider the effects of index i with fixed length j for counterpart list index k. For example, let's fix the length as y = 4, then for index i = 0, we have:

iAl = [0, 1, 2, 3, 4, 5]        ^ <-- now evaluate this index (i) = 0        ========== <-- now this has length (j) of 4  iAl = [0, 1, 2, 3, 4, 5]           ^ <-- now evaluate this index (i) = 1           ========== <-- now this has length (j) of 4  iAl = [0, 1, 2, 3, 4, 5]              ^ <-- now evaluate this index (i) = 2              ========== <-- now this has length (j) of 4  #And no more needed 

In the above example, it can be seen that we need to evaluate 3 possibilities for the given i and j, but if the index i is changed to 1 with the same length j = 4:

iAl = [0, 1, 2, 3, 4, 5]           ^ <-- now evaluate this index (i) = 1           ========== <-- now this has length (j) of 4  iAl = [0, 1, 2, 3, 4, 5]              ^ <-- now evaluate this index (i) = 2              ========== <-- now this has length (j) of 4 

Note that we only need to evaluate 2 possibilities. Thus the increase of index i decreases the number of possible cases to be evaluated!


With all the above patterns found, we almost found all the basis we need to make the algorithm works. But to complete that, we need to find the relationship between indexes which appear in Al-Br pair for a given [i, j] => [k, j] with the indexes in Ar-Bl pair for the same [i, j].

Now, we can actually see that they are simply mirroring the relationship we found in Al-Br pair!

(IMHO, this is really beautiful! and thus I think term "symmetric" permutation is not far from truth)

For example, if we have the following Al-Br pair evaluated with i = 0 and y = 2

Al = [0, y, x, x, x, x] #x means don't care for now Br = [x, x, x, x, 0, y] #y means to be checked later 

Then, to make it symmetric, we must have the corresponding Ar-Bl:

Ar = [x, x, x, x, 3, y] #x means don't care for now Bl = [3, y, x, x, x, x] #y means to be checked later 

The indexing of Al-Br pair is mirroring (or, is symmetric to) the indexing of Ar-Bl pair!


Therefore, combining all the pattern we found above, we now could find the pivot indexes for evaluating Al, Ar, Bl, and Br.

We only need to check the values of the lists in the pivot index first. If the values of the lists in the pivot indexes of Al, Ar, Bl, and Br matches in the evaluation then and only then we need to check for symmetric criteria (thus making the computation efficient!)


Putting up all the knowledge above into code, the following is the resulting for-loop Python code to check for symmetricity:

for i in range(len(Al)): #for every index in the list     lai = la - i #just simplification     for j in range(1, lai + 1): #get the length from 1 to la - i + 1         k = lai - j #get the mirror index         if Al[i] != Br[k] or Ar[k] != Bl[i]: #if the value in the pivot indexes do not match              continue #skip, no need to evaluate         #at this point onwards, then the values in the pivot indexes match         n = i #assign n         m = i + j #assign m         #test if the first two conditions for symmetric are passed         if A[n:m] == B[L-m:L-n] and B[n:m] == A[L-m:L-n]: #possibly symmetric             #if it passes, test the third condition for symmetric, the rests of the elements must stay in its place             if A[0:n] == B[0:n] and A[m:L-m] == B[m:L-m] and A[L-n:] == B[L-n:]:                                    return [n, m] #if all three conditions are passed, symmetric lists are found! return [n, m] immediately!         #passing this but not outside of the loop means          #any of the 3 conditions to find symmetry are failed         #though values in the pivot indexes match, simply continue return [] #nothing can be found - asymmetric lists 

And there go you with the symmetric test!

(OK, this is quite a challenge and it takes quite a while for me to figure out how.)

like image 95
Ian Avatar answered Sep 28 '22 23:09

Ian


I rewrote the code without some of the complexity (and errors).

def test_o_o(a, b):      L = len(a)     H = L//2     n, m = 0, H-1      # find the first difference in the left-side     while n < H:         if a[n] != b[n]: break         n += 1     else: return      # find the last difference in the left-side     while m > -1:         if a[m] != b[m]: break          m -= 1     else: return      # for slicing, we want end_index+1     m += 1      # compare each slice for equality     # order: beginning, block 1, block 2, middle, end     if (a[0:n] == b[0:n] and \         a[n:m] == b[L-m:L-n] and \         b[n:m] == a[L-m:L-n] and \         a[m:L-m] == b[m:L-m] and \         a[L-n:L] == b[L-n:L]):          return n, m 

The implementation is both elegant and efficient.

The break into else: return structures ensure that the function returns at the soonest possible point. They also validate that n and m have been set to valid values, but this does not appear to be necessary when explicitly checking the slices. These lines can be removed with no noticeable impact on the timing.

Explicitly comparing the slices will also short-circuit as soon as one evaluates to False.

Originally, I checked whether a permutation existed by transforming b into a:

b = b[:] b[n:m], b[L-m:L-n] = b[L-m:L-n], b[n:m] if a == b:    return n, m 

But this is slower than explicitly comparing the slices. Let me know if the algorithm doesn't speak for itself and I can offer further explanation (maybe even proof) as to why it works and is minimal.

like image 39
Jared Goguen Avatar answered Sep 28 '22 22:09

Jared Goguen