Quantcast
Channel: CodeSection,代码区,Python开发技术文章_教程 - CodeSec
Viewing all articles
Browse latest Browse all 9596

Andrew Dalke: Faster parity calulation

$
0
0
[previous | next ] /home/ writings / diary / archive / 2016 / 08 /15/fragment_parity_calculation Faster parity calulation

In the previous essay I needed determine the parity of a permutation. I used a Shell sort and counted the number of swaps needed to order the list. The parity is even (or "0") if the number of swaps is even, otherwise it's odd (or "1"). The final code was:

def parity_shell(values): # Simple Shell sort; while O(N^2), we only deal with at most 4 values values = list(values) N = len(values) num_swaps = 0 for i in range(N-1): for j in range(i+1, N): if values[i] > values[j]: values[i], values[j] = values[j], values[i] num_swaps += 1 return num_swaps % 2

I chose this implementation because it's easy to understand, and any failure case is easily found. However, it's not fast.

It's tempting to use a better sort method. The Shell sort takes quadratic time in the number of elements, while others take O(N*ln(N)) time in the asymptotic case.

However, an asymptotic analysis is pointless for this case. The code will only ever receive 3 terms (if there is a chiral hydrogen) or 4 terms because the code will only ever be called for tetrahedral chirality.

Sorting networks

The first time I worked on this problem, I used a sorting networks . A sorting network works on a fixed number of elements. It uses a pre-determined set of pairwise comparisons, each followed by a swap if needed. These are often used where code branches are expensive, like in hardware or on a GPU. A sorting network takes constant time, so can help minimize timing side-channel attacks, where the time to sort may give some insight into what is being sorted.

A general algorithm to find a perfect sorting network for a given value of 'N' element isn't known, though there are non-optimal algorithms like Bose-Nelson and Batcher's odd even mergesort, and optimal solutions are known for up to N=10.

John M. Gamble has a CGI script which will generate a sorting network for a given number of elements and choice of algorithm. For N=4 it generates:

N=4 elements: SWAP(0, 1); SWAP(2, 3); SWAP(0, 2); SWAP(1, 3); SWAP(1, 2);

where the SWAP would modify the elements of an array in-place. Here's one way to turn those instructions into a 4-element sort for python:

def sort4(data): if data[1] < data[0]: # SWAP(0, 1) data[0], data[1] = data[1], data[0] if data[3] < data[2]: # SWAP(2, 3) data[2], data[3] = data[3], data[2] if data[2] < data[0]: # SWAP(0, 2) data[0], data[2] = data[2], data[0] if data[3] < data[1]: # SWAP(1, 3) data[1], data[3] = data[3], data[1] if data[2] < data[1]: # SWAP(1, 2) data[1], data[2] = data[2], data[1]

As a test, I'll sort every permutation of four values and make sure the result is sorted. I could write the test cases out manually, but it's easier to use the " permutations() " function from Python's itertools module, as in this example with 3 values:

>>> list(itertools.permutations([1, 2, 3])) [(1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1)]

Here's the test, which confirms that the function sorts correctly:

> > > for permutation in itertools.permutations([0, 1, 2, 3]): ... permutation = list(permutation) # Convert the tuple to list sort4() can swap elements ... sort4(permutation) ... if permutation != [0, 1, 2, 3]: ... print("ERROR:", permutation) ... >>>

I think it's obvious how to turn this into a parity function by adding a swap counter. If the input array cannot be modified then the parity function need to make a copy of the array first. That's what parity_shell() does.

No need to sort

A sort network will always do D comparisions, but those sorts aren't always needed. The reason is simple - if you think of the network as a decision tree, where each comparison is a branch, then D comparison will always have 2 D leaves. This must be at least as large as N!, where N is the number of elements in the list. But N! for N>2 is not a perfect power of 2, so there will be some unused leaves.

I would like to minimize the number of comparisions. I would also like to not modify the array in-place by actually sorting it.

The key realization is that there's no need to sort in order to determine the parity. For example, if there are only two elements in the list, then the parity is as simple as testing

def two_element_parity(x): assert len(x) == 2 return x[1] > x[0]

The three element parity is a bit harder to do by hand:

def three_element_parity(x): assert len(x) == 3 if x[0] < x[1]: if x[1] < x[2]: return 0 # 1, 2, 3 elif x[0] < x[2]: return 1 # 1, 3, 2 else: return 0 # 2, 3, 1 elif x[0] < x[2]: return 1 # 2, 1, 3 elif x[1] < x[2]: return 0 # 3, 1, 2 else: return 1 # 3, 2, 1

It's complicated enough that it took several attempts before it was correct. I had to fix it using the following test code, which uses parity_shell() as a reference because I'm confident that it gives the correct values. (A useful development technique is to write something that you know works, even if it's slow, so you can use it to test more complicated code which better fits your needs)

The test code is:

def test_three_element_parity(): for x in itertools.permutations([1,2,3]): p1 = parity_shell(x) p2 = three_element_parity(x) if p1 != p2: print("MISMATCH", x, p1, p2) else: print("Match", x, p1, p2)

which gives the output:

>>> test_three_element_parity() Match (1, 2, 3) 0 0 Match (1, 3, 2) 1 1 Match (2, 1, 3) 1 1 Match (2, 3, 1) 0 0 Match (3, 1, 2) 0 0 Match (3, 2, 1) 1 1 A debugging technique

As I said, it took a couple of iterations to get correct code. I wasn't sure sometimes which branch was used to get a 0 or 1. During development I added a second field to each return value, to serve as a tag. The code looked like:

def three_element_parity(x): assert len(x) == 3 if x[0] < x[1]: if x[1] < x[2]: return 0,1 # 1, 2, 3 elif x[0] < x[2]: return 1,2 # 1, 3, 2 else: return 0,3 # 2, 3, 1 … which meant I could see which path

Viewing all articles
Browse latest Browse all 9596

Trending Articles