Problem Summary

Each line of the dataset contains ten cards. The first five belong to Player 1, the next five belong to Player 2. The task is to evaluate both 5-card poker hands under the standard ranking rules and count how many lines are won by Player 1.

This is not a search problem and it is not a probability problem. The whole job is to turn each hand into a mathematically ordered descriptor and then compare the two descriptors.

Mathematical Approach

For a hand \(H=\{(r_i,s_i)\}_{i=1}^5\), the relevant data are the five ranks \(r_i\in\{2,\dots,14\}\), the suit pattern, and the multiplicities of equal ranks. Once those objects are extracted, poker comparison becomes a total lexicographic order.

Ranks, suits, and multiplicity pattern

Write the sorted ranks as \(a_1\le a_2\le a_3\le a_4\le a_5\). Let \(m(v)\) denote the number of cards of rank \(v\). The multiplicity pattern must be one of the partitions of 5:

$$4+1,\quad 3+2,\quad 3+1+1,\quad 2+2+1,\quad 2+1+1+1,\quad 1+1+1+1+1.$$

These patterns already identify all frequency-based hand types: four of a kind, full house, three of a kind, two pairs, one pair, and high card. Two further structural tests are needed:

$$\operatorname{flush}(H)\iff s_1=s_2=s_3=s_4=s_5,$$

$$\operatorname{straight}(H)\iff a_{i+1}=a_i+1\text{ for }i=1,2,3,4.$$

An ace-high straight flush is simply the largest straight flush, so the implementations do not need a separate royal-flush category. The Python implementation also includes the usual normalization \(A,2,3,4,5\mapsto 1,2,3,4,5\) when detecting the special 5-high straight; the ranking framework is otherwise shared.

A canonical evaluation map

The key idea is to encode every hand as

$$E(H)=(c(H),t(H)),$$

where \(c(H)\) is a category number and \(t(H)\) is an ordered tie-break vector. The category order used by the implementations is

$$8=\text{straight flush},\ 7=\text{four of a kind},\ 6=\text{full house},\ 5=\text{flush},\ 4=\text{straight},\ 3=\text{three of a kind},\ 2=\text{two pairs},\ 1=\text{one pair},\ 0=\text{high card}.$$

Now collect the distinct ranks as pairs \((m(v),v)\) and sort these pairs first by descending multiplicity and then by descending rank. Let the sorted list be

$$((c_1,v_1),(c_2,v_2),\dots).$$

This ordering invariant is what makes the solution compact: the same sorted group list automatically places the most important repeated ranks before the less important ones, and among kickers it automatically uses descending card values.

With that convention, the evaluation map is

$$E(H)= \begin{cases} (8,(a_5)) & \text{if }\operatorname{straight}(H)\land \operatorname{flush}(H),\\ (7,(v_1,v_2)) & \text{if }c_1=4,\\ (6,(v_1,v_2)) & \text{if }(c_1,c_2)=(3,2),\\ (5,(a_5,a_4,a_3,a_2,a_1)) & \text{if }\operatorname{flush}(H),\\ (4,(a_5)) & \text{if }\operatorname{straight}(H),\\ (3,(v_1,v_2,v_3)) & \text{if }c_1=3,\\ (2,(v_1,v_2,v_3)) & \text{if }c_1=c_2=2,\\ (1,(v_1,v_2,v_3,v_4)) & \text{if }c_1=2,\\ (0,(a_5,a_4,a_3,a_2,a_1)) & \text{otherwise.} \end{cases}$$

For two pairs, this means \(v_1\) is the higher pair, \(v_2\) is the lower pair, and \(v_3\) is the kicker. For one pair, \(v_1\) is the pair and the remaining entries are the kickers from largest to smallest.

Why lexicographic comparison works

Once both hands have been mapped to \(E(H)\), comparison is reduced to ordinary lexicographic order:

$$H_1\text{ beats }H_2\iff E(H_1)>_{\mathrm{lex}}E(H_2).$$

This works because poker comparison is hierarchical. First compare the hand classes; if the classes tie, compare the most important repeated rank; if that still ties, continue through the remaining kickers in descending priority. The vector \(t(H)\) stores exactly that order of importance.

That is the main invariant used by the code. Instead of writing a different comparison routine for every hand type, the implementations normalize every hand into the same mathematical object: category plus ordered tie-break vector.

Worked examples

Take

$$H_1=\{5H,5C,6S,7S,KD\},\qquad H_2=\{2C,3S,8S,8D,TD\}.$$

Both hands are one pair, so both have category \(1\). Their evaluation vectors are

$$E(H_1)=(1,(5,13,7,6)),\qquad E(H_2)=(1,(8,10,3,2)).$$

The categories match, but the pair rank \(8\) is larger than \(5\), so the second hand wins immediately.

Now consider

$$H_1=\{4D,6S,9H,QH,QC\},\qquad H_2=\{3D,6D,7H,QD,QS\}.$$

Again both hands are one pair, and both pairs are queens. Their vectors are

$$E(H_1)=(1,(12,9,6,4)),\qquad E(H_2)=(1,(12,7,6,3)).$$

The pair value ties at \(12\), so the comparison moves to the largest kicker. Since \(9>7\), the first hand wins. This example shows why storing the kickers in the correct order is essential.

How the Code Works

Parsing the cards

The C++, Python, and Java implementations read one line at a time, split it into ten 2-character tokens, convert \(2,3,\dots,9,T,J,Q,K,A\) into integers \(2\) through \(14\), and separate the first five cards from the last five.

Evaluating one hand

For each hand, the implementation counts equal ranks, checks whether all suits are the same, sorts the ranks, and tests whether the sorted ranks are consecutive. It also builds the sorted multiplicity list \((m(v),v)\). Using these ingredients, it returns the category number together with the correctly ordered tie-break vector.

Comparing the players and counting wins

After both hands are evaluated, the implementation compares categories first and tie-break vectors second. If Player 1 has the larger evaluation, the running total is incremented. The dataset is constructed so that each line has a definite winner, so no extra tie-resolution layer is needed beyond the lexicographic comparison already described.

Complexity Analysis

Every line contains only two 5-card hands, so the per-line cost is constant. Sorting five ranks, counting frequencies of five cards, and checking for a straight or a flush are all \(O(1)\) operations with very small constants.

If there are \(N\) lines, the total running time is \(O(N)\) and the extra memory usage is \(O(1)\). For the given file of 1000 hands, the computation is effectively instantaneous.

Footnotes and References

  1. Problem page: Project Euler 54 - Poker Hands
  2. Poker hand rankings: Wikipedia - List of poker hands
  3. Poker probabilities: Wikipedia - Poker probability
  4. Lexicographic order: Wikipedia - Lexicographic order

Problem Summary / Problemzusammenfassung

Jede Zeile der Daten enthält zehn Karten. Die ersten fünf gehören Spieler 1, die nächsten fünf Spieler 2. Gesucht ist, wie oft die 5-Karten-Hand von Spieler 1 unter den üblichen Pokerregeln gewinnt.

