summer_tree_home

Check iOでPython3をマスターするぜっ

Anagrams By Stacks (Incinerator) - スタックを使って文字列を並べ替え

どんな問題?

Anagrams By Stacks
http://www.checkio.org/mission/anagrams-by-stacks/

文字列を並べ替えて別の単語(アナグラム)を作成せよ。例えば「rice」→「cire」など。
スタック1、2と、バッファ0(1文字分)があり、初期状態ではスタック1に「rice」が入っている。
スタック2が「cire」となるよう1文字ずつ移動させていく。
このときの最短の手順を探せ。

引数は、"rice-cire" という形で、最初の文字列と、最終的な文字列が与えられる。
戻り値は、"10,12,12,12,02" というように手順をコンマ区切りで返す。10であれば、スタック1からバッファ0への移動となる。

どうやって解く?

なんか、ハノイの塔みたいな問題だなぁ、というのが第一印象。ハノイの塔は、たしか一番小さな円盤を右に移動、それ以外の円盤を動かせる場所に動かす、の繰り返しで解けるんだっけ?

ハノイの塔との違いは、文字には円盤と違って大小がないので、どの順番でも積み上げられると言うこと。そして、真ん中の棒(バッファ)には一つしか置けないということ。ハノイの塔と同じ解き方では無理かな。

手順を考えてみる

「rice」を「cire」に変換する例を考えてみた。
まず最初に「c」をスタック2の一番底に積む必要がある。(「c」をtargetとする)

  • スタック1の最後が「c」(target)で、スタック2が空なら、スタック1からスタック2に移動
  • バッファ0が「c」(target)なら、スタック2を一旦すべてスタック1に移動してから、バッファ0からスタック2に移動
  • バッファ0とスタック2が空で、スタック1の最後から2番目に「c」(target)があれば、スタック1からバッファ0に移動し、スタック1からスタック2に移動
  • それ以外の場合(つまりスタック1の奥の方に「c」(target)がある場合)
    • バッファ0に何か入っていればスタック2に移動
    • 「c」(target)が出てくるまで、スタック1からスタック2に移動
    • 「c」(target)をバッファ0に移動
    • スタック2をすべてスタック1に移動
    • 「c」(target)をバッファ0からスタック2に移動

これで「c」がスタック2の一番底に移動できた。次は「i」が新たなtargetとなる。この繰り返しでいけるはずだ。

コードを書いてみた。

def checkio(data):
    first, second = data.split('-')

    stack1 = list(first)
    stack0 = []
    stack2 = []
    result = ''

    target_index = 0
    while stack2 != list(second):
        target = second[target_index]

        if len(stack1) > 0 and stack1[-1] == target and len(stack2) == target_index:
            # スタック1の最後がtargetで、スタック2が空なら、スタック1からスタック2に移動
            stack2.append(stack1.pop())
            result += '12,'
            target_index += 1
        elif len(stack0) > 0 and stack0[-1] == target:
            # バッファ0がtargetなら、スタック2をすべてスタック1に移動してから、バッファ0からスタック2に移動
            for i in range(len(stack2) - 1, target_index - 1, -1):
                stack1.append(stack2.pop())
                result += '21,'
            stack2.append(stack0.pop())
            result += '02,'
            target_index += 1
        elif len(stack1) > 1 and stack1[-2] == target and len(stack0) == 0 and len(stack2) == target_index:
            # バッファ0とスタック2が空で、スタック1の2番目にtargetがあれば、スタック1から0+スタック1から2
            stack0.append(stack1.pop())
            result += '10,'
            stack2.append(stack1.pop())
            result += '12,'
            target_index += 1
        else:
            # それ以外なら、バッファ0から2に移動、スタック1でtargetが出てくるまで1から2へ移動、targetを1から0に移動
            if len(stack0) > 0:
                stack2.append(stack0.pop())
                result += '02,'
            while stack1[-1] != target:
                stack2.append(stack1.pop())
                result += '12,'
            stack0.append(stack1.pop())
            result += '10,'

    return result[:-1]  # 最後のカンマを除去

なんか見づらいコードだ。
とりあえず、これでセルフチェック。
……失敗

最後のmirror-mirorrの問題で、もっと短い方法があると表示されてしまった。

失敗例の分析

何が問題かを分析するために、途中経過を表示するためのshow_progress()を作っておいた。

def show_progress(data, answer):
    def show_stacks():
        s1 = format(''.join(stacks[1]), str(len(first)))
        s2 = format(''.join(stacks[2]), str(len(first)))
        s0 = format(''.join(stacks[0]), '1')
        print('[{0}] [{1}] [{2}]'.format(s1, s0, s2))

    first, second = data.split('-')
    stacks = [[], list(first), []]
    show_stacks()
    for s in answer.split(','):
        src, dest = int(s[0]), int(s[1])
        stacks[dest].append(stacks[src].pop())
        show_stacks()

