Isso segue as linhas do pseudocódigo atualmente incompleto de Thijser. A ideia é pegar o mais frequente dos tipos de itens restantes, a menos que tenha acabado de ser levado. (Veja também a implementação deste algoritmo de Coady .)
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
Prova de correção
Para dois tipos de itens, com contagens k1 e k2, a solução ótima tem k2 - k1 - 1 defeitos se k1 <k2, 0 defeitos se k1 = k2 e k1 - k2 - 1 defeitos se k1> k2. O caso = é óbvio. Os outros são simétricos; cada instância do elemento minoritário evita no máximo dois defeitos de um total de k1 + k2 - 1 possível.
Este algoritmo guloso retorna soluções ótimas, pela seguinte lógica. Chamamos um prefixo (solução parcial) de seguro se ele se estende a uma solução ótima. Claramente, o prefixo vazio é seguro, e se um prefixo seguro for uma solução completa, então essa solução é ótima. Basta mostrar indutivamente que cada passo ganancioso mantém a segurança.
A única maneira de uma etapa gananciosa introduzir um defeito é se apenas um tipo de item permanecer; nesse caso, há apenas uma maneira de continuar e essa maneira é segura. Caso contrário, seja P o prefixo (seguro) logo antes da etapa em consideração, seja P 'o prefixo logo depois e seja S uma solução ótima estendendo P. Se S estende P' também, então está feito. Caso contrário, seja P '= Px e S = PQ e Q = yQ', onde x e y são itens e Q e Q 'são sequências.
Suponha primeiro que P não termine com y. Pela escolha do algoritmo, x é pelo menos tão frequente em Q quanto y. Considere as substrings máximas de Q contendo apenas x e y. Se a primeira substring tiver pelo menos tantos x quanto y, ela poderá ser reescrita sem introduzir defeitos adicionais para começar com x. Se a primeira substring tiver mais y's do que x's, então alguma outra substring terá mais x's do que y's, e podemos reescrever essas substrings sem defeitos adicionais para que x vá primeiro. Em ambos os casos, encontramos uma solução ótima T que estende P ', conforme necessário.
Suponha agora que P termine com y. Modifique Q movendo a primeira ocorrência de x para a frente. Ao fazer isso, introduzimos no máximo um defeito (onde costumava ser x) e eliminamos um defeito (o yy).
Gerando todas as soluções
Esta é a resposta de tobias_k mais testes eficientes para detectar quando a escolha atualmente em consideração é globalmente restrita de alguma forma. O tempo de execução assintótico é ótimo, uma vez que a sobrecarga de geração é da ordem do comprimento da saída. O atraso do pior caso, infelizmente, é quadrático; poderia ser reduzido a linear (ótimo) com melhores estruturas de dados.
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]
é exatamente o mesmo que[1, 3, 1, 2, 1, 4, 1, 5]
pelo seu critério?