Algoritmo in situ para intercalar una matriz

62

Te dan una matriz de 2n elementos

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

La tarea es intercalar la matriz, utilizando un algoritmo en el lugar para que la matriz resultante se vea como

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

Si el requisito in situ no existiera, podríamos crear fácilmente una nueva matriz y copiar elementos dando un algoritmo de tiempo O(n) .

Con el requisito in situ, un algoritmo de divide y vencerás hace que el algoritmo sea θ(nlogn) .

Entonces la pregunta es:

¿Existe un algoritmo de tiempo O(n) , que también está en su lugar?

(Nota: puede asumir el costo uniforme del modelo WORD RAM, por lo que in situ se traduce en restricción de espacio O(1) ).

Aryabhata
fuente
1
Esto está en stackoverflow pero no dan una solución de calidad. La respuesta mejor calificada es: "Este problema no es tan trivial como la gente cree. Tarea? LOL. Hay una solución en arXiv " Pero la solución arxiv requiere alguna teoría de números + pruebas de referencia en otros documentos. Sería bueno tener una solución sucinta aquí.
Joe
1
También en cstheory: cstheory.stackexchange.com/questions/13943/…
Yuval Filmus
Otro hilo sobre Stack Overflow: stackoverflow.com/questions/15996288/…
Nayuki

Respuestas:

43

Aquí está la respuesta que desarrolla el algoritmo del documento vinculado por Joe: http://arxiv.org/abs/0805.1598

Primero consideremos un algoritmo Θ(nlogn) que usa divide y vencerás.

1) Divide y vencerás

Se nos da

a1,a2,,b1,b2,bn

Ahora, para usar divide y vencerás, para algunos m=Θ(n) , intentamos obtener la matriz

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

y recurse.

Observe que la porción

b1,b2,bm,am+1,an
es un desplazamiento cíclico de

am+1,an,b1,bm

por m lugares.

Este es un clásico y se puede hacer en el lugar mediante tres reversiones y en el tiempo O(n) .

Por lo tanto, divide y vencerás te da un algoritmo Θ(nlogn) , con una recursión similar a T(n)=2T(n/2)+Θ(n) .

2) Ciclos de permutación

Ahora, otro enfoque del problema es considerar la permutación como un conjunto de ciclos disjuntos.

La permutación viene dada por (suponiendo que comienza en 1 )

j2jmod2n+1

Si de alguna manera supiéramos exactamente cuáles son los ciclos, usando un espacio adicional constante, podríamos realizar la permutación seleccionando un elemento , determinar a dónde va ese elemento (usando la fórmula anterior), colocar el elemento en la ubicación objetivo en un espacio temporal, colocar el elemento en esa ubicación objetivo y continuar a lo largo del ciclo. Una vez que hemos terminado con un ciclo, pasamos a un elemento del siguiente ciclo y seguimos ese ciclo y así sucesivamente.AA

Esto nos daría un algoritmo de tiempo , pero supone que "de alguna manera sabíamos cuáles eran los ciclos exactos" e intentamos hacer esta contabilidad dentro de la limitación de espacio es lo que dificulta este problema.O(n)O(1)

Aquí es donde el artículo usa la teoría de números.

Se puede demostrar que, en el caso de que , los elementos en las posiciones , están en ciclos diferentes y cada ciclo contiene un elemento en la posición .2n+1=3k13,32,,3k13m,m0

Esto utiliza el hecho de que es un generador de .2(Z/3k)

Por lo tanto, cuando , el enfoque de seguir el ciclo nos da un algoritmo de tiempo , ya que para cada ciclo, sabemos exactamente dónde comenzar: potencias de (incluido ) (esos se puede calcular en espacio).2n+1=3kO(n)31O(1)

3) Algoritmo final

Ahora combinamos los dos anteriores: Divide y vencerás + ciclos de permutación.

Hacemos un divide y vencerás, pero escogen de manera que es una potencia de y .m2m+13m=Θ(n)

Entonces, en lugar de recurrir en ambas "mitades", recurrimos en una sola y hacemos trabajo extra.Θ(n)

Esto nos da la recurrencia (para algunos ) y nos da un tiempo , algoritmo espacial!T(n)=T(cn)+Θ(n)0<c<1O(n)O(1)