[スタック1] [バッファ0] [スタック2] の順に並んで表示される。
自分の解答と正解を比較してみた。

自分の解答 正解
[mirror] [ ] [ ] [mirror] [ ] [ ]
12 [mirro ] [ ] [r ] 12 [mirro ] [ ] [r ]
12 [mirr ] [ ] [ro ] 12 [mirr ] [ ] [ro ]
12 [mir ] [ ] [ror ] 10 [mir ] [r] [ro ]
12 [mi ] [ ] [rorr ] 12 [mi ] [r] [ror ]
12 [m ] [ ] [rorri ] 12 [m ] [r] [rori ]
10 [ ] [m] [rorri ] 02 [m ] [ ] [rorir ]
21 [i ] [m] [rorr ] 10 [ ] [m] [rorir ]
21 [ir ] [m] [ror ] 21 [r ] [m] [rori ]
21 [irr ] [m] [ro ] 21 [ri ] [m] [ror ]
21 [irro ] [m] [r ] 21 [rir ] [m] [ro ]
21 [irror ] [m] [ ] 21 [riro ] [m] [r ]
02 [irror ] [ ] [m ] 21 [riror ] [m] [ ]
12 [irro ] [ ] [mr ] 02 [riror ] [ ] [m ]
12 [irr ] [ ] [mro ] 12 [riro ] [ ] [mr ]
12 [ir ] [ ] [mror ] 12 [rir ] [ ] [mro ]
12 [i ] [ ] [mrorr ] 12 [ri ] [ ] [mror ]
10 [ ] [i] [mrorr ] 10 [r ] [i] [mror ]
21 [r ] [i] [mror ] 21 [rr ] [i] [mro ]
21 [rr ] [i] [mro ] 21 [rro ] [i] [mr ]
21 [rro ] [i] [mr ] 21 [rror ] [i] [m ]
21 [rror ] [i] [m ] 02 [rror ] [ ] [mi ]
02 [rror ] [ ] [mi ] 12 [rro ] [ ] [mir ]
12 [rro ] [ ] [mir ] 12 [rr ] [ ] [miro ]
12 [rr ] [ ] [miro ] 12 [r ] [ ] [miror ]
12 [r ] [ ] [miror ] 12 [ ] [ ] [mirorr]
12 [ ] [ ] [mirorr]

惜しい、あと一手。
気付いたのは、正解では序盤にバッファ0に「r」を入れている。これにより、序盤は一手増えているが、後半に二手減らすことができ、結果的に一手短縮しているわけだ。なぜ、「r」を入れたのか、理由はよく分からない。

  • 「rr」と2つ重なっていることが理由?
  • 「i」は「m」の次に来る文字なので、iが上に来るようにした?

よくわからない。

追加の問題と答え

掲示板(Discuss)を見ていたら、追加の問題と答えが書き込まれていた。これもセルフチェックに追加しておく。
http://www.checkio.org/forum/post/1334/anagrams-by-stacks-examples/

    check_solution(checkio, "arrrrrgh-ghrrrrra", 9)
    check_solution(checkio, "checkio-iocheck", 15)
    check_solution(checkio, "windows-downsiw", 16)
    check_solution(checkio, "tests-setts", 11)
    check_solution(checkio, "solution-tionulos", 17)
    check_solution(checkio, "superman-namperus", 14)
    check_solution(checkio, "library-rarybil", 8)

実行してみると、mirror-mirorr 以外にも windows-downsiw と solution-tionulos でも最短手順ではなく失敗。

