На місці алгоритм переплетення масиву


62

Вам надається масив з 2n елементів

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

Завдання полягає в переплетенні масиву, використовуючи на місці алгоритм, такий, як виглядає отриманий масив

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

Якщо вимоги на місці не було, ми могли б легко створити новий масив та скопіювати елементи, даючи алгоритм часу O(n) .

З вимогою на місці алгоритм ділення та підкорення алгоритм підбиває алгоритм до рівня θ(nlogn) .

Отже, питання:

Чи існує алгоритм часу O(n) , який також є на місці?

(Примітка. Ви можете припустити модель єдиної вартості WORD RAM, тому на місці перекладається на O(1) обмеження простору).


1
Це в потоковому потоці, але вони не дають якісного рішення. Відповідь, що найкраще оцінюється: "Ця проблема не настільки тривіальна, як це роблять люди. Домашнє завдання? LOL. Є рішення на arXiv ", але рішення arxiv вимагає певної теорії чисел + посилання на докази в інших роботах. Було б непогано мати тут лаконічне рішення.
Джо,


Інший потік на переповнення стека: stackoverflow.com/questions/15996288 / ...
Nayuki

Відповіді:


43

Ось відповідь, яка розробляється на алгоритмі з документа, зв'язаного Джо: http://arxiv.org/abs/0805.1598

Θ(nlogn)

1) Розділіть і завоюйте

Нам дано

a1,a2,,b1,b2,bn

m=Θ(n)

[a1,a2,,am,b1,b2,,bm],[am+1,,an,bm+1,bn]

і рецидивують.

b1,b2,bm,am+1,an

am+1,an,b1,bm

m

O(n)

Θ(nlogn)T(n)=2T(n/2)+Θ(n)

2) Цикли перестановки

Тепер іншим підходом до проблеми є розгляд перестановки як набір неперервних циклів.

1

j2jmod2n+1

Якби ми якось точно знали, що таке цикли, використовуючи постійний додатковий простір, ми могли б реалізувати перестановку, вибравши елемент , визначити, куди йде цей елемент (використовуючи вищевказану формулу), помістивши елемент у цільове місце у тимчасовий простір, поставивши елемент в цільове місце і продовжуйте по циклу. Після того, як ми закінчимо один цикл, ми переходимо до елемента наступного циклу і слідуємо за цим циклом тощо.AA

Це дасть нам алгоритм часу , але він передбачає, що ми "якось знали, що таке точні цикли", і намагаємось зробити це ведення бухгалтерського обліку в межах обмеження простору це те, що ускладнює цю проблему.O(n)O(1)

У цьому документі використовується теорія чисел.

Можна показати, що у випадку, коли , елементи в положеннях , знаходяться в різних циклах, і кожен цикл містить елемент у положенні .2n+1=3k13,32,,3k13m,m0

Для цього використовується той факт, що є генератором .2(Z/3k)

Таким чином, коли , наступний цикльний підхід дає нам алгоритм часу, оскільки для кожного циклу ми точно знаємо, з чого починати: потужності (включаючи ) (ті, що можна обчислити в просторі ).2n+1=3kO(n)31O(1)

3) Заключний алгоритм

Тепер ми поєднуємо вищевказані два: ділимо і перемагаємо + перемикання циклів.

Робимо ділення і перемагаємо, але вибираємо так, що - сила і .m2m+13m=Θ(n)

Тож замість повторення на обох "половинках" ми повторюємо лише одну і робимо додаткову роботу.Θ(n)

Це дає нам повторення (для деяких ) і, таким чином, дає нам час, алгоритм простору!T(n)=T(cn)+Θ(n)0<c<1O(n)O(1)


4
Це прекрасно.
Рафаель

1
Дуже хороша. Переглядаючи приклади перестановки, я зараз розумію більшість із цього. Два питання: 1. Як ви насправді знаходите значення m? Папір стверджує, що потрібно O (log n), чому? 2. Чи можливо DE-переплетення масиву, використовуючи аналогічний підхід?
num3ric

2
@ num3ric: 1) Ви знаходите найвищу потужність яка є . Так це буде . 2). Так, можливо, я вважаю, що я десь додав відповідь на stackoverflow. Я вважаю, що лідери циклу в такому випадку виявились величиною для (для = потужність ). 3<nO(logn)2a3b2m+13
Ар'ябхата

@Aryabhata, чому ми повторюємо лише одну "половину", а не дві "половинки"?
sinoTrinity

1
@Aryabhata Чи можна розширити цей алгоритм, щоб переплутати більше двох масивів? Наприклад, перетворіть в чи щось подібне. a1,a2,,an,b1,b2,,bn,c1,c2,,cnc1,b1,a1,c2,b2,a2,,cn,bn,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 один від одного. Таким чином, ми можемо розробити алгоритм, який говорить нам про те, яким буде наступне найменше число: механізм в значній мірі точно працює як двійкові числа. У вас є трохи для останньої половини масиву, трохи для другої чверті тощо.

Якщо нам дозволено достатньо місця для зберігання цих бітів (нам потрібні бітів, але наша обчислювальна модель дозволяє це - вказівник на масив також потребує бітів), ми можемо визначити, на яке число потрібно поміняти місцями амортизований час.log n O ( 1 )lognlognO(1)

Таким чином, ми можемо перевести першу половину масиву в його переплетений стан за час та підкачки. Однак нам потрібно виправити другу половину нашого масиву, яка, здається, все переплутана ("8 6 5 7 13 14 15 16").O ( n )O(n)O(n)

Тепер, якщо ми зможемо "сортувати" першу половину цієї другої частини, ми закінчимо "5 6 7 8 13 14 15 16", і рекурсивно переплетення цієї половини зробить трюк: ми переплетемо масив в час ( рекурсивні дзвінки, кожен з яких вдвічі зменшує розмір введення). Зауважте, нам не потрібен стек, оскільки ці дзвінки є рекурсивними, тому наше використання місця залишається .O ( log n ) O ( 1 )O(n)O(logn)O(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". Це сортує першу частину.

Як і вище, загальна вартість цієї операції становить . Додавши все це, ми все ще встигаємо отримати загальний час роботи .O ( n )O(n)O(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]

Простір підкачки збільшується за такою схемою: А) збільшуйте простір, видаляючи сусідній правий елемент і замінюючи новий елемент зліва; Б) обміняйте найстаріший предмет лівим на новий. Якщо ліві елементи пронумеровані 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 , яку можна обчислити простим процесом. Це дозволяє знайти місце підкачки, враховуючи лише кількість доданих до цього часу елементів, що також є індексом поточного елемента, що розміщується.

Це вся інформація, яка нам потрібна для заповнення 1-ї половини послідовності у лінійний час.

Коли ми дістанемося до середини, масив матиме три частини: [ placed_items | swapped_left_items | remaining_right_items] Якщо ми можемо розкрутити замінені елементи, ми зменшили проблему до половини розміру і можемо повторити.

Щоб розшифрувати простір swap, ми використовуємо таке властивість: Послідовність, побудована за допомогою Nчергування операцій додавання та 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.