Алгоритм на месте для перемежения массива


62

Вам дан массив из 2n элементов

a1,a2,,an,b1,b2,bn

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

b1,a1,b2,a2,,bn,an

Если бы требования на месте не было, мы могли бы легко создать новый массив и скопировать элементы, используя алгоритм времени О(N) .

При наличии требования на месте алгоритм «разделяй и властвуй» увеличивает алгоритм до θ(NжурналN) .

Итак, вопрос:

Существует ли алгоритм времени О(N) , который также используется?

(Примечание: вы можете принять модель оперативной памяти WORD с одинаковой стоимостью, поэтому на месте это означает ограничение пространства О(1) ).


1
Это на стеке потока, но они не дают качественного решения. Самый популярный ответ: «Эта проблема не так тривиальна, как об этом думают люди. Домашняя работа? LOL. Есть решение для arXiv ». Но решение arxiv требует некоторой теории чисел + ссылки на доказательства в других статьях. Было бы неплохо иметь краткое решение здесь.
Джо


Еще одна тема о переполнении стека: stackoverflow.com/questions/15996288/…
Наюки,

Ответы:


43

Вот ответ, который детализирует алгоритм из статьи, на которую ссылается Джо: http://arxiv.org/abs/0805.1598

Сначала рассмотрим Θ(NжурналN) алгоритм , который использует разделяй и властвуй.

1) Разделяй и властвуй

Нам дают

a1,a2,...,б1,б2,...бN

Теперь, чтобы использовать разделяй и властвуй, для некоторого мзнак равноΘ(N) мы пытаемся получить массив

[a1,a2,...,aм,б1,б2,...,бм],[aм+1,...,aN,бм+1,...бN]

и рекурсировать.

Обратите внимание, что часть

б1,б2,...бм,aм+1,...aN
является циклическим сдвигом

aм+1,...aN,б1,...бм

по м местам.

Это классика, и ее можно сделать на месте тремя разворотами и за О(N) раз.

Таким образом, «разделяй и властвуй» дает вам алгоритм Θ(NжурналN) с рекурсией, аналогичной T(N)знак равно2T(N/2)+Θ(N) .

2) Перестановочные циклы

Теперь другим подходом к проблеме является рассмотрение перестановки как множества непересекающихся циклов.

Перестановка дается (при условии, начиная с 1 )

J2Jмодификация2N+1

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

Это дало бы нам алгоритм времени , но предполагает, что мы «каким-то образом знали, какими были точные циклы», и пытались вести этот бухгалтерский учет в пределах ограничения пространства это то, что делает эту проблему сложной.О(N)О(1)

Здесь статья использует теорию чисел.

Можно показать, что в случае, когда , элементы в позициях , находятся в разных циклах, и каждый цикл содержит элемент в позиции .2N+1знак равно3К13,32,...,3К-13м,м0

При этом используется тот факт, что является генератором .2(Z/3К)*

Таким образом, когда , подход, основанный на цикле, дает нам алгоритм времени, поскольку для каждого цикла мы точно знаем, с чего начать: степени (включая ) (те, может быть вычислено в пространстве ).2N+1знак равно3КО(N)31О(1)

3) Конечный алгоритм

Теперь мы объединяем два вышеупомянутых: делить и завоевывать + циклы перестановок.

Делаем и разделяем, но выбираем так, чтобы была степенью и .м2м+13мзнак равноΘ(N)

Таким образом, вместо рекурсии на обеих «половинках» мы рекурсируем только на одной и делаем дополнительную работу.Θ(N)

Это дает нам рекуррентность (для некоторого ) и, таким образом, дает нам время, космический алгоритм!T(N)знак равноT(сN)+Θ(N)0<с<1О(N)О(1)


4
Это красиво.
Рафаэль

1
Очень хорошо. Проходя примеры перестановок, я теперь понимаю большинство из них. Два вопроса: 1. Как вы на самом деле находите значение m? Бумага утверждает, что это занимает O (журнал N), почему? 2. Можно ли DE-чередовать массив, используя аналогичный подход?
num3ric

2
@ num3ric: 1) Вы найдете самую высокую степень которая составляет < n . Так что это будет O ( log n ) . 2). Да, возможно, я думаю, что где-то добавил ответ на stackoverflow. Лидеры цикла в этом случае, я полагаю, оказались за 2 a 3 b (для 2 m + 1 = степень 3 ). 3<NО(журналN)2a3б2м+13
Арьябхата

@ Арьябхата, почему мы используем только одну «половину» вместо двух «половин»?
sinoTrinity

1
@Aryabhata Может ли этот алгоритм быть расширен для чередования более двух массивов? Например, превратить в c 1 , b 1 , a 1 , c 2 , b 2 , 2 , ... ,a1,a2,...,aN,б1,б2,...,бN,с1,с2,...,сN или что-то подобное. с1,б1,a1,с2,б2,a2,...,сN,бN,aN
Суббота,

18

Я почти уверен, что нашел алгоритм, который не опирается на теорию чисел или теорию циклов. Обратите внимание, что есть некоторые детали, которые нужно проработать (возможно, завтра), но я вполне уверен, что они сработают. Я не сплю, потому что я пытаюсь скрыть проблемы :)

