Guslix's blog

By Guslix, history, 12 months ago, In English

To get the next permutation of array of its subsegment there is a simple and very old Narayana's algorithm. This algorithm invented by Indian mathematician Pandit Narayana more than 600 years ago.

Given array $$$a$$$. How to go to its next permutation?

  1. Find the longest non-increasing suffix of the array (LNIS). If whole array is non-increasing, permutations are exhausted.

  2. Let LNIS be a suffix $$$k$$$, where $$$k$$$ is position where it begins. Swap previous element, $$$a_{k-1}$$$ and the next largest number (NLN) that exists in suffix $$$k$$$.

  3. Reverse suffix $$$k$$$.

Time complexity: $$$O(n)$$$ in the worst case. But on average length LNIS is about 3 elements, so iteration over all permutations is doing for $$$O(n!)$$$, thus $$$O(1)$$$ for one permutation.

Although suddenly there is such a problem. Given an array of 100500 elements and 100500 queries to get next or previous permutation. If the array is sorted and first query is prev permutation on whole array, second query is next permutation on whole array, and further this loop is repeating; you must to walk over all array, answer each query for $$$O(n)$$$.

This algorithm can be implemented for $$$O(\log n)$$$ on a treap. Reversal is known, and swapping is just a combination of split and merge operations. Also we need to find LNIS and NLN. LNIS is sorted by definition, so we can apply binary search in order to find NLN of $$$a_{k-1}$$$.

Position $$$k$$$ of LNIS is found a recursive algorithm too. Let we in subtree $$$t$$$ now. First, find LNIS of right subtree of $$$t$$$. If the right subtree is not sorted in non-increasing order, we don't heed visit left subtree of $$$t$$$.

Else we should to compare value of root of $$$t$$$ with the first element of right subtree and the last element of left subtree. If still keeps a non-increasing order, continue the search in left subtree. To avoid additional costs, we need to maintain first and last elements and non-increasing order flag for each subtree and recalc it after each operation.

To get the previous permutation there's similar algorithm. Here is non-decreasing order instead of non-increasing one and next smallest element instead next largest one.

The Code

Time complexity: $$$O(\log n)$$$

Struct node
Lazy propagation of reverse
Recalc of size and other node parameters
Split
Merge
Build a treap from array
Find k, the position of start LNIS
Find the position of NLN
void next_p(int L, int R, treap t=root){
    treap l, r, swap1, swap2, between;
    split(t, t, r, R+1, 0);
    int k = find_k(t, R)+1; //find LNIS
    split(t, l, t, max(L,k), 0); //separate LNIS
    t->reversed ^= 1;
    if(k>L) { //if the subsegment isn't in non-increasing order
        split(l, l, swap1, k-1, 0); //separate a_(k-1)
        int nln = find_nln(t, swap1->val, k);
        split(t, between, t, nln, k); split(t, swap2, t, nln+1, nln); //separate NLN
        merge(t, swap1, t); merge(t, between, t); merge(t, swap2, t);
    }
    merge(t, l, t); merge(t, t, r);
}
 
 
 
 
  • Vote: I like it
  • +57
  • Vote: I do not like it

»
12 months ago, # |
  Vote: I like it 0 Vote: I do not like it

The code has been changed and bugs are fixed

»
12 months ago, # |
  Vote: I like it +3 Vote: I do not like it

I think there is also a simpler approach. Observe that queries really just add +1 or -1 to a counter (how far are we from the initial permutation). Therefore, we can compute the total balance added to the counter as a number; e.g. +23, or -13, or something like that, and then just use the classical algorithm to go forward 23 times or backward 13 times. It looks like amortized complexity is linear (although I haven't checked very carefully). However, this only solves "finding the last permutation after all operations are computed". Printing all of them along the way is too print-heavy, so let's say queries are of the form "what is the i-th position of the permutation after j steps" to make it manageable. This can also be solved offline by slightly-tweaking the approach above.

The more interesting idea is solving everything online: one can cache all permutations obtained using the above-defined balance as a key. To make storing the cache manageable I think you can represent permutations as a trie/prefix tree, where leaves represent permutations and edges are elements of the permutation. To reverse a suffix of a permutation (leaf) just walk the trie up to the reversal point and create a new chain with the new suffix in the trie starting there, essentially "sharing the common prefix". This makes space of the same order as time complexity and this idea might have some other usages?

  • »
    »
    12 months ago, # ^ |
      Vote: I like it 0 Vote: I do not like it

    Very interesting and advanced ideas. Of course, no need to explicitly make each permutation if isn't requiring to answer queries online.

    Also my algorithm can support on each subsegment: sum, minimum, addition, assignment, etc.

    Also it can overpower and such thing. Insert and delete many elements at once. Let be such queries among 100500 ones, and insert query makes many element with the same value. Also it's guaranteed that size of the array never don't exceed $$$k \cdot 10^5$$$. Let's create a "trash bucket" treap and move deleted nodes there. While insert query, saw off from the "trash bucket" a subtree of desired size, assign it with new value and merge into main treap in desired place