TigerCow.Door

안녕하세요.

이번에는 python을 이용한 CSP algorithm 중 하나인 Einstein's puzzle(아인슈타인 퍼즐) 해결 코드를 작성해 보겠습니다.



먼저 아인슈타인 퍼즐에 대해 소개해 드릴게요.

아인슈타인의 퍼즐은 아래와 같은 문제입니다.


* 문제의 배경

1. 색깔이 서로 다른 5채의 집이 일렬로 지어져 있다.

2. 각 집에는 서로 다른 국적의 사람이 살고 있다.

3. 다섯 사람은 서로 다른 음료를 마시고, 서로 다른 담배를 피며, 서로 다른 동물을 기른다.


* 15개의 정보

1. 영국인은 빨간 집에서 산다.

2. 스웨덴인은 개를 기른다.

3. 덴마크인은 차를 마신다.

4. 초록집은 하얀집의 왼쪽 집이다.

5. 초록집에 사는 사람은 커피를 마신다.

6. 펠몰 담배를 피는 사람은 새를 기른다.

7. 노란집에 사는 사람은 던힐 담배를 피운다.

8. 한 가운데 집에 사는 사람은 우유를 마신다.

9. 노르웨이 인은 첫번째 집에 산다.

10. 블렌드 담배를 피우는 사람은 고양이를 기르는 사람 옆 집에 산다.

11. 말을 기르른 사람은 던힐 담배를 피우는 사람 옆 집에 산다.

12. 블루매스터 담배를 피는 사람은 맥주를 마신다.

13. 독일인은 프린스 담배를 피운다.

14. 노르웨이인은 파란 집 옆 집에 산다.

15. 블렌드 담배를 피우는 사람은 생수를 마시는 사람과 이웃이다.


위의 문제 배경과 15개의 정보가 있을때, '물고기를 기르는 사람의 국적은 무엇인가'?

