Quel est l'algorithme optimal de coupe d'ongle juif?

118

Je travaille sur le logiciel d'une machine qui coupe automatiquement les ongles des pieds, afin que les utilisateurs puissent simplement y mettre leurs pieds et l'exécuter au lieu d'avoir à le faire manuellement en les mordant ou en utilisant un coupe-ongles.

Un pourcentage important de notre base d'utilisateurs potentiels sera probablement juif et, de toute évidence, il existe une tradition de ne pas couper les ongles des pieds ( ou des ongles ) dans un ordre séquentiel.

Il semble y avoir une opinion dissidente sur l'application précise de cette tradition, mais nous pensons que les règles suivantes sont suffisantes pour accueillir les personnes dont les pratiques religieuses interdisent de couper les ongles dans l'ordre:

  • Pas deux ongles adjacents ne doivent être coupés consécutivement
  • La séquence de coupe sur le pied gauche ne doit pas correspondre à la séquence sur le pied droit
  • La séquence de coupe sur deux passages consécutifs ne doit pas être la même. Les séquences ne devraient pas être facilement prévisibles, donc coder en dur une séquence alternée ne fonctionne pas.

C'est ainsi que nous avons décidé de numéroter les orteils:

5 4 3 2 1  1 2 3 4 5
Left foot  Right foot

J'ai écrit du code pour résoudre le problème, mais l'algorithme utilisé est sous-optimal: en fait, le pire des cas est O (∞) . Son fonctionnement est comparable à celui du bogosort . Voici une simplification du pseudocode du code réel utilisé:

function GenerateRandomSequence
   sequence = Array[5]
   foreach (item in sequence)
       item = RandomNumberBetween(1,5)
   return sequence

function GetToenailCuttingOrder
   while (true)
      sequence = GenerateRandomSequence()
      if (!AllItemsAreUnique(sequence))
         continue
      if (NoTwoAdjacentItemsHaveConsecutiveNumbers(sequence))
         return sequence

do
    leftFootSequence = GetToenailCuttingOrder()
    rightFootSequence = GetToenailCuttingOrder()
until (leftFootSequence != rightFootSequence &&
       leftFootSequence != leftFootSequenceFromLastRun &&
       rightFootSequence != rightFootSequenceFromLastRun)

Fondamentalement, il génère des séquences aléatoires et vérifie si elles répondent aux critères. S'il ne répond pas aux critères, il recommence. Cela ne prend pas un temps ridiculement long, mais c'est très imprévisible.

Je me rends compte que la façon dont je le fais actuellement est assez terrible, mais j'ai du mal à trouver une meilleure solution. L'un de vous peut-il suggérer un algorithme plus élégant et plus performant?

Peter Olson
la source
28
Cela sent comme un problème de devoirs. Sinon, pourquoi ne pas simplement coder en dur la séquence?
Michael Brown
24
J'ai entendu parler de se ronger les ongles, mais les ongles des pieds?
vole
63
L'idée d'une machine à couper les ongles est assez effrayante. J'espère que ce sont effectivement des devoirs et non une tragédie douloureuse qui attend de se produire.
Peter Recore
43
Le défi de la programmation ici est de contrôler la machine qui coupe les ongles des pieds afin qu'elle ne blesse pas les gens. S'il y a un programmeur capable de résoudre ce problème, alors cette personne peut sûrement résoudre ce problème en deux minutes.
vole
41
J'aime le fait qu'une question sur la tradition juive soit étiquetée comme agnostique (de la langue) ... :-)
Steve Melnikoff

Réponses:

87

Vous pouvez générer toutes les séquences de coupe d'ongle d'orteil possibles sans aucune restriction, puis filtrer toutes les séquences qui violent la règle juive. Heureusement, les humains n'ont que cinq orteils par pied *, donc il n'y en a que 5! = 120 séquences sans restriction.

Exemple Python:

#seq is only valid when consecutive elements in the list differ by at least two.
def isValid(seq):
    for i in range(len(seq)-1):
        a = seq[i]
        b = seq[i+1]
        if abs(a-b) == 1:
            return False
    return True


from itertools import ifilter, permutations
validseqs = ifilter(isValid, permutations([1,2,3,4,5]))
for i in validseqs:
    print i

(1, 3, 5, 2, 4)
(1, 4, 2, 5, 3)
(2, 4, 1, 3, 5)
(2, 4, 1, 5, 3)
(2, 5, 3, 1, 4)
(3, 1, 4, 2, 5)
(3, 1, 5, 2, 4)
(3, 5, 1, 4, 2)
(3, 5, 2, 4, 1)
(4, 1, 3, 5, 2)
(4, 2, 5, 1, 3)
(4, 2, 5, 3, 1)
(5, 2, 4, 1, 3)
(5, 3, 1, 4, 2)

