2012年9月2日日曜日

Project Euler - Problem 54

Project Euler(プロジェクト オイラー)の問題をpythonでやってみます。

出典: Project Euler(日本語 翻訳サイト)
   ProjectEuler.net(英語サイト)

Problem 54
カードゲームのポーカーでは, 手札は5枚のカードからなりランク付けされている.
役を低い方から高い方へ順に並べると以下である.

0.役無し: 一番値が大きいカード
1.ワン・ペア: 同じ値のカードが2枚
2.ツー・ペア: 2つの異なる値のペア
3.スリーカード: 同じ値のカードが3枚
4.ストレート: 5枚の連続する値のカード
5.フラッシュ: 全てのカードが同じスート
 (注: スートとはダイヤ・ハート・クラブ/スペードというカードの絵柄のこと)
6.フルハウス: スリーカードとペア
7.フォーカード: 同じ値のカードが4枚
8.ストレートフラッシュ: ストレートかつフラッシュ
9.ロイヤルフラッシュ: 同じスートの10, J, Q, K, A

ここでカードの値は小さい方から
2, 3, 4, 5, 6, 7, 8, 9, 10, J, Q, K, Aである.
(訳注:データ中で10は'T'と表される)

もし2人のプレイヤーが同じ役の場合には, 役を構成する中で値が最も大きいカードによってランクが決まる:
例えば, 8のペアは5のペアより強い (下の例1を見よ).
それでも同じランクの場合には (例えば, 両者ともQのペアの場合),
一番値が大きいカードによってランクが決まる (下の例4を見よ).
一番値が大きいカードが同じ場合には, 次に値が大きいカードが比べれられ, 以下同様にランクを決定する.

例:
プレイヤー1プレイヤー2勝者
15H 5C 6S 7S KD
(5のペア)
2C 3S 8S 8D TD
(8のペア)
プレイヤー2
25D 8C 9S JS AC
(役無し, A)
2C 5C 7D 8S QH
(役無し, Q)
プレイヤー1
32D 9C AS AH AC
(Aのスリーカード)
3D 6D 7D TD QD
(ダイヤのフラッシュ)
プレイヤー2
44D 6S 9H QH QC
(Qのペア, 9)
3D 6D 7H QD QS
(Qのペア, 7)
プレイヤー1
52H 2D 4C 4D 4S
(4-2のフルハウス)
3C 3D 3S 9S 9D
(3-9のフルハウス)
プレイヤー1

poker.txtには1000個のランダムな手札の組が含まれている.
各行は10枚のカードからなる(スペースで区切られている):
最初の5枚がプレイヤー1の手札であり, 残りの5枚がプレイヤー2の手札である.
以下のことを仮定してよい

  • 全ての手札は正しい (使われない文字が出現しない. 同じカードは繰り返されない)
  • 各プレイヤーの手札は特に決まった順に並んでいるわけではない
  • 各勝負で勝敗は必ず決まる
1000回中プレイヤー1が勝つのは何回か?

-----

私の解答例は以下です。畳んでいます。
def L2dic(L):
	d = {"T":10,"J":11,"Q":12,"K":13,"A":14}
	dN, dM = {}, {}
	for s in L:
		if s[0]>"9": t = d[s[0]]
		else: t = int(s[0])
		dN[t] = dN.get(t, 0)+1
		dM[s[1]] = dM.get(s[1], 0)+1
	return dN, dM

def g(L):
	dN, dM = L2dic(L)
	maxN, minN, nN, maxNV = max(dN), min(dN), len(dN), max(dN.values())
	nM = len(dM)
	difN = maxN-minN
	if nN==5:
		if difN>4:
			if nM>1: t = 0	#0:High Card
			else: t = 5		#5:Flush
		elif nM>1: t = 4		#4:Straight
		elif minN<10: t = 8	#8:Straight Flush
		else: t = 9			#9:Royal Straight Flush
	elif nN==4: t = 1		#1:One Pair
	elif nN==3:
		if maxNV==3: t = 3	#3:Three cards
		else: t = 2			#2:Two pairs
	elif nN==2:
		if maxNV==4: t = 7	#7:Four in a kind
		else: t = 6			#6:Full House

	s = str(t)
	for k,v in sorted(dN.items(), key=lambda (k,v):(v,k), reverse=True):
		s += str(k).zfill(2) * v
	return s