Позвольте Aбыть первый массив, Bвторой, |A| = |B| = Nи предположим N=2^kдля некоторых k, для простоты. Позвольте A[i..j]быть подмассива Aс индексами iдо j, включительно. Массивы основаны на 0. Давайте RightmostBitPos(i)вернем (основанную на 0) позицию самого правого бита, который равен '1' i, считая справа. Алгоритм работает следующим образом.

GetIndex(i) {
    int rightPos = RightmostBitPos(i) + 1;
    return i >> rightPos;
}

Interleave(A, B, N) {
    if (n == 1) {
        swap(a[0], b[0]);
    }
    else {
        for (i = 0; i < N; i++)
            swap(A[i], B[GetIndex(i+1)]);

        for (i = 1; i <= N/2; i*=2)
            Interleave(B[0..i/2-1], B[i/2..i-1], i/2);

        Interleave(B[0..N/2], B[N/2+1..N], n/2);
    }
}

Давайте возьмем массив из 16 чисел, и давайте просто начнем чередовать их, используя свопы, и посмотрим, что произойдет:

1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16

Особый интерес представляет первая часть второго массива:

|
| 1
| 2
| 2 3
| 4 3
| 4 3 5
| 4 6 5
| 4 6 5 7
| 8 6 5 7

Шаблон должен быть четким: мы поочередно добавляем число в конец и заменяем наименьшее число большим. Обратите внимание, что мы всегда добавляем число, которое на единицу больше, чем наибольшее число, которое у нас уже есть. Если бы мы каким-то образом смогли точно определить, какое число является самым низким в любой момент времени, мы могли бы сделать это легко.

Теперь перейдем к более крупным примерам, чтобы увидеть, можем ли мы увидеть шаблон. Обратите внимание, что нам не нужно фиксировать размер массива для построения приведенного выше примера. В какой-то момент мы получаем эту конфигурацию (вторая строка вычитает 16 из каждого числа):

16 24 20 28 18 22 26 30 17 19 21 23 25 27 29 31
0   8  4 12  2  6 10 14  1  3  5  7  9 11 13 15

Теперь это ясно показывает схему: «1 3 5 7 9 11 13 15» - все 2, «2 6 10 14» - все 4, а «4 12» - 8. Поэтому мы можем разработать алгоритм, который говорит нам, каким будет следующее наименьшее число: механизм в значительной степени точно работает как двоичные числа. У вас есть бит для последней половины массива, бит для второй четверти и так далее.

журналNжурналNО(1)

О(N)О(N)

О(N)О(журналN)О(1)

Теперь вопрос: есть ли какой-то шаблон в той части, которую нам нужно отсортировать? Попытка 32 чисел дает нам «16 12 10 14 9 11 13 15» для исправления. Обратите внимание, что у нас точно такая же схема! «9 11 13 15», «10 14» и «12» сгруппированы таким же образом, как мы видели ранее.

Теперь дело в том, чтобы рекурсивно чередовать эти части. Мы чередуем «16» и «12» в «12 16». Мы чередуем «12 16» и «10 14» в «10 12 14 16». Мы чередуем «10 12 14 16» и «9 11 13 15» в «9 10 11 12 13 14 15 16». Это сортирует первую часть.

О(N)О(N)

Пример:

Interleave the first half:
1 2 3 4 5 6 7 8    | 9 10 11 12 13 14 15 16
9 2 3 4 5 6 7 8    | 1 10 11 12 13 14 15 16
9 1 3 4 5 6 7 8    | 2 10 11 12 13 14 15 16
9 1 10 4 5 6 7 8   | 2 3 11 12 13 14 15 16
9 1 10 2 5 6 7 8   | 4 3 11 12 13 14 15 16
9 1 10 2 11 6 7 8  | 4 3 5 12 13 14 15 16
9 1 10 2 11 3 7 8  | 4 6 5 12 13 14 15 16
9 1 10 2 11 3 12 8 | 4 6 5 7 13 14 15 16
9 1 10 2 11 3 12 4 | 8 6 5 7 13 14 15 16
Sort out the first part of the second array (recursion not explicit):
8 6 5 7 13 14 15 16
6 8 5 7 13 14 15 16
5 8 6 7 13 14 15 16
5 6 8 7 13 14 15 16
5 6 7 8 13 14 15 16
Interleave again:
5 6 7 8   | 13 14 15 16
13 6 7 8  | 5 14 15 16
13 5 7 8  | 6 14 15 16
13 5 14 8 | 6 7 15 16
13 5 14 6 | 8 7 15 16
Sort out the first part of the second array:
8 7 15 16
7 8 15 16
Interleave again:
7 8 | 15 16
15 8 | 7 16
15 7 | 8 16
Interleave again:
8 16
16 8
Merge all the above:
9 1 10 2 11 3 12 4 | 13 5 14 6 | 15 7 | 16 8

