Мне нужен лучший алгоритм, чтобы решить эту проблему

Вот вопрос (ссылка: http://opc.iarcs.org.in/index.php/problems/FINDPERM):

Перестановка чисел 1,...,N есть перестановка этих чисел. Например,
2 4 5 1 7 6 3 8
является перестановкой 1,2, ..., 8. Конечно,
1 2 3 4 5 6 7 8
также является перестановкой 1, 2, ..., 8.
С каждой перестановкой N связана специальная последовательность натуральных чисел длины N, называемая инверсионной последовательностью. i-й элемент этой последовательности — это количество чисел j, строго меньших i и стоящих справа от i в этой перестановке. Для перестановки
2 4 5 1 7 6 3 8
последовательность инверсии
0 1 0 2 2 1 2 0
2-й элемент равен 1, потому что 1 строго меньше, чем 2, и он появляется справа от 2 в этой перестановке. Точно так же 5-й элемент равен 2, поскольку 1 и 3 строго меньше 5, но появляются справа от 5 в этой перестановке и т. д.
Другой пример: последовательность инверсии перестановки
8 7 6 5 4 3 2 1
is
0 1 2 3 4 5 6 7
В этой задаче вам будет задана инверсионная последовательность некоторой перестановки. Ваша задача состоит в том, чтобы восстановить перестановку из этой последовательности.

Я придумал этот код:

#include <iostream>

using namespace std;

void insert(int key, int *array, int value , int size){
    int i = 0;
    for(i = 0; i < key; i++){
        int j = size - i;
        array[ j ] = array[ j - 1 ];
    }
    array[ size - i ] = value;
}

int main(){

    int n;
    cin >> n;
    int array[ n ];
    int key;

    for( int i = 0; i < n; i++ ){
        cin >> key;
        insert( key, array, i + 1, i);
    }

    for(int i = 0;i < n;i ++){
        cout << array[i] << " ";
    }

return 0;
} 

Он работает нормально и дает правильный ответ для 70% тестовых случаев, но превышает лимит времени для остальных. Есть ли другой, более быстрый алгоритм для решения этого вопроса?


person 2147483647    schedule 27.10.2012    source источник
comment
Вы можете предположить, что N ≤ 100000.   -  person Ivan Vergiliev    schedule 27.10.2012
comment
Если не требуется использовать С++, в питоне есть наборы, перестановки и перестановки (я думаю, они называются по-другому). Вы также можете создавать объединения пересечений и различия между наборами в python. Если вы никогда не пробовали python, попробуйте, он очень хорош для математики.   -  person jokoon    schedule 27.10.2012


Ответы (4)


Ваш алгоритм имеет сложность O(N^2) операций, поэтому для массивов размером 10^5 требуется слишком много времени для выполнения. Я пытаюсь описать лучшее решение:

У нас есть N номера. Вызовем обратный массив I. Для решения этой задачи нам нужно знать, где находится K-th позиция от конца перестановки, которая еще свободна (назовем эту функцию F(K)). Сначала мы ставим число N на позицию F(I[N] + 1), затем ставим число N-1 на позицию F(I[N-1] + 1) и так далее.

F можно рассчитать следующим образом: объявить массив M размера N: 1 1 1 ... 1, определить S(X) = M[1] + M[2] + ... + M[X]. S называется сумма префикса. F(K) равно N плюс 1 минус такое меньшее X, что S(X) = K. Каждый раз, когда мы помещаем число Z в позицию N + 1 - X(for K = I[Z] + 1), мы прибавляем ноль к M[X]. Чтобы найти X быстрее, чем за O(N) раз, я могу предложить использовать бинарные индексированные деревья для вычисления сумм префиксов за O(logN) времени и бинарный поиск для поиска таких X, которые S(X) равны к некоторому заранее определенному значению.

Общая сложность такого алгоритма O(N(log(N))^2). Это реализация на Ruby (вы можете поиграться с ней прямо в ideone: изменить ввод, запустить и проверить вывод):

# O(n(log(n))^2) solution for http://opc.iarcs.org.in/index.php/problems/FINDPERM

# Binary Indexed Tree (by Peter Fenwick)
# http://community.topcoder.com/tc?module=Static&d1=tutorials&d2=binaryIndexedTrees
class FenwickTree

  # Initialize array 1..n with 0s
  def initialize(n)
    @n = n
    @m = [0] * (n + 1)
  end

  # Add value v to cell i
  def add(i, v)
    while i <= @n
      @m[i] += v
      i += i & -i
    end
  end

  # Get sum on 1..i
  def sum(i)
    s = 0
    while i > 0
      s += @m[i]
      i -= i & -i
    end
    s
  end

  # Array size
  def n
    return @n
  end

end

# Classical binary search
# http://en.wikipedia.org/wiki/Binary_search_algorithm
class BinarySearch

  # Find lower index i such that ft.sum(i) == s
  def self.lower_bound(ft, s)
    l, r = 1, ft.n
    while l < r
      c = (l + r) / 2
      if ft.sum(c) < s
        l = c + 1
      else
        r = c
      end
    end
    l
  end

end

# Read input data
n = gets.to_i
q = gets.split.map &:to_i

# Initialize Fenwick tree
ft = FenwickTree.new(n)
1.upto(n) do |i|
  ft.add i, 1
end

# Find the answer
ans = [0] * n
(n - 1).downto(0) do |i|
  k = BinarySearch.lower_bound(ft, q[i] + 1)
  ans[n - k] = i + 1
  ft.add k, -1
end
puts ans.join(' ')

Также существует решение с временем O(N(log(N))). Он использует своего рода Двоичное дерево поиска: мы создаем BST с номерами 1, 2, 3, ... N на вершинах, затем мы можем найти K-th число по значению в O(log(N)) и также удалить вершины за O(log(N)) время.

Также может существовать решение с std::set, но я не думаю, что оно правильное сейчас.

PS. Я также могу предложить вам прочитать некоторые книги по алгоритмам и олимпиадам, такие как Скиенна (Проблемы программирования) или Кормен (Введение в алгоритмы).

PPS. Извините за неправильное решение, которое я описал ранее.

person 907th    schedule 27.10.2012
comment
для ввода 0 1 0 2 2 1 2 0 ваш алгоритм выводит 0 4 5 2 7 1 3 8 вместо 2 4 5 1 7 6 3 8. Не могли бы вы объяснить, почему? - person 2147483647; 30.10.2012
comment
Извините, мое решение было неправильным... Позвольте мне дать вам другое решение. - person 907th; 30.10.2012
comment
Спасибо! После изменения некоторых частей вашего предыдущего алгоритма он работал, но тогда это также было n ^ 2 раз. - person 2147483647; 31.10.2012
comment
(извините, что беспокою вас еще раз) Ваш алгоритм работает идеально, но все же он такой же медленный, как и мой первоначальный алгоритм, я все еще получаю 70% оценок. - person 2147483647; 06.11.2012
comment
Да, потому что вашей функции get_x требуется ~ N операций для выполнения + вы вызываете ее N раз. Таким образом, результат O (N ^ 2) алгоритма. Но я предлагаю вам использовать двоичный поиск в двоичном индексированном дереве: см. В моем коде выше - k = BinarySearch.lower_bound(ft, q[i] + 1) требуется ~(log(N))^2 операций каждый раз, когда он вызывается. Я также называю это N раз, поэтому общая сложность составляет O (N ((LogN) ^ 2)) - person 907th; 07.11.2012

Очевидно, что самая затратная часть — это смещение до 100 000 элементов в массиве результатов.

Если вы разделите этот массив на несколько кусков, вы можете ускорить его в несколько раз.

Если у вас есть, скажем, 10 фрагментов и вы помните количество элементов для каждого фрагмента, вы выбираете правильный фрагмент для записи в соответствии с ключом, а затем должны сдвигать элементы только для этого фрагмента (до 10 раз меньше).

Новая проблема заключается в том, как добиться хорошего распределения чисел по частям.


Также вы можете использовать связанный список: http://www.cplusplus.com/reference/stl/list/

Это очень эффективно при вставках, но отстой для случайного поиска. Но все же поиск — это просто операция чтения, поэтому поиск x элементов может быть быстрее (IDK), чем сдвиг x элементов в массиве.

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

person Petr ''Bubák'' Šedivý    schedule 27.10.2012
comment
Тоже хороший способ. Вы можете получить решение O (Nsqrt (N)), если будете использовать куски размера sqrt (N)... это называется sqrt-разложением. - person 907th; 27.10.2012

Вот действительно хороший алгоритм вместе с необходимым кодированием на C++:

Задача решается тем, что если на 7 месте стоит 2, то перед постановкой 7 остается два пустых ящика. Итак, если 0 на 8, а 2 на 7, то обратный массив результатов выглядит так: 8 _ _ 7 _ _ _ _.

Теперь выполняется разложение квадратного корня и выполняется вставка:

#include <iostream>
#include <math.h>
using namespace std;

int main()
{
    int n, k = 0, d, r, s, sum, temp, m, diff, check = 1;
    cin >> n;

    d = sqrt(n) + 1;
    int arr[n], result[n], indices[d], arr2[d][d], num = 1;

    for (int i = 0; i < n; i++)
        cin >> arr[i];               //The inversion sequence is accepted.

    for (int i = 0; i < d; i++)
        indices[i] = 0;              //Indices tell where to start counting from in each row.

    for (r = 0; r < d; r++)
    {
        for (s = 0; s < d; s++)
        {
            arr2[r][s] = num;       //Array is filled with 1 to n (after sqrt decomposition).
            num = num + 1;
            if (num == n+1)
            {
                check = 0; break;
            }
        }
        if (check == 0)
            break;
    }

    int l = s;
    while (l >= 0)                  //Non-Zero numbers are shifted to right and 0 placed in
    {                               //empty spaces.
        arr2[r][d-1 - k] = arr2[r][l];
        k = k + 1; l = l - 1;
    }

    k = d-1 - k + 1;
    for (int t = 0; t < k; t++)
        arr2[r][t] = 0;

    indices[r] = indices[r] + k;    //Index of the last row is shifted to first non-zero no.

    for (int i = n-1; i >= 0; i--)
    {
        sum = 0; m = 0;
        while (sum < arr[i] + 1)
        {
            sum = sum + (d - indices[m]); //Empty boxes in each row are counted.
            m = m + 1;
        }

        m = m - 1;
        sum = sum - (d - indices[m]);     //When sum = 1 + corresponding value in inversion
        diff = arr[i] + 1 - sum;          //sequence, then that particular value is made 0
        temp = indices[m] + diff - 1;     //and (that value - 1) is the index of the number
                                      //to be placed in result array.
        result[arr2[m][temp] - 1] = i+1;
        for (int w = temp - 1; w >= indices[m]; w--)
        {
            arr2[m][w + 1] = arr2[m][w];  //Then, 0 is shifted to just before non-zero number
        }                                 //in the row, and the elements are shifted right
        arr2[m][indices[m]] = 0;          //to complete the sort.
        indices[m] = indices[m] + 1;
    }                                     //This is done n times for 1 to n integers thus
                                      //giving the permutation in reverse order in result
    for (int p = n-1; p >= 0; p--)        //array.
        cout << result[p] << ' ';

    return 0;
}
person Somebody    schedule 25.11.2012

Ваш алгоритм не эффективен для этой проблемы, потому что ваша сложность равна O (n ^ 2), что означает 10 ^ 10 операций для некоторых входных случаев. Вы должны придумать решение, которое дешевле.

Предлагаю вам следующий алгоритм (индексы от 1 до N):

for i=1 to N
   input a(i)
   if i==1 insert 1 into b
   else insert i into b at place i-a(i)
end
person orezvani    schedule 27.10.2012
comment
Я предлагаю вам использовать связанный список вместо массива для b - person orezvani; 27.10.2012
comment
Если используется связанный список, не будет ли это создавать алгоритм O (n ^ 2)? - person Dialecticus; 27.10.2012
comment
если вы считаете количество вставок, это O (n). Но если вы подсчитаете количество сравнений, это O (n ^ 2). - person orezvani; 27.10.2012