(문제 출처: https://web.stanford.edu/~laurik/fsmbook/examples/Einstein%27sPuzzle.html)



이론적인 문제 풀이 방식은 아래와 같은 순서로 풀이가 가능합니다.

먼저 15개의 정보 중에서 확실히 알 수 있는 사실들을 이용한다. 15개의 정보 중에서 8, 9, 14번의 정보는 순서대로 하여 확실히 특정 사실을 알 수 있는 정보이다. 세가지 정보를 이용하면 위의 표와 같이 알 수 있다.


두번째로는 특정 조건들을 결합하여 이용한다. 먼저 4번 정보와 5번 정보를 이용하였다. 4번 정보를 통해 초록집은 하얀 집의 왼쪽 집임을 만족시켜야 한다. 4번 정보를 다르게 해석하면, 초록집의 오른쪽 집은 하얀 집이다. , 1, 2, 5번은 초록집이 될 수 없다. 그리고 5번 정보를 이용하면 초록 집은 3번이 될 수 없다. 따라서 남은 것은 4번뿐이므로 초록집은 4번이다. 또한 4번 집사람은 커피를 마시고 5번집은 하얀집이다. 다음 1번 정보를 확인하면, 2,4,5번 집은 이미 색이 있으므로 빨간집이 아니며 1번집은 노르웨이사람이 살고 있으므로 1번도 아니다. 3번 집이 빨간집이며 영국사람이 산다. 이어서 남은 1번은 자연스럽게 노란색 집이되며 7번 정보를 통해 1번집 사람은 던힐담배를 피운다. 또한 11번 정보를 통해 2번 집 사람은 말을 기른다.



이제 주어진 조건들을 응용하여 추리한다. 3번 정보와 12번 정보를 해석한다. 현재 남은 음료는 생수, , 맥주이다. 3번 정보에서 덴마크인이 차를 마신다고 하였으므로 노르웨이인은 생수 또는 맥주를 마실 것이다. 12번 정보에서 블루매스터 담배를 피우는 사람은 맥주를 마신다고 하였는데, 노르웨이인은 던힐 담배를 피우므로 맥주를 마시지 않는다. 즉 노르웨이인은 생수를 마신다. 또한 15번 정보를 통해서 2번 집 사람은 블렌드 담배를 피운다.

다시 12번 정보를 확인한다. 2번 집 사람은 차 또는 맥주를 마실 텐데 12번 정보에서 블루매스터 담배를 피는 사람이 맥주를 마신다고 하였다. 2번 집 사람은 블렌드 담배를 피우므로 맥주를 마시지 않고 차를 마신다. 그리고 남은 맥주는 당연히 5번 집 사람이 마시고, 5번 집 사람은 블루매스터 담배를 핀다. 이어서 3번 정보를 통해 차를 마시는 2번 집 사람은 덴마크인이다. 




마지막으로 남은 조건들로 추리를 완성한다. 먼저 13번 정보를 확인한다. 독일인은 4번 또는 5번집에 살텐데 5번 집 사람은 블루매스터 담배를 핀다. 따라서 13번 정보를 통해 독일인은 4번집에 살며 프린스 담배를 피운다. 이어서 5번집 사람은 스웨덴인이며, 3번집 사람은 펠몰담배를 핀다.

다음 6번 정보를 통해 펠몰담배를 피는 3번집 사람은 새를 기른다. 또한 2번 정보를 통해 5번 집에 사는 스웨덴인은 개를 기른다.

마지막으로 10번 정보를 통해서 3번 집사람은 이미 새를 키우는 것을 알았으므로 1번집 사람이 고양이를 기른다. 최종적으로 남은 4번집 사람은 물고기를 기른다.




이렇게 해서 아인슈타인의 퍼즐을 해결할 수 있습니다.

이러한 문제를 CSP(Constraint Satisfaction Problems: 제약조건 만족문제)라고 합니다.


아인슈타인 퍼즐을 해결하기 위해 2가지 정도의 알고리즘을 생각해볼 수 있습니다.

첫번째로는, 모든 경우의 수(5^25 or (5!)^5 둘 다 매우 큰 수 입니다...)를 고려하여 그 중 하나를 꺼내 정보와 비교하는 방법.

해당 방법은, 모든 경우의 수를 (5!)^5 으로 고려했을때 경우의 수가 약 240억개 입니다.

우리들의 컴퓨터로는 해결하기가 매우 힘들 수 있죠..


그래서 생각한 것이 두번째 알고리즘 입니다.

두번째 알고리즘은 각각을 카테고리 또는 조건으로 나누어 해결하는 방법입니다.

즉, A라는 조건을 다수의 배열 중 만족하는 배열을 찾아내는 것이 아니라 다수의 배열을 선언하기 전에 A라는 조건을 먼저 대입하고

이를 만족하는 배열 a가 있다면 a를 다음 조건, B에 대입하여 확인해나갑니다.

이렇게 해결을 진행하면 결국 DFS 방법으로 해결방법을 찾아나간다는 것을 아실 수 있습니다.


python으로 코드를 작성하기 위해 검색하던 중, python에서 제공하는 강력한 함수를 찾았습니다.

바로, python-constraint 함수 입니다.

함수에 대한 추가적인 사항은 아래 링크를 참고하시면 좋을 것 같습니다.

https://labix.org/python-constraint

https://pypi.python.org/pypi/python-constraint


python-constraint 함수를 통해, 직접 변수를 설정하고 제약조건을 추가하여 문제를 해결할 수 있습니다.

생각보다 code가 매우 간단해질 수 있어서 꼭 한번 다시 공부해보고 싶은 함수입니다.

추가적인 질문 이나 궁금사항은 댓글로 달아주시거나 이메일 주세요!


전체적인 python code와 실행결과는 아래와 같습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# Houses
# 1 2 3 4 5
 
from constraint import *
problem = Problem()
 
nationality = ["영국""스웨덴""독일""노르웨이""덴마크"]
pet = ["개""고양이""새""말""물고기"]
cigarette = ["던힐""블렌드"
"펠몰""프린스""블루매스터"]
colour = ["빨강""초록""노랑""파랑""하양"]
beverage = ["커피""우유""맥주""생수""차"]
 
criteria = nationality + pet + cigarette + colour + beverage
problem.addVariables(criteria,[1,2,3,4,5])
 
problem.addConstraint(AllDifferentConstraint(), nationality)
problem.addConstraint(AllDifferentConstraint(), pet)
problem.addConstraint(AllDifferentConstraint(), cigarette)
problem.addConstraint(AllDifferentConstraint(), colour)
problem.addConstraint(AllDifferentConstraint(), beverage)
 
problem.addConstraint(lambda e, r: e == r, ["영국","빨강"])#1
problem.addConstraint(lambda s, d: s == d, ("스웨덴","개"))#2
problem.addConstraint(lambda c, g: c == g, ("커피","초록"))#5
problem.addConstraint(lambda u, t: u == t, ("덴마크","차"))#3
problem.addConstraint(lambda g, i: g-== 1, ("하양","초록"))#4
problem.addConstraint(lambda o, s: o == s, ("펠몰","새"))#6
problem.addConstraint(lambda k, y: k == y, ("던힐","노랑"))#7
problem.addConstraint(InSetConstraint([3]), ["우유"])#8
problem.addConstraint(InSetConstraint([1]), ["노르웨이"])#9
problem.addConstraint(lambda c, f: abs(c-f) == 1, ("블렌드","고양이"))#10
problem.addConstraint(lambda k, h: abs(k-h) == 1, ("던힐","말"))#11
problem.addConstraint(lambda l, o: l == o, ["블루매스터","맥주"])#12
problem.addConstraint(lambda j, p: j == p, ["독일","프린스"])#13
problem.addConstraint(lambda k, h: abs(k-h) == 1, ("노르웨이","파랑"))#14
 
solution = problem.getSolutions()[0]
 
for i in range(1,6):
  for x in solution:
    if solution[x] == i:
      print (str(i), x)
cs



블로그 이미지

Tigercow.Door

Data-Analysis / AI / back-end / Algorithm / DeepLearning / etc

많은 사람들에게 유명한
missionaries and cannibals problem
(선교사와 식인종 문제)

강의 왼쪽에 선교사 3명과 식인종 3명이 있고 모두를 강 오른쪽으로 건너게끔 해야한다.
1. 강의 왼쪽이나 오른쪽에서 선교사의 수보다 식인종의 수가 많으면 안된다.
2. 보트에는 최대 2명이 탑승할 수 있으며 최소1명이 탑승해야 움직일 수 있다.

학교 AI시간에 각 state와 action에 대해서 제출하라는 과제가 있었는데 그것을 통해 DFS algorithm을 응용하여 python으로 구현해보았다.
분명히 잘못된 부분이나 부족한 부분이 있을 것 같은데
일단 포스팅을 진행하고 천천히 더 공부해야 겠다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
import copy
# 전역변수 선언
state = [1,[3,3],[0,0]]
action = [1,0,0]
goal = [2,[0,0],[3,3]]
visited = []
stack = []
temp = []
 
# 함수선언
# 왼쪽에서 오른쪽으로 가는 움직임 반환
def LtoRaction():
  return [[1,1,1],[1,1,0],[1,2,0],[1,0,1],[1,0,2]]
 
# 오른쪽에서 왼쪽으로 가는 움직임 반환
def RtoLaction():
  return [[2,1,1],[2,1,0],[2,2,0],[2,0,1],[2,0,2]]
 
# 스테이트와 액션에 맞게 움직이기
def move(state,action):
  after_state = [0,[0,0],[0,0]]
  after_state = copy.deepcopy(state)
  if (action[0== state[0]):
    # 보트가 왼쪽에서 오른쪽으로 움직이는 행동
    if (after_state[0]==1):
      after_state[0= 2      
      after_state[1][0= after_state[1][0- action[1]
      after_state[1][1= after_state[1][1- action[2]
      after_state[2][0= after_state[2][0+ action[1]
      after_state[2][1= after_state[2][1+ action[2]
    # 보트가 오른쪽에서 왼쪽으로 움직이는 행동
    elif (after_state[0]==2):
      after_state[0= 1            
      after_state[1][0= after_state[1][0+ action[1]
      after_state[1][1= after_state[1][1+ action[2]
      after_state[2][0= after_state[2][0- action[1]
      after_state[2][1= after_state[2][1- action[2]
 
    return after_state
 
  else:
    print("@ @ @ 보트의 위치와 action의 시작 위치가 맞지 않습니다.@ @ @")
    return 0
 
# 올바른 움직임 인지 체크
def check(after_state):
  left = after_state[1]
  right = after_state[2]
  # 선교사 또는 식인종의 수가 음수이면 잘못된 것
  if (left[0]<0)or(left[1]<0)or(right[0]<0)or(right[1]<0):
    return -1
  # 선교사 또는 식인종의 수가 4를 넘으면 잘못된 것
  elif (left[0]>=4)or(left[1]>=4)or(right[0]>=4)or(right[1]>=4):
    return -1
  # 왼쪽, 오른쪽 모두 선교사의 숫자가 식인종보다 크거나 많아야함
  elif (left[0]>=left[1]) and (right[0]>=right[1]):
    return 1
  # 왼쪽 선교사의 숫자가 0일때 오른쪽의 선교사의 숫자가 3이면 정상
  elif (left[0]==0and (right[0]==3):
    return 2
  # 왼쪽 선교사의 숫자가 3일때 오른쪽의 선교사의 숫자가 0이면 정상
  elif (left[0]==3and (right[0]==0):
    return 2
  else:
    return -3
 
# 목표에 도달했을때 과정 출력
def done(list):
  print("끝!",list)
  temp = []
  num = -1
  while list:
    temp = list.pop(0)
    num += 1
    if (temp[0]==1):
      print("Moon.B.W   * * * * * 현재상태 * * * * *",num,"번째 움직임")
      print(" --------------------------------------------------")
      print("|                  ||~~~~~~~~~~||                  |")
      print("|   선교사 : %d명   ||~~~~~~~~~~||   선교사 : %d명   |"%(temp[1][0],temp[2][0]))
      print("|                  ||<->~~~~~~~||                  |")    
      print("|   식인종 : %d명   ||~~~~~~~~~~||   식인종 : %d명   |"%(temp[1][1],temp[2][1]))
      print("|                  ||~~~~~~~~~~||                  |")
      print(" --------------------------------------------------")
    
    elif (temp[0]==2):
      print("Moon.B.W   * * * * * 현재상태 * * * * *",num,"번째 움직임")
      print(" --------------------------------------------------")
      print("|                  ||~~~~~~~~~~||                  |")
      print("|   선교사 : %d명   ||~~~~~~~~~~||   선교사 : %d명   |"%(temp[1][0],temp[2][0]))
      print("|                  ||~~~~~~~<->||                  |")
      print("|   식인종 : %d명   ||~~~~~~~~~~||   식인종 : %d명   |"%(temp[1][1],temp[2][1]))
      print("|                  ||~~~~~~~~~~||                  |")
      print(" --------------------------------------------------")
  print("움직인 횟수 :",num,"번")
 
# 깊이 우선 탐색으로 솔루션 찾기
def DFS():
  global stack, visited, goal
  while stack:
    none = 0
    # 하나의 노드를 꺼내서 탐색 시작
    now_state = stack.pop()
    state = copy.deepcopy(now_state)
    # 기록
    visited.extend([now_state])
    # 보트가 왼쪽에 있는 상태
    if (now_state[0]==1):
      if (now_state == goal):
        print("탐색 성공!")
        done(visited)
        return 0
      else:
        available_action = LtoRaction()
        while(available_action):
          action = available_action.pop()
          after_state = move(now_state,action)
          can = check(after_state)
          if (can>0):
            if after_state not in visited:
              none = 1
              stack.extend([after_state])
          else:
            pass
        if (none==0):
          visited.pop()
 
    # 보트가 오른쪽에 있는 상태
    elif (now_state[0]==2):
      if (now_state == goal):
        print("탐색 성공!")
        done(visited)
        return 0
      else:
        available_action = RtoLaction()
        while(available_action):
          action = available_action.pop()
          after_state = move(now_state,action)
          can = check(after_state)
          if (can>0):
            if after_state not in visited:
              none = 1
              stack.extend([after_state])
          else:
            pass
        if (none==0):
          visited.pop()
 
# 실행
stack.extend([state])
DFS()
cs



블로그 이미지

Tigercow.Door

Data-Analysis / AI / back-end / Algorithm / DeepLearning / etc