简体   繁体   中英

Algorithm to generate an array with n length and k number of inversions in O(n log n) time?

I'm writing an algorithm that will return an array with determined length and number of inversions (number pairs, where the left side number is larger than the right side number). Ie array [3, 1, 4, 2] contains three inversions (3, 1), (3, 2) and (4, 2). So in practice, when given the length of n=3 and number of inversions k=3, the algorithm should generate an array [3, 1, 4, 2] (or another array that fulfills these requirements).

Since the number of inversions is also the number of swaps that has to be made for the array to be sorted in ascending order, I approached this problem by creating an array from 1 to n - 1 and using an insertion sort algorithm in reverse to make k swaps.

This approach works just fine for smaller inputs, but the algorithm should be able to efficiently generate arrays up to n=10^6 and k=n(n-1)/2 and anything in between, so the algorithm should be working in O(n log n) time instead of O(n^2). Below is the code:

import java.util.*;

public class Inversions {

    public int[] generate(int n, long k) {        

        // Don't mind these special cases

        if (n == 1) {

            int[] arr = {1};

            return arr;
        }

        if (k == 0) {

            int[] arr = new int[n];

            for (int i = 0; i < n; i++) {

                arr[i] = 1;                    
            }

            return arr;
        }

        int[] arr = new int[n];

        for (int i = 0; i < n; i++) {

            arr[i] = i + 1;
        } 

        int inversions = 0;
        int i = 0;    

        while (inversions < k && i < n) {                                    

            int j = i - 1;                        

            while (j >= 0 && arr[j] < arr[j + 1] && inversions < k) {

                int helper = arr[j];
                arr[j] = arr[j + 1];
                arr[j + 1] = helper;
                inversions++;
                j--;
            }     

            i++;
        }

        return arr;
    }
}

And the main class for testing with different input arrays:

public class Main {

    public static void main(String[] args) {

        Inversions in = new Inversions();
        int[] arr1 = in.generate(4,3);
        int[] arr2 = in.generate(4,0);
        int[] arr3 = in.generate(4,6);        

        System.out.println(Arrays.toString(arr1)); // [3,1,4,2]
        System.out.println(Arrays.toString(arr2)); // [1,1,1,1]
        System.out.println(Arrays.toString(arr3)); // [4,3,2,1]
    }
}

The algorithm does not return exactly the same arrays as the sample results, but passes all the tests, except the ones where the input size is very large. I have also tried different variations with merge sort, since it's working in O(n log n time) but with no avail.

It would be great if you guys have some ideas. If you are not familiar with Java, doesn't matter, pseudocode or any other kinds of suggestions are more than welcome!

If you reverse the initial m elements in the array, you create m(m-1)/2 inversions.

If you reverse the initial m+1 elements, you create m(m+1)/2 inversions.

The difference between these is only m .

So:

  1. Generate a sorted array
  2. Find the largest m such that m(m-1)/2 <= k
  3. Reverse the first m elements in the array to create m(m-1)/2 inversions
  4. Shift the next element forward k - m(m-1)/2 positions to create the remaining required inversions.

This takes O(n) time, which is better than you require.

Another O(n) algorithm: Start with a sorted array. When you swap the first and last elements, you get x = 2 * (n-2) + 1 inversions. Consider these two elements fixed and work on the remaining array only. If x is too large, consider a smaller array. Repeat this as long as needed.

Untested code:

for (int first=0, last = n-1; remainingInversions>0; ) {
    int x = 2 * (last-first-1) + 1;
    if (x <= remainingInversion) {
        first++;
        last--;
        remainingInversion -= x;
    } else {
        last--; // consider a smaller array
    }
}

If k >= n - 1, put element n - 1 first in the array, so that it is inverted with n - 1 elements; otherwise put it last in the array, so that it is inverted with 0 elements. Continue this greedy approach to determine where the rest of the elements go.

Here's a solution that implements generate() to run in linear time with a little bit of math.

public class Inversions {

  public static int[] generate(int n, long k) {

    int[] array = new int[n];
    
    // locate k in various sums of (n-1), (n-2), ..., 1
    int a = (int) Math.sqrt((n * (n - 1) - 2 * k)); // between the sum of [(n-1)+...+(n-a)] and the sum of [(n-1)+...+(n-a-1)]
    int b = n - 1 - a; // counts of (n-1), (n-2), ..., (n-a)
    int c = (int) (k - n * b + (b * b + b) / 2); // spillover = k - [(n-1)+(n-b)]*b/2;

    // put elements in the array
    for (int i = 0; i < b; i++) {
        array[i] = n - 1 - i;
    }
    for (int i = b; i < n - 1 - c; i++) {
        array[i] = i - b;
    }
    array[n - 1 - c] = n - 1 - b;
    for (int i = n - c; i < n; i++) {
        array[i] = i - b - 1;
    }

    return array;

  }