def f(sIn):
	t = 0
	for s in open(sIn).readlines():
		L = s.split()
		p1, p2 = g(L[:5]), g(L[5:])
		t += int(p1>p2)
	return t

import os, sys
sIn = os.path.join(os.path.dirname(sys.argv[0]), "poker.txt")
print f(sIn)


3つの関数を使っています。

1.関数L2dic(L):
・カードのリストから、番号辞書とマーク辞書を返します。
 番号辞書dNは、カードの番号をキーにその枚数を値に持つ辞書です。
 マーク辞書dMは、カードのマークをキーにその枚数を値に持つ辞書です。
・9を超える番号でも並べ替え可能にするため、
 T,J,Q,K,Aを順に10から14までの連番に置き換えます。

・d = {"T":10,"J":11,"Q":12,"K":13,"A":14}
 10以上の値のための変換辞書です。

・for s in L:
 カードのリストから1枚ずつ取り出し、ループ変数sとします。
 sは2桁の文字列で、位置0の値s[0]が番号で、位置1の値s[1]が絵柄です。

・if s[0]>"9": t = d[s[0]]
 else: t = int(s[0])
 番号が9を超える場合、変換辞書で数値にします。
 番号が9を超えない場合、int関数でそのまま数値にします。

・dN[t] = dN.get(t, 0)+1
 番号辞書では、上記で数値化した値tをキーにカウントアップします。
 辞書[キー]ではキー未設定の場合エラーになるので、
 get関数を使い、引数1未設定の場合に引数2、この場合0を返すようにします。
 こうして得た値に、+1します。

・dM[s[1]] = dM.get(s[1], 0)+1
 マーク辞書は、マークのアルファベット値をキーに件数カウントアップします。

2.関数g(L)
・カードの強さ(役+すべての番号)の文字列を返します。
 ただし、すべての番号の並び順は、
 役を構成している番号群を優先し、番号の大きい順です。

・dN, dM = L2dic(L)
 番号辞書dN、マーク辞書dMを取得しておきます。

・maxN, minN, nN, maxNV = max(dN), min(dN), len(dN), max(dN.values())
 nM = len(dM)
 difN = maxN-minN
 役の判定で必要な情報を以下のように前もって準備しておきます。
 最大番号maxN、最小番号minN、番号の種類数nN、番号別件数の最大値maxNV、
 マークの種類数nM、最大番号と最小番号の差difN。
 difNは連番判定で使います。
 
・if nN==5:
 数字が5種類の場合の判定です。

・if difN>4:
  if nM>1: t = 0 #0:High Card
  else: t = 5  #5:Flush
 数字が5種類で、連番以外の場合のチェックです。
 最大番号と最小番号の差が4より大きいということが連番ではないということです。
 この場合はさらにマークの種類がいくつあるかみて、
 マークが複数あれば、役無しで、強さtは0です。
 マークが1種類であれば、フラッシュで、強さtは5です。
 
・elif nM>1: t = 4 #4:Straight
 数字が5種類で、連番の場合、
 マークが複数であれば、ストレートで、強さt=4です。

・elif minN<10: t = 8 #8:Straight Flush
 数字が5種類で、連番で、マークが1種類の場合、
 最小番号が10未満の場合、ストレートフラッシュで、強さt=8です。

・else: t = 9   #9:Royal Straight Flush
 数字が5種類で、連番で、マークが1種類で、最小番号が10以上の場合、
 ストレートフラッシュで、強さt=8です。

・elif nN==4: t = 1  #1:One Pair
 数字が4種類の場合はワンペアしかなく、強さt=1。

