En fin de compte, vous aurez besoin d'une preuve mathématique d'exactitude. Je vais aborder quelques techniques de preuve pour cela ci-dessous, mais avant de plonger dans cela, laissez-moi vous faire gagner du temps: avant de chercher une preuve, essayez des tests aléatoires.
Tests aléatoires
Dans un premier temps, je vous recommande d'utiliser des tests aléatoires pour tester votre algorithme. C'est incroyable à quel point c'est efficace: d'après mon expérience, pour les algorithmes gourmands, les tests aléatoires semblent être déraisonnablement efficaces. Passez 5 minutes à coder votre algorithme, et vous pourriez vous épargner une heure ou deux en essayant de trouver une preuve.
L'idée de base est simple: implémentez votre algorithme. De plus, implémentez un algorithme de référence que vous savez être correct (par exemple, un algorithme qui essaie de manière exhaustive toutes les possibilités et tire le meilleur parti). C'est bien si votre algorithme de référence est asymptotiquement inefficace, car vous ne l'exécuterez que sur de petites instances à problème. Ensuite, générez au hasard un million de petites instances de problème, exécutez les deux algorithmes sur chacun et vérifiez si votre algorithme candidat donne la bonne réponse dans chaque cas.
Empiriquement, si votre algorithme gourmand candidat est incorrect, vous le découvrirez souvent lors de tests aléatoires. S'il semble être correct dans tous les cas de test, vous devez passer à l'étape suivante: proposer une preuve mathématique de correction.
Preuves mathématiques d'exactitude
OK, nous devons donc prouver que notre algorithme gourmand est correct: qu'il génère la solution optimale (ou, s'il existe plusieurs solutions optimales qui sont également bonnes, qu'il génère l'une d'entre elles).
Le principe de base est intuitif:
Principe: Si vous ne faites jamais de mauvais choix, vous ferez OK.
Les algorithmes gourmands impliquent généralement une séquence de choix. La stratégie de preuve de base est que nous allons essayer de prouver que l'algorithme ne fait jamais un mauvais choix. Les algorithmes gourmands ne peuvent pas revenir en arrière - une fois qu'ils ont fait un choix, ils sont engagés et n'annuleront jamais ce choix - il est donc essentiel qu'ils ne fassent jamais un mauvais choix.
Qu'est-ce qui constituerait un bon choix? S'il n'y a qu'une seule solution optimale, il est facile de voir ce qui est un bon choix: tout choix identique à celui fait par la solution optimale. En d'autres termes, nous essaierons de prouver que, à n'importe quelle étape de l'exécution des algorithmes gourmands, la séquence de choix effectuée par l'algorithme jusqu'à présent correspond exactement à un préfixe de la solution optimale. S'il existe plusieurs solutions optimales tout aussi bonnes, un bon choix est celui qui est compatible avec au moins l'un des optima. En d'autres termes, si la séquence de choix de l'algorithme jusqu'à présent correspond à un préfixe de l'une des solutions optimales, tout va bien jusqu'à présent (rien ne s'est encore mal passé).
Pour simplifier la vie et éliminer les distractions, concentrons-nous sur le cas où il n'y a pas de liens: il existe une solution optimale unique et unique. Toutes les machines seront reportées au cas où il peut y avoir plusieurs optima tout aussi bons sans aucun changement fondamental, mais vous devez être un peu plus prudent sur les détails techniques. Commencez par ignorer ces détails et concentrez-vous sur le cas où la solution optimale est unique; cela vous aidera à vous concentrer sur l'essentiel.
Il existe un modèle de preuve très courant que nous utilisons. Nous travaillerons dur pour prouver la propriété suivante de l'algorithme:
Affirmation: Soit la solution issue de l'algorithme et O la solution optimale. Si S est différent de O , alors nous pouvons modifions O pour obtenir une autre solution O * qui est différent de O et strictement mieux que O .SOSOOO∗OO
Remarquez pourquoi cela est utile. Si l'affirmation est vraie, il s'ensuit que l'algorithme est correct. Il s'agit essentiellement d'une preuve par contradiction. Soit est identique à O, soit différent. S'il est différent, alors nous pouvons trouver une autre solution O ∗ qui est strictement meilleure que O - mais c'est une contradiction, car nous avons défini O comme étant la solution optimale et il ne peut y avoir de solution meilleure que cela. Nous sommes donc obligés de conclure que S ne peut pas être différent de O ; S doit toujours être égal à OSOO∗OOSOSO, c'est-à-dire que l'algorithme gourmand génère toujours la bonne solution. Si nous pouvons prouver la revendication ci-dessus, alors nous avons prouvé que notre algorithme est correct.
Bien. Alors, comment pouvons-nous prouver la réclamation? On pense à une solution comme un vecteur ( S 1 , … , S n ) qui correspond à la séquence de n choix faite par l'algorithme, et de même, on pense à la solution optimale O comme un vecteur ( O 1 , … , O n ) correspondant à la séquence de choix qui conduirait à O . Si S est différent de O , il doit exister un indice i où S i ≠S( S1, … , Sn)nO( O1, … , On)OSOje ; nous allons nous concentrer sur le plus petit tel i . Ensuite, nous ajusterons O en changeant O un peu dans la i ème position pour correspondre à S i , c'est-à-dire que nous ajusterons la solution optimale O en changeant le i ème choix en celui choisi par l'algorithme gourmand, puis nous montrerons que cela conduit à une solution encore meilleure. En particulier, nous définirons O ∗ comme quelque chose commeSje≠ OjeiOOiSiOiO∗
O∗=(O1,O2,…,Oi−1,Si,Oi+1,Oi+2,…,On),
sauf que souvent nous devrons modifier légèrement la partie pour maintenir la cohérence globale. Une partie de la stratégie de preuve implique une certaine habileté à définir correctement O ∗ . Ensuite, la viande de la preuve sera en quelque sorte en utilisant des faits sur l'algorithme et le problème pour montrer que O ∗ est strictement meilleur que OOi+1,Oi+2,…,OnO∗O∗O; c'est là que vous aurez besoin de quelques informations spécifiques au problème. À un moment donné, vous devrez vous plonger dans les détails de votre problème spécifique. Mais cela vous donne une idée de la structure d'une preuve de correction typique pour un algorithme gourmand.
Un exemple simple: sous-ensemble avec somme maximale
Cela pourrait être plus facile à comprendre en travaillant à travers un exemple simple en détail. Considérons le problème suivant:
Entrée: Un ensemble d'entiers, un entier k Sortie: Un ensemble S ⊆ U de taille k dont la somme est la plus grande possibleUk
S⊆Uk
Il existe un algorithme naturel gourmand pour ce problème:
- Réglez .S:=∅
- Pour :
i:=1,2,…,k
Des tests aléatoires suggèrent que cela donne toujours la solution optimale, alors prouvons formellement que cet algorithme est correct. Notez que la solution optimale est unique, nous n'aurons donc pas à nous soucier des liens. Prouvons la revendication décrite ci-dessus:
SU,kOS≠OO∗O
S≠Oixi∉OiS≠OS={x1,…,xk}ix1,…,xi−1∈OOO={x1,x2,…,xi−1,x′i,x′i+1,…,x′n}x1,…,xi−1,x′i,…,x′nx1,…,xixi>x′jj≥ixi>x′iO=O∪{xi}∖{x′i}O∗iOxiO∗Oxi−x′ixi−x′i>0O∗O■
OOOO
Ox′ixi