Stratégie optimale pour un jeu abstrait

12

On m'a donné le problème suivant dans une interview (que j'ai déjà échoué à résoudre, n'essayant pas de me tromper): Le jeu commence avec un nombre entier positif . (Par exemple ) Ce nombre est converti en représentation binaire, et est le nombre de bits mis à . (Par exemple , )A0A0=1234N1A0=b100 1101 0010N=5.

Le joueur 1 choisit un nombre inférieur à . doit avoir qu'un seul bit mis à 1. (Par exemple ) Soit . (Par exemple ). Un mouvement est valide si satisfait les contraintes précédentes, et si le nombre de bits mis en est toujours égal à n .A 0 B 0 B 0 = b 10 0000 0000 = 512 A 1 = A 0 - B 0 A 1 = 1234 - 512 = 722 = b 10 1101 0010 B 0 A 1B0A0B0B0=b10 0000 0000=512A1=A0B0A1=1234512=722=b1011010010B0A1

Le joueur 2 continue depuis en choisissant un valide , puis le joueur 1 continue depuis , et ainsi de suite. Un joueur perd s'il n'a plus de coups valides.B 1 A 2A1B1A2

En supposant que les deux joueurs jouent de manière optimale, déterminez le joueur gagnant en utilisant une méthode raisonnablement efficace. (Dans ma définition de problème, les contraintes à cela étaient que le programme devait être en mesure de fournir une solution pour quelques millions de nombres d'entrée qui correspondent à un entier signé de 32 bits.) Autrement dit, la solution n'a pas besoin d'être entièrement analytique.


Mon intérêt personnel ici est de déterminer si l'attente de moi d'avoir trouvé et mis en œuvre la bonne solution sans aucun retour sur l'exactitude dans les 120 minutes qui m'ont été donnée était raisonnable; ou si c'était l'une de ces questions "voyons s'ils ont déjà vu ce puzzle".

J'avais échoué parce que j'avais choisi de mettre en œuvre ce qui semblait être une stratégie raisonnable, ce qui m'a donné des résultats corrects pour les quelques cas de test qui m'avaient été donnés à l'avance, j'ai perdu trop de temps à accélérer cette course et j'ai fini par remettre des données incorrectes. pleine sortie que mon temps est écoulé.

Rétrospectivement, j'aurais dû implémenter une recherche par force brute et mémoriser des solutions partielles pour les petits nombres de départ, mais le recul est toujours de 20/20. Je suis curieux cependant s'il y a une approche commune différente qui m'a échappé en tant que flunkee.

millimoose
la source
D'après la description, je n'ai pas compris que les mouvements choisis doivent avoir un seul bit mis à 1 (je pensais que c'était juste une partie de l'exemple).
jjmontes
@jjmontes - Il est indiqué comme la toute première règle pour choisir un nombre B - tous les exemples sont indiqués comme tels, tout ce qui est en dehors des parenthèses est général. Avez-vous une suggestion sur la façon dont cela aurait pu être plus clair?
millimoose
1
Peut-être que "le joueur 1 choisit un nombre inférieur à , qui ne doit avoir qu'un seul bit à 1."? (peut-être que c'était juste moi, mais j'ai dû lire la réponse @orlp pour réaliser que c'était une contrainte). A 0B0A0
jjmontes
@Veedrac - Man, si je l'avais su, tous mes efforts pour faire fonctionner le bitcount assez rapidement n'auraient pas été un gaspillage. Une réponse expliquant pourquoi cela fonctionne serait excellente.
millimoose
@millimoose Bitcount est en matériel pour la plupart des processeurs modernes!
Veedrac

Réponses:

21

Prenez un moment pour vous rendre compte que si nous ne pouvons soustraire qu'un pouvoir de deux, et que le nombre ne peut pas changer, nous devons soustraire dans une position où l'autre nombre est . Le résultat est toujours dans cette position, et le nombre ne change nulle part ailleurs.10 01011001

En d'autres termes, le jeu est une série de swaps de , et le jeu se termine si tous sont du côté droit. Notez qu'il est impossible que ce jeu se termine tôt - vous ne pouvez pas rester bloqué. Vous vous retrouverez toujours à une position où tous les zéros sont à gauche et tous ceux à droite.1001

Donc, le seul facteur déterminant dans un jeu est le nombre de swaps nécessaires pour arriver à l'état où tous sont à droite, et il n'y a pas de stratégie gagnante ou perdante. La parité du nombre de swaps qu'il faut est le seul facteur déterminant.

Alors, combien de swaps faut-il? Notez que les ne peuvent pas se croiser, donc si nous les numérotions et les suivions via des swaps, ils resteraient dans le même ordre dans l'état final. Chaque échange les rapproche de leur position finale.1

Donc, si le th (en comptant à partir de la droite, le le plus à droite est le th ) est en position partir de la droite, il a besoin de swaps pour arriver à sa position correcte. Cela nous donne un algorithme pour compter le nombre de swaps requis:1 1 0 1 k k - ii1101kki

i = 0
k = 0
total = 0
while n > 0:
    if n & 1:
       total += k - i
       i += 1
    n >>= 1
    k += 1

Nous pouvons maintenant regarder la parité de totalpour voir qui gagne. La complexité temporelle est .O(logn)

orlp
la source
Cela semble à peu près juste, je suis tombé sur des morceaux de cette approche après le hand-in. Je suppose que j'en avais fini en sautant le pistolet en commençant à coder et en étant embourbé dans la chasse aux oies sauvages qui en résultait.
millimoose
En y réfléchissant, le fait que la stratégie n'ait pas d'importance signifie soit que j'ai un bug plutôt obscur dans mon implémentation, soit qu'elle aurait dû produire les mêmes résultats que n'importe quelle autre implémentation qui joue correctement le jeu…
millimoose
5

Une façon de résoudre un tel problème est la suivante:

  • Trouvez la solution pour quelques valeurs simples en utilisant l'approche de "force brute mémorisée" que vous proposez.

  • Devinez la réponse (quelles positions gagnent et lesquelles perdent).

  • Essayez de prouver votre réponse. Si vous réussissez, tant mieux. Sinon, essayez de trouver un contre-exemple et utilisez-le pour deviner une autre réponse. Ici, il pourrait être utile de résoudre quelques cas supplémentaires.

Il est vraiment difficile de dire combien de temps cela prend. Cependant, lors des entretiens, vous ne devez pas nécessairement trouver la solution. Les enquêteurs veulent plutôt savoir comment vous avez abordé la résolution du problème et quels progrès vous avez réussi à réaliser.

Yuval Filmus
la source
Ouais non, ils m'ont rejeté parce que ma sortie était fausse et que je n'avais plus de temps.
millimoose
L'approche de la force brute mémorisée aurait été correcte, car elle ne nécessite aucun raccourci concernant la stratégie. Cependant, cela aurait aussi - je pensais - être insupportablement lent, et la mémorisation aurait peut-être été trop de travail pour peut-être pas beaucoup d'aide sans utiliser une quantité stupide de mémoire. Ou peut-être pas, je vais essayer plus tard juste pour effacer cela de mon système.
millimoose
5

Notez de la réponse de @ orlp que nous voulons la parité de la somme des déplacements de la position de départ à la position de fin. Annotons ceci:

       9876543210
       9 76 4  1    (positions at start)
start: 1011010010
end:   0000011111
            43210   (positions at end)

Nous voulons donc

  ((1 - 0) + (4 - 1) + (6 - 2) + (7 - 3) + (9 - 4)) & 1
= ((1 + 4 + 6 + 7 + 9) - (0 + 1 + 2 + 3 + 4)) & 1
= ((1 + 0 + 0 + 1 + 1) - (0 + 1 + 0 + 1 + 0)) & 1

La première partie est juste la parité du nombre de bits dans les positions impaires. Vous pouvez masquer cela en prenant le nombre entier non signé maximum, en divisant par 0b11 et en niant.

= (bitcount(x & ~(UINT_MAX / 0b11)) ^ (0 + 1 + 0 + 1 + 0)) & 1

La deuxième partie est la parité de la moitié du nombre de bits x.

= (bitcount(x & ~(UINT_MAX / 0b11)) ^ (bitcount(x) >> 1)) & 1

bitcountpeut soit utiliser l' popcntinstruction matérielle , soit être implémentée manuellement en utilisant uniquement le dernier ou l'avant-dernier bit, avec des réductions rapides comme celle-ci .

Veedrac
la source