Aryabhata
fuente
44
Eso es hermoso.
Raphael
1
Muy agradable. Al pasar por ejemplos de permutación, ahora entiendo la mayor parte. Dos preguntas: 1. ¿Cómo encuentras realmente el valor m? El papel afirma que se necesita O (log n), ¿por qué? 2. ¿Es posible desintercalar una matriz usando un enfoque similar?
num3ric
2
@ num3ric: 1) Encuentra la potencia más alta de que es . Entonces será . 2) Sí, es posible, creo que había agregado una respuesta en stackoverflow en alguna parte. Los líderes del ciclo en ese caso, creo que salieron del (para = potencia de ). <3O ( log n ) 2 a 3 b 2 m + 1 3<nO(logn)2a3b2m+13
Aryabhata
@Aryabhata ¿por qué solo recurrimos en una "mitad", en lugar de dos "mitades"?
sinoTrinity
1
@Aryabhata ¿Se puede ampliar este algoritmo para intercalar más de dos matrices? Por ejemplo, convierta en o algo similar. c 1 , b 1 , a 1 , c 2 , b 2 , a 2 , ... , c n , b na1,a2,,an,b1,b2,,bn,c1,c2,,cnc1,b1,a1,c2,b2,a2,,cn,bn,an
Doub
18

Estoy bastante seguro de que he encontrado un algoritmo que no se basa en la teoría de números o la teoría del ciclo. Tenga en cuenta que hay algunos detalles que resolver (posiblemente mañana), pero estoy bastante seguro de que funcionarán. Agito las manos como se supone que debo estar durmiendo, no porque esté tratando de ocultar problemas :)

Sea Ala primera matriz, Bla segunda, |A| = |B| = Ny asuma N=2^kpara algunos k, por simplicidad. Deje A[i..j]ser el subconjunto de Acon índices ihasta j, inclusive. Las matrices están basadas en 0. Devuelva RightmostBitPos(i)la posición (basada en 0) del bit más a la derecha que es '1' i, contando desde la derecha. El algoritmo funciona de la siguiente manera.

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

Tomemos una matriz de 16 números, y comencemos a intercalarlos usando intercambios, y veamos qué sucede:

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

De particular interés es la primera parte de la segunda matriz:

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

El patrón debe ser claro: alternativamente agregamos un número al final y reemplazamos el número más bajo por un número alto. Tenga en cuenta que siempre agregamos un número que es uno más alto que el número más alto que ya tenemos. Si de alguna manera pudiéramos averiguar exactamente qué número es el más bajo en un momento dado, podemos hacerlo fácilmente.

Ahora, buscamos ejemplos más grandes para ver si podemos ver un patrón. Tenga en cuenta que no necesitamos arreglar el tamaño de la matriz para construir el ejemplo anterior. En algún momento, obtenemos esta configuración (la segunda línea resta 16 de cada número):

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

Ahora esto muestra claramente un patrón: "1 3 5 7 9 11 13 15" están todos separados, "2 6 10 14" están separados por 4 y "4 12" están separados por 8. Por lo tanto, podemos idear un algoritmo que nos diga cuál será el próximo número más pequeño: el mecanismo es más o menos exactamente cómo funcionan los números binarios. Tienes un poco para la última mitad de la matriz, un poco para el segundo trimestre, y así sucesivamente.

Por lo tanto, si se nos permite suficiente espacio para almacenar estos bits (necesitamos bits, pero nuestro modelo computacional lo permite: un puntero en la matriz también necesita bits), podemos averiguar qué número intercambiar en tiempo amortizado.log n O ( 1 )lognlognO(1)

Por lo tanto, podemos obtener la primera mitad de la matriz en su estado intercalado en tiempo y intercambios. Sin embargo, tenemos que arreglar la segunda mitad de nuestra matriz, que parece desordenada ("8 6 5 7 13 14 15 16").O ( n )O(n)O(n)

Ahora, si podemos 'ordenar' la primera mitad de esta segunda parte, terminamos con "5 6 7 8 13 14 15 16", y el intercalado recursivo de esta mitad hará el truco: intercalamos la matriz en tiempo ( llamadas recursivas cada una de las cuales reduce a la mitad el tamaño de entrada). Tenga en cuenta que no necesitamos una pila ya que estas llamadas son recursivas de cola, por lo que nuestro uso de espacio sigue siendo .O ( log n ) O ( 1 )O(n)O(logn)O(1)

Ahora, la pregunta es: ¿hay algún patrón en la parte que necesitamos clasificar? Probar 32 números nos da "16 12 10 14 9 11 13 15" para arreglarlo. ¡Tenga en cuenta que tenemos exactamente el mismo patrón aquí! "9 11 13 15", "10 14" y "12" se agrupan de la misma manera que vimos anteriormente.

Ahora, el truco es intercalar recursivamente estas subpartes. Intercalamos "16" y "12" a "12 16". Intercalamos "12 16" y "10 14" a "10 12 14 16". Intercalamos "10 12 14 16" y "9 11 13 15" a "9 10 11 12 13 14 15 16". Esto ordena la primera parte.