Pour appliquer votre règle "pas de répétition de la même séquence", vous pouvez simplement choisir quatre des séquences ci-dessus et les utiliser en alternance. Le seul hic ici est que si vous comptez les deux gros orteils comme "consécutifs", vous ne pouvez pas choisir deux séquences qui se terminent et commencent par 1, respectivement.

* Vous pouvez créer une variable numberOfToesPerFoot afin de pouvoir la modifier facilement plus tard si l'un de vos clients s'avère avoir moins d'orteils que prévu, ou plus.

Kevin
la source
22
Vous avez raison! Je n'ai même jamais considéré les gens avec polydactly . Ce serait une erreur de les exclure.
Peter Olson
1
le cas du moins d'orteils est couvert par l'algorithme d'origine (des séquences acceptables pour 5 orteils sont acceptables pour 4 orteils). Ce sont ces orteils supplémentaires fous qui causent des problèmes;)
vole
4
Très belle solution! J'aborderais cependant "pas de répétition de la même séquence" légèrement différent. Faites simplement que la machine se souvienne de la séquence qu'elle a utilisée en dernier et utilisez ensuite une séquence aléatoire (mais pas la même). Cela fonctionne aussi bien pour le deuxième pied que pour les nouveaux clients et c'est plus aléatoire que de s'en tenir à 4 séquences.
Jakob
3
Il faudrait également tenir compte des orteils manquants lors de l'amputation, comme le 3e orteil manquant. Cela pose des problèmes si, par exemple, la suppression du 3ème orteil fait maintenant considérer les orteils 2 et 4 comme séquentiels.
cdeszaq
2
Qu'en est-il des personnes qui n'ont que 2 orteils dans un pied? Sont-ils autorisés à se couper les ongles?
matiasg le
26

Il existe un nombre fini de séquences qui répondent à vos besoins.

  1. Générez toutes les permutations de {1,2,3,4,5}. Il n'y en a que 120.
  2. Rejetez ceux qui ne satisfont pas aux exigences et stockez l'ensemble restant (en permanence).
  3. Choisissez au hasard deux séquences différentes. Rappelez-vous ceux que vous avez utilisés la dernière fois.

EDIT: S'il ne s'agit pas vraiment d'orteils, mais d'un problème aléatoire où l'ensemble peut être beaucoup plus grand que 5, l'espace de séquence devient très grand et la chance de répéter la même séquence sur le deuxième pied devient très petite. Donc, générer des séquences au hasard et les rejeter si elles correspondent est une bonne idée. Générer des séquences aléatoires selon une règle comme "sautez par deux ou trois, puis remplissez les blancs" sera probablement plus rapide que de générer des permutations aléatoires et des tests, et le risque de chevauchement sera encore faible si le nombre de "orteils" est grand .

mouches
la source
20

En fait, j'aime mieux votre algorithme d'origine.

Puisque 14 permutations sur 120 fonctionnent, 106 sur 120 ne le font pas. Ainsi, chaque test a une chance d'échouer de 106/120.

Cela signifie que le nombre d'échecs attendu est:

1*(106/120) + 2*(106/120)^2 + 3*(106/120)^3 + ...

Pas trop difficile de résumer cette série infinie:

S       = 1*x + 2*x^2 + 3*x^3 + ...

Multipliez par x:

x*S     =       1*x^2 + 2*x^3 + ...

Soustraire:

S - x*S = x + x^2 + x^3 + ...

Multipliez à nouveau par x et soustrayez à nouveau:

x*S - x^2*S = x^2 + x^3 + ...
S - 2*x*S + x^2S = x
S*(1-x)^2 = x
S = x/(1-x)^2

Puisque x = 106/120, S = 64,9.

Ainsi, en moyenne, votre boucle n'a besoin que de 65 itérations pour trouver une solution.

Quelle est la probabilité que cela prenne, disons, mille itérations?

Eh bien, la probabilité d'échouer à une seule itération est de 104/120, donc la probabilité d'échouer 1000 itérations est de (104/120) ^ 1000, ce qui équivaut à 10 ^ (- 63). Autrement dit, vous ne verrez jamais cela se produire de votre vie, et probablement pas de la vie de l'univers.

Pas de tableaux précalculés, adaptation facile à différents nombres de doigts / orteils / mains / pieds, adaptation facile à différents ensembles de règles ... Qu'est-ce qu'il ne faut pas aimer? La décomposition exponentielle est une chose merveilleuse.

[mettre à jour]

Oups, je me suis trompé dans la formule originale ... Puisque mes probabilités ne totalisent pas 1 :-)

L'expression correcte du nombre d'échecs attendu est:

0*(14/120) + 1*(106/120)*(14/120) + 2*(106/120)^2*(14/120) + ...

