Por ejemplo, tengo listas:
a[0] = [1, 1, 1, 0, 0]
a[1] = [1, 1, 0, 0, 1]
a[2] = [0, 1, 1, 1, 0]
# and so on
Parecen ser diferentes, pero si se supone que el inicio y el final están conectados, entonces son circularmente idénticos.
El problema es que cada lista que tengo tiene una longitud de 55 y contiene solo tres unos y 52 ceros. Sin condición circular, hay 26,235 (55 elegir 3) listas. Sin embargo, si existe la condición 'circular', hay una gran cantidad de listas circularmente idénticas
Actualmente verifico circularmente la identidad siguiendo:
def is_dup(a, b):
for i in range(len(a)):
if a == list(numpy.roll(b, i)): # shift b circularly by i
return True
return False
Esta función requiere 55 operaciones de desplazamiento cíclico en el peor de los casos. Y hay 26.235 listas para comparar entre sí. En resumen, necesito 55 * 26,235 * (26,235 - 1) / 2 = 18,926,847,225 cálculos. ¡Se trata de casi 20 Giga!
¿Hay alguna buena manera de hacerlo con menos cálculos? ¿O algún tipo de datos que admita circular ?
Respuestas:
En primer lugar, esto se puede hacer en
O(n)
términos de la longitud de la lista. Puede notar que si duplica su lista 2 veces ([1, 2, 3]
),[1, 2, 3, 1, 2, 3]
su nueva lista definitivamente tendrá todas las listas cíclicas posibles.Entonces, todo lo que necesita es verificar si la lista que está buscando está dentro de 2 veces de su lista de inicio. En python puede lograr esto de la siguiente manera (suponiendo que las longitudes sean las mismas).
Alguna explicación sobre mi línea:
list * 2
combinará una lista consigo misma,map(str, [1, 2])
convertirá todos los números en una cadena y' '.join()
convertirá la matriz['1', '2', '111']
en una cadena'1 2 111'
.Como señalaron algunas personas en los comentarios, oneliner potencialmente puede dar algunos falsos positivos, por lo que para cubrir todos los casos límite posibles:
PS1 cuando se habla de la complejidad del tiempo, vale la pena notar que
O(n)
se logrará si la subcadena se puede encontrar aO(n)
tiempo. No siempre es así y depende de la implementación en su idioma ( aunque potencialmente se puede hacer en tiempo lineal KMP, por ejemplo).PS2 para personas que tienen miedo a la operación de cadenas y debido a este hecho piensan que la respuesta no es buena. Lo importante es la complejidad y la velocidad. Este algoritmo potencialmente se ejecuta en
O(n)
tiempo yO(n)
espacio, lo que lo hace mucho mejor que cualquier cosa en elO(n^2)
dominio. Para ver esto usted mismo, puede ejecutar un pequeño punto de referencia (crea una lista aleatoria que muestra el primer elemento y lo agrega al final, creando así una lista cíclica. Usted es libre de hacer sus propias manipulaciones)0.3 segundos en mi máquina. No mucho tiempo. Ahora intenta comparar esto con
O(n^2)
soluciones. Mientras lo compara, puede viajar de EE. UU. A Australia (muy probablemente en un crucero)fuente
No estoy lo suficientemente informado en Python para responder esto en el lenguaje solicitado, pero en C / C ++, dados los parámetros de su pregunta, convertiría los ceros y unos en bits y los insertaría en los bits menos significativos de un uint64_t. Esto le permitirá comparar los 55 bits de una sola vez: 1 reloj.
Perversamente rápido, y todo encajará en cachés en chip (209,880 bytes). El soporte de hardware para desplazar los 55 miembros de la lista a la derecha simultáneamente está disponible solo en los registros de una CPU. Lo mismo ocurre con la comparación de los 55 miembros simultáneamente. Esto permite un mapeo 1 por 1 del problema a una solución de software. (y utilizando los registros SIMD / SSE de 256 bits, hasta 256 miembros si es necesario) Como resultado, el código es inmediatamente obvio para el lector.
Es posible que pueda implementar esto en Python, pero no lo sé lo suficiente como para saber si eso es posible o cuál podría ser el rendimiento.
Después de dormir, algunas cosas se volvieron obvias, y todo para mejor.
1.) Es tan fácil girar la lista enlazada circularmente con bits que el ingenioso truco de Dali no es necesario. Dentro de un registro de 64 bits, el desplazamiento de bits estándar logrará la rotación de manera muy simple, y en un intento de hacer que esto sea más amigable con Python, usando aritmética en lugar de operaciones de bits.
2.) El desplazamiento de bits se puede lograr fácilmente usando dividir entre 2.
3.) El módulo 2 puede verificar fácilmente el final de la lista para 0 o 1.
4.) "Mover" un 0 al principio de la lista desde la cola se puede hacer dividiendo entre 2. Esto porque si el cero se moviera realmente haría que el bit 55 sea falso, lo que ya es al no hacer absolutamente nada.
5.) "Mover" un 1 al comienzo de la lista desde la cola se puede hacer dividiendo por 2 y sumando 18,014,398,509,481,984, que es el valor creado al marcar el 55 ° bit verdadero y todo lo demás falso.
6.) Si una comparación del ancla y el compuesto uint64_t es VERDADERO después de una rotación dada, rompa y devuelva VERDADERO.
Convertiría toda la matriz de listas en una matriz de uint64_ts por adelantado para evitar tener que hacer la conversión repetidamente.
Después de pasar algunas horas tratando de optimizar el código, estudiando el lenguaje ensamblador, pude ahorrar un 20% del tiempo de ejecución. Debo agregar que ayer también se actualizó el compilador O / S y MSVC. Por cualquier motivo, la calidad del código que el compilador C produjo mejoró dramáticamente después de la actualización (15/11/2014). El tiempo de ejecución es ahora ~ 70 relojes, 17 nanosegundos para componer y comparar un anillo de anclaje con las 55 vueltas de un anillo de prueba y NxN de todos los anillos contra todos los demás se realiza en 12.5 segundos .
Este código es tan estricto que todos menos 4 registros están sentados sin hacer nada el 99% del tiempo. El lenguaje ensamblador coincide con el código C casi línea por línea. Muy fácil de leer y entender. Un gran proyecto de montaje si alguien se estuviera enseñando eso.
El hardware es Hazwell i7, MSVC de 64 bits, optimizaciones completas.
fuente
Leyendo entre líneas, parece que estás tratando de enumerar un representante de cada clase de cadenas de equivalencia circular con 3 unidades y 52 ceros. Cambiemos de una representación densa a una escasa (conjunto de tres números
range(55)
). En esta representación, el desplazamiento circular des
byk
viene dado por la comprensiónset((i + k) % 55 for i in s)
. El representante mínimo lexicográfico en una clase siempre contiene la posición 0. Dado un conjunto del formulario{0, i, j}
con0 < i < j
, los otros candidatos para el mínimo en la clase son{0, j - i, 55 - i}
y{0, 55 - j, 55 + i - j}
. Por lo tanto, necesitamos(i, j) <= min((j - i, 55 - i), (55 - j, 55 + i - j))
que el original sea mínimo. Aquí hay un código de enumeración.fuente
Repita la primera matriz, luego use el algoritmo Z (tiempo O (n)) para encontrar la segunda matriz dentro de la primera.
(Nota: no tiene que copiar físicamente la primera matriz. Simplemente puede ajustar durante la coincidencia).
Lo bueno del algoritmo Z es que es muy simple en comparación con KMP, BM, etc.
Sin embargo, si se siente ambicioso, podría hacer una coincidencia de cadenas en tiempo lineal y espacio constante
strstr
; por ejemplo, hace esto. Sin embargo, implementarlo sería más doloroso.fuente
Continuando con la solución muy inteligente de Salvador Dalí, la mejor manera de manejarla es asegurarse de que todos los elementos tengan la misma longitud, así como que ambas LISTAS tengan la misma longitud.
No tengo idea si esto es más rápido o más lento que la solución de expresiones regulares recomendada por AshwiniChaudhary en la respuesta de Salvador Dali, que dice:
fuente
str.format
n
tiempos para formatear la cadena resultante. SUPONGO .... :)Dado que necesita hacer tantas comparaciones, ¿podría valer la pena tomar un primer paso por sus listas para convertirlas en algún tipo de forma canónica que se pueda comparar fácilmente?
¿Estás tratando de obtener un conjunto de listas únicas circulares? Si es así, puedes tirarlos en un conjunto después de convertirlos en tuplas.
Disculpas a David Eisenstat por no detectar su respuesta muy similar.
fuente
Puede rodar una lista como esta:
fuente
Primero convierta cada uno de los elementos de su lista (en una copia si es necesario) a esa versión rotada que es léxicamente mejor.
Luego ordene la lista de listas resultante (manteniendo un índice en la posición original de la lista) y unifique la lista ordenada, marcando todos los duplicados en la lista original según sea necesario.
fuente
Piggybacking en la observación de @ SalvadorDali sobre la búsqueda de coincidencias de a en cualquier segmento de tamaño alargado en b + b, aquí hay una solución que usa solo operaciones de lista.
Segundo enfoque: [eliminado]
fuente
rollmatch([1, 0, 1, 1], [0, 1, 1, 1])
.No es una respuesta completa e independiente, pero sobre el tema de la optimización mediante la reducción de las comparaciones, yo también estaba pensando en representaciones normalizadas.
Es decir, si su alfabeto de entrada es {0, 1}, podría reducir significativamente el número de permutaciones permitidas. Gire la primera lista a una forma (pseudo-) normalizada (dada la distribución en su pregunta, elegiría uno donde uno de los 1 bits esté en el extremo izquierdo y uno de los 0 bits esté en el extremo derecho). Ahora, antes de cada comparación, gire sucesivamente la otra lista a través de las posibles posiciones con el mismo patrón de alineación.
Por ejemplo, si tiene un total de cuatro bits 1, puede haber como máximo 4 permutaciones con esta alineación, y si tiene grupos de 1 bits adyacentes, cada bit adicional en dicho grupo reduce la cantidad de posiciones.
Esto se generaliza a alfabetos más grandes y diferentes patrones de alineación; El principal desafío es encontrar una buena normalización con solo unas pocas representaciones posibles. Idealmente, sería una normalización adecuada, con una única representación única, pero dado el problema, no creo que sea posible.
fuente
Continuando con la respuesta de Rocket Roy: Convierta todas sus listas por adelantado en números de 64 bits sin signo. Para cada lista, gire esos 55 bits para encontrar el valor numérico más pequeño.
Ahora le queda un único valor de 64 bits sin signo para cada lista que puede comparar directamente con el valor de las otras listas. La función is_circular_identical () ya no es necesaria.
(En esencia, crea un valor de identidad para sus listas que no se ve afectado por la rotación de los elementos de las listas) Eso incluso funcionaría si tiene un número arbitrario de uno en sus listas.
fuente
Esta es la misma idea de Salvador Dalí, pero no necesita la conversión de cadena. Detrás está la misma idea de recuperación de KMP para evitar una inspección de turno imposible. Solo llaman a KMPModified (list1, list2 + list2).
¡Espero que esto ayude!
fuente
Simplificando el problema
(0,1)
1
s consecutivos en un recuento0
s consecutivos en un recuento negativoEjemplo
Proceso de verificación
La empuñadura
lookup
ylook-ahead
Pseudocódigo
Las funciones
MAP_LIST(LIST A):LIST
MAPA ELEMENTOS CONSECUTIVOS COMO CUENTA EN UNA NUEVA LISTALOOKUP_INDEX(LIST A, INTEGER E):LIST
VOLVER LISTA DE ÍNDICES DONDE EL ELEMENTOE
EXISTE EN LA LISTAA
COUNT_CHAR(LIST A , INTEGER E):INTEGER
CUENTA CUANTAS VECES OCURRE UN ELEMENTOE
EN UNA LISTAA
ALPHA_NGRAM(LIST A,LIST B,INTEGER I,INTEGER N):BOOLEAN
COMPRUEBE SIB[I]
ES EQUIVALENTEA[0]
N-GRAM
EN AMBAS DIRECCIONESFinalmente
Si el tamaño de la lista va a ser bastante grande o si el elemento del que estamos comenzando a verificar el ciclo es con frecuencia alto, entonces podemos hacer lo siguiente:
Busque el elemento menos frecuente en la primera lista para comenzar
aumente el parámetro N de n-gramo para disminuir la probabilidad de pasar por una verificación lineal
fuente
Una "forma canónica" eficiente y rápida de calcular para las listas en cuestión puede derivarse como:
a
) debe estar entre18
y52
(inclusive). Vuelva a codificarlo entre0
y34
.b
) debe estar entre0
y26
, pero no importa mucho.52 - (a + b)
y no agrega informaciónLa forma canónica es el número entero
b * 35 + a
, que está entre0
y936
(inclusive), que es bastante compacto (hay477
listas circulares únicas en total).fuente
Escribí una solución sencilla que compara ambas listas y solo aumenta (y ajusta) el índice del valor comparado para cada iteración.
No conozco Python bien, así que lo escribí en Java, pero es realmente simple, por lo que debería ser fácil adaptarlo a cualquier otro idioma.
De este modo, también podría comparar listas de otros tipos.
fuente
Como otros han mencionado, una vez que encuentre la rotación normalizada de una lista, puede compararlos.
Aquí hay un código de trabajo que hace esto, el método básico es encontrar una rotación normalizada para cada lista y comparar:
Tenga en cuenta que este método no depende de los números, puede pasar listas de cadenas (cualquier valor que se pueda comparar).
En lugar de hacer una búsqueda de lista en lista, sabemos que queremos que la lista comience con el valor mínimo, para que podamos recorrer los valores mínimos, buscando hasta encontrar cuál tiene los valores sucesivos más bajos, almacenando esto para futuras comparaciones. hasta que tengamos lo mejor.
Hay muchas oportunidades para salir temprano al calcular el índice, detalles sobre algunas optimizaciones.
Tenga en cuenta que en Python una búsqueda lista por lista puede ser más rápida, sin embargo, estaba interesado en encontrar un algoritmo eficiente, que también podría usarse en otros idiomas. Además, hay algunas ventajas en evitar crear nuevas listas.
Ver: este fragmento para algunas pruebas / ejemplos más.
fuente
Puede verificar si una lista A es igual a un cambio cíclico de la lista B en el tiempo esperado de O (N) con bastante facilidad.
Usaría una función de hash polinomial para calcular el hash de la lista A, y cada cambio cíclico de la lista B. Cuando un cambio de la lista B tiene el mismo hash que la lista A, compararía los elementos reales para ver si son iguales .
La razón por la que esto es rápido es que con las funciones de hash polinomiales (¡que son extremadamente comunes!), Puede calcular el hash de cada cambio cíclico del anterior en tiempo constante, por lo que puede calcular los hash para todos los cambios cíclicos en O ( N) tiempo.
Funciona así:
Digamos que B tiene N elementos, entonces el hash de B usando P principal es:
Esta es una forma optimizada de evaluar un polinomio en P, y es equivalente a:
Observe cómo cada B [i] se multiplica por P ^ (N-1-i). Si desplazamos B a la izquierda por 1, entonces cada B [i] se multiplicará por una P adicional, excepto la primera. Dado que la multiplicación se distribuye sobre la suma, podemos multiplicar todos los componentes a la vez simplemente multiplicando todo el hash y luego arreglar el factor para el primer elemento.
El hash del desplazamiento a la izquierda de B es solo
El segundo turno a la izquierda:
y así...
NOTA: todas las matemáticas anteriores se realizan en un módulo de tamaño de palabra de máquina, y solo tiene que calcular P ^ N una vez.
fuente
Para pegarte a la forma más pitónica de hacerlo, ¡usa sets!
fuente