川渡りパズルの解答を探すプログラムを書いてみました。
※この記事ではパズルの解答を1つしか求めていませんが、複数の解答が考えられる問題(3.危険な家族)も存在します。
川渡りパズルとは
狼とヤギとキャベツの問題 http://r27.jp/quiz/across-cabbage/
1匹のオオカミと1匹のヤギを連れ、キャベツを携えた旅人が、舟で川を渡ろうとしている。 舟には、旅人の他に1つのものしか乗せることができない。 ヤギは旅人が近くにいないとキャベツを食べてしまう。 オオカミは旅人が近くにいないとヤギを食べてしまうのである。 全てのものを無事に向こう岸に渡すには、どうすればよいだろうか?
宣教師と先住民の問題 http://r27.jp/quiz/across-or-die/
3人の宣教師と3人の人喰い人種が、舟で川を渡ろうとしている。 舟は1艘しかないが、1度に2人まで乗ることができ、誰でも漕ぐことが可能である。 ただし、それぞれの岸において、人喰い人種の数が宣教師の数を上回ると、宣教師は食べられてしまう。 さて、全員が無事に川を渡り切るには、どうすればよいだろうか?
危険な家族の問題 http://r27.jp/quiz/across-family/
ある家族が、舟で川を渡ろうとしている。 家族は父、母、長男、次男、長女、次女、メイド、犬の8人であり、 舟は1艘しかないが、1度に2人まで乗ることができる。 また、舟を漕げるのは父、母、メイドの3人だけである。 ただしこの家族、実はとても危険な家族なのである。 まず父は、母が近くにいないと娘たちをいじめてしまう。 また母は、父が近くにいないと息子たちをいじめてしまう。 そして犬は、メイドが近くにいないと家族全員に襲い掛かってしまうのである。 さて、全員が無事に川を渡り切るには、どうすればよいだろうか?
解き方、考え方
- ルールで禁止されていない選択肢を幅優先探索
- ゴール(全員が川を渡り切る)すると解答
- 同じ局面に戻ってくるのは手数の無駄なので、各局面の左岸の状態とその直前の左岸の状態を記録しておき、過去に現れた局面を拒否する
- queueを使っているので一番初めに現れた解答が最短であることは確か
実装
ban
はそれぞれのタプルの第一項のメンバー全てが第二項のメンバー全てと異なる岸に居てはいけないという制約。
import queue import itertools def leftover(all_items, remove_items): leftover_items = [] for i in all_items: if i not in remove_items: leftover_items.append(i) return leftover_items LEFT = 0 RIGHT = 1 characters = sorted(['man', 'wolf', 'goat', 'cabbage', 'boat']) left_side = sorted(['man', 'wolf', 'goat', 'cabbage', 'boat']) # everybody in the left side of rever right_side = sorted([]) # nobady in the right side of rever rowers = ['man'] # the man only can row the boat boat_size = 2 ban = [(['man'], ['wolf', 'goat']), (['man'], ['goat', 'cabbage'])] # wolf eat goat if no man with them, goat eat cabbage if no man with them. goal = [[], sorted(['man', 'wolf', 'goat', 'cabbage', 'boat'])] Q = queue.Queue() past = dict() Q.put((left_side[:], right_side[:], ['start'])) while not Q.empty(): q = Q.get() if tuple(q[LEFT]) in past: continue else: past.update({tuple(q[LEFT]): q[2]}) if q[LEFT] == goal[LEFT] and q[RIGHT] == goal[RIGHT]: path = [] t = q[LEFT] while t != ['start']: path.append(t) t = past[tuple(t)] path.reverse() for p in path: print(p, leftover(characters, p)) exit() boat = RIGHT if 'boat' in q[LEFT]: boat = LEFT for rower in rowers: if rower in q[boat]: for passengers in itertools.combinations([''] * (boat_size - 1) + q[boat], boat_size - 1): if rower in passengers or 'boat' in passengers: continue left = q[LEFT][:] right = q[RIGHT][:] if boat == LEFT: left.remove('boat') right.append('boat') if rower != '': left.remove(rower) right.append(rower) for passenger in passengers: if passenger != '': left.remove(passenger) right.append(passenger) else: right.remove('boat') left.append('boat') if rower != '': right.remove(rower) left.append(rower) for passenger in passengers: if passenger != '': right.remove(passenger) left.append(passenger) is_banned = False for b in ban: if (all(c in left for c in b[0]) and all(c in right for c in b[1])) or (all(c in left for c in b[1]) and all(c in right for c in b[0])): is_banned = True if not is_banned: Q.put((sorted(left[:]), sorted(right[:]), sorted(q[LEFT][:]))) print('no answer')
dictはlistをキーにできない、タプルは必ず複数個の要素が必要という点にハマりました。 他の問題を解いたコードはgithubにあげてあります。(所要時間3時間) github.com