From 1a85702dfe781878d0be3cb1c9aaaa73292e71d9 Mon Sep 17 00:00:00 2001 From: Subangkar Date: Thu, 22 Nov 2018 11:12:12 +0600 Subject: [PATCH] Implementation in Python 3 --- .gitignore | 2 + Constants.py | 17 +++++++ Graph.py | 111 ++++++++++++++++++++++++++++++++++++++++++ Problem Statement.txt | 8 +++ README.md | 20 +++++++- State.py | 91 ++++++++++++++++++++++++++++++++++ in.txt | 80 ++++++++++++++++++++++++++++++ main.py | 89 +++++++++++++++++++++++++++++++++ outBFS.txt | 56 +++++++++++++++++++++ outDFS.txt | 56 +++++++++++++++++++++ 10 files changed, 529 insertions(+), 1 deletion(-) create mode 100644 Constants.py create mode 100644 Graph.py create mode 100644 Problem Statement.txt create mode 100644 State.py create mode 100644 in.txt create mode 100755 main.py create mode 100644 outBFS.txt create mode 100644 outDFS.txt diff --git a/.gitignore b/.gitignore index a65d046..e87bcbe 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,5 @@ docs/_build/ # PyBuilder target/ + +\.vscode/ diff --git a/Constants.py b/Constants.py new file mode 100644 index 0000000..97f2e9d --- /dev/null +++ b/Constants.py @@ -0,0 +1,17 @@ +class Direction: + OLD_TO_NEW = 1 + NEW_TO_OLD = 0 + + +class CONST: + def __init__(self, MAX_M, MAX_C, CAP_BOAT, MAX_TIME_S, MAX_NODES): + self.MAX_M = MAX_M + self.MAX_C = MAX_C + self.CAP_BOAT = CAP_BOAT + + self.MAX_TIME = MAX_TIME_S + self.MAX_NODES = MAX_NODES + +# TERMINAL_STATE = State(-1, -1, Direction.NEW_TO_OLD, -1, -1, 0) +# INITIAL_STATE = None +# # State(MAX_M, MAX_C, Direction.OLD_TO_NEW, 0, 0,0) diff --git a/Graph.py b/Graph.py new file mode 100644 index 0000000..ea02c97 --- /dev/null +++ b/Graph.py @@ -0,0 +1,111 @@ +from collections import defaultdict + +from State import TERMINAL_STATE + +import time + + +class Graph: + + def __init__(self): + + self.bfs_parent = {} + self.dfs_parent = {} + + self.expandedBFS = 0 + self.expandedDFS = 0 + + def BFS(self, s): + self.expandedBFS = 0 + self.bfs_parent[s] = None + visited = {(s.missionaries, s.cannibals, s.dir): True} + s.level = 0 + + start_time = time.time() + queue = [s] + while queue: + self.expandedBFS += 1 + + u = queue.pop(0) + + if u.isGoalState(): + print("No of Expanded Nodes: " + str(self.expandedBFS)) + print("No of Explored Nodes: " + str(visited.__len__())) + queue.clear() + self.bfs_parent[TERMINAL_STATE] = u + return self.bfs_parent + + # Stops searching after a certain time/node limit + t = time.time() - start_time + if t > u.CONSTANTS.MAX_TIME or self.expandedBFS > u.CONSTANTS.MAX_NODES: + if t > u.CONSTANTS.MAX_TIME: + print("%.2fs EXCEEDED TIME LIMIT of %.2fs" % (t, u.CONSTANTS.MAX_TIME)) + else: + print("EXCEEDED NODE LIMIT of %d" % u.CONSTANTS.MAX_NODES) + print("No of Expanded Nodes: " + str(self.expandedBFS)) + print("No of Explored Nodes: " + str(visited.__len__())) + queue.clear() + return {} + + for v in reversed(u.successors()): + if (v.missionaries, v.cannibals, v.dir) not in visited.keys(): + self.bfs_parent[v] = u + v.level = u.level + 1 + queue.append(v) + visited[(v.missionaries, v.cannibals, v.dir)] = True + + return {} + + def DFS(self, s): + self.expandedDFS = 0 + self.dfs_parent[s] = None + visited = {(s.missionaries, s.cannibals, s.dir): True} + + start_time = time.time() + stack = [s] + while stack: + u = stack.pop() + self.expandedDFS += 1 + + if u.isGoalState(): + print("No of Expanded Nodes: " + str(self.expandedDFS)) + print("No of Explored Nodes: " + str(visited.__len__())) + self.dfs_parent[TERMINAL_STATE] = u + stack.clear() + return self.dfs_parent + + t = time.time() - start_time + # Stops searching after a certain time/node limit + if t > u.CONSTANTS.MAX_TIME or self.expandedDFS > u.CONSTANTS.MAX_NODES: + if t > u.CONSTANTS.MAX_TIME: + print("%.2fs EXCEEDED TIME LIMIT of %.2fs" % (t, u.CONSTANTS.MAX_TIME)) + else: + print("EXCEEDED NODE LIMIT of %d" % u.CONSTANTS.MAX_NODES) + print("No of Expanded Nodes: " + str(self.expandedDFS)) + print("No of Explored Nodes: " + str(visited.__len__())) + stack.clear() + return {} + + for v in u.successors(): + if (v.missionaries, v.cannibals, v.dir) not in visited.keys(): + visited[(v.missionaries, v.cannibals, v.dir)] = True + self.dfs_parent[v] = u + stack.append(v) + return {} + + # Prints the path returned by BFS/DFS + def printPath(self, parentList, tail): + if tail is None: + return + if parentList == {} or parentList is None: # tail not in parentList.keys(): + return + if tail == TERMINAL_STATE: tail = parentList[tail] + + stack = [] + + while tail is not None: + stack.append(tail) + tail = parentList[tail] + + while stack: + print(stack.pop()) diff --git a/Problem Statement.txt b/Problem Statement.txt new file mode 100644 index 0000000..0590fc3 --- /dev/null +++ b/Problem Statement.txt @@ -0,0 +1,8 @@ + +The missionaries and cannibals problem, which is a famous problem in AI, is usually stated as follows. Three missionaries and three cannibals are on one side of a river, along with a boat that can hold one or two people. Find a way to get everyone to the other side without ever leaving a group of missionaries in one place outnumbered by the cannibals in that place. + +In this assignment, your task is to do the problem formulation so it can be solved by searching, and do the computer implementation in order to experimentally compare the performance of the BFS and the DFS search strategy. For performance comparison, you may use time, number of nodes explored, number of nodes expanded, effective branching factor etc. + +In addition, your computer implementation need to be able to deal with a scaled-up version of this problem (for example, a problem with +five missionaries and five cannibals). The implementation may have m number of missionaries, c number of cannibals, k number of maximum allowable passengers in the boat. There should a search cut-off limit (for example, termination after 30 seconds, or after 1,000,000 nodes have been expanded) which you should be able to vary. + diff --git a/README.md b/README.md index 5eb86b9..0bcb366 100644 --- a/README.md +++ b/README.md @@ -1 +1,19 @@ -Missionaries-and-Cannibals +The missionaries and cannibals problem, which is a famous problem in AI, is usually stated as follows. Three missionaries and three cannibals are on one side of a river, along with a boat that can hold one or two people. Find a way to get everyone to the other side without ever leaving a group of missionaries in one place outnumbered by the cannibals in that place. + + +Here the problem formulation has been solved by BFS and the DFS search strategy. + + +This implementation is able to deal with a scaled-up version of this problem (for example, a problem with five missionaries and five cannibals). The implementation may have m number of missionaries, c number of cannibals, k number of maximum allowable passengers in the boat. There is a search cut-off limit (for example, termination after 30 seconds, or after 1,000,000 nodes have been expanded) which was passed as input via "in.txt". + +>> Run "main.py"(in Python3) +>> Input format("in.txt"): +>Line1:m +>Line2:c +>Line3:k +>Line4:TIME_LIMIT_IN_SECONDS +>Line5:NO_OF_EXPLORED_NODES_LIMIT + +>> Output: +"outBFS.txt" contains the output generated by Breadth First Search and +"outDFS.txt" contains the output generated by Depth First Search. \ No newline at end of file diff --git a/State.py b/State.py new file mode 100644 index 0000000..a54c54e --- /dev/null +++ b/State.py @@ -0,0 +1,91 @@ +from Constants import Direction + +MAX_M = 30 +MAX_C = 30 +CAP_BOAT = 20 +CNST = None + + +class State(object): + + def __init__(self, missionaries, cannibals, dir, missionariesPassed, cannibalsPassed, level, CONSTS,moves): + self.missionaries = missionaries + self.cannibals = cannibals + self.dir = dir + self.action = "" + self.level = level + self.missionariesPassed = missionariesPassed + self.cannibalsPassed = cannibalsPassed + self.CONSTANTS = CONSTS + + self.moves = moves + + global MAX_M + global MAX_C + global CAP_BOAT + global CNST + + if not CONSTS is None: + CNST = CONSTS + + MAX_M = CONSTS.MAX_M + MAX_C = CONSTS.MAX_C + CAP_BOAT = CONSTS.CAP_BOAT + + # pass True to count forward + def successors(self): + listChild = [] + if not self.isValid() or self.isGoalState(): + return listChild + if self.dir == Direction.OLD_TO_NEW: + sgn = -1 + direction = "from the original shore to the new shore" + else: + sgn = 1 + direction = "back from the new shore to the original shore" + for i in self.moves: + (m, c) = i + self.addValidSuccessors(listChild, m, c, sgn, direction) + return listChild + + def addValidSuccessors(self, listChild, m, c, sgn, direction): + newState = State(self.missionaries + sgn * m, self.cannibals + sgn * c, self.dir + sgn * 1, + self.missionariesPassed - sgn * m, self.cannibalsPassed - sgn * c, self.level + 1, + self.CONSTANTS,self.moves) + if newState.isValid(): + newState.action = " take %d missionaries and %d cannibals %s." % (m, c, direction) + listChild.append(newState) + + def isValid(self): + # obvious + if self.missionaries < 0 or self.cannibals < 0 or self.missionaries > MAX_M or self.cannibals > MAX_C or ( + self.dir != 0 and self.dir != 1): + return False + + # then check whether missionaries outnumbered by cannibals in any shore + if (self.cannibals > self.missionaries > 0) or ( + self.cannibalsPassed > self.missionariesPassed > 0): # more cannibals then missionaries on original shore + return False + + return True + + def isGoalState(self): + return self.cannibals == 0 and self.missionaries == 0 and self.dir == Direction.NEW_TO_OLD + + def __repr__(self): + return "\n%s\n\n< @Depth:%d State (%d, %d, %d, %d, %d) >" % ( + self.action, self.level, self.missionaries, self.cannibals, self.dir, self.missionariesPassed, + self.cannibalsPassed) + + def __eq__(self, other): + return self.missionaries == other.missionaries and self.cannibals == other.cannibals and self.dir == other.dir + + def __hash__(self): + return hash((self.missionaries, self.cannibals, self.dir)) + + def __ne__(self, other): + return not (self == other) + + +TERMINAL_STATE = State(-1, -1, Direction.NEW_TO_OLD, -1, -1, 0, CNST,None) +# INITIAL_STATE = State(MAX_M, MAX_C, Direction.OLD_TO_NEW, 0, 0, 0, CNST) diff --git a/in.txt b/in.txt new file mode 100644 index 0000000..975d23f --- /dev/null +++ b/in.txt @@ -0,0 +1,80 @@ +3 +3 +2 +30 +100000 + +10000 +10000 +5 +120 +100000 + +10000 +10000 +20 +120 +100000 + +300 +300 +4 +30 +100000 + +329 +297 +17 +30 +100000 + +300 +300 +200 +120 +100000 + +200 +200 +11 +120 +100000 + +30 +30 +20 +30 +100000 + +4 +4 +3 +120 +100000 + +77 +47 +11 +30 +10000 + +2 +2 +2 +120 +100000 + +79 +47 +23 +120 +100000 + + + +9000 +8000 +200 +2 +10000 + diff --git a/main.py b/main.py new file mode 100755 index 0000000..807a4c4 --- /dev/null +++ b/main.py @@ -0,0 +1,89 @@ +#! /usr/bin/env python3 +import sys +import time + +from Graph import Graph + +from State import State, Direction, TERMINAL_STATE + +from Constants import CONST + +CON_IN = sys.stdin +CON_OUT = sys.stdout + +# Generate All possible next moves for each state to reduce number of iterations on each node +def genPossibleMoves(CAP_BOAT): + moves = [] + for m in range(CAP_BOAT + 1): + for c in range(CAP_BOAT + 1): + if 0 < m < c: + continue + if 1 <= m + c <= CAP_BOAT: + moves.append((m, c)) + return moves + + +def runBFS(g, INITIAL_STATE): + sys.stdout = open("outBFS.txt", "w") + print("\n\nBFS :: \n") + start_time = time.time() + p = g.BFS(INITIAL_STATE) + end_time = time.time() + # print("Printing Solution...") + if len(p): + g.printPath(p, TERMINAL_STATE) + else: + print("No Solution") + print("\n Elapsed time in BFS: %.2fms" % ((end_time - start_time)*1000)) + + +def runDFS(g, INITIAL_STATE): + sys.stdout = open("outDFS.txt", "w") + print("\n\nDFS :: \n") + start_time = time.time() + p = g.DFS(INITIAL_STATE) + end_time = time.time() + if len(p): + g.printPath(p, TERMINAL_STATE) + else: + print("No Solution") + print("\n Elapsed time in DFS: %.2fms" % ((end_time - start_time)*1000)) + + +def main(): + sys.stdin = open("in.txt", "r") + + m = int(input("m=")) + print(m, end="\n") + c = int(input("c=")) + print(c, end="\n") + k = int(input("k=")) + print(k, end="\n") + t = int(input("TIME_LIMIT_s=")) + print(t, end="\n") + n = int(input("NODE_LIMIT=")) + print(n, end="\n") + + CNST = CONST(m, c, k, t, n) + + moves = genPossibleMoves(CNST.CAP_BOAT) + print(str(moves.__len__())+" iterations per Node.") + + INITIAL_STATE = State(CNST.MAX_M, CNST.MAX_C, Direction.OLD_TO_NEW, 0, 0, 0, CNST, moves) + # TERMINAL_STATE = State(-1, -1, Direction.NEW_TO_OLD, -1, -1, 0) + + g = Graph() + sys.stdout = CON_OUT + print("\nRunning BFS>") + runBFS(g, INITIAL_STATE) + sys.stdout = CON_OUT + print("Executed BFS>") + + print("\nRunning DFS>") + runDFS(g, INITIAL_STATE) + sys.stdout = CON_OUT + print("Executed DFS>") + + +if __name__ == '__main__': + main() diff --git a/outBFS.txt b/outBFS.txt new file mode 100644 index 0000000..4af9f78 --- /dev/null +++ b/outBFS.txt @@ -0,0 +1,56 @@ + + +BFS :: + +No of Expanded Nodes: 15 +No of Explored Nodes: 15 + + + +< @Depth:0 State (3, 3, 1, 0, 0) > + + take 1 missionaries and 1 cannibals from the original shore to the new shore. + +< @Depth:1 State (2, 2, 0, 1, 1) > + + take 1 missionaries and 0 cannibals back from the new shore to the original shore. + +< @Depth:2 State (3, 2, 1, 0, 1) > + + take 0 missionaries and 2 cannibals from the original shore to the new shore. + +< @Depth:3 State (3, 0, 0, 0, 3) > + + take 0 missionaries and 1 cannibals back from the new shore to the original shore. + +< @Depth:4 State (3, 1, 1, 0, 2) > + + take 2 missionaries and 0 cannibals from the original shore to the new shore. + +< @Depth:5 State (1, 1, 0, 2, 2) > + + take 1 missionaries and 1 cannibals back from the new shore to the original shore. + +< @Depth:6 State (2, 2, 1, 1, 1) > + + take 2 missionaries and 0 cannibals from the original shore to the new shore. + +< @Depth:7 State (0, 2, 0, 3, 1) > + + take 0 missionaries and 1 cannibals back from the new shore to the original shore. + +< @Depth:8 State (0, 3, 1, 3, 0) > + + take 0 missionaries and 2 cannibals from the original shore to the new shore. + +< @Depth:9 State (0, 1, 0, 3, 2) > + + take 1 missionaries and 0 cannibals back from the new shore to the original shore. + +< @Depth:10 State (1, 1, 1, 2, 2) > + + take 1 missionaries and 1 cannibals from the original shore to the new shore. + +< @Depth:11 State (0, 0, 0, 3, 3) > + + Elapsed time in BFS: 0.37ms diff --git a/outDFS.txt b/outDFS.txt new file mode 100644 index 0000000..910328a --- /dev/null +++ b/outDFS.txt @@ -0,0 +1,56 @@ + + +DFS :: + +No of Expanded Nodes: 12 +No of Explored Nodes: 15 + + + +< @Depth:0 State (3, 3, 1, 0, 0) > + + take 1 missionaries and 1 cannibals from the original shore to the new shore. + +< @Depth:1 State (2, 2, 0, 1, 1) > + + take 1 missionaries and 0 cannibals back from the new shore to the original shore. + +< @Depth:2 State (3, 2, 1, 0, 1) > + + take 0 missionaries and 2 cannibals from the original shore to the new shore. + +< @Depth:3 State (3, 0, 0, 0, 3) > + + take 0 missionaries and 1 cannibals back from the new shore to the original shore. + +< @Depth:4 State (3, 1, 1, 0, 2) > + + take 2 missionaries and 0 cannibals from the original shore to the new shore. + +< @Depth:5 State (1, 1, 0, 2, 2) > + + take 1 missionaries and 1 cannibals back from the new shore to the original shore. + +< @Depth:6 State (2, 2, 1, 1, 1) > + + take 2 missionaries and 0 cannibals from the original shore to the new shore. + +< @Depth:7 State (0, 2, 0, 3, 1) > + + take 0 missionaries and 1 cannibals back from the new shore to the original shore. + +< @Depth:8 State (0, 3, 1, 3, 0) > + + take 0 missionaries and 2 cannibals from the original shore to the new shore. + +< @Depth:9 State (0, 1, 0, 3, 2) > + + take 1 missionaries and 0 cannibals back from the new shore to the original shore. + +< @Depth:10 State (1, 1, 1, 2, 2) > + + take 1 missionaries and 1 cannibals from the original shore to the new shore. + +< @Depth:11 State (0, 0, 0, 3, 3) > + + Elapsed time in DFS: 0.29ms