麻雀の手牌に対する待ちを出力する問題をRubyで解いた
スポンサーリンク
スポンサーリンク
ライフスタイル関連のコンテンツ
お金 | 仕事 | 勉強 | プライベート | 健康 | 心
プログラミング関連のコンテンツ
C言語/C++入門 | Ruby入門 | Python入門 | プログラミング全般
お金 | 仕事 | 勉強 | プライベート | 健康 | 心
プログラミング関連のコンテンツ
C言語/C++入門 | Ruby入門 | Python入門 | プログラミング全般
makeplex salon:あなたのスキルで飯は食えるか? 史上最大のコーディングスキル判定 (1/2) – ITmedia エンタープライズ
麻雀の手牌が入力として与えられたとき、「待ち」を出力するプログラムを書いてください。
前回の迷路問題に続いて、麻雀の手待ちを求める面白そうな問題が出題されていたので、またRubyで解いてみました。
スポンサーリンク
前回の問題よりも今回の問題の方が、私にとっては解きやすかったです。麻雀好きだったのが功を奏したのか?
問題には書いてなかったけど、おまけでチートイツ(七対子)の待ちも求めるようにした。
今回は、迷路問題と違って、一切アルゴリズム調べたりとかをせずに、約2時間弱で解けました。たぶん解けてるはず。
書きっぱなしで、DRY違反する部分とか多いですが、面倒くさいのでそのままで。
ループ回数もたいしたことなさそうだったので、探索を減らす努力とかしてませんが、気が向いたらリファクタリングと共にチャレンジする。
では、まず問題の全文から。
麻雀の手牌が入力として与えられたとき、「待ち」を出力するプログラムを書いてください。 * 字牌なし・萬子のみの想定、つまり、いわゆる「チンイツ」限定で結構です(プログラミングの本質的にはこの限定でまったく問題ないため) * 1~9の数字13個からなる文字列を受け取り、できている順子・刻子・アタマを()、待ちの部分を[]でくくって出力してください * 面前かつ槓子は存在しない前提でOKです * ()[]の出力順は自由ですが、順序だけが違うものは同一視してください(例:111222を刻子2つで構成するとき、(111)(222)が(222)(111)に入れ替わるだけのものは同一解答とします) * 多面待ちのときも含めすべての待ちを出力してください * 待ちがないときは何も出力しないでください コメント 無駄な探索をできるだけしないための工夫、デバッグを効率化するための工夫などがあればソースコードにコメントとして記入してください。 出力例 1112224588899 : 単純なケースです。45を軸にする両面の待ちなので、(111)(222)(888)(99)[45]になります。 1122335556799 : “99”をアタマの両面か“55”“99”のシャボであるので、(123)(123)(555)(99)[67]、(123)(123)(55)(567)[99]、(123)(123)(99)(567)[55]が正解です。 1112223335559 : 待ちは“9”単騎ですが、(123)(123)(123)(555)[9]と(111)(222)(333)(555)[9]の2つあります。 1223344888999 : 1-4の“ノベタン”待ちですが、4をアタマにしての[23]待ちと、1単騎、4単騎で3個の答えになります。 (123)[23](44)(888)(999), [1](234)(234)(888)(999), (123)(234)[4](888)(999) 1112345678999 : 「九蓮宝燈」という役です。1~9すべてが待ちになっています。これに正しく答えが出るのであれば、プログラムはほぼ正しいでしょう。
以下、書いたコード。
かなり力技な部分もあるし、もっと短く書けそうだけど、ざっと書いたら僕の技量だとこんなもん。
特に残り4牌の構成を求める部分と、順子、刻子を探索する部分はどうにかしなきゃですね。
require "pp" TEHAI = 13 LIMIT = 3 # 槓子(同じ牌が4枚)は存在しない class Majan attr_reader :tehais, :len, :used # ソート済みの手牌を渡す def initialize(tehais) @tehais = tehais @len = @tehais.length # 手牌を面子で使ったかどうかのフラグ @used = [] @len.times do |i| @used[i] = false end @answer = [] end # 未使用の手牌の中でインデックスnを起点にして順子を探索 def make_shuntu(n) shuntu = [] used_indexes = [] if @used[n] == false shuntu.push(@tehais[n]) @used[n] = true used_indexes.push(n) end (n...@len).each do |i| if @used[i] == false if @tehais[i] == @tehais[n] + 1 && !shuntu.include?(@tehais[i]) shuntu.push(@tehais[i]) @used[i] = true used_indexes.push(i) end if @tehais[i] == @tehais[n] + 2 && !shuntu.include?(@tehais[i]) shuntu.push(@tehais[i]) @used[i] = true used_indexes.push(i) end if (shuntu.length == 3) return "(#{shuntu.to_s})" end end end # 順子ができなかったらusedフラグをクリア used_indexes.each do |u| @used[u] = false end return false end # 未使用の手牌の中でインデックスnを起点にして刻子を探索 def make_koutu(n) koutu = [] used_indexes = [] if @used[n] == false koutu.push(@tehais[n]) @used[n] = true used_indexes.push(n) end (n...@len).each do |i| if @used[i] == false if @tehais[i] == @tehais[n] koutu.push(@tehais[i]) @used[i] = true used_indexes.push(i) end if (koutu.length == 3) return "(#{koutu})" end end end # 刻子ができなかったらusedフラグをクリア used_indexes.each do |u| @used[u] = false end return false end # 順子・刻子が3つ出来上がった後の残り4牌の構成を調査。聴牌のケースは以下の通り。 # 1144 / 2256 / 5688 / 3456 / 3345 / 3455 / 3445 / 1345 / 3458 / 1333 / 3338 / 2333 / 3334 def rest_hai rest = [] (0...@len).each do |i| if @used[i] == false rest.push(@tehais[i]) end end rest.sort! if rest.length != 4 return "error: rest num is not 4" end a, b, c, d = rest[0], rest[1], rest[2], rest[3] # 1144 => [ ["(11)", "[44]"], ["[11]", "(44)"] ] if a == b && c == d return [ [mentu(a, b), mati(c, d)], [mati(a, b), mentu(c, d)] ] # 2256 => [ ["(22)", "[56]"] ] elsif a == b && b+1 < c && c+1 == d return [ [mentu(a, b), mati(c, d)] ] # 5688 => [ ["[56]", "(88)"] ] elsif c == d && c-1 > b && b-1 == a return [ [mati(a, b), mentu(c, d)] ] # 3456 => [ ["[3]", "(456)"], ["(345)", "[6]"] ] elsif a+1 == b && a+2 == c && a+3 == d return [ [mati(a), mentu(b, c, d)], [mentu(a, b, c), mati(d)] ] # 3345 => [ ["[3]", "(345)"], ["(33)", "[45]"] ] elsif a == b && b+1 == c && c+1 == d return [ [mati(a), mentu(b, c, d)], [mentu(a, b), mati(c, d)] ] # 3455 => [ ["[34]", "(55)"], ["(345)", "[5]"] ] elsif c == d && c-1 == b && b-1 == a return [ [mati(a, b), mentu(c, d)], [mentu(a, b, c), mati(d)] ] # 3445 => [ ["(345)", "[4]"] ] elsif b == c && a == b-1 && d == c+1 return [ [mentu(a, b, d), mati(c)] ] # 1345 => [ ["[1]", "(345)"] ] elsif a+1 < b && b+1 == c && c+1 == d return [ [mati(a), mentu(b, c, d)] ] # 3458 => [ ["(345)", "[8]"] ] elsif a+1 == b && b+1 == c && c+1 < d return [ [mentu(a, b, c), mati(d)] ] # 1333 => [ ["[1]", "(333)"] ] elsif a+1 < b && b == c && c == d return [ [mati(a), mentu(b, c, d)] ] # 3338 => [ ["(333)", "[8]"] ] elsif a == b && b == c && c+1 < d return [ [mentu(a, b, c), mati(d)] ] # 2333 => [ ["[2]", "(333)"], ["[23]", "(33)"] ] elsif a+1 == b && b == c && c == d return [ [mati(a), mentu(b, c, d)], [mati(a, b), mentu(c, d)] ] # 3334 => [ ["(333)", "[4]"], ["(33)", "[34]"] ] elsif a == b && b == c && c+1 == d return [ [mentu(a, b, c), mati(d)], [mentu(a, b), mati(c, d)] ] # テンパってない else return false end end def mentu(i1=nil, i2=nil, i3=nil) return "(" + i1.to_s + i2.to_s + i3.to_s + ")" end def mati(i1=nil, i2=nil, i3=nil) return "[" + i1.to_s + i2.to_s + i3.to_s + "]" end # チートイツ判別 def titoitu tenpai = [] rest = [] # 裸単騎待ちの牌を抜き出す i = nil (0...@len).each do |i| # @tehais[i]が2つ含まれなかったら、@tehais[i]がとりあえず裸単騎待ちの候補 rest = @tehais.clone rest.delete_at(i) rest.sort! break unless rest.include?(@tehais[i]) end tenpai.push(mati(@tehais[i])) # 裸単騎待ち候補以外が全部ペアならチートイツ聴牌、そうでないならチートイツ聴牌じゃない 0.step(rest.length-1, 2) do |j| if rest[j] != rest[j+1] return false end tenpai.push(mentu(rest[j], rest[j+1])) end return sort_tenpai(tenpai).to_s end def sort_tenpai(arr) return arr.sort_by{|i| i[1..(i.length-1)]} end def clear_used # usedフラグをクリア (0...@len).each do |i| @used[i] = false end end def process_rest_hai(tenpai) if (rh = rest_hai) != false rest_hai.each do |rests| tenpai_tmp = tenpai.clone rests.each do |rest| tenpai_tmp.push(rest) end if tenpai_tmp.length == 5 @answer.push(sort_tenpai(tenpai_tmp).to_s) end end end end def solve # チートイツ if (ti = titoitu) != false @answer.push(ti) end # 順子→刻子 の順で探索。面子が3個できたらbreak (0...@len).each do |i| tenpai =[] # make_shuntu hai_range = (i..@len-1).map + (0..i-1).map hai_range.each do |n| if (shuntu = make_shuntu(n)) != false tenpai.push(shuntu) end # rest if tenpai.length == 3 process_rest_hai(tenpai) break end end # make_koutu hai_range.each do |n| if (koutu = make_koutu(n)) != false tenpai.push(koutu) end # rest if tenpai.length == 3 process_rest_hai(tenpai) break end end clear_used end clear_used # 刻子→順子 の順で探索。面子が3個できたらbreak (0...@len).each do |i| tenpai =[] # make_koutu hai_range = (i..@len-1).map + (0..i-1).map hai_range.each do |n| if (koutu = make_koutu(n)) != false tenpai.push(koutu) end # rest if tenpai.length == 3 process_rest_hai(tenpai) break end end # make_shuntu hai_range.each do |n| if (shuntu = make_shuntu(n)) != false tenpai.push(shuntu) end # rest if tenpai.length == 3 process_rest_hai(tenpai) break end end clear_used end clear_used # 順子刻子と並行に調査。面子が3個できたらbreak (0...@len).each do |i| tenpai =[] hai_range = (i..@len-1).map + (0..i-1).map hai_range.each do |n| # make_shuntu if (shuntu = make_shuntu(n)) != false tenpai.push(shuntu) end # make_koutu if (koutu = make_koutu(n)) != false tenpai.push(koutu) end # rest if tenpai.length == 3 process_rest_hai(tenpai) break end end clear_used end clear_used # 刻子順子と並行に調査。面子が3個できたらbreak (0...@len).each do |i| tenpai =[] hai_range = (i..@len-1).map + (0..i-1).map hai_range.each do |n| # make_koutu if (koutu = make_koutu(n)) != false tenpai.push(koutu) end # make_shuntu if (shuntu = make_shuntu(n)) != false tenpai.push(shuntu) end # rest if tenpai.length == 3 process_rest_hai(tenpai) break end end clear_used end clear_used if (@answer.length > 0) return @answer.uniq else return "no answer" end end end # 手牌を13枚ランダムに配る def random_tehai tehais = [] hai_num = Hash.new(0) TEHAI.times do |i| hai = rand(9) + 1 hai_num[hai] += 1 if (hai_num[hai] > LIMIT) while (true) hai = rand(9) + 1 hai_num[hai] += 1 if (hai_num[hai] <= LIMIT) break end end end tehais.push(hai) end tehais.sort.to_s end def display_answer(tehai) puts "----- #{tehai} -----" arr = tehai.split(//).map{|i| i.to_i} majan = Majan.new(arr.sort) puts majan.solve puts end puts "■例題\n\n" tehai = "1112224588899" display_answer(tehai) tehai = "1122335556799" display_answer(tehai) tehai = "1112223335559" display_answer(tehai) tehai = "1223344888999" display_answer(tehai) tehai = "1112345678999" display_answer(tehai) puts puts "■チートイツ絡み\n\n" # チートイツ聴牌かつ多面待ち tehai = "3344556778899" display_answer(tehai) # チートイツ崩れノーテン tehai = "2234456677899" display_answer(tehai) # チートイツ崩れだが聴牌 tehai = "2334456678899" display_answer(tehai) puts puts "■ランダム手牌\n\n" 10.times do display_answer(random_tehai) end
実行結果。
一応、けっこう速攻で答えが求まりました。
■例題 ----- 1112224588899 ----- (111)(222)[45](888)(99) ----- 1122335556799 ----- (123)(123)(55)(567)[99] (123)(123)[55](567)(99) (123)(123)(555)[67](99) ----- 1112223335559 ----- (123)(123)(123)(555)[9] (111)(222)(333)(555)[9] ----- 1223344888999 ----- (123)(234)[4](888)(999) [1](234)(234)(888)(999) (123)[23](44)(888)(999) ----- 1112345678999 ----- (11)(123)(456)(789)[99] [11](123)(456)(789)(99) (111)(234)(567)[8](999) (111)[2](345)(678)(999) (11)[12](345)(678)(999) (11)(123)[45](678)(999) (111)(234)[5](678)(999) (11)(123)(456)[78](999) (111)(234)(567)[89](99) (111)[23](456)(789)(99) (111)(234)[56](789)(99) ■チートイツ絡み ----- 3344556778899 ----- (33)(44)(55)[6](77)(88)(99) (345)(345)(678)[78](99) (345)(345)(678)(789)[9] (345)[3](456)(789)(789) (33)(456)[45](789)(789) (345)(345)[6](789)(789) ----- 2234456677899 ----- no answer ----- 2334456678899 ----- (234)(345)(66)(789)[89] ■ランダム手牌 ----- 1112244555689 ----- no answer ----- 2223356778899 ----- (222)(33)(567)(789)[89] (222)(33)[56](789)(789) ----- 1222446667799 ----- no answer ----- 1112234567999 ----- (111)(234)[2](567)(999) (11)[12](234)(567)(999) (111)(22)(345)[67](999) (111)(22)[34](567)(999) ----- 1112333446677 ----- no answer ----- 1123346677899 ----- no answer ----- 1222335558899 ----- no answer ----- 1222345567899 ----- no answer ----- 1233344455678 ----- (123)(345)(345)[4](678) [1](234)(345)(345)(678) [12](333)(444)(55)(678) ----- 1223344556678 ----- (123)(234)(456)[5](678) (123)(234)(456)(567)[8] (123)[2](345)(456)(678)
九蓮宝燈も解けてるっぽいので、まぁ良いかな・・・
アルゴリズム一切ググらずに解けたのはちょっと自信つきました。
スポンサーリンク
>> 次の記事 : Rubyでエイトクイーン問題をバックトラック法で解く
<< 前の記事 : Rubyで迷路問題を解いた
スポンサーリンク