(Par exemple, pour obtenir exactement deux échecs, vous avez besoin de deux échecs suivis d'un succès . Deux échecs ont une probabilité (106/120) ^ 2; un succès a une probabilité (14/120); multipliez-les ensemble pour obtenir le poids du Terme "2".)

Donc mon S est décalé d'un facteur (1-x) (c'est-à-dire 14/120). Le nombre réel d'échecs attendu est simplement x / (1-x) = 106/14 = 7,57. Ainsi, en moyenne, il ne faut que 8 à 9 itérations pour trouver une solution (7,5 échecs plus un succès).

Mon calcul pour le cas des «1000 échecs» est toujours correct, je pense.

Nemo
la source
1
+1 pour sortir des sentiers battus et donner une manière différente de voir le problème.
nalply
9

L'évidence: trouvez une commande qui fonctionne et codez-la en dur. Mais je ne pense pas que vous vouliez faire ça.

Vous pouvez générer des permutations bien mieux que la façon dont vous le faites. Vous n'avez pas besoin de faire un échantillonnage de rejet. Utilisez un mélange Fisher Yates sur une permutation initialement triée (1, 2, .. 5) et vous obtiendrez une permutation aléatoire. http://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle

Mais en général, la méthode de génération et de test me semble tout à fait correcte, tant que la probabilité de générer une entrée réussie est suffisamment élevée. Je suis sûr qu'il existe de nombreuses séquences valides selon vos critères, une fois que vous passez à une permutation aléatoire, je doute que vous deviez faire de nombreuses itérations de rejet.

Rob Neuhaus
la source
2

Rien de vraiment nouveau ici, la même solution @Kevin déjà postée, mais je trouve intéressant de voir comment cela se traduit en langage fonctionnel. Dans ce cas, Mathematica :

Extract[#,Position[Times @@ (Abs@#-1)&/@ Differences/@ #, Except@0, 1][[2 ;;]]]  
         &@ Permutations@Range@5

Quelques explications:

Permutations@Range@5 Calculates all permutations of {1, 2, 3, 4, 5}

Differences          Calculate the differences between adjacent elements
                     (we wish to discard all lists containing +1 or -1)

Times @@ (Abs@#-1)   Abs turns the -1s into +1s, and then to zeros by subtracting
                     one, then TIMES multiplies all elements, giving zero when 
                     the original result of "Differences" contained a +1 or a -1

Position ... Except@0 Returns the position of the non zero results

Extract              Returns the original elements according to the calculated 
                     positions

Le résultat final est:

{{1, 3, 5, 2, 4}, {1, 4, 2, 5, 3}, {2, 4, 1, 3, 5}, {2, 4, 1, 5, 3}, 
 {2, 5, 3, 1, 4}, {3, 1, 4, 2, 5}, {3, 1, 5, 2, 4}, {3, 5, 1, 4, 2}, 
 {3, 5, 2, 4, 1}, {4, 1, 3, 5, 2}, {4, 2, 5, 1, 3}, {4, 2, 5, 3, 1}, 
 {5, 2, 4, 1, 3}, {5, 3, 1, 4, 2}}

Éditer

Ou, plus difficile à expliquer, mais plus court:

Reap[ Table[ If[Times @@ (Abs@Differences@i - 1) != 0, Sow@i],
           {i, Permutations@Range@5}]][[2, 1]]
Dr Bélisarius
la source
0

Il n'y a vraiment aucune raison d'introduire le hasard dans ce problème. Il n'y a que 14 séquences qui répondent à ce problème, et un certain ordre de ces séquences satisferait certainement le mieux le sens esthétique que vous essayez d'accommoder. Ainsi, vous devriez simplement réduire ce problème à un algorithme pour choisir une séquence parmi ces 14, probablement dans un ordre prédéfini.

Implémentation Javascript de l'algorithme pour trouver le 14:

function swap (a, i, j) {
  var temp = a[i]
  a[i]=a[j]
  a[j]=temp
}

function permute (b, n, a) {
  if (n==4) {
    b.push(a.slice(0)) //copy array
  }
  else {
    for (var i = n; i < 5; i++) {
      swap(a,n,i)
      permute(b, n+1, a)
      swap(a,n,i)
    }
  }
}

var a = [1,2,3,4,5]
var b = []
var c = []

permute(b,0,a)

for (var i = 1; i < b.length-1; i++) {
  var good = true
  for (var j = 0; j < b[i].length; j++) {
    if (Math.abs(b[i][j-1]-b[i][j]) < 2 || Math.abs(b[i][j]-b[i][j+1]) < 2) {
      good = false
    }
  }
  if (good) c.push(b[i].join(''))
}

console.log(c)

EDIT: La nouvelle exigence selon laquelle les séquences doivent être générées de manière aléatoire ne peut pas être facilement satisfaite. Le mieux que vous puissiez probablement faire est de les générer de manière pseudo-aléatoire, ce qui est tout aussi déterministe que de les coder en dur à l'avance, et ne devrait donc satisfaire les superstitions de personne.

Tamzin Blake
la source