Cela va dans le sens du pseudocode actuellement incomplet de Thijser. L'idée est de prendre le plus fréquent des types d'éléments restants, sauf s'il vient d'être pris. (Voir aussi l'implémentation de cet algorithme par 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
Preuve d'exactitude
Pour deux types d'items, avec des comptages k1 et k2, la solution optimale a k2 - k1 - 1 défauts si k1 <k2, 0 défaut si k1 = k2, et k1 - k2 - 1 défauts si k1> k2. Le cas = est évident. Les autres sont symétriques; chaque instance de l'élément minoritaire empêche au plus deux défauts sur un total de k1 + k2 - 1 possible.
Cet algorithme gourmand renvoie des solutions optimales, par la logique suivante. Nous appelons un préfixe (solution partielle) sûr s'il s'étend à une solution optimale. Il est clair que le préfixe vide est sûr, et si un préfixe sûr est une solution complète, cette solution est optimale. Il suffit de montrer de manière inductive que chaque pas gourmand maintient la sécurité.
La seule façon pour une étape gourmande d'introduire un défaut est s'il ne reste qu'un seul type d'élément, auquel cas il n'y a qu'une seule façon de continuer, et cette façon est sûre. Sinon, soit P le préfixe (sûr) juste avant l'étape considérée, soit P 'le préfixe juste après, et soit S une solution optimale étendant P. Si S étend P' aussi, alors c'est fini. Sinon, soit P '= Px et S = PQ et Q = yQ', où x et y sont des éléments et Q et Q 'sont des séquences.
Supposons d'abord que P ne se termine pas par y. Par le choix de l'algorithme, x est au moins aussi fréquent dans Q que y. Considérons les sous-chaînes maximales de Q contenant uniquement x et y. Si la première sous-chaîne a au moins autant de x que de y, elle peut être réécrite sans introduire de défauts supplémentaires pour commencer par x. Si la première sous-chaîne a plus de y que de x, alors une autre sous-chaîne a plus de x que de y, et nous pouvons réécrire ces sous-chaînes sans défauts supplémentaires afin que x passe en premier. Dans les deux cas, on trouve une solution optimale T qui étend P ', selon les besoins.
Supposons maintenant que P finisse par y. Modifiez Q en déplaçant la première occurrence de x vers l'avant. Ce faisant, nous introduisons au plus un défaut (où x était auparavant) et éliminons un défaut (le yy).
Générer toutes les solutions
C'est la réponse de tobias_k plus des tests efficaces pour détecter quand le choix actuellement considéré est globalement contraint d'une manière ou d'une autre. Le temps de fonctionnement asymptotique est optimal, puisque le surcoût de génération est de l'ordre de la longueur de la sortie. Le retard dans le pire des cas est malheureusement quadratique; il pourrait être réduit à linéaire (optimal) avec de meilleures structures de données.
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]
est exactement le même que[1, 3, 1, 2, 1, 4, 1, 5]
par votre critère?