Das ist weder ein Suchproblem noch ein Wahrscheinlichkeitsproblem. Man muss jede Hand nur in einen mathematisch geordneten Deskriptor umwandeln und dann beide Deskriptoren vergleichen.

Mathematical Approach / Mathematischer Ansatz

Für eine Hand \(H=\{(r_i,s_i)\}_{i=1}^5\) sind nur die fünf Kartenwerte \(r_i\in\{2,\dots,14\}\), das Farb-Muster und die Häufigkeiten gleicher Werte relevant. Sobald diese Objekte bestimmt sind, wird der Pokervergleich zu einer totalen lexikographischen Ordnung.

Ränge, Farben und Häufigkeitsmuster

Schreibe die sortierten Werte als \(a_1\le a_2\le a_3\le a_4\le a_5\). Sei \(m(v)\) die Anzahl der Karten mit Wert \(v\). Das Häufigkeitsmuster ist genau eine der Zerlegungen von 5:

$$4+1,\quad 3+2,\quad 3+1+1,\quad 2+2+1,\quad 2+1+1+1,\quad 1+1+1+1+1.$$

Damit sind bereits alle durch Wiederholungen definierten Handtypen festgelegt: Vierling, Full House, Drilling, zwei Paare, ein Paar oder High Card. Zusätzlich braucht man zwei strukturelle Tests:

$$\operatorname{flush}(H)\iff s_1=s_2=s_3=s_4=s_5,$$

$$\operatorname{straight}(H)\iff a_{i+1}=a_i+1\text{ für }i=1,2,3,4.$$

Ein Ass-hoher Straight Flush ist einfach der größte Straight Flush; deshalb brauchen die Implementierungen keine eigene Royal-Flush-Kategorie. Die Python-Implementierung enthält außerdem die übliche Normalisierung \(A,2,3,4,5\mapsto 1,2,3,4,5\) für die 5-hohe Straße; das Grundschema der Bewertung ist ansonsten identisch.

Eine kanonische Bewertungsabbildung

Der zentrale Schritt ist, jeder Hand den Wert

$$E(H)=(c(H),t(H))$$

zuzuordnen, wobei \(c(H)\) die Kategorie und \(t(H)\) der geordnete Tie-Break-Vektor ist. Die von den Implementierungen verwendete Kategorienordnung lautet

$$8=\text{Straight Flush},\ 7=\text{Vierling},\ 6=\text{Full House},\ 5=\text{Flush},\ 4=\text{Straight},\ 3=\text{Drilling},\ 2=\text{zwei Paare},\ 1=\text{ein Paar},\ 0=\text{High Card}.$$

Fasse nun jeden unterschiedlichen Wert als Paar \((m(v),v)\) zusammen und sortiere diese Paare zuerst nach absteigender Häufigkeit und dann nach absteigendem Wert. Die sortierte Liste sei

$$((c_1,v_1),(c_2,v_2),\dots).$$

Genau diese Ordnungsinvariante macht die Lösung kurz: Wiederholte Kartenwerte mit höherer Priorität stehen automatisch vorne, und Kicker stehen automatisch in absteigender Reihenfolge ihrer Werte.

Damit ist die Bewertungsabbildung

$$E(H)= \begin{cases} (8,(a_5)) & \text{falls }\operatorname{straight}(H)\land \operatorname{flush}(H),\\ (7,(v_1,v_2)) & \text{falls }c_1=4,\\ (6,(v_1,v_2)) & \text{falls }(c_1,c_2)=(3,2),\\ (5,(a_5,a_4,a_3,a_2,a_1)) & \text{falls }\operatorname{flush}(H),\\ (4,(a_5)) & \text{falls }\operatorname{straight}(H),\\ (3,(v_1,v_2,v_3)) & \text{falls }c_1=3,\\ (2,(v_1,v_2,v_3)) & \text{falls }c_1=c_2=2,\\ (1,(v_1,v_2,v_3,v_4)) & \text{falls }c_1=2,\\ (0,(a_5,a_4,a_3,a_2,a_1)) & \text{sonst.} \end{cases}$$

Bei zwei Paaren ist also \(v_1\) automatisch das höhere Paar, \(v_2\) das niedrigere Paar und \(v_3\) der Kicker. Bei einem Paar ist \(v_1\) das Paar und die restlichen Einträge sind die Kicker vom größten zum kleinsten.

Warum lexikographischer Vergleich genügt

Sobald beide Hände auf \(E(H)\) abgebildet sind, entscheidet gewöhnliche lexikographische Ordnung:

$$H_1\text{ schlägt }H_2\iff E(H_1)>_{\mathrm{lex}}E(H_2).$$

Das funktioniert, weil Pokervergleiche hierarchisch sind. Zuerst vergleicht man die Handklasse, dann den wichtigsten wiederholten Wert und danach die verbleibenden Kicker in absteigender Priorität. Der Vektor \(t(H)\) speichert genau diese Reihenfolge.

Das ist die wichtigste Invariante im Code. Anstatt für jede Handart einen eigenen Vergleich zu schreiben, normieren die Implementierungen jede Hand auf dasselbe mathematische Objekt: Kategorie plus geordneter Tie-Break-Vektor.

Durchgerechnete Beispiele

Betrachte

$$H_1=\{5H,5C,6S,7S,KD\},\qquad H_2=\{2C,3S,8S,8D,TD\}.$$

Beide Hände sind ein Paar, also Kategorie \(1\). Ihre Bewertungsvektoren sind

$$E(H_1)=(1,(5,13,7,6)),\qquad E(H_2)=(1,(8,10,3,2)).$$

Die Kategorie ist gleich, aber der Paarwert \(8\) ist größer als \(5\). Deshalb gewinnt sofort die zweite Hand.

Noch feiner ist

$$H_1=\{4D,6S,9H,QH,QC\},\qquad H_2=\{3D,6D,7H,QD,QS\}.$$

Wieder sind beide Hände ein Paar, und beide Paare sind Damen. Die Vektoren lauten

$$E(H_1)=(1,(12,9,6,4)),\qquad E(H_2)=(1,(12,7,6,3)).$$

Der Paarwert ist identisch, also entscheidet der größte Kicker. Weil \(9>7\), gewinnt die erste Hand. Genau deshalb muss die Kicker-Reihenfolge korrekt gespeichert werden.

How the Code Works

Die Karten einlesen

Die C++-, Python- und Java-Implementierungen lesen die Datei zeilenweise, zerlegen jede Zeile in zehn 2-stellige Kartentokens, übersetzen \(2,3,\dots,9,T,J,Q,K,A\) in die Zahlen \(2\) bis \(14\) und trennen die ersten fünf von den letzten fünf Karten.

Eine einzelne Hand bewerten

Für jede 5-Karten-Hand zählt die Implementierung gleiche Werte, prüft, ob alle Farben gleich sind, sortiert die Werte und testet auf aufeinanderfolgende Zahlen. Zusätzlich wird die sortierte Häufigkeitsliste \((m(v),v)\) aufgebaut. Daraus entstehen dann die Kategorie und der korrekt geordnete Tie-Break-Vektor.