・elif nN==3:
  if maxNV==3: t = 3 #3:Three cards
  else: t = 2   #2:Two pairs
 数字が3種類の場合、番号別の件数は(3,1,1)(2,2,1)のどちらかです。
 そこで番号別件数の最大値maxNVが3の場合、スリーカードで、強さt=3です。
 それ以外はツーペアで、強さt=2です。

・elif nN==2:
  if maxNV==4: t = 7 #7:Four in a kind
  else: t = 6   #6:Full House
 数字が2種類の場合、番号別の件数は(4,1)(3,2)のどちらかです。
 そこで番号別件数の最大値maxNVが4の場合、フォーカードで、強さt=7です。
 それ以外はフルハウスで、強さt=6

・s = str(t)
 上記で判定した役の強さをstr関数で文字列にしてsとします。

・for k,v in sorted(dN.items(), key=lambda (k,v):(v,k), reverse=True):
 ループ処理で1ずつ値を取り出せるものをイテレータといいます。
 sorted関数は並べ替えたイテレータを返します。
 最初の引数は並べ替え対象のイテレータです。
 dN.items()は、番号辞書dNのキーと値の組のタプルを返すイテレータです。
 つまり番号8が2枚のときに(8,2)のように組み合わせて返すイテレータです。
 これが並べ替え元です。

 引数keyは並べ替えキーの指定関数です。
 lambda関数で指定しています。lambda関数は無名関数といって、
 関数名を付けられないだけで通常の関数と同様に数式を指定できます。
 これはdef文による名前付き関数を別途定義して、その関数名を指定しても同じです。
 書式は「lambda 引数:数式」です。
 ここでは「lambda (k,v):(v,k)」なので、引数として
 番号辞書dNのキーと値の組み合わせタプル(k,v)を受け取って、(v,k)を返します。
 役を構成する番号はフラッシュ以外すべて複数枚であり、
 しかもその枚数の多い方が判定で優先使用されるため、
 ここでは番号辞書dNの(枚数, 番号)の順に並べ替えます。
 なおフラッシュは番号順に優先使用なのでこの並べ替え優先順位はそのまま使えます。

 引数reverseは逆順指定です。
 初期値はFalseで小さい順です。ここでは大きい順にしたいのでTrueとします。

・s += str(k).zfill(2) * v
 zfillプロパティは文字列を頭0埋めして指定桁数にした値を返します。
 上記で並べ替えた順に番号kと枚数vが設定されるので、
 頭0埋め文字列に変換して枚数の分だけ、順に連結します。

・return s
 上記で設定した、カードの強さ、つまり役+すべての番号の文字列sを呼び出し元に返します。
 例えば、カードが「TC TD QC 6D TS」の場合、10のスリーカードで、
 s=031010101206となります。

3.関数f(sIn)
・問題にあるポーカー用テキストファイルのフルパスを受け取り、プレーヤー1が勝つ回数を返します。

・for s in open(sIn).readlines():
 入力テキストファイルの全行を読み込んで、1行ずつループ変数sとします。

・L = s.split()
 半角空白区切りでリストにします。

・p1, p2 = g(L[:5]), g(L[5:])
 p1,p2はプレーヤー1,2の手札、前半5枚と後半5枚を関数gで変換した強さ文字列です。

・t += int(p1>p2)
 プレーヤー1の方が強い場合、カウンターtを1つカウントアップします。

4.関数の外
・import os, sys
 pythonの標準モジュールであるosモジュールとsysモジュールを使えるように準備します。

・sIn = os.path.join(os.path.dirname(sys.argv[0]), "poker.txt")
 ポーカー用テキストファイルのフルパスです。
 当pyソースと同じフォルダに置いていることを想定しています。
 sys.argv[0]は当pyソースのフルパスです。
 os.path.dirnameでその親フォルダ名を取得し、
 os.path.joinで親フォルダ名とファイル名を結合します。
 
・print f(sIn)
 上記関数fにポーカー用テキストファイルのフルパスを渡し、戻り値を表示します。
 

解答はこのすぐ下の行です。文字の色を白にしてます。選択状態にすると見えます。
376



0 件のコメント:

コメントを投稿