  public static void main(String[] args) {

    int n = Integer.parseInt(args[0]);
    long k = Long.parseLong(args[1]);

    for (int i = 0; i < n; i++) {
        StdOut.print(generate(n, k)[i] + " ");
    }

  }
}

In fact, every time you exchange the last element with the one before it, the number of inversions increments. Here is a java solution:

public static int[] generate(int n, long k) {
    int[] arr = new int[n];

    for(int i = 0; i < n; i++) {
        arr[i] = i;
    }

    long inversions = 0;
    int j = (n-1);
    int s = 0;

    while(inversions < k) {
        int temp = arr[j];
        arr[j] = arr[j-1];
        arr[j-1] = temp;

        inversions++;
        j--;
        if(j == s) {
            j = (n-1);
            s++;
        }
    }

    return arr;
}

I got an implementation in Python with O(n) complexity.

It is based on two rules.

  1. Reversing an array of size m gives m*(m-1)/2 inversions.
  2. Shifting an element by m positions, creates m inversions.
def get_m(k):
    m=0
    while m*(m-1)/2<=k:
        m+=1
    else:
        m-=1
    return m

def generate(l, k):
    """
    Generate array of length l with k inversions.
    """
    # Generate a sorted array of length l
    arr = list(range(0,l))
    
    # If no inversions are needed, return sorted array. 
    if k==0:
        return arr
    
    # Find largest m such that m*(m-1)/2 <= k
    m=get_m(k)

    # Reverse first m elements in the array which will give m*(m-1)/2 inversions
    arr = arr[m-1::-1]+arr[m:]

    # Calculate for any remaining inversions
    remaining_k = k-(m*(m-1)/2)

    # For remaining inversions, move the last element to its left by remaining_k
    if remaining_k>0:
        arr.insert(int(len(arr)-remaining_k - 1), arr[-1])
        arr = arr[:-1]
    return arr

if __name__ == '__main__':
    l = int(sys.argv[1])
    k = int(sys.argv[2])
    arr = generate(l, k)
    print(arr)

There's a very easy way to create n inversions... That is to move the last element to the front. It's not exactly efficient due to the additional memory used, but I would do something like this:

Create an array that is twice the length n. Fill it from the start to the middle with a sentinel (ie null) if we use an Integer[] instead of int[]. Fill it from the middle, ascending. Then do something like the below... I'm sure I have off by one errors and other bugs but the general idea is captured in the below code.

int start = 0;
int mid = arr.length / 2;
int end = arr.length - 1;

while (v > 0)
{
    if (v < (end - mid))
    {
        arr[start++] = arr[mid + v];
        arr[mid + v] = null;
    }
    else
    {
        arr[start++] = arr[end];
        v -= (end - mid);
        end--;
    }
}

So we have an array filled with the starting values, a bunch of nulls, then the original incremental values, with one that may have become null, and an "end" pointer that points to the middle of the original zone.

So the final step is to copy from 0 -> endPos, ignoring the nulls, to the final array.

The logic is not much difficult. For example, we have 10 numbers [0,1,2,3,4,5,6,7,8,9] say, to generate like 18 inversions. Firstly, insert 9 before 0, --->[9,0,1,2,3,4,5,6,7,8], which generates 9 inversions. Still 9 inversions left, so we insert 8 before 0, ---->[9,8,0,1,2,3,4,5,6,7], so we get additional 8 inversions. Finally, 1 inversions left, we insert 7 before 6----->[9,8,0,1,2,3,4,5,7,6]. I only use arrays in this case. This program works in O(n) complexity. The following code only considering n numbers (0,1,2.....n-1) and their inversions.

    public static int[] generate(int n, long k) {
    int[] a = new int[n];
    int[] b = new int[n];
    for (int i = 1; i < n; i++) {
        a[i] = 1 + a[i - 1];
    }
    if (n == 0 || k == 0) return a;
    else {
        int i = 0;
        while (k > 0) {
            if (k > n - i - 1) {
                b[i] = a[n - 1 - i];
            }
            else {
                //auxilary array c to store value 
                int[] c = new int[(int) (k + 1)];
                for (int j = i; j < n - 1 - k; j++) {
                    b[j] = j - i;
                }
                for (int j = (int) (n - 1 - k); j < n; j++) {
                    c[j - (int) (n - 1 - k)] = j - i;
                }
                b[(int) (n - 1 - k)] = c[(int) k];
                for (int j = (int) (n - k); j < n; j++) {
                    b[j] = c[j - (int) (n - k)];
                }
                break;
            }
            k = k - (n - 1 - i);
            i++;

        }
        return b;
    }
}

@zhong yang:它在预期范围 0 <= k <= n(n-1)/2 内运行良好,但如果 k 超出此范围,则最好抛出异常或null而不是返回一些数组!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM