Te dan una matriz de elementos
La tarea es intercalar la matriz, utilizando un algoritmo en el lugar para que la matriz resultante se vea como
Si el requisito in situ no existiera, podríamos crear fácilmente una nueva matriz y copiar elementos dando un algoritmo de tiempo .
Con el requisito in situ, un algoritmo de divide y vencerás hace que el algoritmo sea .
Entonces la pregunta es:
¿Existe un algoritmo de tiempo , 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 ).
algorithms
arrays
permutations
in-place
Aryabhata
fuente
fuente
Respuestas:
Aquí está la respuesta que desarrolla el algoritmo del documento vinculado por Joe: http://arxiv.org/abs/0805.1598
Primero consideremos un algoritmoΘ ( n logn ) que usa divide y vencerás.
1) Divide y vencerás
Se nos da
Ahora, para usar divide y vencerás, para algunosm = Θ ( n ) , intentamos obtener la matriz
y recurse.
Observe que la porciónsi1, b2, ... bmetro, unam + 1, ... anorte es un desplazamiento cíclico de
pormetro lugares.
Este es un clásico y se puede hacer en el lugar mediante tres reversiones y en el tiempoO (n) .
Por lo tanto, divide y vencerás te da un algoritmoΘ ( n logn ) , con una recursión similar a T( n ) = 2 T( 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 en1 )
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.UNA UNA
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 .2 n + 1 = 3k 1 3 , 32, ... , 3k - 1 3metro, m ≥ 0
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).2 n + 1 = 3k O (n) 3 1 O (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 .metro 2 m + 1 3 m = Θ ( 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( c n ) + Θ ( n ) 0 < c < 1 O (n) O (1)
fuente
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
A
la primera matriz,B
la segunda,|A| = |B| = N
y asumaN=2^k
para algunosk
, por simplicidad. DejeA[i..j]
ser el subconjunto deA
con índicesi
hastaj
, inclusive. Las matrices están basadas en 0. DevuelvaRightmostBitPos(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.Tomemos una matriz de 16 números, y comencemos a intercalarlos usando intercambios, y veamos qué sucede:
De particular interés es la primera parte de la segunda matriz:
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):
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 )logn logn O(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:
fuente
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
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
N
alternando las operaciones append y swap_oldest contendráN/2
elementos donde sus edades están dadas porA025480(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]
conS[ 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/4
intercambios exactos . Podemos continuar a través del resto de la matriz para un total deN + N/4 + N/8 + ....
intercambios que es estrictamente menor que3N/2
.Cómo calcular A025480:
Esto se define en OEIS como
a(2n) = n, a(2n+1) = a(n).
Una formulación alternativa esa(n) = isEven(n)? n/2 : a((n-1)/2)
. Esto lleva a un algoritmo simple que utiliza operaciones bit a bit: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:
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_even
prueba al comienzo del bucle.fuente