Интересно. Хотели бы вы написать официальное доказательство? Я знаю, что существует другой алгоритм (упомянутый в статье Джо), который работает с битами. Возможно, вы открыли это заново!
Арьябхата

1

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

Общая идея проста: пройтись по первой половине массива слева направо, поменяв правильные значения. По мере продвижения все еще используемые левые значения меняются местами, освобожденными правыми значениями. Единственный трюк - выяснить, как вытащить их снова.

Мы начнем с массива размера N, разделенного на 2 почти равные половины.
[ left_items | right_items ]
Поскольку мы обрабатываем это, это становится
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]

Пространство подкачки увеличивается по следующей схеме: A) увеличивает пространство, удаляя соседний правый элемент и заменяя новый элемент слева; B) поменяйте местами самый старый элемент с новым элементом слева. Если левые элементы пронумерованы 1..N, этот шаблон выглядит

step swapspace index changed
1    A: 1         0
2    B: 2         0
3    A: 2 3       1
4    B: 4 3       0     
5    A: 4 3 5     2
6    B: 4 6 5     1
7    A: 4 6 5 7   3
...

Последовательность изменения индекса в точности соответствует OEIS A025480 , который можно рассчитать с помощью простого процесса. Это позволяет найти место подкачки, учитывая только количество добавленных элементов, которое также является индексом текущего размещаемого элемента.

Это все, что нам нужно, чтобы заполнить первую половину последовательности за линейное время.

Когда мы доберемся до средней точки, массив будет состоять из трех частей: [ placed_items | swapped_left_items | remaining_right_items] если мы сможем расшифровать поменяемые местами, мы уменьшим проблему до половины размера и можем повторить.

Чтобы расшифровать пространство подкачки, мы используем следующее свойство: Последовательность, построенная с помощью Nчередующихся операций append и swap_oldest, будет содержать N/2элементы, которым дан их возраст A025480(N/2)..A025480(N-1). (Целочисленное деление, меньшие значения старше).

Например, если левая половина изначально содержала значения 1..19, то пространство подкачки содержало бы [16, 12, 10, 14, 18, 11, 13, 15, 17, 19]. A025480 (9..18) - [2, 5, 1, 6, 3, 7, 0, 8, 4, 9]это список индексов предметов от самого старого до самого нового.

Таким образом , мы можем расшифровывать наше пространство подкачки, продвигая через него и обменивать S[i]с S[ A(N/2 + i)]. Это также линейное время.

Оставшееся осложнение заключается в том, что в конечном итоге вы достигнете позиции, где правильное значение должно быть с более низким индексом, но оно уже было заменено. Найти новое местоположение легко: просто выполните расчет индекса еще раз, чтобы определить, куда был заменен элемент. Может потребоваться пройти несколько шагов по цепочке, пока вы не найдете свободное место.

К этому моменту мы объединили половину массива и сохранили порядок неотделенных частей в другой половине с точными перестановками N/2 + N/4. Мы можем продолжить через остальную часть массива для общего количества N + N/4 + N/8 + ....перестановок, которое строго меньше 3N/2.

Как рассчитать A025480:
Это определено в OEIS как a(2n) = n, a(2n+1) = a(n).альтернативная формулировка a(n) = isEven(n)? n/2 : a((n-1)/2). Это приводит к простому алгоритму, использующему побитовые операции:

index_t a025480(index_t n){
    while (n&1) n=n>>1;
    return n>>1;  
}

Это операция амортизации O (1) по всем возможным значениям для N. (1/2 нужно за 1 смену, 1/4 нужно 2, 1/8 нужно 3, ...) . Существует еще более быстрый метод, который использует небольшую таблицу поиска, чтобы найти позицию младшего значащего нулевого бита.

Учитывая это, вот реализация в C:

static inline index_t larger_half(index_t sz) {return sz - (sz / 2); }
static inline bool is_even(index_t i) { return ((i & 1) ^ 1); }

index_t unshuffle_item(index_t j, index_t sz)
{
  index_t i = j;
  do {
    i = a025480(sz / 2 + i);
  }
  while (i < j);
  return i;
}

void interleave(value_t a[], index_t n_items)
{
  index_t i = 0;
  index_t midpt = larger_half(n_items);
  while (i < n_items - 1) {

    //for out-shuffle, the left item is at an even index
    if (is_even(i)) { i++; }
    index_t base = i;

    //emplace left half.
    for (; i < midpt; i++) {
      index_t j = a025480(i - base);
      SWAP(a + i, a + midpt + j);
    }

    //unscramble swapped items
    index_t swap_ct  = larger_half(i - base);
    for (index_t j = 0; j + 1 < swap_ct ; j++) {
      index_t k = unshuffle_item(j, i - base);
      if (j != k) {
        SWAP(a + midpt + j, a + midpt + k);
      }
    }
    midpt += swap_ct;
  }
}

Это должен быть довольно дружественный к кэшу алгоритм, поскольку к 2 из 3 местоположений данных обращаются последовательно, а объем обрабатываемых данных строго уменьшается. Этот метод можно превратить из перемешивания в перемешивание, отменив is_evenтест в начале цикла.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.