Beide Spieler vergleichen und Siege zählen

Nachdem beide Hände ausgewertet sind, vergleicht die Implementierung zuerst die Kategorie und danach den Tie-Break-Vektor. Ist die Auswertung von Spieler 1 größer, wird der Zähler erhöht. Die Eingabedaten sind so gewählt, dass jede Zeile einen eindeutigen Sieger hat; daher reicht der bereits beschriebene lexikographische Vergleich vollständig aus.

Complexity Analysis / Komplexitätsanalyse

Jede Zeile enthält nur zwei 5-Karten-Hände, daher ist der Aufwand pro Zeile konstant. Das Sortieren von fünf Werten, das Zählen von fünf Karten und die Prüfungen auf Straight oder Flush sind alles \(O(1)\)-Operationen mit winzigen Konstanten.

Bei \(N\) Zeilen beträgt die Gesamtlaufzeit \(O(N)\), und der zusätzliche Speicherverbrauch ist \(O(1)\). Für den gegebenen Datensatz mit 1000 Händen ist die Rechnung praktisch sofort fertig.

Footnotes and References

  1. Problemseite: Project Euler 54 - Poker Hands
  2. Poker-Handrangfolgen: Wikipedia - Pokerblätter
  3. Poker-Wahrscheinlichkeiten: Wikipedia - Poker probability
  4. Lexikographische Ordnung: Wikipedia - Lexikografische Ordnung

Problem Summary / Sorun Özeti

Veri kümesindeki her satırda on kart vardır. İlk beş kart Oyuncu 1'e, sonraki beş kart Oyuncu 2'ye aittir. Amaç, standart poker sıralamasına göre Oyuncu 1'in kaç satırda kazandığını saymaktır.

Bu problem bir arama problemi değildir; olasılık hesabı da gerektirmez. Yapılması gereken tek şey, her eli matematiksel olarak sıralanabilir bir gösterime dönüştürmek ve sonra bu iki gösterimi karşılaştırmaktır.

Mathematical Approach / Matematiksel Yaklaşım

\(H=\{(r_i,s_i)\}_{i=1}^5\) biçimindeki bir el için önemli olan veriler beş kartın değerleri \(r_i\in\{2,\dots,14\}\), renk desenleri ve eşit değerlerin çokluklarıdır. Bu nesneler çıkarıldıktan sonra poker karşılaştırması tam bir leksikografik sıralamaya indirgenir.

Değerler, renkler ve çokluk deseni

Sıralanmış değerleri \(a_1\le a_2\le a_3\le a_4\le a_5\) olarak yazalım. \(m(v)\), değeri \(v\) olan kart sayısı olsun. Çokluk deseni ancak 5'in şu ayrışımlarından biri olabilir:

$$4+1,\quad 3+2,\quad 3+1+1,\quad 2+2+1,\quad 2+1+1+1,\quad 1+1+1+1+1.$$

Bunlar zaten frekans tabanlı tüm el tiplerini belirler: dörtlü, full house, üçlü, iki çift, bir çift ve yüksek kart. Ayrıca iki yapısal test gerekir:

$$\operatorname{flush}(H)\iff s_1=s_2=s_3=s_4=s_5,$$

$$\operatorname{straight}(H)\iff a_{i+1}=a_i+1\text{ her }i=1,2,3,4\text{ için.}$$

As-yüksek bir straight flush, zaten en büyük straight flush'tır; bu yüzden uygulamalar ayrı bir royal flush kategorisine ihtiyaç duymaz. Python uygulaması ayrıca özel \(5\)-yüksek straight durumu için \(A,2,3,4,5\mapsto 1,2,3,4,5\) normalizasyonunu da içerir; geri kalan sıralama çerçevesi ortaktır.

Kanonik değerlendirme dönüşümü

Ana fikir, her eli

$$E(H)=(c(H),t(H))$$

şeklinde kodlamaktır. Burada \(c(H)\) kategori numarası, \(t(H)\) ise sıralı eşitlik bozma vektörüdür. Uygulamaların kullandığı kategori düzeni şudur:

$$8=\text{straight flush},\ 7=\text{dörtlü},\ 6=\text{full house},\ 5=\text{flush},\ 4=\text{straight},\ 3=\text{üçlü},\ 2=\text{iki çift},\ 1=\text{bir çift},\ 0=\text{yüksek kart}.$$

Şimdi her farklı kart değerini \((m(v),v)\) çifti olarak toplayalım ve bu çiftleri önce çokluğa göre azalan, sonra değere göre azalan biçimde sıralayalım. Sıralı liste

$$((c_1,v_1),(c_2,v_2),\dots)$$

olsun. Çözümün kısa olmasını sağlayan esas invariant budur: en önemli tekrar grupları otomatik olarak öne gelir, kicker kartları da otomatik olarak büyükten küçüğe dizilir.

Bu durumda değerlendirme dönüşümü

$$E(H)= \begin{cases} (8,(a_5)) & \text{eğer }\operatorname{straight}(H)\land \operatorname{flush}(H),\\ (7,(v_1,v_2)) & \text{eğer }c_1=4,\\ (6,(v_1,v_2)) & \text{eğer }(c_1,c_2)=(3,2),\\ (5,(a_5,a_4,a_3,a_2,a_1)) & \text{eğer }\operatorname{flush}(H),\\ (4,(a_5)) & \text{eğer }\operatorname{straight}(H),\\ (3,(v_1,v_2,v_3)) & \text{eğer }c_1=3,\\ (2,(v_1,v_2,v_3)) & \text{eğer }c_1=c_2=2,\\ (1,(v_1,v_2,v_3,v_4)) & \text{eğer }c_1=2,\\ (0,(a_5,a_4,a_3,a_2,a_1)) & \text{aksi halde.} \end{cases}$$

Mesela iki çift durumunda \(v_1\) yüksek çifti, \(v_2\) düşük çifti, \(v_3\) ise kicker'ı verir. Bir çift durumunda \(v_1\) çiftin değeri olur; kalan girişler en büyükten en küçüğe kicker'lardır.

Leksikografik karşılaştırma neden yeterlidir?

İki el de \(E(H)\) biçimine dönüştürüldükten sonra karşılaştırma sıradan leksikografik sıraya iner:

$$H_1\text{, }H_2\text{'yi yener}\iff E(H_1)>_{\mathrm{lex}}E(H_2).$$

Bunun nedeni poker karşılaştırmasının hiyerarşik olmasıdır. Önce el sınıfı karşılaştırılır; sınıf eşitse en önemli tekrar eden değer karşılaştırılır; o da eşitse kalan kicker'lar önem sırasıyla karşılaştırılır. \(t(H)\) vektörü tam olarak bu öncelik listesini saklar.

Koddaki temel fikir budur. Her el tipi için ayrı bir karşılaştırıcı yazmak yerine, uygulamalar bütün elleri aynı matematiksel nesneye indirger: kategori ve sıralı eşitlik bozma vektörü.

Çalışılmış örnekler

Şu elleri ele alalım:

$$H_1=\{5H,5C,6S,7S,KD\},\qquad H_2=\{2C,3S,8S,8D,TD\}.$$

Her iki el de bir çifttir, dolayısıyla kategori \(1\)'dir. Değerlendirme vektörleri

$$E(H_1)=(1,(5,13,7,6)),\qquad E(H_2)=(1,(8,10,3,2)).$$

Kategoriler eşit ama çift değeri olarak \(8\), \(5\)'ten büyüktür; bu yüzden ikinci el hemen kazanır.

Daha ince bir örnek:

$$H_1=\{4D,6S,9H,QH,QC\},\qquad H_2=\{3D,6D,7H,QD,QS\}.$$

Yine iki el de bir çifttir ve iki çift de kızdır. Vektörler

$$E(H_1)=(1,(12,9,6,4)),\qquad E(H_2)=(1,(12,7,6,3)).$$

Çift değeri \(12\) ile beraberedir; bu nedenle karşılaştırma en büyük kicker'a geçer. \(9>7\) olduğundan ilk el kazanır. Kicker sırasının doğru tutulması tam da bu yüzden önemlidir.

How the Code Works

Kartları ayrıştırma

C++, Python ve Java uygulamaları girdiyi satır satır okur, her satırı on adet 2 karakterli kart belirtecine ayırır, \(2,3,\dots,9,T,J,Q,K,A\) sembollerini \(2\) ile \(14\) arasındaki tam sayılara dönüştürür ve ilk beş kartı son beş karttan ayırır.

Tek bir eli değerlendirme

Her 5 kartlı el için uygulama eşit değerleri sayar, bütün renklerin aynı olup olmadığını kontrol eder, değerleri sıralar ve ardışık olup olmadıklarını test eder. Aynı anda \((m(v),v)\) biçimindeki sıralı çokluk listesi de kurulur. Bu malzemelerden kategori numarası ve doğru sıradaki eşitlik bozma vektörü elde edilir.

Oyuncuları karşılaştırma ve galibiyet sayma

İki el de değerlendirildikten sonra uygulama önce kategorileri, sonra eşitlik bozma vektörlerini karşılaştırır. Oyuncu 1'in değerlendirmesi büyükse sayaç artırılır. Veri kümesi her satırda kesin bir kazanan verecek şekilde hazırlanmıştır; bu yüzden anlatılan leksikografik karşılaştırma tek başına yeterlidir.

Complexity Analysis / Karmaşıklık Analizi

Her satırda yalnızca iki adet 5 kartlı el vardır; dolayısıyla satır başına maliyet sabittir. Beş değeri sıralamak, beş kartın frekansını saymak ve straight ya da flush kontrolü yapmak küçük sabitlerle \(O(1)\) işlemleridir.

Satır sayısı \(N\) ise toplam çalışma süresi \(O(N)\), ek bellek kullanımı ise \(O(1)\)'dir. Verilen 1000 ellik veri kümesi için hesaplama pratikte anlıktır.

Footnotes and References

  1. Problem sayfası: Project Euler 54 - Poker Hands
  2. Poker el sıraları: Wikipedia - Poker
  3. Poker olasılıkları: Wikipedia - Poker probability
  4. Leksikografik sıralama: Wikipedia - Leksikografik sıralama

Problem Summary / Resumen del Problema

Cada línea del conjunto de datos contiene diez cartas. Las primeras cinco pertenecen al Jugador 1 y las cinco siguientes al Jugador 2. El objetivo es determinar en cuántas líneas gana el Jugador 1 según la jerarquía usual de manos de póker.

No hace falta explorar estados ni calcular probabilidades. Todo se reduce a transformar cada mano en un descriptor matemáticamente ordenado y luego comparar ambos descriptores.

Mathematical Approach / Enfoque Matemático

Para una mano \(H=\{(r_i,s_i)\}_{i=1}^5\), solo importan los cinco valores \(r_i\in\{2,\dots,14\}\), el patrón de palos y las multiplicidades de los valores repetidos. Una vez extraídos esos objetos, la comparación de póker se convierte en un orden lexicográfico total.

Valores, palos y patrón de multiplicidades

Escribamos los valores ordenados como \(a_1\le a_2\le a_3\le a_4\le a_5\). Sea \(m(v)\) el número de cartas con valor \(v\). El patrón de multiplicidades debe ser una de las particiones de 5:

$$4+1,\quad 3+2,\quad 3+1+1,\quad 2+2+1,\quad 2+1+1+1,\quad 1+1+1+1+1.$$

Eso ya identifica todas las manos basadas en repeticiones: póker, full house, trío, dos pares, un par y carta alta. Además hay que comprobar dos propiedades estructurales:

$$\operatorname{flush}(H)\iff s_1=s_2=s_3=s_4=s_5,$$

$$\operatorname{straight}(H)\iff a_{i+1}=a_i+1\text{ para }i=1,2,3,4.$$

Una escalera de color con as alto ya es simplemente la escalera de color máxima, así que las implementaciones no necesitan una categoría separada para la escalera real. La implementación en Python también incluye la normalización habitual \(A,2,3,4,5\mapsto 1,2,3,4,5\) para la escalera de 5 alta; por lo demás, el marco de clasificación es el mismo.

Una aplicación de evaluación canónica

La idea central es asignar a cada mano el valor

$$E(H)=(c(H),t(H)),$$

donde \(c(H)\) es el número de categoría y \(t(H)\) es el vector ordenado de desempate. El orden de categorías usado por las implementaciones es

$$8=\text{escalera de color},\ 7=\text{póker},\ 6=\text{full house},\ 5=\text{color},\ 4=\text{escalera},\ 3=\text{trío},\ 2=\text{dos pares},\ 1=\text{un par},\ 0=\text{carta alta}.$$

Ahora agrupamos cada valor distinto como un par \((m(v),v)\) y ordenamos esos pares primero por multiplicidad descendente y luego por valor descendente. Sea

$$((c_1,v_1),(c_2,v_2),\dots)$$

la lista ordenada. Esta invariante es lo que compacta la solución: los grupos repetidos de mayor prioridad quedan delante de forma automática y los kickers aparecen ya de mayor a menor.

Con esa convención, la función de evaluación es

$$E(H)= \begin{cases} (8,(a_5)) & \text{si }\operatorname{straight}(H)\land \operatorname{flush}(H),\\ (7,(v_1,v_2)) & \text{si }c_1=4,\\ (6,(v_1,v_2)) & \text{si }(c_1,c_2)=(3,2),\\ (5,(a_5,a_4,a_3,a_2,a_1)) & \text{si }\operatorname{flush}(H),\\ (4,(a_5)) & \text{si }\operatorname{straight}(H),\\ (3,(v_1,v_2,v_3)) & \text{si }c_1=3,\\ (2,(v_1,v_2,v_3)) & \text{si }c_1=c_2=2,\\ (1,(v_1,v_2,v_3,v_4)) & \text{si }c_1=2,\\ (0,(a_5,a_4,a_3,a_2,a_1)) & \text{en otro caso.} \end{cases}$$

En una mano de dos pares, \(v_1\) es automáticamente el par alto, \(v_2\) el par bajo y \(v_3\) el kicker. En una mano de un par, \(v_1\) es el valor del par y las entradas restantes son los kickers en orden descendente.

Por qué basta la comparación lexicográfica

Una vez calculado \(E(H)\), decidir el ganador es simplemente aplicar el orden lexicográfico:

$$H_1\text{ vence a }H_2\iff E(H_1)>_{\mathrm{lex}}E(H_2).$$

Funciona porque la comparación de manos de póker es jerárquica. Primero se compara la clase de mano; si coincide, se compara el valor repetido más importante; si todavía hay empate, se continúa con los kickers según su prioridad. El vector \(t(H)\) guarda exactamente esa lista de prioridades.

Esa es la invariante principal del código. En lugar de escribir un comparador distinto para cada tipo de mano, las implementaciones normalizan todas las manos al mismo objeto matemático: categoría más vector ordenado de desempate.

Ejemplos trabajados

Consideremos

$$H_1=\{5H,5C,6S,7S,KD\},\qquad H_2=\{2C,3S,8S,8D,TD\}.$$

Ambas manos son un par, así que tienen categoría \(1\). Sus vectores son

$$E(H_1)=(1,(5,13,7,6)),\qquad E(H_2)=(1,(8,10,3,2)).$$

Las categorías empatan, pero el valor del par \(8\) es mayor que \(5\), de modo que la segunda mano gana de inmediato.

Ahora miremos

$$H_1=\{4D,6S,9H,QH,QC\},\qquad H_2=\{3D,6D,7H,QD,QS\}.$$

Otra vez las dos manos son un par, y en ambos casos el par es de damas. Los vectores son

$$E(H_1)=(1,(12,9,6,4)),\qquad E(H_2)=(1,(12,7,6,3)).$$

El valor del par empata en \(12\), así que decide el kicker más grande. Como \(9>7\), gana la primera mano. Este ejemplo muestra por qué el orden correcto de los kickers es indispensable.

How the Code Works

Análisis de cada línea

Las implementaciones en C++, Python y Java leen la entrada línea por línea, separan cada línea en diez fichas de dos caracteres, convierten \(2,3,\dots,9,T,J,Q,K,A\) en enteros entre \(2\) y \(14\), y dividen las primeras cinco cartas de las últimas cinco.

Evaluación de una mano

Para cada mano de 5 cartas, la implementación cuenta las repeticiones de valores, comprueba si todos los palos coinciden, ordena los valores y decide si forman una sucesión consecutiva. También construye la lista ordenada de multiplicidades \((m(v),v)\). A partir de ahí obtiene el número de categoría y el vector de desempate en el orden exacto que exige el póker.

Comparación de los dos jugadores

Después de evaluar ambas manos, la implementación compara primero las categorías y luego los vectores de desempate. Si la evaluación del Jugador 1 es mayor, se incrementa el contador. El conjunto de datos está diseñado para que cada línea tenga un ganador claro, así que no hace falta ningún mecanismo adicional más allá de la comparación lexicográfica ya descrita.

Complexity Analysis / Análisis de Complejidad

Cada línea contiene solo dos manos de 5 cartas, por lo que el costo por línea es constante. Ordenar cinco valores, contar frecuencias entre cinco cartas y verificar escalera o color son operaciones \(O(1)\) con constantes muy pequeñas.

Si el archivo tiene \(N\) líneas, el tiempo total es \(O(N)\) y la memoria extra es \(O(1)\). Para el conjunto dado de 1000 manos, el cálculo es prácticamente instantáneo.

Footnotes and References

  1. Página del problema: Project Euler 54 - Poker Hands
  2. Jerarquía de manos de póker: Wikipedia - Póquer
  3. Probabilidades de póker: Wikipedia - Poker probability
  4. Orden lexicográfico: Wikipedia - Orden lexicográfico

Problem Summary / 问题概述

数据集的每一行都给出十张牌。前五张属于玩家 1,后五张属于玩家 2。任务是在标准扑克牌型比较规则下,判断玩家 1 一共赢了多少行。

这不是搜索题,也不是概率题。核心工作只有一件事:把每手 5 张牌转换成一个可以严格排序的数学描述,然后比较这两个描述。

Mathematical Approach / 数学方法

设一手牌为 \(H=\{(r_i,s_i)\}_{i=1}^5\)。真正有用的信息只有三类:五张牌的点数 \(r_i\in\{2,\dots,14\}\)、花色模式,以及相同点数出现的次数。一旦把这些对象抽取出来,扑克比大小就变成了一个完全的字典序比较问题。

点数、花色与重数结构

把点数从小到大写成 \(a_1\le a_2\le a_3\le a_4\le a_5\)。记 \(m(v)\) 为点数 \(v\) 在这手牌中出现的次数。那么重数模式只能是 5 的以下拆分之一:

$$4+1,\quad 3+2,\quad 3+1+1,\quad 2+2+1,\quad 2+1+1+1,\quad 1+1+1+1+1.$$

这已经足够区分所有“由重复点数组成”的牌型:四条、葫芦、三条、两对、一对和高牌。除此以外,还要额外检查两个结构性质:

$$\operatorname{flush}(H)\iff s_1=s_2=s_3=s_4=s_5,$$

$$\operatorname{straight}(H)\iff a_{i+1}=a_i+1\text{ 对 }i=1,2,3,4\text{ 成立。}$$

如果是一手 A 高同花顺,它本质上只是“最大的同花顺”,因此实现里并不需要再单独设一个皇家同花顺类别。Python 实现还显式加入了特殊的 \(5\) 高顺子归一化 \(A,2,3,4,5\mapsto 1,2,3,4,5\);除此之外,三种语言使用的是同一套排名框架。

构造规范化的评估映射

关键思想是把每手牌编码成

$$E(H)=(c(H),t(H)),$$

其中 \(c(H)\) 是牌型类别编号,\(t(H)\) 是按优先级排列好的平局判定向量。实现中使用的类别顺序是

$$8=\text{同花顺},\ 7=\text{四条},\ 6=\text{葫芦},\ 5=\text{同花},\ 4=\text{顺子},\ 3=\text{三条},\ 2=\text{两对},\ 1=\text{一对},\ 0=\text{高牌}.$$

接着,把每个不同点数写成一对 \((m(v),v)\),并且先按出现次数降序、再按点数降序排序。设排好序的列表为

$$((c_1,v_1),(c_2,v_2),\dots).$$

这是整个解法最重要的一个不变量:重要的重复组会自动排在前面,而 kicker 牌也会自动按照点数从大到小排列。这样就不用为不同牌型分别设计完全不同的比较过程。

在这个约定下,评估映射就是

$$E(H)= \begin{cases} (8,(a_5)) & \text{若 }\operatorname{straight}(H)\land \operatorname{flush}(H),\\ (7,(v_1,v_2)) & \text{若 }c_1=4,\\ (6,(v_1,v_2)) & \text{若 }(c_1,c_2)=(3,2),\\ (5,(a_5,a_4,a_3,a_2,a_1)) & \text{若 }\operatorname{flush}(H),\\ (4,(a_5)) & \text{若 }\operatorname{straight}(H),\\ (3,(v_1,v_2,v_3)) & \text{若 }c_1=3,\\ (2,(v_1,v_2,v_3)) & \text{若 }c_1=c_2=2,\\ (1,(v_1,v_2,v_3,v_4)) & \text{若 }c_1=2,\\ (0,(a_5,a_4,a_3,a_2,a_1)) & \text{其他情况。} \end{cases}$$

例如在“两对”里,\(v_1\) 自动就是较大的那一对,\(v_2\) 是较小的一对,\(v_3\) 是单张 kicker。在“一对”里,\(v_1\) 是那一对的点数,其余项则是从大到小的三张 kicker。

为什么字典序比较就够了

一旦两手牌都被映射成 \(E(H)\),胜负判定就变成了普通的字典序比较:

$$H_1\text{ 胜过 }H_2\iff E(H_1)>_{\mathrm{lex}}E(H_2).$$

原因在于扑克比牌本身就是分层进行的。先比较牌型类别;类别相同,再比较最关键的重复点数;如果还相同,再按优先级逐个比较剩余的 kicker。向量 \(t(H)\) 正好把这个优先级顺序完整编码了进去。

这也是代码背后的主不变量。实现并没有为每一种牌型写一套互不相关的比较器,而是先把每手牌规范化成同一种数学对象:类别加有序平局向量。

具体例子

先看

$$H_1=\{5H,5C,6S,7S,KD\},\qquad H_2=\{2C,3S,8S,8D,TD\}.$$

这两手牌都是一对,因此类别都是 \(1\)。对应的评估向量为

$$E(H_1)=(1,(5,13,7,6)),\qquad E(H_2)=(1,(8,10,3,2)).$$

类别相同,但对子点数 \(8\) 大于 \(5\),所以第二手直接获胜。

再看一个更细的例子:

$$H_1=\{4D,6S,9H,QH,QC\},\qquad H_2=\{3D,6D,7H,QD,QS\}.$$

这两手牌也都是一对,而且对子都是 Q。评估向量变为

$$E(H_1)=(1,(12,9,6,4)),\qquad E(H_2)=(1,(12,7,6,3)).$$

对子点数在 \(12\) 处打平,于是比较最大的 kicker。因为 \(9>7\),第一手获胜。这个例子正说明了为什么 kicker 的存储顺序必须完全正确。

How the Code Works

解析每一行牌面

C++、Python 和 Java 实现都会逐行读取输入,把每一行拆成十个长度为 2 的牌面记号,再把 \(2,3,\dots,9,T,J,Q,K,A\) 映射成整数 \(2\) 到 \(14\),并分离出前五张与后五张。

评估单手牌

对于每一手 5 张牌,实现会统计点数频次,检查五张牌是否同花,排序点数并测试是否连续,同时构造前面定义的 \((m(v),v)\) 排序列表。然后根据这些信息生成类别编号以及按正确优先级排列的平局向量。

比较两位玩家并累计答案

当两手牌都评估完成后,实现先比较类别,再比较平局向量。如果玩家 1 的评估结果更大,就把答案加一。题目数据保证每一行都有唯一胜者,因此不需要在字典序比较之外再设计额外的平局处理机制。

Complexity Analysis / 复杂度分析

每一行只有两手 5 张牌,所以单行处理代价是常数。对五个点数排序、统计五张牌的频次、检查是否为顺子或同花,全都是常数时间 \(O(1)\) 操作,而且常数非常小。

如果总共有 \(N\) 行,那么总时间复杂度是 \(O(N)\),额外空间复杂度是 \(O(1)\)。对题目给出的 1000 行数据来说,计算几乎是瞬间完成的。

Footnotes and References

  1. 题目页面: Project Euler 54 - Poker Hands
  2. 扑克牌型: Wikipedia - 扑克
  3. 扑克概率: Wikipedia - Poker probability
  4. 字典序: Wikipedia - 字典序

Problem Summary / Краткое описание

Каждая строка входных данных содержит десять карт. Первые пять принадлежат Игроку 1, следующие пять принадлежат Игроку 2. Нужно определить, в скольких строках рука Игрока 1 сильнее по стандартным правилам покера.

Это не задача на перебор и не задача на вероятности. Здесь нужно лишь преобразовать каждую 5-карточную руку в математически упорядоченное описание и затем сравнить два таких описания.

Mathematical Approach / Математический подход

Для руки \(H=\{(r_i,s_i)\}_{i=1}^5\) важны только три вида данных: достоинства карт \(r_i\in\{2,\dots,14\}\), конфигурация мастей и кратности одинаковых достоинств. Как только эти объекты выделены, сравнение покерных рук сводится к полному лексикографическому порядку.

Достоинства, масти и структура кратностей

Пусть отсортированные достоинства равны \(a_1\le a_2\le a_3\le a_4\le a_5\). Обозначим через \(m(v)\) число карт достоинства \(v\). Тогда шаблон кратностей обязан быть одним из разбиений числа 5:

$$4+1,\quad 3+2,\quad 3+1+1,\quad 2+2+1,\quad 2+1+1+1,\quad 1+1+1+1+1.$$

Этого уже достаточно, чтобы различить все типы рук, зависящие только от повторов: каре, фулл-хаус, тройку, две пары, одну пару и старшую карту. Дополнительно нужны две структурные проверки:

$$\operatorname{flush}(H)\iff s_1=s_2=s_3=s_4=s_5,$$

$$\operatorname{straight}(H)\iff a_{i+1}=a_i+1\text{ для }i=1,2,3,4.$$

Стрит-флеш с тузом сверху — это просто максимальный стрит-флеш, поэтому отдельная категория для роял-флеша реализациям не нужна. Python-реализация также явно нормализует специальный случай \(A,2,3,4,5\mapsto 1,2,3,4,5\) для 5-старшего стрита; в остальном схема ранжирования одинакова.

Каноническое отображение оценки

Главная идея состоит в том, чтобы каждой руке сопоставить значение

$$E(H)=(c(H),t(H)),$$

где \(c(H)\) — номер категории, а \(t(H)\) — упорядоченный вектор для разрешения ничьих. Порядок категорий, используемый реализациями, таков:

$$8=\text{стрит-флеш},\ 7=\text{каре},\ 6=\text{фулл-хаус},\ 5=\text{флеш},\ 4=\text{стрит},\ 3=\text{тройка},\ 2=\text{две пары},\ 1=\text{одна пара},\ 0=\text{старшая карта}.$$

Теперь соберем каждое различное достоинство в пару \((m(v),v)\) и отсортируем такие пары сначала по убыванию кратности, а затем по убыванию достоинства. Пусть получился список

$$((c_1,v_1),(c_2,v_2),\dots).$$

Именно этот инвариант делает решение компактным: самые важные повторяющиеся группы автоматически оказываются впереди, а кикеры автоматически идут по убыванию достоинства.

Тогда отображение оценки имеет вид

$$E(H)= \begin{cases} (8,(a_5)) & \text{если }\operatorname{straight}(H)\land \operatorname{flush}(H),\\ (7,(v_1,v_2)) & \text{если }c_1=4,\\ (6,(v_1,v_2)) & \text{если }(c_1,c_2)=(3,2),\\ (5,(a_5,a_4,a_3,a_2,a_1)) & \text{если }\operatorname{flush}(H),\\ (4,(a_5)) & \text{если }\operatorname{straight}(H),\\ (3,(v_1,v_2,v_3)) & \text{если }c_1=3,\\ (2,(v_1,v_2,v_3)) & \text{если }c_1=c_2=2,\\ (1,(v_1,v_2,v_3,v_4)) & \text{если }c_1=2,\\ (0,(a_5,a_4,a_3,a_2,a_1)) & \text{иначе.} \end{cases}$$

Например, для двух пар \(v_1\) автоматически обозначает старшую пару, \(v_2\) — младшую пару, а \(v_3\) — кикер. Для одной пары \(v_1\) — это достоинство пары, а остальные значения дают кикеры от большего к меньшему.

Почему достаточно лексикографического сравнения

После преобразования обеих рук в \(E(H)\) победитель определяется обычным лексикографическим порядком:

$$H_1\text{ сильнее }H_2\iff E(H_1)>_{\mathrm{lex}}E(H_2).$$

Это верно, потому что сравнение покерных рук и так иерархично. Сначала сравнивается класс комбинации, затем самый важный повторяющийся ранг, а если и он совпадает, то оставшиеся кикеры по убыванию приоритета. Вектор \(t(H)\) точно хранит этот порядок приоритетов.

Это и есть основной инвариант кода. Вместо отдельной процедуры сравнения для каждого типа комбинации реализации сначала нормализуют любую руку до одного и того же математического объекта: категория плюс упорядоченный вектор разрешения ничьих.

Разобранные примеры

Рассмотрим

$$H_1=\{5H,5C,6S,7S,KD\},\qquad H_2=\{2C,3S,8S,8D,TD\}.$$

Обе руки содержат одну пару, значит их категория равна \(1\). Их векторы оценки:

$$E(H_1)=(1,(5,13,7,6)),\qquad E(H_2)=(1,(8,10,3,2)).$$

Категории совпадают, но пара восьмерок сильнее пары пятерок, поэтому вторая рука побеждает сразу.

Более тонкий пример:

$$H_1=\{4D,6S,9H,QH,QC\},\qquad H_2=\{3D,6D,7H,QD,QS\}.$$

Снова обе руки содержат одну пару, и в обоих случаях это пара дам. Тогда

$$E(H_1)=(1,(12,9,6,4)),\qquad E(H_2)=(1,(12,7,6,3)).$$

Значение пары совпадает и равно \(12\), поэтому сравнение переходит к старшему кикеру. Так как \(9>7\), выигрывает первая рука. Именно поэтому правильный порядок кикеров принципиален.

How the Code Works

Разбор входной строки

Реализации на C++, Python и Java читают вход построчно, разбивают каждую строку на десять двухсимвольных обозначений карт, переводят \(2,3,\dots,9,T,J,Q,K,A\) в числа от \(2\) до \(14\) и отделяют первые пять карт от последних пяти.

Оценка одной руки

Для каждой 5-карточной руки реализация считает частоты достоинств, проверяет совпадение мастей, сортирует достоинства и тестирует их на последовательность. Одновременно строится отсортированный список \((m(v),v)\). Из этих данных формируются номер категории и вектор разрешения ничьих в правильном порядке приоритетов.

Сравнение игроков и подсчет побед

После оценки обеих рук реализация сначала сравнивает категории, а затем векторы разрешения ничьих. Если оценка Игрока 1 больше, ответ увеличивается на единицу. Входной набор подобран так, что в каждой строке есть однозначный победитель, поэтому дополнительный механизм поверх лексикографического сравнения не требуется.

Complexity Analysis / Анализ сложности

Каждая строка содержит только две руки по пять карт, поэтому стоимость обработки одной строки постоянна. Сортировка пяти достоинств, подсчет частот пяти карт и проверки на стрит и флеш — все это операции \(O(1)\) с очень маленькими константами.

Если вход содержит \(N\) строк, общее время равно \(O(N)\), а дополнительная память равна \(O(1)\). Для данного набора из 1000 раздач вычисление практически мгновенно.

Footnotes and References

  1. Страница задачи: Project Euler 54 - Poker Hands
  2. Покерные комбинации: Wikipedia - Покер
  3. Вероятности в покере: Wikipedia - Poker probability
  4. Лексикографический порядок: Wikipedia - Лексикографический порядок

Problem Summary / ملخص المشكلة

كل سطر في البيانات يحتوي على عشر بطاقات. أول خمس بطاقات تخص اللاعب الأول، والخمس التالية تخص اللاعب الثاني. المطلوب هو تحديد عدد الأسطر التي يفوز فيها اللاعب الأول وفق ترتيب أيدي البوكر القياسي.

هذه ليست مسألة بحث ولا مسألة احتمالات. كل ما نحتاجه هو تحويل كل يد مكوّنة من خمس بطاقات إلى وصف رياضي قابل للترتيب، ثم مقارنة الوصفين.

Mathematical Approach / المنهج الرياضي

إذا كانت اليد \(H=\{(r_i,s_i)\}_{i=1}^5\)، فالمعلومات المهمة فيها هي قيم البطاقات الخمس \(r_i\in\{2,\dots,14\}\)، ونمط الأنواع، وتكرارات القيم المتساوية. بعد استخراج هذه العناصر، تصبح مقارنة أيدي البوكر مجرد ترتيب معجمي كامل.

القيم والأنواع وبنية التكرارات

لنكتب القيم بعد الترتيب على الصورة \(a_1\le a_2\le a_3\le a_4\le a_5\). ولتكن \(m(v)\) عدد البطاقات ذات القيمة \(v\). نمط التكرارات لا بد أن يكون أحد تفكيكات العدد 5 التالية:

$$4+1,\quad 3+2,\quad 3+1+1,\quad 2+2+1,\quad 2+1+1+1,\quad 1+1+1+1+1.$$

وهذا يكفي بالفعل لتمييز جميع الأنواع المبنية على التكرار: أربعة من نوع واحد، منزل كامل، ثلاثة من نوع واحد، زوجان، زوج واحد، وبطاقة عالية. ثم نضيف اختبارين بنيويين:

$$\operatorname{flush}(H)\iff s_1=s_2=s_3=s_4=s_5,$$

$$\operatorname{straight}(H)\iff a_{i+1}=a_i+1\text{ for }i=1,2,3,4.$$

إذا كانت اليد عبارة عن straight flush مرتفع بالآس فهي ببساطة أكبر straight flush، ولذلك لا تحتاج التطبيقات إلى فئة مستقلة باسم royal flush. كما أن تنفيذ Python يضيف التطبيع المعتاد للحالة الخاصة \(A,2,3,4,5\mapsto 1,2,3,4,5\) عند التعامل مع المستقيم ذي الخمسة العالية؛ وما عدا ذلك فإطار الترتيب مشترك.

دالة تقييم قياسية

الفكرة الأساسية هي أن نمنح كل يد القيمة

$$E(H)=(c(H),t(H)),$$

حيث \(c(H)\) هو رقم الفئة، و\(t(H)\) هو متجه فكّ التعادل المرتب. ترتيب الفئات الذي تستخدمه التطبيقات هو

$$8=\text{straight flush},\ 7=\text{four of a kind},\ 6=\text{full house},\ 5=\text{flush},\ 4=\text{straight},\ 3=\text{three of a kind},\ 2=\text{two pairs},\ 1=\text{one pair},\ 0=\text{high card}.$$

بعد ذلك نمثل كل قيمة مختلفة بالزوج \((m(v),v)\)، ثم نفرز هذه الأزواج أولاً بحسب التكرار تنازلياً، ثم بحسب القيمة تنازلياً. ولتكن القائمة المرتبة

$$((c_1,v_1),(c_2,v_2),\dots).$$

هذا الثابت البنيوي هو ما يجعل الحل مختصراً: المجموعات المكررة الأكثر أهمية تظهر تلقائياً في المقدمة، وبطاقات الكيكر تظهر تلقائياً من الأكبر إلى الأصغر.

عندئذ تصبح دالة التقييم

$$E(H)= \begin{cases} (8,(a_5)) & \text{if }\operatorname{straight}(H)\land \operatorname{flush}(H),\\ (7,(v_1,v_2)) & \text{if }c_1=4,\\ (6,(v_1,v_2)) & \text{if }(c_1,c_2)=(3,2),\\ (5,(a_5,a_4,a_3,a_2,a_1)) & \text{if }\operatorname{flush}(H),\\ (4,(a_5)) & \text{if }\operatorname{straight}(H),\\ (3,(v_1,v_2,v_3)) & \text{if }c_1=3,\\ (2,(v_1,v_2,v_3)) & \text{if }c_1=c_2=2,\\ (1,(v_1,v_2,v_3,v_4)) & \text{if }c_1=2,\\ (0,(a_5,a_4,a_3,a_2,a_1)) & \text{otherwise.} \end{cases}$$

في حالة الزوجين مثلاً يكون \(v_1\) هو الزوج الأعلى و\(v_2\) هو الزوج الأدنى و\(v_3\) هو الكيكر. وفي حالة الزوج الواحد يكون \(v_1\) هو قيمة الزوج، ثم تأتي القيم الباقية ككيكرات من الأكبر إلى الأصغر.

لماذا يكفي الترتيب المعجمي؟

بعد تحويل اليدين إلى \(E(H)\)، يصبح تحديد الفائز مجرد مقارنة معجمية عادية:

$$H_1\text{ beats }H_2\iff E(H_1)>_{\mathrm{lex}}E(H_2).$$

والسبب هو أن مقارنة أيدي البوكر ذات طبيعة هرمية. نقارن أولاً فئة اليد نفسها، ثم أهم قيمة مكررة، ثم ننتقل إلى الكيكرات بالترتيب من الأكثر أهمية إلى الأقل. المتجه \(t(H)\) يخزن هذا التسلسل بدقة.

وهذا هو الثابت الرئيسي في الشيفرة. بدلاً من كتابة مقارن مختلف لكل نوع من الأيدي، تقوم التطبيقات بتطبيع كل يد إلى نفس الكائن الرياضي: فئة ومعها متجه مرتب لفك التعادل.

أمثلة محلولة

لنأخذ

$$H_1=\{5H,5C,6S,7S,KD\},\qquad H_2=\{2C,3S,8S,8D,TD\}.$$

كلتا اليدين من فئة الزوج الواحد، ولذلك فئتهما هي \(1\). متجهات التقييم هي

$$E(H_1)=(1,(5,13,7,6)),\qquad E(H_2)=(1,(8,10,3,2)).$$

الفئة نفسها متساوية، لكن قيمة الزوج \(8\) أكبر من \(5\)، لذلك تفوز اليد الثانية مباشرة.

والآن مثال أدق:

$$H_1=\{4D,6S,9H,QH,QC\},\qquad H_2=\{3D,6D,7H,QD,QS\}.$$

هنا أيضاً كلتا اليدين زوج واحد، وفي كلتا الحالتين الزوج من الملكات. تصبح المتجهات

$$E(H_1)=(1,(12,9,6,4)),\qquad E(H_2)=(1,(12,7,6,3)).$$

قيمة الزوج متعادلة عند \(12\)، ولذلك ننتقل إلى أكبر كيكر. بما أن \(9>7\)، فإن اليد الأولى هي الفائزة. هذا المثال يوضح لماذا يجب أن يكون ترتيب الكيكرات صحيحاً تماماً.

How the Code Works

تحليل بطاقات كل سطر

تقرأ تطبيقات C++ وPython وJava الإدخال سطراً سطراً، وتفصل كل سطر إلى عشرة رموز بطول حرفين، ثم تحول \(2,3,\dots,9,T,J,Q,K,A\) إلى الأعداد \(2\) حتى \(14\)، وبعد ذلك تفصل أول خمس بطاقات عن آخر خمس بطاقات.

تقييم يد واحدة

لكل يد مكوّنة من خمس بطاقات، يقوم التنفيذ بعدّ القيم المتساوية، وفحص ما إذا كانت الأنواع كلها متماثلة، وترتيب القيم، واختبار ما إذا كانت متتالية. كما يبني قائمة التكرارات المرتبة \((m(v),v)\). ومن هذه المعلومات يستنتج رقم الفئة ومتجه فك التعادل بالترتيب الصحيح للمقارنة.

مقارنة اللاعبين وعدّ مرات الفوز

بعد تقييم اليدين، يقارن التنفيذ الفئة أولاً ثم متجه فك التعادل ثانياً. إذا كانت قيمة تقييم اللاعب الأول أكبر، يزاد العداد. وقد صيغت البيانات بحيث يوجد فائز واضح في كل سطر، لذلك لا يلزم أي منطق إضافي فوق المقارنة المعجمية الموصوفة.

Complexity Analysis / تحليل التعقيد

كل سطر يحتوي فقط على يدين من خمس بطاقات، ولذلك فإن كلفة المعالجة لكل سطر ثابتة. ترتيب خمس قيم، وعدّ تكرارات خمس بطاقات، وفحص المستقيم أو الفلاش كلها عمليات \(O(1)\) بثوابت صغيرة جداً.

إذا كان عدد الأسطر هو \(N\)، فإن الزمن الكلي يساوي \(O(N)\)، واستهلاك الذاكرة الإضافي يساوي \(O(1)\). وبالنسبة إلى مجموعة البيانات التي تضم 1000 يد، فإن الحساب يكاد يكون فورياً.

Footnotes and References

  1. صفحة المسألة: Project Euler 54 - Poker Hands
  2. ترتيب أيدي البوكر: Wikipedia - بوكر
  3. احتمالات البوكر: Wikipedia - Poker probability
  4. الترتيب المعجمي: Wikipedia - الترتيب المعجمي