Al igual que arriba, el costo total de esta operación es . Sumando todo esto, todavía logramos obtener un tiempo de ejecución total de .O ( n )O(n)O(n)

Un ejemplo:

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
Alex ten Brink
fuente
Interesante. ¿Estaría dispuesto a intentar y escribir una prueba formal? Sí sé que hay otro algoritmo (mencionado en el documento que encontró Joe) que se ocupa de los bits. ¡Quizás lo hayas redescubierto!
Aryabhata
1

Aquí hay un algoritmo de tiempo lineal no recursivo in situ para intercalar dos mitades de una matriz sin almacenamiento adicional.

La idea general es simple: recorra la primera mitad de la matriz de izquierda a derecha, intercambiando los valores correctos en su lugar. A medida que avanza, los valores izquierdos aún por usar se intercambian en el espacio desocupado por los valores correctos. El único truco es descubrir cómo sacarlos nuevamente.

Comenzamos con una matriz de tamaño N dividida en 2 mitades casi iguales.
[ left_items | right_items ]
A medida que lo procesamos, se convierte
[ placed_items | remaining_left_items| swapped_left_items | remaining_right_items]

El espacio de intercambio crece con el siguiente patrón: A) crece el espacio eliminando el elemento derecho adyacente e intercambiando un nuevo elemento desde la izquierda; B) intercambie el elemento más antiguo por uno nuevo desde la izquierda. Si los elementos de la izquierda están numerados 1..N, este patrón se ve como

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
...

La secuencia de qué índice cambió es exactamente OEIS A025480 , que se puede calcular con un proceso simple. Esto permite encontrar la ubicación de intercambio dada la cantidad de elementos agregados hasta ahora, que también es el índice del elemento actual que se está colocando.

Esa es toda la información que necesitamos para completar la primera mitad de la secuencia en tiempo lineal.

Cuando lleguemos al punto medio, la matriz tendrá tres partes: [ placed_items | swapped_left_items | remaining_right_items] si podemos descifrar los elementos intercambiados, hemos reducido el problema a la mitad del tamaño y podemos repetir.

Para descifrar el espacio de intercambio, utilizamos la siguiente propiedad: Una secuencia construida Nalternando las operaciones append y swap_oldest contendrá N/2elementos donde sus edades están dadas por A025480(N/2)..A025480(N-1). (División entera, los valores más pequeños son más antiguos).

Por ejemplo, si la mitad izquierda originalmente contenía los valores 1..19, entonces el espacio de intercambio contendría [16, 12, 10, 14, 18, 11, 13, 15, 17, 19]. A025480 (9..18) es [2, 5, 1, 6, 3, 7, 0, 8, 4, 9], que es exactamente la lista de índices de los elementos del más antiguo al más nuevo.

Entonces podemos descifrar nuestro espacio de intercambio avanzando a través de él e intercambiando S[i]con S[ A(N/2 + i)]. Esto también es tiempo lineal.

La complicación restante es que eventualmente llegarás a una posición donde el valor correcto debería estar en un índice más bajo, pero ya se ha cambiado. Es fácil encontrar la nueva ubicación: simplemente haga el cálculo del índice nuevamente para descubrir dónde se cambió el artículo. Puede ser necesario seguir algunos pasos de la cadena hasta que encuentre una ubicación sin cambiar.

En este punto, hemos fusionado la mitad de la matriz y hemos mantenido el orden de las partes no fusionadas en la otra mitad, con N/2 + N/4intercambios exactos . Podemos continuar a través del resto de la matriz para un total de N + N/4 + N/8 + ....intercambios que es estrictamente menor que 3N/2.

Cómo calcular A025480:
Esto se define en OEIS como a(2n) = n, a(2n+1) = a(n).Una formulación alternativa es a(n) = isEven(n)? n/2 : a((n-1)/2). Esto lleva a un algoritmo simple que utiliza operaciones bit a bit:

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

Esta es una operación de O (1) amortizada sobre todos los valores posibles para N. (1/2 necesita 1 turno, 1/4 necesita 2, 1/8 necesita 3, ...) . Existe un método aún más rápido que utiliza una pequeña tabla de búsqueda para encontrar la posición del bit cero menos significativo.

Dado eso, aquí hay una implementación en 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;
  }
}

Este debería ser un algoritmo bastante amigable con el caché, ya que se accede secuencialmente a 2 de las 3 ubicaciones de datos y la cantidad de datos que se procesa está disminuyendo estrictamente. Este método se puede convertir de una combinación aleatoria a una aleatoria al negar la is_evenprueba al comienzo del bucle.

AShelly
fuente