Алгоритм для создания массива с длиной n и количеством инверсий k за время O (n log n)?

Я пишу алгоритм, который будет возвращать массив с определенной длиной и количеством инверсий (пары чисел, где левое число больше, чем правое). т.е. массив [3, 1, 4, 2] содержит три инверсии (3, 1), (3, 2) и (4, 2). Таким образом, на практике при заданной длине n=3 и количестве инверсий k=3 алгоритм должен генерировать массив [3, 1, 4, 2] (или другой массив, удовлетворяющий этим требованиям).

Поскольку количество инверсий также является количеством перестановок, которые необходимо сделать для сортировки массива в порядке возрастания, я подошел к этой проблеме, создав массив от 1 до n - 1 и используя алгоритм сортировки вставками в обратном порядке, чтобы сделать к обменивается.

Этот подход отлично работает для небольших входных данных, но алгоритм должен быть в состоянии эффективно генерировать массивы до n = 10 ^ 6 и k = n (n-1)/2 и все, что между ними, поэтому алгоритм должен работать в O (n log n) раз вместо O (n ^ 2). Ниже приведен код:

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;
    }
}

И основной класс для тестирования с разными входными массивами:

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]
    }
}

Алгоритм не возвращает точно такие же массивы, как и результаты выборки, но проходит все тесты, кроме тех, где размер входных данных очень велик. Я также пробовал разные варианты сортировки слиянием, поскольку она работает за время O (n log n), но безрезультатно.

Было бы здорово, если бы у вас были идеи. Если вы не знакомы с Java, не имеет значения, псевдокод или любые другие предложения более чем приветствуются!


person Mikael Törnwall    schedule 03.02.2019    source источник
comment
Технически это дублировать. Однако он довольно старый и уже ответил. Кто-нибудь не против сообщить мне, следует ли помечать отвеченный вопрос как дубликат или нет? Я удалю этот комментарий, когда ответят, просто не знаю, у кого спросить!   -  person AviFS    schedule 20.12.2019


Ответы (8)


Если вы перевернете начальные m элементы в массиве, вы создадите инверсию m(m-1)/2.

Если вы перевернете исходные m+1 элементы, вы создадите m(m+1)/2 инверсии.

Разница между ними всего m.

So:

  1. Создать отсортированный массив
  2. Найдите наибольшее m такое, что m(m-1)/2 ‹= k
  3. Переверните первые m элементы в массиве, чтобы создать m(m-1)/2 инверсии.
  4. Сдвиньте следующий элемент вперед на k - m(m-1)/2 позиций, чтобы создать оставшиеся требуемые инверсии.

Это занимает O(n) времени, что лучше, чем вам требуется.

person Matt Timmermans    schedule 03.02.2019
comment
Как это будет работать для длины входного массива = 5 и k = 5? Исходный массив = [0, 1, 2, 3, 4]; Значение наибольшего m = 3; Первая половина инвертирована = [2, 1, 0]; Теперь в оставшемся массиве [3,4] как вы переместите первый элемент на 5-3 = 2 позиции? Сама длина равна всего 2. [4,3] даст еще 1 инверсию. Таким образом, общая инверсия становится равной 4, а не k=5. - person Nazil Khan; 27.06.2020
comment
@Nazil переворачивает первые 3, чтобы получить [2,1,0,3,4] с 3 инверсиями. Затем переместите 3 2 позиции влево, чтобы получить [2,3,1,0,4] с 5 инверсиями. - person Matt Timmermans; 27.06.2020
comment
Спасибо @Мэтт. У меня есть реализация Python, в которой я перемещаю последний элемент массива [2,1,0,3,4] на 2 позиции влево, чтобы получить 5 инверсий. Результат = [2, 1, 4, 0, 3] - person Nazil Khan; 28.06.2020
comment
Какая? (т + 1 - т) = 1, (т + 1 - т + 1) = 2? - person user894319twitter; 11.02.2021
comment
Если вам нужно подробное объяснение формулы: math.stackexchange.com/questions/905700/ - person user894319twitter; 12.02.2021

Еще один алгоритм O(n): начните с отсортированного массива. Когда вы меняете местами первый и последний элементы, вы получаете x = 2 * (n-2) + 1 инверсии. Считайте эти два элемента фиксированными и работайте только с оставшимся массивом. Если x слишком велико, рассмотрите меньший массив. Повторяйте это столько, сколько необходимо.

Непроверенный код:

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
    }
}
person maaartinus    schedule 03.02.2019

Если k ›= n - 1, поставить элемент n - 1 первым в массиве, чтобы он инвертировался с n - 1 элементами; в противном случае поместите его последним в массиве, чтобы он инвертировался с 0 элементами. Продолжайте этот жадный подход, чтобы определить, куда идут остальные элементы.

Вот решение, которое реализует generate() для работы в линейном времени с небольшим количеством математики.

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] + " ");
    }

  }
}
person yliang    schedule 09.08.2020

На самом деле, каждый раз, когда вы заменяете последний элемент на предыдущий, количество инверсий увеличивается. Вот java-решение:

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;
}
person zhong yang    schedule 11.01.2020

Я получил реализацию на Python со сложностью O(n).

Он основан на двух правилах.

  1. Обращение массива размером m дает m*(m-1)/2 инверсий.
  2. Сдвиг элемента на m позиции создает m инверсии.
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)
person Nazil Khan    schedule 28.06.2020

Есть очень простой способ создать n инверсий... То есть переместить последний элемент на передний план. Это не совсем эффективно из-за используемой дополнительной памяти, но я бы сделал что-то вроде этого:

Создайте массив, длина которого в два раза больше n. Заполните его от начала до середины дозорным (т.е. нулем), если мы используем Integer[] вместо int[]. Заполняйте его от середины, по возрастанию. Затем сделайте что-то вроде приведенного ниже... Я уверен, что у меня есть одна ошибка и другие ошибки, но общая идея отражена в приведенном ниже коде.

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--;
    }
}

Итак, у нас есть массив, заполненный начальными значениями, набором нулей, затем исходными добавочными значениями, одно из которых могло стать нулевым, и конечный указатель, указывающий на середину исходной зоны.

Итак, последний шаг — копирование из 0 -> endPos, игнорируя нули, в окончательный массив.

person John    schedule 02.08.2020

Логика не очень сложная. Например, у нас есть 10 чисел [0,1,2,3,4,5,6,7,8,9], скажем, для генерации 18 инверсий. Во-первых, вставьте 9 перед 0, ---›[9,0,1,2,3,4,5,6,7,8], что создаст 9 инверсий. Осталось еще 9 инверсий, поэтому мы вставляем 8 перед 0, ----›[9,8,0,1,2,3,4,5,6,7], так что мы получаем дополнительные 8 инверсий. Наконец, осталась 1 инверсия, вставляем 7 перед 6-----›[9,8,0,1,2,3,4,5,7,6]. Я использую только массивы в этом случае. Эта программа работает со сложностью O(n). Следующий код рассматривает только n чисел (0,1,2.....n-1) и их инверсии.

    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;
    }
}
person Emma    schedule 12.05.2021

@zhong yang: он хорошо работает в ожидаемом диапазоне 0 ‹= k ‹= n(n-1)/2, но лучше выдать либо исключение, либо null, если k выходит за пределы этого диапазона, вместо того, чтобы возвращать какой-то массив!

person Tien Phan    schedule 24.07.2020