[windows] [ ] [ ] [windows] [ ] [ ]
12 [window ] [ ] [s ] 12 [window ] [ ] [s ]
12 [windo ] [ ] [sw ] 10 [windo ] [w] [s ]
12 [wind ] [ ] [swo ] 12 [wind ] [w] [so ]
10 [win ] [d] [swo ] 02 [wind ] [ ] [sow ]
21 [wino ] [d] [sw ] 10 [win ] [d] [sow ]
21 [winow ] [d] [s ] 21 [winw ] [d] [so ]
21 [winows ] [d] [ ] 21 [winwo ] [d] [s ]
02 [winows ] [ ] [d ] 01 [winwod ] [ ] [s ]
12 [winow ] [ ] [ds ] 20 [winwod ] [s] [ ]
12 [wino ] [ ] [dsw ] 12 [winwo ] [s] [d ]
10 [win ] [o] [dsw ] 12 [winw ] [s] [do ]
21 [winw ] [o] [ds ] 12 [win ] [s] [dow ]
21 [winws ] [o] [d ] 12 [wi ] [s] [down ]
02 [winws ] [ ] [do ] 02 [wi ] [ ] [downs ]
10 [winw ] [s] [do ] 12 [w ] [ ] [downsi ]
12 [win ] [s] [dow ] 12 [ ] [ ] [downsiw]
12 [wi ] [s] [down ]
02 [wi ] [ ] [downs ]
12 [w ] [ ] [downsi ]
12 [ ] [ ] [downsiw]
[solution] [ ] [ ] [solution] [ ] [ ]
12 [solutio ] [ ] [n ] 10 [solutio ] [n] [ ]
12 [soluti ] [ ] [no ] 12 [soluti ] [n] [o ]
12 [solut ] [ ] [noi ] 12 [solut ] [n] [oi ]
10 [solu ] [t] [noi ] 02 [solut ] [ ] [oin ]
21 [solui ] [t] [no ] 10 [solu ] [t] [oin ]
21 [soluio ] [t] [n ] 21 [solun ] [t] [oi ]
21 [soluion ] [t] [ ] 21 [soluni ] [t] [o ]
02 [soluion ] [ ] [t ] 01 [solunit ] [ ] [o ]
12 [soluio ] [ ] [tn ] 20 [solunit ] [o] [ ]
12 [solui ] [ ] [tno ] 12 [soluni ] [o] [t ]
10 [solu ] [i] [tno ] 12 [solun ] [o] [ti ]
21 [soluo ] [i] [tn ] 02 [solun ] [ ] [tio ]
21 [soluon ] [i] [t ] 12 [solu ] [ ] [tion ]
02 [soluon ] [ ] [ti ] 12 [sol ] [ ] [tionu ]
10 [soluo ] [n] [ti ] 12 [so ] [ ] [tionul ]
12 [solu ] [n] [tio ] 12 [s ] [ ] [tionulo ]
02 [solu ] [ ] [tion ] 12 [ ] [ ] [tionulos]
12 [sol ] [ ] [tionu ]
12 [so ] [ ] [tionul ]
12 [s ] [ ] [tionulo ]
12 [ ] [ ] [tionulos]

一から考え直してみる

なんか、根本的に違う気がしてきた。最短手順を探すってことは、最短経路問題ってことで、これもBFSやA*で解くような問題なのかな?

迷路の探索と比較して考えてみる。

  • 上下左右への移動は、毎回のアクション('10'や'12'など)で置き換えられる。
  • 現在位置とは、現在のスタックの状態と考える。
  • スタートはスタック1に'rice'が入った状態、ゴールはスタック2に'cire'が入った状態と考える。
  • 壁や障害物は、アクションが実行できない状態(例えばスタックが空なのにpop()する)だと考える。

こう考えると、迷路の探索と同じ考え方でいけそうだ。

BFSを使ったコード

2つのスタックとバッファは、文字列のタプルで表現する。
バッファ0が[m]、スタック1が[ir]、スタック2が[ror]なら、('m','ir','ror')となる。

def checkio(data):
    def get_next(stacks, action):
        # 次の行動("10"や"02"など)によって変化するスタックの状態を返す。
        src, dest = int(action[0]), int(action[1])
        dest_max_length = 1 if dest == 0 else len(first)
        if stacks[src] == '' or len(stacks[0]) >= dest_max_length:
            return None
        else:
            r = list(stacks)
            r[dest] += r[src][-1]
            r[src] = r[src][:-1]
            return tuple(r)

    def get_path():
        # visitedフラグを逆にたどってスタートからの経路を作成
        pt = goal
        result = ''
        while pt != start:
            prev, action = visited[pt]
            result = action + ',' + result
            pt = prev
        return result[:-1]

    first, second = data.split('-')

    start = ('', first, '')  # 初期のスタックの状態
    goal = ('', '', second)  # 最終的なスタックの状態

    visited = dict()
    q = [start]

    while len(q) > 0:
        current_stacks = q.pop(0)
        if current_stacks[2] == second:
            return get_path()
        for act in ("12", "10", "01", "02", "20", "21"):
            next_stacks = get_next(current_stacks, act)
            if next_stacks is not None and next_stacks not in visited:
                visited[next_stacks] = (current_stacks, act)
                q.append(next_stacks)

BFSなので、Queueを使っている。(q.append()で追加、q.pop(0)で取り出し。)

いつもと同じようなコードだが、これでいけるはず。
まずはセルフチェック……OK!
Run & Check……クリア~!

これまた、あんなに苦労した問題が、あっさりと解けてしまった。
BFSってすごいなぁと思う反面、結局いつもこのパターンかと少しがっかり。


もしA*探索を使うなら、ゴールまでの距離を何に置き換えればいいんだろう?
最終的なスタックの状態にどれだけ近いかを数値化、という感じかな。

まあ、それはまた後日のチャレンジということで、今日はここまで。