Esto está en la línea del pseudocódigo actualmente incompleto de Thijser. La idea es tomar el más frecuente de los tipos de elementos restantes, a menos que se acabe de tomar. (Consulte también la implementación de Coady de este algoritmo).
import collections
import heapq
class Sentinel:
pass
def david_eisenstat(lst):
counts = collections.Counter(lst)
heap = [(-count, key) for key, count in counts.items()]
heapq.heapify(heap)
output = []
last = Sentinel()
while heap:
minuscount1, key1 = heapq.heappop(heap)
if key1 != last or not heap:
last = key1
minuscount1 += 1
else:
minuscount2, key2 = heapq.heappop(heap)
last = key2
minuscount2 += 1
if minuscount2 != 0:
heapq.heappush(heap, (minuscount2, key2))
output.append(last)
if minuscount1 != 0:
heapq.heappush(heap, (minuscount1, key1))
return output
Prueba de corrección
Para dos tipos de elementos, con recuentos k1 y k2, la solución óptima tiene k2 - k1 - 1 defectos si k1 <k2, 0 defectos si k1 = k2 y k1 - k2 - 1 defectos si k1> k2. El caso = es obvio. Los otros son simétricos; cada instancia del elemento minoritario previene como máximo dos defectos de un total de k1 + k2 - 1 posible.
Este algoritmo codicioso devuelve soluciones óptimas, mediante la siguiente lógica. Llamamos seguro a un prefijo (solución parcial) si se extiende a una solución óptima. Claramente, el prefijo vacío es seguro, y si un prefijo seguro es una solución completa, entonces esa solución es óptima. Basta mostrar inductivamente que cada paso codicioso mantiene la seguridad.
La única forma en que un paso codicioso introduce un defecto es si solo queda un tipo de elemento, en cuyo caso solo hay una forma de continuar, y esa forma es segura. De lo contrario, sea P el prefijo (seguro) justo antes del paso en consideración, sea P 'el prefijo inmediatamente después y sea S una solución óptima que extiende P. Si S extiende P' también, entonces hemos terminado. De lo contrario, sea P '= Px y S = PQ y Q = yQ', donde xey son elementos y Q y Q 'son secuencias.
Supongamos primero que P no termina con y. Por elección del algoritmo, x es al menos tan frecuente en Q como y. Considere las subcadenas máximas de Q que contienen solo x e y. Si la primera subcadena tiene al menos tantas x como y, entonces se puede reescribir sin introducir defectos adicionales para comenzar con x. Si la primera subcadena tiene más y que x, entonces alguna otra subcadena tiene más x que y, y podemos reescribir estas subcadenas sin defectos adicionales para que x vaya primero. En ambos casos, encontramos una solución óptima T que extiende P ', según sea necesario.
Supongamos ahora que P termina en y. Modifique Q moviendo la primera aparición de x al frente. Al hacerlo, introducimos como máximo un defecto (donde solía estar x) y eliminamos un defecto (el yy).
Generando todas las soluciones
Esta es la respuesta de tobias_k más pruebas eficientes para detectar cuándo la opción que se está considerando actualmente está restringida globalmente de alguna manera. El tiempo de ejecución asintótico es óptimo, ya que la sobrecarga de generación es del orden de la longitud de la salida. Lamentablemente, el retraso del peor de los casos es cuadrático; podría reducirse a lineal (óptimo) con mejores estructuras de datos.
from collections import Counter
from itertools import permutations
from operator import itemgetter
from random import randrange
def get_mode(count):
return max(count.items(), key=itemgetter(1))[0]
def enum2(prefix, x, count, total, mode):
prefix.append(x)
count_x = count[x]
if count_x == 1:
del count[x]
else:
count[x] = count_x - 1
yield from enum1(prefix, count, total - 1, mode)
count[x] = count_x
del prefix[-1]
def enum1(prefix, count, total, mode):
if total == 0:
yield tuple(prefix)
return
if count[mode] * 2 - 1 >= total and [mode] != prefix[-1:]:
yield from enum2(prefix, mode, count, total, mode)
else:
defect_okay = not prefix or count[prefix[-1]] * 2 > total
mode = get_mode(count)
for x in list(count.keys()):
if defect_okay or [x] != prefix[-1:]:
yield from enum2(prefix, x, count, total, mode)
def enum(seq):
count = Counter(seq)
if count:
yield from enum1([], count, sum(count.values()), get_mode(count))
else:
yield ()
def defects(lst):
return sum(lst[i - 1] == lst[i] for i in range(1, len(lst)))
def test(lst):
perms = set(permutations(lst))
opt = min(map(defects, perms))
slow = {perm for perm in perms if defects(perm) == opt}
fast = set(enum(lst))
print(lst, fast, slow)
assert slow == fast
for r in range(10000):
test([randrange(3) for i in range(randrange(6))])
[1, 2, 1, 3, 1, 4, 1, 5]
es exactamente lo mismo que[1, 3, 1, 2, 1, 4, 1, 5]
según su criterio?