From b0f2945c3a737157ed47e2f3990e30af402cf751 Mon Sep 17 00:00:00 2001 From: Astin CHOI Date: Mon, 11 Nov 2019 23:45:45 +0900 Subject: [PATCH 1/3] add korean ver --- Chapter10 copy/csp.py | 82 ++++++++++++ Chapter10 copy/map_coloring.py | 57 ++++++++ Chapter10 copy/queens.py | 50 +++++++ Chapter10 copy/send_more_money.py | 59 ++++++++ Chapter10 copy/word_search.py | 93 +++++++++++++ Chapter10/csp.py | 82 ++++++++++++ Chapter10/map_coloring.py | 57 ++++++++ Chapter10/queens.py | 50 +++++++ Chapter10/send_more_money.py | 59 ++++++++ Chapter10/word_search.py | 93 +++++++++++++ ch1/calculating_pi.py | 31 +++++ ch1/fib1.py | 24 ++++ ch1/fib2.py | 26 ++++ ch1/fib3.py | 28 ++++ ch1/fib4.py | 28 ++++ ch1/fib5.py | 28 ++++ ch1/fib6.py | 31 +++++ ch1/hanoi.py | 56 ++++++++ ch1/trivial_compression.py | 66 +++++++++ ch1/unbreakable_encryption.py | 44 ++++++ ch2/dna_search.py | 70 ++++++++++ ch2/generic_search.py | 216 ++++++++++++++++++++++++++++++ ch2/maze.py | 141 +++++++++++++++++++ ch2/missionaries.py | 99 ++++++++++++++ ch3/csp.py | 83 ++++++++++++ ch3/map_coloring.py | 60 +++++++++ ch3/queens.py | 50 +++++++ ch3/send_more_money.py | 59 ++++++++ ch3/word_search.py | 98 ++++++++++++++ ch4/dijkstra.py | 131 ++++++++++++++++++ ch4/edge.py | 29 ++++ ch4/graph.py | 135 +++++++++++++++++++ ch4/mst.py | 96 +++++++++++++ ch4/priority_queue.py | 38 ++++++ ch4/weighted_edge.py | 33 +++++ ch4/weighted_graph.py | 81 +++++++++++ ch5/chromosome.py | 40 ++++++ ch5/genetic_algorithm.py | 94 +++++++++++++ ch5/list_compression.py | 76 +++++++++++ ch5/send_more_money2.py | 86 ++++++++++++ ch5/simple_equation.py | 65 +++++++++ ch6/data_point.py | 41 ++++++ ch6/governors.py | 85 ++++++++++++ ch6/kmeans.py | 121 +++++++++++++++++ ch6/mj.py | 47 +++++++ ch7/iris.csv | 150 +++++++++++++++++++++ ch7/iris_test.py | 64 +++++++++ ch7/layer.py | 61 +++++++++ ch7/network.py | 84 ++++++++++++ ch7/neuron.py | 32 +++++ ch7/util.py | 43 ++++++ ch7/wine.csv | 178 ++++++++++++++++++++++++ ch7/wine_test.py | 64 +++++++++ ch8/board.py | 55 ++++++++ ch8/connectfour.py | 183 +++++++++++++++++++++++++ ch8/connectfour_ai.py | 51 +++++++ ch8/minimax.py | 77 +++++++++++ ch8/tictactoe.py | 83 ++++++++++++ ch8/tictactoe_ai.py | 51 +++++++ ch8/tictactoe_tests.py | 53 ++++++++ ch9/knapsack.py | 63 +++++++++ ch9/phone_number_mnemonics.py | 42 ++++++ ch9/tsp.py | 63 +++++++++ 63 files changed, 4515 insertions(+) create mode 100644 Chapter10 copy/csp.py create mode 100644 Chapter10 copy/map_coloring.py create mode 100644 Chapter10 copy/queens.py create mode 100644 Chapter10 copy/send_more_money.py create mode 100644 Chapter10 copy/word_search.py create mode 100644 Chapter10/csp.py create mode 100644 Chapter10/map_coloring.py create mode 100644 Chapter10/queens.py create mode 100644 Chapter10/send_more_money.py create mode 100644 Chapter10/word_search.py create mode 100644 ch1/calculating_pi.py create mode 100644 ch1/fib1.py create mode 100644 ch1/fib2.py create mode 100644 ch1/fib3.py create mode 100644 ch1/fib4.py create mode 100644 ch1/fib5.py create mode 100644 ch1/fib6.py create mode 100644 ch1/hanoi.py create mode 100644 ch1/trivial_compression.py create mode 100644 ch1/unbreakable_encryption.py create mode 100644 ch2/dna_search.py create mode 100644 ch2/generic_search.py create mode 100644 ch2/maze.py create mode 100644 ch2/missionaries.py create mode 100644 ch3/csp.py create mode 100644 ch3/map_coloring.py create mode 100644 ch3/queens.py create mode 100644 ch3/send_more_money.py create mode 100644 ch3/word_search.py create mode 100644 ch4/dijkstra.py create mode 100644 ch4/edge.py create mode 100644 ch4/graph.py create mode 100644 ch4/mst.py create mode 100644 ch4/priority_queue.py create mode 100644 ch4/weighted_edge.py create mode 100644 ch4/weighted_graph.py create mode 100644 ch5/chromosome.py create mode 100644 ch5/genetic_algorithm.py create mode 100644 ch5/list_compression.py create mode 100644 ch5/send_more_money2.py create mode 100644 ch5/simple_equation.py create mode 100644 ch6/data_point.py create mode 100644 ch6/governors.py create mode 100644 ch6/kmeans.py create mode 100644 ch6/mj.py create mode 100644 ch7/iris.csv create mode 100644 ch7/iris_test.py create mode 100644 ch7/layer.py create mode 100644 ch7/network.py create mode 100644 ch7/neuron.py create mode 100644 ch7/util.py create mode 100644 ch7/wine.csv create mode 100644 ch7/wine_test.py create mode 100644 ch8/board.py create mode 100644 ch8/connectfour.py create mode 100644 ch8/connectfour_ai.py create mode 100644 ch8/minimax.py create mode 100644 ch8/tictactoe.py create mode 100644 ch8/tictactoe_ai.py create mode 100644 ch8/tictactoe_tests.py create mode 100644 ch9/knapsack.py create mode 100644 ch9/phone_number_mnemonics.py create mode 100644 ch9/tsp.py diff --git a/Chapter10 copy/csp.py b/Chapter10 copy/csp.py new file mode 100644 index 0000000..c8f9ce3 --- /dev/null +++ b/Chapter10 copy/csp.py @@ -0,0 +1,82 @@ +# csp.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Generic, TypeVar, Dict, List, Optional +from abc import ABC, abstractmethod + +V = TypeVar('V') # variable type +D = TypeVar('D') # domain type + + +# Base class for all constraints +class Constraint(Generic[V, D], ABC): + # The variables that the constraint is between + def __init__(self, variables: List[V]) -> None: + self.variables = variables + + # Must be overridden by subclasses + @abstractmethod + def satisfied(self, assignment: Dict[V, D]) -> bool: + ... + + +# A constraint satisfaction problem consists of variables of type V +# that have ranges of values known as domains of type D and constraints +# that determine whether a particular variable's domain selection is valid +class CSP(Generic[V, D]): + def __init__(self, variables: List[V], domains: Dict[V, List[D]]) -> None: + self.variables: List[V] = variables # variables to be constrained + self.domains: Dict[V, List[D]] = domains # domain of each variable + self.constraints: Dict[V, List[Constraint[V, D]]] = {} + for variable in self.variables: + self.constraints[variable] = [] + if variable not in self.domains: + raise LookupError("Every variable should have a domain assigned to it.") + + def add_constraint(self, constraint: Constraint[V, D]) -> None: + for variable in constraint.variables: + if variable not in self.variables: + raise LookupError("Variable in constraint not in CSP") + else: + self.constraints[variable].append(constraint) + + # Check if the value assignment is consistent by checking all constraints + # for the given variable against it + def consistent(self, variable: V, assignment: Dict[V, D]) -> bool: + for constraint in self.constraints[variable]: + if not constraint.satisfied(assignment): + return False + return True + + def backtracking_search(self, assignment: Dict[V, D] = {}) -> Optional[Dict[V, D]]: + # assignment is complete if every variable is assigned (our base case) + if len(assignment) == len(self.variables): + return assignment + + # get all variables in the CSP but not in the assignment + unassigned: List[V] = [v for v in self.variables if v not in assignment] + + # get the every possible domain value of the first unassigned variable + first: V = unassigned[0] + for value in self.domains[first]: + local_assignment = assignment.copy() + local_assignment[first] = value + # if we're still consistent, we recurse (continue) + if self.consistent(first, local_assignment): + result: Optional[Dict[V, D]] = self.backtracking_search(local_assignment) + # if we didn't find the result, we will end up backtracking + if result is not None: + return result + return None diff --git a/Chapter10 copy/map_coloring.py b/Chapter10 copy/map_coloring.py new file mode 100644 index 0000000..4332f11 --- /dev/null +++ b/Chapter10 copy/map_coloring.py @@ -0,0 +1,57 @@ +# map_coloring.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from csp import Constraint, CSP +from typing import Dict, List, Optional + + +class MapColoringConstraint(Constraint[str, str]): + def __init__(self, place1: str, place2: str) -> None: + super().__init__([place1, place2]) + self.place1: str = place1 + self.place2: str = place2 + + def satisfied(self, assignment: Dict[str, str]) -> bool: + # If either place is not in the assignment then it is not + # yet possible for their colors to be conflicting + if self.place1 not in assignment or self.place2 not in assignment: + return True + # check the color assigned to place1 is not the same as the + # color assigned to place2 + return assignment[self.place1] != assignment[self.place2] + + +if __name__ == "__main__": + variables: List[str] = ["Western Australia", "Northern Territory", "South Australia", + "Queensland", "New South Wales", "Victoria", "Tasmania"] + domains: Dict[str, List[str]] = {} + for variable in variables: + domains[variable] = ["red", "green", "blue"] + csp: CSP[str, str] = CSP(variables, domains) + csp.add_constraint(MapColoringConstraint("Western Australia", "Northern Territory")) + csp.add_constraint(MapColoringConstraint("Western Australia", "South Australia")) + csp.add_constraint(MapColoringConstraint("South Australia", "Northern Territory")) + csp.add_constraint(MapColoringConstraint("Queensland", "Northern Territory")) + csp.add_constraint(MapColoringConstraint("Queensland", "South Australia")) + csp.add_constraint(MapColoringConstraint("Queensland", "New South Wales")) + csp.add_constraint(MapColoringConstraint("New South Wales", "South Australia")) + csp.add_constraint(MapColoringConstraint("Victoria", "South Australia")) + csp.add_constraint(MapColoringConstraint("Victoria", "New South Wales")) + csp.add_constraint(MapColoringConstraint("Victoria", "Tasmania")) + solution: Optional[Dict[str, str]] = csp.backtracking_search() + if solution is None: + print("No solution found!") + else: + print(solution) \ No newline at end of file diff --git a/Chapter10 copy/queens.py b/Chapter10 copy/queens.py new file mode 100644 index 0000000..31a82a5 --- /dev/null +++ b/Chapter10 copy/queens.py @@ -0,0 +1,50 @@ +# queens.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from csp import Constraint, CSP +from typing import Dict, List, Optional + + +class QueensConstraint(Constraint[int, int]): + def __init__(self, columns: List[int]) -> None: + super().__init__(columns) + self.columns: List[int] = columns + + def satisfied(self, assignment: Dict[int, int]) -> bool: + # q1c = queen 1 column, q1r = queen 1 row + for q1c, q1r in assignment.items(): + # q2c = queen 2 column + for q2c in range(q1c + 1, len(self.columns) + 1): + if q2c in assignment: + q2r: int = assignment[q2c] # q2r = queen 2 row + if q1r == q2r: # same row? + return False + if abs(q1r - q2r) == abs(q1c - q2c): # same diagonal? + return False + return True # no conflict + + +if __name__ == "__main__": + columns: List[int] = [1, 2, 3, 4, 5, 6, 7, 8] + rows: Dict[int, List[int]] = {} + for column in columns: + rows[column] = [1, 2, 3, 4, 5, 6, 7, 8] + csp: CSP[int, int] = CSP(columns, rows) + csp.add_constraint(QueensConstraint(columns)) + solution: Optional[Dict[int, int]] = csp.backtracking_search() + if solution is None: + print("No solution found!") + else: + print(solution) \ No newline at end of file diff --git a/Chapter10 copy/send_more_money.py b/Chapter10 copy/send_more_money.py new file mode 100644 index 0000000..2071c10 --- /dev/null +++ b/Chapter10 copy/send_more_money.py @@ -0,0 +1,59 @@ +# send_more_money.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from csp import Constraint, CSP +from typing import Dict, List, Optional + + +class SendMoreMoneyConstraint(Constraint[str, int]): + def __init__(self, letters: List[str]) -> None: + super().__init__(letters) + self.letters: List[str] = letters + + def satisfied(self, assignment: Dict[str, int]) -> bool: + # if there are duplicate values then it's not a solution + if len(set(assignment.values())) < len(assignment): + return False + + # if all variables have been assigned, check if it adds correctly + if len(assignment) == len(self.letters): + s: int = assignment["S"] + e: int = assignment["E"] + n: int = assignment["N"] + d: int = assignment["D"] + m: int = assignment["M"] + o: int = assignment["O"] + r: int = assignment["R"] + y: int = assignment["Y"] + send: int = s * 1000 + e * 100 + n * 10 + d + more: int = m * 1000 + o * 100 + r * 10 + e + money: int = m * 10000 + o * 1000 + n * 100 + e * 10 + y + return send + more == money + return True # no conflict + + +if __name__ == "__main__": + letters: List[str] = ["S", "E", "N", "D", "M", "O", "R", "Y"] + possible_digits: Dict[str, List[int]] = {} + for letter in letters: + possible_digits[letter] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + possible_digits["M"] = [1] # so we don't get answers starting with a 0 + csp: CSP[str, int] = CSP(letters, possible_digits) + csp.add_constraint(SendMoreMoneyConstraint(letters)) + solution: Optional[Dict[str, int]] = csp.backtracking_search() + if solution is None: + print("No solution found!") + else: + print(solution) \ No newline at end of file diff --git a/Chapter10 copy/word_search.py b/Chapter10 copy/word_search.py new file mode 100644 index 0000000..890a0d5 --- /dev/null +++ b/Chapter10 copy/word_search.py @@ -0,0 +1,93 @@ +# word_search.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import NamedTuple, List, Dict, Optional +from random import choice +from string import ascii_uppercase +from csp import CSP, Constraint + +Grid = List[List[str]] # type alias for grids + + +class GridLocation(NamedTuple): + row: int + column: int + + +def generate_grid(rows: int, columns: int) -> Grid: + # initialize grid with random letters + return [[choice(ascii_uppercase) for c in range(columns)] for r in range(rows)] + + +def display_grid(grid: Grid) -> None: + for row in grid: + print("".join(row)) + + +def generate_domain(word: str, grid: Grid) -> List[List[GridLocation]]: + domain: List[List[GridLocation]] = [] + height: int = len(grid) + width: int = len(grid[0]) + length: int = len(word) + for row in range(height): + for col in range(width): + columns: range = range(col, col + length + 1) + rows: range = range(row, row + length + 1) + if col + length <= width: + # left to right + domain.append([GridLocation(row, c) for c in columns]) + # diagonal towards bottom right + if row + length <= height: + domain.append([GridLocation(r, col + (r - row)) for r in rows]) + if row + length <= height: + # top to bottom + domain.append([GridLocation(r, col) for r in rows]) + # diagonal towards bottom left + if col - length >= 0: + domain.append([GridLocation(r, col - (r - row)) for r in rows]) + return domain + + +class WordSearchConstraint(Constraint[str, List[GridLocation]]): + def __init__(self, words: List[str]) -> None: + super().__init__(words) + self.words: List[str] = words + + def satisfied(self, assignment: Dict[str, List[GridLocation]]) -> bool: + # if there are any duplicates grid locations then there is an overlap + all_locations = [locs for values in assignment.values() for locs in values] + return len(set(all_locations)) == len(all_locations) + + +if __name__ == "__main__": + grid: Grid = generate_grid(9, 9) + words: List[str] = ["MATTHEW", "JOE", "MARY", "SARAH", "SALLY"] + locations: Dict[str, List[List[GridLocation]]] = {} + for word in words: + locations[word] = generate_domain(word, grid) + csp: CSP[str, List[GridLocation]] = CSP(words, locations) + csp.add_constraint(WordSearchConstraint(words)) + solution: Optional[Dict[str, List[GridLocation]]] = csp.backtracking_search() + if solution is None: + print("No solution found!") + else: + for word, grid_locations in solution.items(): + # random reverse half the time + if choice([True, False]): + grid_locations.reverse() + for index, letter in enumerate(word): + (row, col) = (grid_locations[index].row, grid_locations[index].column) + grid[row][col] = letter + display_grid(grid) diff --git a/Chapter10/csp.py b/Chapter10/csp.py new file mode 100644 index 0000000..c8f9ce3 --- /dev/null +++ b/Chapter10/csp.py @@ -0,0 +1,82 @@ +# csp.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Generic, TypeVar, Dict, List, Optional +from abc import ABC, abstractmethod + +V = TypeVar('V') # variable type +D = TypeVar('D') # domain type + + +# Base class for all constraints +class Constraint(Generic[V, D], ABC): + # The variables that the constraint is between + def __init__(self, variables: List[V]) -> None: + self.variables = variables + + # Must be overridden by subclasses + @abstractmethod + def satisfied(self, assignment: Dict[V, D]) -> bool: + ... + + +# A constraint satisfaction problem consists of variables of type V +# that have ranges of values known as domains of type D and constraints +# that determine whether a particular variable's domain selection is valid +class CSP(Generic[V, D]): + def __init__(self, variables: List[V], domains: Dict[V, List[D]]) -> None: + self.variables: List[V] = variables # variables to be constrained + self.domains: Dict[V, List[D]] = domains # domain of each variable + self.constraints: Dict[V, List[Constraint[V, D]]] = {} + for variable in self.variables: + self.constraints[variable] = [] + if variable not in self.domains: + raise LookupError("Every variable should have a domain assigned to it.") + + def add_constraint(self, constraint: Constraint[V, D]) -> None: + for variable in constraint.variables: + if variable not in self.variables: + raise LookupError("Variable in constraint not in CSP") + else: + self.constraints[variable].append(constraint) + + # Check if the value assignment is consistent by checking all constraints + # for the given variable against it + def consistent(self, variable: V, assignment: Dict[V, D]) -> bool: + for constraint in self.constraints[variable]: + if not constraint.satisfied(assignment): + return False + return True + + def backtracking_search(self, assignment: Dict[V, D] = {}) -> Optional[Dict[V, D]]: + # assignment is complete if every variable is assigned (our base case) + if len(assignment) == len(self.variables): + return assignment + + # get all variables in the CSP but not in the assignment + unassigned: List[V] = [v for v in self.variables if v not in assignment] + + # get the every possible domain value of the first unassigned variable + first: V = unassigned[0] + for value in self.domains[first]: + local_assignment = assignment.copy() + local_assignment[first] = value + # if we're still consistent, we recurse (continue) + if self.consistent(first, local_assignment): + result: Optional[Dict[V, D]] = self.backtracking_search(local_assignment) + # if we didn't find the result, we will end up backtracking + if result is not None: + return result + return None diff --git a/Chapter10/map_coloring.py b/Chapter10/map_coloring.py new file mode 100644 index 0000000..4332f11 --- /dev/null +++ b/Chapter10/map_coloring.py @@ -0,0 +1,57 @@ +# map_coloring.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from csp import Constraint, CSP +from typing import Dict, List, Optional + + +class MapColoringConstraint(Constraint[str, str]): + def __init__(self, place1: str, place2: str) -> None: + super().__init__([place1, place2]) + self.place1: str = place1 + self.place2: str = place2 + + def satisfied(self, assignment: Dict[str, str]) -> bool: + # If either place is not in the assignment then it is not + # yet possible for their colors to be conflicting + if self.place1 not in assignment or self.place2 not in assignment: + return True + # check the color assigned to place1 is not the same as the + # color assigned to place2 + return assignment[self.place1] != assignment[self.place2] + + +if __name__ == "__main__": + variables: List[str] = ["Western Australia", "Northern Territory", "South Australia", + "Queensland", "New South Wales", "Victoria", "Tasmania"] + domains: Dict[str, List[str]] = {} + for variable in variables: + domains[variable] = ["red", "green", "blue"] + csp: CSP[str, str] = CSP(variables, domains) + csp.add_constraint(MapColoringConstraint("Western Australia", "Northern Territory")) + csp.add_constraint(MapColoringConstraint("Western Australia", "South Australia")) + csp.add_constraint(MapColoringConstraint("South Australia", "Northern Territory")) + csp.add_constraint(MapColoringConstraint("Queensland", "Northern Territory")) + csp.add_constraint(MapColoringConstraint("Queensland", "South Australia")) + csp.add_constraint(MapColoringConstraint("Queensland", "New South Wales")) + csp.add_constraint(MapColoringConstraint("New South Wales", "South Australia")) + csp.add_constraint(MapColoringConstraint("Victoria", "South Australia")) + csp.add_constraint(MapColoringConstraint("Victoria", "New South Wales")) + csp.add_constraint(MapColoringConstraint("Victoria", "Tasmania")) + solution: Optional[Dict[str, str]] = csp.backtracking_search() + if solution is None: + print("No solution found!") + else: + print(solution) \ No newline at end of file diff --git a/Chapter10/queens.py b/Chapter10/queens.py new file mode 100644 index 0000000..31a82a5 --- /dev/null +++ b/Chapter10/queens.py @@ -0,0 +1,50 @@ +# queens.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from csp import Constraint, CSP +from typing import Dict, List, Optional + + +class QueensConstraint(Constraint[int, int]): + def __init__(self, columns: List[int]) -> None: + super().__init__(columns) + self.columns: List[int] = columns + + def satisfied(self, assignment: Dict[int, int]) -> bool: + # q1c = queen 1 column, q1r = queen 1 row + for q1c, q1r in assignment.items(): + # q2c = queen 2 column + for q2c in range(q1c + 1, len(self.columns) + 1): + if q2c in assignment: + q2r: int = assignment[q2c] # q2r = queen 2 row + if q1r == q2r: # same row? + return False + if abs(q1r - q2r) == abs(q1c - q2c): # same diagonal? + return False + return True # no conflict + + +if __name__ == "__main__": + columns: List[int] = [1, 2, 3, 4, 5, 6, 7, 8] + rows: Dict[int, List[int]] = {} + for column in columns: + rows[column] = [1, 2, 3, 4, 5, 6, 7, 8] + csp: CSP[int, int] = CSP(columns, rows) + csp.add_constraint(QueensConstraint(columns)) + solution: Optional[Dict[int, int]] = csp.backtracking_search() + if solution is None: + print("No solution found!") + else: + print(solution) \ No newline at end of file diff --git a/Chapter10/send_more_money.py b/Chapter10/send_more_money.py new file mode 100644 index 0000000..2071c10 --- /dev/null +++ b/Chapter10/send_more_money.py @@ -0,0 +1,59 @@ +# send_more_money.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from csp import Constraint, CSP +from typing import Dict, List, Optional + + +class SendMoreMoneyConstraint(Constraint[str, int]): + def __init__(self, letters: List[str]) -> None: + super().__init__(letters) + self.letters: List[str] = letters + + def satisfied(self, assignment: Dict[str, int]) -> bool: + # if there are duplicate values then it's not a solution + if len(set(assignment.values())) < len(assignment): + return False + + # if all variables have been assigned, check if it adds correctly + if len(assignment) == len(self.letters): + s: int = assignment["S"] + e: int = assignment["E"] + n: int = assignment["N"] + d: int = assignment["D"] + m: int = assignment["M"] + o: int = assignment["O"] + r: int = assignment["R"] + y: int = assignment["Y"] + send: int = s * 1000 + e * 100 + n * 10 + d + more: int = m * 1000 + o * 100 + r * 10 + e + money: int = m * 10000 + o * 1000 + n * 100 + e * 10 + y + return send + more == money + return True # no conflict + + +if __name__ == "__main__": + letters: List[str] = ["S", "E", "N", "D", "M", "O", "R", "Y"] + possible_digits: Dict[str, List[int]] = {} + for letter in letters: + possible_digits[letter] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + possible_digits["M"] = [1] # so we don't get answers starting with a 0 + csp: CSP[str, int] = CSP(letters, possible_digits) + csp.add_constraint(SendMoreMoneyConstraint(letters)) + solution: Optional[Dict[str, int]] = csp.backtracking_search() + if solution is None: + print("No solution found!") + else: + print(solution) \ No newline at end of file diff --git a/Chapter10/word_search.py b/Chapter10/word_search.py new file mode 100644 index 0000000..890a0d5 --- /dev/null +++ b/Chapter10/word_search.py @@ -0,0 +1,93 @@ +# word_search.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import NamedTuple, List, Dict, Optional +from random import choice +from string import ascii_uppercase +from csp import CSP, Constraint + +Grid = List[List[str]] # type alias for grids + + +class GridLocation(NamedTuple): + row: int + column: int + + +def generate_grid(rows: int, columns: int) -> Grid: + # initialize grid with random letters + return [[choice(ascii_uppercase) for c in range(columns)] for r in range(rows)] + + +def display_grid(grid: Grid) -> None: + for row in grid: + print("".join(row)) + + +def generate_domain(word: str, grid: Grid) -> List[List[GridLocation]]: + domain: List[List[GridLocation]] = [] + height: int = len(grid) + width: int = len(grid[0]) + length: int = len(word) + for row in range(height): + for col in range(width): + columns: range = range(col, col + length + 1) + rows: range = range(row, row + length + 1) + if col + length <= width: + # left to right + domain.append([GridLocation(row, c) for c in columns]) + # diagonal towards bottom right + if row + length <= height: + domain.append([GridLocation(r, col + (r - row)) for r in rows]) + if row + length <= height: + # top to bottom + domain.append([GridLocation(r, col) for r in rows]) + # diagonal towards bottom left + if col - length >= 0: + domain.append([GridLocation(r, col - (r - row)) for r in rows]) + return domain + + +class WordSearchConstraint(Constraint[str, List[GridLocation]]): + def __init__(self, words: List[str]) -> None: + super().__init__(words) + self.words: List[str] = words + + def satisfied(self, assignment: Dict[str, List[GridLocation]]) -> bool: + # if there are any duplicates grid locations then there is an overlap + all_locations = [locs for values in assignment.values() for locs in values] + return len(set(all_locations)) == len(all_locations) + + +if __name__ == "__main__": + grid: Grid = generate_grid(9, 9) + words: List[str] = ["MATTHEW", "JOE", "MARY", "SARAH", "SALLY"] + locations: Dict[str, List[List[GridLocation]]] = {} + for word in words: + locations[word] = generate_domain(word, grid) + csp: CSP[str, List[GridLocation]] = CSP(words, locations) + csp.add_constraint(WordSearchConstraint(words)) + solution: Optional[Dict[str, List[GridLocation]]] = csp.backtracking_search() + if solution is None: + print("No solution found!") + else: + for word, grid_locations in solution.items(): + # random reverse half the time + if choice([True, False]): + grid_locations.reverse() + for index, letter in enumerate(word): + (row, col) = (grid_locations[index].row, grid_locations[index].column) + grid[row][col] = letter + display_grid(grid) diff --git a/ch1/calculating_pi.py b/ch1/calculating_pi.py new file mode 100644 index 0000000..61abfc6 --- /dev/null +++ b/ch1/calculating_pi.py @@ -0,0 +1,31 @@ +# calculating_pi.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def calculate_pi(n_terms: int) -> float: + numerator: float = 4.0 + denominator: float = 1.0 + operation: float = 1.0 + pi: float = 0.0 + for _ in range(n_terms): + pi += operation * (numerator / denominator) + denominator += 2.0 + operation *= -1.0 + return pi + + +if __name__ == "__main__": + print(calculate_pi(1000000)) diff --git a/ch1/fib1.py b/ch1/fib1.py new file mode 100644 index 0000000..90355d1 --- /dev/null +++ b/ch1/fib1.py @@ -0,0 +1,24 @@ +# fib1.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def fib1(n: int) -> int: + return fib1(n - 1) + fib1(n - 2) + + +if __name__ == "__main__": + print(fib1(5)) +# 이 예제는 의도적으로 잘못 작성되었다. diff --git a/ch1/fib2.py b/ch1/fib2.py new file mode 100644 index 0000000..4d3c1cf --- /dev/null +++ b/ch1/fib2.py @@ -0,0 +1,26 @@ +# fib2.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +def fib2(n: int) -> int: + if n < 2: # 기저 조건 + return n + return fib2(n - 2) + fib2(n - 1) # 재귀 조건 + + +if __name__ == "__main__": + print(fib2(5)) + print(fib2(10)) diff --git a/ch1/fib3.py b/ch1/fib3.py new file mode 100644 index 0000000..91dcf81 --- /dev/null +++ b/ch1/fib3.py @@ -0,0 +1,28 @@ +# fib3.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict +memo: Dict[int, int] = {0: 0, 1: 1} # 기저 조건 + + +def fib3(n: int) -> int: + if n not in memo: + memo[n] = fib3(n - 1) + fib3(n - 2) # 메모이제이션 + return memo[n] + + +if __name__ == "__main__": + print(fib3(5)) + print(fib3(50)) diff --git a/ch1/fib4.py b/ch1/fib4.py new file mode 100644 index 0000000..86446c2 --- /dev/null +++ b/ch1/fib4.py @@ -0,0 +1,28 @@ +# fib4.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from functools import lru_cache + + +@lru_cache(maxsize=None) +def fib4(n: int) -> int: # same definition as fib2() + if n < 2: # base case + return n + return fib4(n - 2) + fib4(n - 1) # recursive case + + +if __name__ == "__main__": + print(fib4(5)) + print(fib4(50)) diff --git a/ch1/fib5.py b/ch1/fib5.py new file mode 100644 index 0000000..adde39d --- /dev/null +++ b/ch1/fib5.py @@ -0,0 +1,28 @@ +# fib5.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +def fib5(n: int) -> int: + if n == 0: return n # special case + last: int = 0 # initially set to fib(0) + next: int = 1 # initially set to fib(1) + for _ in range(1, n): + last, next = next, last + next + return next + + +if __name__ == "__main__": + print(fib5(2)) + print(fib5(50)) + diff --git a/ch1/fib6.py b/ch1/fib6.py new file mode 100644 index 0000000..0f036fd --- /dev/null +++ b/ch1/fib6.py @@ -0,0 +1,31 @@ +# fib6.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Generator + + +def fib6(n: int) -> Generator[int, None, None]: + yield 0 # special case + if n > 0: yield 1 # special case + last: int = 0 # initially set to fib(0) + next: int = 1 # initially set to fib(1) + for _ in range(1, n): + last, next = next, last + next + yield next # main generation step + + +if __name__ == "__main__": + for i in fib6(50): + print(i) diff --git a/ch1/hanoi.py b/ch1/hanoi.py new file mode 100644 index 0000000..48ab600 --- /dev/null +++ b/ch1/hanoi.py @@ -0,0 +1,56 @@ +# hanoi.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import TypeVar, Generic, List +T = TypeVar('T') + + +class Stack(Generic[T]): + + def __init__(self) -> None: + self._container: List[T] = [] + + def push(self, item: T) -> None: + self._container.append(item) + + def pop(self) -> T: + return self._container.pop() + + def __repr__(self) -> str: + return repr(self._container) + + +num_discs: int = 3 +tower_a: Stack[int] = Stack() +tower_b: Stack[int] = Stack() +tower_c: Stack[int] = Stack() +for i in range(1, num_discs + 1): + tower_a.push(i) + + +def hanoi(begin: Stack[int], end: Stack[int], temp: Stack[int], n: int) -> None: + if n == 1: + end.push(begin.pop()) + else: + hanoi(begin, temp, end, n - 1) + hanoi(begin, end, temp, 1) + hanoi(temp, end, begin, n - 1) + + +if __name__ == "__main__": + hanoi(tower_a, tower_c, tower_b, num_discs) + print(tower_a) + print(tower_b) + print(tower_c) diff --git a/ch1/trivial_compression.py b/ch1/trivial_compression.py new file mode 100644 index 0000000..1e84d36 --- /dev/null +++ b/ch1/trivial_compression.py @@ -0,0 +1,66 @@ +# trivial_compression.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class CompressedGene: + def __init__(self, gene: str) -> None: + self._compress(gene) + + def _compress(self, gene: str) -> None: + self.bit_string: int = 1 # 1로 시작한다. + for nucleotide in gene.upper(): + self.bit_string <<= 2 # 왼쪽으로 두 비트 이동 + if nucleotide == "A": # 마지막 두 비트를 00으로 변경 + self.bit_string |= 0b00 + elif nucleotide == "C": # 마지막 두 비트를 01으로 변경 + self.bit_string |= 0b01 + elif nucleotide == "G": # 마지막 두 비트를 10으로 변경 + self.bit_string |= 0b10 + elif nucleotide == "T": # 마지막 두 비트를 11으로 변경 + self.bit_string |= 0b11 + else: + raise ValueError("유효하지 않은 뉴클레오타이드 입니다:{}".format(nucleotide)) + + def decompress(self) -> str: + gene: str = "" + # 1로 시작해서 - 1이 있다. + for i in range(0, self.bit_string.bit_length() - 1, 2): + bits: int = self.bit_string >> i & 0b11 # 마지막 두 비트를 추출한다. + if bits == 0b00: # A + gene += "A" + elif bits == 0b01: # C + gene += "C" + elif bits == 0b10: # G + gene += "G" + elif bits == 0b11: # T + gene += "T" + else: + raise ValueError("Invalid bits:{}".format(bits)) + return gene[::-1] # [::-1] 문자열을 뒤집는다. + + def __str__(self) -> str: # 출력을 위한 문자열 표현 + return self.decompress() + + +if __name__ == "__main__": + from sys import getsizeof + original: str = "TAGGGATTAACCGTTATATATATATAGCCATGGATCGATTATATAGGGATTAACCGTTATATATATATAGCCATGGATCGATTATA" * 100 + print("원본: {} 바이트".format(getsizeof(original))) + compressed: CompressedGene = CompressedGene(original) # 압축 + print("압축: {} 바이트".format(getsizeof(compressed.bit_string))) + print(compressed) # 압축 해제 + print("원본 문자열과 압축 해제한 문자열은 같습니까? {}".format( + original == compressed.decompress())) diff --git a/ch1/unbreakable_encryption.py b/ch1/unbreakable_encryption.py new file mode 100644 index 0000000..0d025aa --- /dev/null +++ b/ch1/unbreakable_encryption.py @@ -0,0 +1,44 @@ +# unbreakable_encryption.py +# From Classic Computer Science Problems in Python Chapter 1 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from secrets import token_bytes +from typing import Tuple + + +def random_key(length: int) -> int: + # length 만큼 임의의 바이트를 생성한다. + tb: bytes = token_bytes(length) + # 바이트를 비트 문자열로 변환한 후 반환한다. + return int.from_bytes(tb, "big") + + +def encrypt(original: str) -> Tuple[int, int]: + original_bytes: bytes = original.encode() + dummy: int = random_key(len(original_bytes)) + original_key: int = int.from_bytes(original_bytes, "big") + encrypted: int = original_key ^ dummy # XOR + return dummy, encrypted + + +def decrypt(key1: int, key2: int) -> str: + decrypted: int = key1 ^ key2 # XOR + temp: bytes = decrypted.to_bytes((decrypted.bit_length() + 7) // 8, "big") + return temp.decode() + + +if __name__ == "__main__": + key1, key2 = encrypt("One Time Pad!") + result: str = decrypt(key1, key2) + print(result) diff --git a/ch2/dna_search.py b/ch2/dna_search.py new file mode 100644 index 0000000..f37b662 --- /dev/null +++ b/ch2/dna_search.py @@ -0,0 +1,70 @@ +# dna_search.py +# From Classic Computer Science Problems in Python Chapter 2 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import IntEnum +from typing import Tuple, List + +Nucleotide: IntEnum = IntEnum('Nucleotide', ('A', 'C', 'G', 'T')) +Codon = Tuple[Nucleotide, Nucleotide, Nucleotide] # 코돈 타입 앨리어스(alias) +Gene = List[Codon] # 유전자 타입 앨리어스 + +gene_str: str = "ACGTGGCTCTCTAACGTACGTACGTACGGGGTTTATATATACCCTAGGACTCCCTTT" + + +def string_to_gene(s: str) -> Gene: + gene: Gene = [] + for i in range(0, len(s), 3): + if (i + 2) >= len(s): # 현재 위치 다음에 2개의 문자가 없으면 실행하지 않는다. + return gene + # 3개의 뉴클레오타이드에서 코돈을 초기화한다. + codon: Codon = (Nucleotide[s[i]], + Nucleotide[s[i + 1]], Nucleotide[s[i + 2]]) + gene.append(codon) # 코돈을 유전자에 추가한다. + return gene + + +my_gene: Gene = string_to_gene(gene_str) + + +def linear_contains(gene: Gene, key_codon: Codon) -> bool: + for codon in gene: + if codon == key_codon: + return True + return False + + +acg: Codon = (Nucleotide.A, Nucleotide.C, Nucleotide.G) +gat: Codon = (Nucleotide.G, Nucleotide.A, Nucleotide.T) +print(linear_contains(my_gene, acg)) # True +print(linear_contains(my_gene, gat)) # False + + +def binary_contains(gene: Gene, key_codon: Codon) -> bool: + low: int = 0 + high: int = len(gene) - 1 + while low <= high: # 검색 공간(범위)이 있을 때 까지 수행 + mid: int = (low + high) // 2 + if gene[mid] < key_codon: + low = mid + 1 + elif gene[mid] > key_codon: + high = mid - 1 + else: + return True + return False + + +my_sorted_gene: Gene = sorted(my_gene) +print(binary_contains(my_sorted_gene, acg)) # True +print(binary_contains(my_sorted_gene, gat)) # False diff --git a/ch2/generic_search.py b/ch2/generic_search.py new file mode 100644 index 0000000..90784f9 --- /dev/null +++ b/ch2/generic_search.py @@ -0,0 +1,216 @@ +# generic_search.py +# From Classic Computer Science Problems in Python Chapter 2 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import TypeVar, Iterable, Sequence, Generic, List, Callable, Set, Deque, Dict, Any, Optional +from typing_extensions import Protocol +from heapq import heappush, heappop + +T = TypeVar('T') + + +def linear_contains(iterable: Iterable[T], key: T) -> bool: + for item in iterable: + if item == key: + return True + return False + + +C = TypeVar("C", bound="Comparable") + + +class Comparable(Protocol): + def __eq__(self, other: Any) -> bool: + ... + + def __lt__(self: C, other: C) -> bool: + ... + + def __gt__(self: C, other: C) -> bool: + return (not self < other) and self != other + + def __le__(self: C, other: C) -> bool: + return self < other or self == other + + def __ge__(self: C, other: C) -> bool: + return not self < other + + +def binary_contains(sequence: Sequence[C], key: C) -> bool: + low: int = 0 + high: int = len(sequence) - 1 + while low <= high: # 검색 공간(범위)이 있을 때 까지 수행 + mid: int = (low + high) // 2 + if sequence[mid] < key: + low = mid + 1 + elif sequence[mid] > key: + high = mid - 1 + else: + return True + return False + + +class Stack(Generic[T]): + def __init__(self) -> None: + self._container: List[T] = [] + + @property + def empty(self) -> bool: + return not self._container # 컨테이너가 비었다면 false가 아니다(=true) + + def push(self, item: T) -> None: + self._container.append(item) + + def pop(self) -> T: + return self._container.pop() # LIFO + + def __repr__(self) -> str: + return repr(self._container) + + +class Node(Generic[T]): + def __init__(self, state: T, parent: Optional[Node], cost: float = 0.0, heuristic: float = 0.0) -> None: + self.state: T = state + self.parent: Optional[Node] = parent + self.cost: float = cost + self.heuristic: float = heuristic + + def __lt__(self, other: Node) -> bool: + return (self.cost + self.heuristic) < (other.cost + other.heuristic) + + +def dfs(initial: T, goal_test: Callable[[T], bool], successors: Callable[[T], List[T]]) -> Optional[Node[T]]: + # frontier는 아직 방문하지 않은 곳이다. + frontier: Stack[Node[T]] = Stack() + frontier.push(Node(initial, None)) + # explored는 이미 방문한 곳이다. + explored: Set[T] = {initial} + + # 방문할 곳이 더 있는지 탐색한다. + while not frontier.empty: + current_node: Node[T] = frontier.pop() + current_state: T = current_node.state + # 목표 지점을 찾았다면 종료한다. + if goal_test(current_state): + return current_node + # 방문하지 않은 다음 장소가 있는지 확인한다. + for child in successors(current_state): + if child in explored: # 이미 방문한 자식 노드(장소)라면 건너뛴다. + continue + explored.add(child) + frontier.push(Node(child, current_node)) + return None # 모든 곳을 방문했지만 결국 목표 지점을 찾지 못했다. + + +def node_to_path(node: Node[T]) -> List[T]: + path: List[T] = [node.state] + # 노드 경로를 반전한다. + while node.parent is not None: + node = node.parent + path.append(node.state) + path.reverse() + return path + + +class Queue(Generic[T]): + def __init__(self) -> None: + self._container: Deque[T] = Deque() + + @property + def empty(self) -> bool: + return not self._container # 컨테이너가 비었다면 false가 아니다(=true) + + def push(self, item: T) -> None: + self._container.append(item) + + def pop(self) -> T: + return self._container.popleft() # 선입선출(FIFO) + + def __repr__(self) -> str: + return repr(self._container) + + +def bfs(initial: T, goal_test: Callable[[T], bool], successors: Callable[[T], List[T]]) -> Optional[Node[T]]: + # frontier는 아직 방문하지 않은 곳이다. + frontier: Queue[Node[T]] = Queue() + frontier.push(Node(initial, None)) + # explored는 이미 방문한 곳이다. + explored: Set[T] = {initial} + + # 방문할 곳이 더 있는지 탐색한다. + while not frontier.empty: + current_node: Node[T] = frontier.pop() + current_state: T = current_node.state + # 목표 지점을 찾았다면 종료한다. + if goal_test(current_state): + return current_node + # 방문하지 않은 다음 장소가 있는지 확인한다. + for child in successors(current_state): + if child in explored: # 이미 방문한 자식 노드(장소)라면 건너뛴다. + continue + explored.add(child) + frontier.push(Node(child, current_node)) + return None # 모든 곳을 방문했지만 결국 목표 지점을 찾지 못했다. + + +class PriorityQueue(Generic[T]): + def __init__(self) -> None: + self._container: List[T] = [] + + @property + def empty(self) -> bool: + return not self._container # 컨테이너가 비었다면 false가 아니다(=true) + + def push(self, item: T) -> None: + heappush(self._container, item) # 우선순위 push + + def pop(self) -> T: + return heappop(self._container) # 우선순위 pop + + def __repr__(self) -> str: + return repr(self._container) + + +def astar(initial: T, goal_test: Callable[[T], bool], successors: Callable[[T], List[T]], heuristic: Callable[[T], float]) -> Optional[Node[T]]: + # frontier는 아직 방문하지 않은 곳이다. + frontier: PriorityQueue[Node[T]] = PriorityQueue() + frontier.push(Node(initial, None, 0.0, heuristic(initial))) + # explored는 이미 방문한 곳이다. + explored: Dict[T, float] = {initial: 0.0} + + # 방문할 곳이 더 있는지 탐색한다. + while not frontier.empty: + current_node: Node[T] = frontier.pop() + current_state: T = current_node.state + # 목표 지점을 찾았다면 종료한다. + if goal_test(current_state): + return current_node + # 방문하지 않은 다음 장소가 있는지 확인한다. + for child in successors(current_state): + # 현재 장소에서 갈 수 있는 다음 장소의 비용은 1이라 가정한다. + new_cost: float = current_node.cost + 1 + + if child not in explored or explored[child] > new_cost: + explored[child] = new_cost + frontier.push(Node(child, current_node, + new_cost, heuristic(child))) + return None # 모든 곳을 방문했지만 결국 목표 지점을 찾지 못했다. + + +if __name__ == "__main__": + print(linear_contains([1, 5, 15, 15, 15, 15, 20], 5)) # True + print(binary_contains(["a", "d", "e", "f", "z"], "f")) # True + print(binary_contains( + ["john", "mark", "ronald", "sarah"], "sheila")) # False diff --git a/ch2/maze.py b/ch2/maze.py new file mode 100644 index 0000000..6da91b9 --- /dev/null +++ b/ch2/maze.py @@ -0,0 +1,141 @@ +# maze.py +# From Classic Computer Science Problems in Python Chapter 2 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from enum import Enum +from typing import List, NamedTuple, Callable, Optional +import random +from math import sqrt +from generic_search import dfs, bfs, node_to_path, astar, Node + + +class Cell(str, Enum): + EMPTY = " " + BLOCKED = "X" + START = "S" + GOAL = "G" + PATH = "*" + + +class MazeLocation(NamedTuple): + row: int + column: int + + +class Maze: + def __init__(self, rows: int = 10, columns: int = 10, sparseness: float = 0.2, start: MazeLocation = MazeLocation(0, 0), goal: MazeLocation = MazeLocation(9, 9)) -> None: + # 기본 인스턴스 변수 초기화 + self._rows: int = rows + self._columns: int = columns + self.start: MazeLocation = start + self.goal: MazeLocation = goal + # 격자를 빈 공간으로 채운다. + self._grid: List[List[Cell]] = [ + [Cell.EMPTY for c in range(columns)] for r in range(rows)] + # 빈 공간에 막힌 공간을 무작위로 채운다. + self._randomly_fill(rows, columns, sparseness) + # 시작 위치와 목표 위치를 설정한다. + self._grid[start.row][start.column] = Cell.START + self._grid[goal.row][goal.column] = Cell.GOAL + + def _randomly_fill(self, rows: int, columns: int, sparseness: float): + for row in range(rows): + for column in range(columns): + if random.uniform(0, 1.0) < sparseness: + self._grid[row][column] = Cell.BLOCKED + + # 미로 출력 + def __str__(self) -> str: + output: str = "" + for row in self._grid: + output += "".join([c.value for c in row]) + "\n" + return output + + def goal_test(self, ml: MazeLocation) -> bool: + return ml == self.goal + + def successors(self, ml: MazeLocation) -> List[MazeLocation]: + locations: List[MazeLocation] = [] + if ml.row + 1 < self._rows and self._grid[ml.row + 1][ml.column] != Cell.BLOCKED: + locations.append(MazeLocation(ml.row + 1, ml.column)) + if ml.row - 1 >= 0 and self._grid[ml.row - 1][ml.column] != Cell.BLOCKED: + locations.append(MazeLocation(ml.row - 1, ml.column)) + if ml.column + 1 < self._columns and self._grid[ml.row][ml.column + 1] != Cell.BLOCKED: + locations.append(MazeLocation(ml.row, ml.column + 1)) + if ml.column - 1 >= 0 and self._grid[ml.row][ml.column - 1] != Cell.BLOCKED: + locations.append(MazeLocation(ml.row, ml.column - 1)) + return locations + + def mark(self, path: List[MazeLocation]): + for maze_location in path: + self._grid[maze_location.row][maze_location.column] = Cell.PATH + self._grid[self.start.row][self.start.column] = Cell.START + self._grid[self.goal.row][self.goal.column] = Cell.GOAL + + def clear(self, path: List[MazeLocation]): + for maze_location in path: + self._grid[maze_location.row][maze_location.column] = Cell.EMPTY + self._grid[self.start.row][self.start.column] = Cell.START + self._grid[self.goal.row][self.goal.column] = Cell.GOAL + + +def euclidean_distance(goal: MazeLocation) -> Callable[[MazeLocation], float]: + def distance(ml: MazeLocation) -> float: + xdist: int = ml.column - goal.column + ydist: int = ml.row - goal.row + return sqrt((xdist * xdist) + (ydist * ydist)) + return distance + + +def manhattan_distance(goal: MazeLocation) -> Callable[[MazeLocation], float]: + def distance(ml: MazeLocation) -> float: + xdist: int = abs(ml.column - goal.column) + ydist: int = abs(ml.row - goal.row) + return (xdist + ydist) + return distance + + +if __name__ == "__main__": + # 깊이 우선 탐색(DFS) + m: Maze = Maze() + print(m) + solution1: Optional[Node[MazeLocation]] = dfs( + m.start, m.goal_test, m.successors) + if solution1 is None: + print("[깊이 우선 탐색] 길을 찾을 수 없습니다.") + else: + path1: List[MazeLocation] = node_to_path(solution1) + m.mark(path1) + print(m) + m.clear(path1) + # 너비 우선 탐색(BFS) + solution2: Optional[Node[MazeLocation]] = bfs( + m.start, m.goal_test, m.successors) + if solution2 is None: + print("[너비 우선 탐색] 길을 찾을 수 없습니다.") + else: + path2: List[MazeLocation] = node_to_path(solution2) + m.mark(path2) + print(m) + m.clear(path2) + # Test A* + distance: Callable[[MazeLocation], float] = manhattan_distance(m.goal) + solution3: Optional[Node[MazeLocation]] = astar( + m.start, m.goal_test, m.successors, distance) + if solution3 is None: + print("[A* 알고리즘] 길을 찾을 수 없습니다.") + else: + path3: List[MazeLocation] = node_to_path(solution3) + m.mark(path3) + print(m) diff --git a/ch2/missionaries.py b/ch2/missionaries.py new file mode 100644 index 0000000..cc46824 --- /dev/null +++ b/ch2/missionaries.py @@ -0,0 +1,99 @@ +# missionaries.py +# From Classic Computer Science Problems in Python Chapter 2 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import List, Optional +from generic_search import dfs, bfs, Node, node_to_path + +MAX_NUM: int = 3 + + +class MCState: + def __init__(self, missionaries: int, cannibals: int, boat: bool) -> None: + self.wm: int = missionaries # 서쪽 강뚝에 있는 선교사 수 + self.wc: int = cannibals # 서쪽 강뚝에 있는 식인종 수 + self.em: int = MAX_NUM - self.wm # 동쪽 강뚝에 있는 선교사 수 + self.ec: int = MAX_NUM - self.wc # 동쪽 강뚝에 있는 선교사 수 + self.boat: bool = boat + + def __str__(self) -> str: + return ("서쪽 강뚝에는 {}명의 선교사와 {}명의 식인종이 있다.\n" + "동쪽 강뚝에는 {}명의 선교사와 {}명의 식인종이 있다.\n" + "배는 {}쪽에 있다.")\ + .format(self.wm, self.wc, self.em, self.ec, ("서" if self.boat else "동")) + + def goal_test(self) -> bool: + return self.is_legal and self.em == MAX_NUM and self.ec == MAX_NUM + + @property + def is_legal(self) -> bool: + if self.wm < self.wc and self.wm > 0: + return False + if self.em < self.ec and self.em > 0: + return False + return True + + def successors(self) -> List[MCState]: + sucs: List[MCState] = [] + if self.boat: # 왼쪽 강뚝에 있는 배 + if self.wm > 1: + sucs.append(MCState(self.wm - 2, self.wc, not self.boat)) + if self.wm > 0: + sucs.append(MCState(self.wm - 1, self.wc, not self.boat)) + if self.wc > 1: + sucs.append(MCState(self.wm, self.wc - 2, not self.boat)) + if self.wc > 0: + sucs.append(MCState(self.wm, self.wc - 1, not self.boat)) + if (self.wc > 0) and (self.wm > 0): + sucs.append(MCState(self.wm - 1, self.wc - 1, not self.boat)) + else: # 동쪽 강뚝에 있는 배 + if self.em > 1: + sucs.append(MCState(self.wm + 2, self.wc, not self.boat)) + if self.em > 0: + sucs.append(MCState(self.wm + 1, self.wc, not self.boat)) + if self.ec > 1: + sucs.append(MCState(self.wm, self.wc + 2, not self.boat)) + if self.ec > 0: + sucs.append(MCState(self.wm, self.wc + 1, not self.boat)) + if (self.ec > 0) and (self.em > 0): + sucs.append(MCState(self.wm + 1, self.wc + 1, not self.boat)) + return [x for x in sucs if x.is_legal] + + +def display_solution(path: List[MCState]): + if len(path) == 0: # sanity check + return + old_state: MCState = path[0] + print(old_state) + for current_state in path[1:]: + if current_state.boat: + print("{}명의 선교사와 {}명의 식인종이 동쪽 강뚝에서 서쪽 강뚝으로 갔다.\n" + .format(old_state.em - current_state.em, old_state.ec - current_state.ec)) + else: + print("{}명의 선교사와 {}명의 식인종이 서쪽 강뚝에서 동쪽 강뚝으로 갔다.\n" + .format(old_state.wm - current_state.wm, old_state.wc - current_state.wc)) + print(current_state) + old_state = current_state + + +if __name__ == "__main__": + start: MCState = MCState(MAX_NUM, MAX_NUM, True) + solution: Optional[Node[MCState]] = bfs( + start, MCState.goal_test, MCState.successors) + if solution is None: + print("답을 찾을 수 없습니다.") + else: + path: List[MCState] = node_to_path(solution) + display_solution(path) diff --git a/ch3/csp.py b/ch3/csp.py new file mode 100644 index 0000000..d463426 --- /dev/null +++ b/ch3/csp.py @@ -0,0 +1,83 @@ +# csp.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Generic, TypeVar, Dict, List, Optional +from abc import ABC, abstractmethod + +V = TypeVar('V') # 변수(Variable) 타입 +D = TypeVar('D') # 도메인(Domain) 타입 + + +# 모든 제약 조건에 대한 베이스 클래스 +class Constraint(Generic[V, D], ABC): + # 제약 조건 변수 + def __init__(self, variables: List[V]) -> None: + self.variables = variables + + # 서브 클래스 메서드에 의해서 오버라이드된다. + @abstractmethod + def satisfied(self, assignment: Dict[V, D]) -> bool: + ... + + +# 제약 만족 문제는 타입 V의 (변수)와 범위를 나타내는 타입 D의 (도메인), +# 특정 변수의 도메인이 유효한지 확인하는 (제약 조건)으로 구성된다. +class CSP(Generic[V, D]): + def __init__(self, variables: List[V], domains: Dict[V, List[D]]) -> None: + self.variables: List[V] = variables # 제약 조건을 확인할 변수 + self.domains: Dict[V, List[D]] = domains # 각 변수의 도메인 + self.constraints: Dict[V, List[Constraint[V, D]]] = {} + for variable in self.variables: + self.constraints[variable] = [] + if variable not in self.domains: + raise LookupError( + "모든 변수에 도메인이 할당되어야 합니다.") + + def add_constraint(self, constraint: Constraint[V, D]) -> None: + for variable in constraint.variables: + if variable not in self.variables: + raise LookupError("제약 조건 변수가 아닙니다.") + else: + self.constraints[variable].append(constraint) + + # 주어진 변수의 모든 제약 조건을 검사하여 assignment 값이 일관적인지 확인한다. + def consistent(self, variable: V, assignment: Dict[V, D]) -> bool: + for constraint in self.constraints[variable]: + if not constraint.satisfied(assignment): + return False + return True + + def backtracking_search(self, assignment: Dict[V, D] = {}) -> Optional[Dict[V, D]]: + # assignment는 모든 변수가 할당될 때 완료된다(기저 조건) + if len(assignment) == len(self.variables): + return assignment + + # 할당되지 않은 모든 변수를 가져온다. + unassigned: List[V] = [ + v for v in self.variables if v not in assignment] + + # 할당되지 않은 첫 번째 변수의 가능한 모든 도메인 값을 가져온다. + first: V = unassigned[0] + for value in self.domains[first]: + local_assignment = assignment.copy() + local_assignment[first] = value + # local_assignment 값이 일관적이면, 재귀 호출한다. + if self.consistent(first, local_assignment): + result: Optional[Dict[V, D]] = self.backtracking_search( + local_assignment) + # 결과를 못찾았을 때, 백트래킹을 종료한다. + if result is not None: + return result + return None diff --git a/ch3/map_coloring.py b/ch3/map_coloring.py new file mode 100644 index 0000000..426f322 --- /dev/null +++ b/ch3/map_coloring.py @@ -0,0 +1,60 @@ +# map_coloring.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from csp import Constraint, CSP +from typing import Dict, List, Optional + + +class MapColoringConstraint(Constraint[str, str]): + def __init__(self, place1: str, place2: str) -> None: + super().__init__([place1, place2]) + self.place1: str = place1 + self.place2: str = place2 + + def satisfied(self, assignment: Dict[str, str]) -> bool: + # 두 지역 중 하나가 색상이 할당되지 않았다면, 색상 충돌은 발생하지 않는다. + if self.place1 not in assignment or self.place2 not in assignment: + return True + # place1과 place2에 할당된 색상이 다른지 확인한다. + return assignment[self.place1] != assignment[self.place2] + + +if __name__ == "__main__": + variables: List[str] = ["웨스턴 오스트레일리아 주", "노던 준주", "사우스 오스트레일리아 주", + "퀸즐랜드 주", "뉴사우스웨일스 주", "빅토리아 주", "태즈메이니아 주"] + domains: Dict[str, List[str]] = {} + for variable in variables: + domains[variable] = ["빨강", "초록", "파랑"] + csp: CSP[str, str] = CSP(variables, domains) + csp.add_constraint(MapColoringConstraint( + "웨스턴 오스트레일리아 주", "노던 준주")) + csp.add_constraint(MapColoringConstraint( + "웨스턴 오스트레일리아 주", "사우스 오스트레일리아 주")) + csp.add_constraint(MapColoringConstraint( + "사우스 오스트레일리아 주", "노던 준주")) + csp.add_constraint(MapColoringConstraint( + "퀸즐랜드 주", "노던 준주")) + csp.add_constraint(MapColoringConstraint("퀸즐랜드 주", "사우스 오스트레일리아 주")) + csp.add_constraint(MapColoringConstraint("퀸즐랜드 주", "뉴사우스웨일스 주")) + csp.add_constraint(MapColoringConstraint( + "뉴사우스웨일스 주", "사우스 오스트레일리아 주")) + csp.add_constraint(MapColoringConstraint("빅토리아 주", "사우스 오스트레일리아 주")) + csp.add_constraint(MapColoringConstraint("빅토리아 주", "뉴사우스웨일스 주")) + csp.add_constraint(MapColoringConstraint("빅토리아 주", "태즈메이니아 주")) + solution: Optional[Dict[str, str]] = csp.backtracking_search() + if solution is None: + print("답이 없습니다!") + else: + print(solution) diff --git a/ch3/queens.py b/ch3/queens.py new file mode 100644 index 0000000..ae28ca0 --- /dev/null +++ b/ch3/queens.py @@ -0,0 +1,50 @@ +# queens.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from csp import Constraint, CSP +from typing import Dict, List, Optional + + +class QueensConstraint(Constraint[int, int]): + def __init__(self, columns: List[int]) -> None: + super().__init__(columns) + self.columns: List[int] = columns + + def satisfied(self, assignment: Dict[int, int]) -> bool: + # q1c = 퀸1 열, q1r = 퀸1 행 + for q1c, q1r in assignment.items(): + # q2c = 퀸2 열 + for q2c in range(q1c + 1, len(self.columns) + 1): + if q2c in assignment: + q2r: int = assignment[q2c] # q2r = 퀸2 행 + if q1r == q2r: # 같은 열? + return False + if abs(q1r - q2r) == abs(q1c - q2c): # 같은 대각선? + return False + return True # 충돌 X + + +if __name__ == "__main__": + columns: List[int] = [1, 2, 3, 4, 5, 6, 7, 8] + rows: Dict[int, List[int]] = {} + for column in columns: + rows[column] = [1, 2, 3, 4, 5, 6, 7, 8] + csp: CSP[int, int] = CSP(columns, rows) + csp.add_constraint(QueensConstraint(columns)) + solution: Optional[Dict[int, int]] = csp.backtracking_search() + if solution is None: + print("답을 찾을 수 없습니다!") + else: + print(solution) diff --git a/ch3/send_more_money.py b/ch3/send_more_money.py new file mode 100644 index 0000000..261ffa3 --- /dev/null +++ b/ch3/send_more_money.py @@ -0,0 +1,59 @@ +# send_more_money.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from csp import Constraint, CSP +from typing import Dict, List, Optional + + +class SendMoreMoneyConstraint(Constraint[str, int]): + def __init__(self, letters: List[str]) -> None: + super().__init__(letters) + self.letters: List[str] = letters + + def satisfied(self, assignment: Dict[str, int]) -> bool: + # 중복 값이 있다면, 이 할당은 답이 아니다. + if len(set(assignment.values())) < len(assignment): + return False + + # 모든 변수에 숫자를 할당해서, 계산이 맞는지 확인한다. + if len(assignment) == len(self.letters): + s: int = assignment["S"] + e: int = assignment["E"] + n: int = assignment["N"] + d: int = assignment["D"] + m: int = assignment["M"] + o: int = assignment["O"] + r: int = assignment["R"] + y: int = assignment["Y"] + send: int = s * 1000 + e * 100 + n * 10 + d + more: int = m * 1000 + o * 100 + r * 10 + e + money: int = m * 10000 + o * 1000 + n * 100 + e * 10 + y + return send + more == money + return True # 충돌 없음 + + +if __name__ == "__main__": + letters: List[str] = ["S", "E", "N", "D", "M", "O", "R", "Y"] + possible_digits: Dict[str, List[int]] = {} + for letter in letters: + possible_digits[letter] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + possible_digits["M"] = [1] # 답은 0으로 시작하지 않는다. + csp: CSP[str, int] = CSP(letters, possible_digits) + csp.add_constraint(SendMoreMoneyConstraint(letters)) + solution: Optional[Dict[str, int]] = csp.backtracking_search() + if solution is None: + print("답을 찾을 수 없습니다!") + else: + print(solution) diff --git a/ch3/word_search.py b/ch3/word_search.py new file mode 100644 index 0000000..8cf5612 --- /dev/null +++ b/ch3/word_search.py @@ -0,0 +1,98 @@ +# word_search.py +# From Classic Computer Science Problems in Python Chapter 3 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import NamedTuple, List, Dict, Optional +from random import choice +from string import ascii_uppercase +from csp import CSP, Constraint + +Grid = List[List[str]] # 격자를 위한 타입 앨리어스 + + +class GridLocation(NamedTuple): + row: int + column: int + + +def generate_grid(rows: int, columns: int) -> Grid: + # 임의 문자로 격자를 초기화한다. + return [[choice(ascii_uppercase) for c in range(columns)] for r in range(rows)] + + +def display_grid(grid: Grid) -> None: + for row in grid: + print("".join(row)) + + +def generate_domain(word: str, grid: Grid) -> List[List[GridLocation]]: + domain: List[List[GridLocation]] = [] + height: int = len(grid) + width: int = len(grid[0]) + length: int = len(word) + for row in range(height): + for col in range(width): + columns: range = range(col, col + length + 1) + rows: range = range(row, row + length + 1) + if col + length <= width: + # 왼쪽에서 오른쪽으로 + domain.append([GridLocation(row, c) for c in columns]) + # 대각선 오른쪽 아래로 + if row + length <= height: + domain.append([GridLocation(r, col + (r - row)) + for r in rows]) + if row + length <= height: + # 위에서 아래로 + domain.append([GridLocation(r, col) for r in rows]) + # 대각선 왼쪽 아래로 + if col - length >= 0: + domain.append([GridLocation(r, col - (r - row)) + for r in rows]) + return domain + + +class WordSearchConstraint(Constraint[str, List[GridLocation]]): + def __init__(self, words: List[str]) -> None: + super().__init__(words) + self.words: List[str] = words + + def satisfied(self, assignment: Dict[str, List[GridLocation]]) -> bool: + # 중복된 격자 위치가 있다면, 그 위치는 겹치는 부분이다. + all_locations = [locs for values in assignment.values() + for locs in values] + return len(set(all_locations)) == len(all_locations) + + +if __name__ == "__main__": + grid: Grid = generate_grid(9, 9) + words: List[str] = ["MATTHEW", "JOE", "MARY", "SARAH", "SALLY"] + locations: Dict[str, List[List[GridLocation]]] = {} + for word in words: + locations[word] = generate_domain(word, grid) + csp: CSP[str, List[GridLocation]] = CSP(words, locations) + csp.add_constraint(WordSearchConstraint(words)) + solution: Optional[Dict[str, List[GridLocation]] + ] = csp.backtracking_search() + if solution is None: + print("답을 찾을 수 없습니다.") + else: + for word, grid_locations in solution.items(): + # 50% 확률로 grid_locations을 반전(reverse)한다. + if choice([True, False]): + grid_locations.reverse() + for index, letter in enumerate(word): + (row, col) = ( + grid_locations[index].row, grid_locations[index].column) + grid[row][col] = letter + display_grid(grid) diff --git a/ch4/dijkstra.py b/ch4/dijkstra.py new file mode 100644 index 0000000..6400f37 --- /dev/null +++ b/ch4/dijkstra.py @@ -0,0 +1,131 @@ +# dijkstra.py +# From Classic Computer Science Problems in Python Chapter 4 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import TypeVar, List, Optional, Tuple, Dict +from dataclasses import dataclass +from mst import WeightedPath, print_weighted_path +from weighted_graph import WeightedGraph +from weighted_edge import WeightedEdge +from priority_queue import PriorityQueue + +V = TypeVar('V') # 그래프 정점(vertice) 타입 + + +@dataclass +class DijkstraNode: + vertex: int + distance: float + + def __lt__(self, other: DijkstraNode) -> bool: + return self.distance < other.distance + + def __eq__(self, other: DijkstraNode) -> bool: + return self.distance == other.distance + + +def dijkstra(wg: WeightedGraph[V], root: V) -> Tuple[List[Optional[float]], Dict[int, WeightedEdge]]: + first: int = wg.index_of(root) # 시작 인덱스를 찾는다. + # 처음에는 거리(distances)를 알 수 없다. + distances: List[Optional[float]] = [None] * wg.vertex_count + distances[first] = 0 # 루트(root)에서 루트 자신의 거리는 0이다. + path_dict: Dict[int, WeightedEdge] = {} # 정점에 대한 경로 + pq: PriorityQueue[DijkstraNode] = PriorityQueue() + pq.push(DijkstraNode(first, 0)) + + while not pq.empty: + u: int = pq.pop().vertex # 다음 가까운 정점을 탐색한다. + dist_u: float = distances[u] # 이 정점에 대한 거리를 이미 알고 있다. + # 이 정점에서 모든 에지 및 정점을 살펴본다. + for we in wg.edges_for_index(u): + # 이 정점에 대한 이전 거리 + dist_v: float = distances[we.v] + # 이전 거리가 없거나 혹은 새 최단 경로가 존재한다면, + if dist_v is None or dist_v > we.weight + dist_u: + # 정점의 거리를 갱신한다. + distances[we.v] = we.weight + dist_u + # 정점의 최단 경로의 에지를 갱신한다. + path_dict[we.v] = we + # 해당 정점을 나중에 곧 탐색한다. + pq.push(DijkstraNode(we.v, we.weight + dist_u)) + + return distances, path_dict + + +# 다익스트라 알고리즘 결과를 더 쉽게 접근하게 하는 헬퍼 함수 +def distance_array_to_vertex_dict(wg: WeightedGraph[V], distances: List[Optional[float]]) -> Dict[V, Optional[float]]: + distance_dict: Dict[V, Optional[float]] = {} + for i in range(len(distances)): + distance_dict[wg.vertex_at(i)] = distances[i] + return distance_dict + + +# 에지의 딕셔너리 인자를 취해 각 노드에 접근하여, +# 정점 start 에서 end 까지가는 에지 리스트를 반환한다. +def path_dict_to_path(start: int, end: int, path_dict: Dict[int, WeightedEdge]) -> WeightedPath: + if len(path_dict) == 0: + return [] + edge_path: WeightedPath = [] + e: WeightedEdge = path_dict[end] + edge_path.append(e) + while e.u != start: + e = path_dict[e.u] + edge_path.append(e) + return list(reversed(edge_path)) + + +if __name__ == "__main__": + city_graph2: WeightedGraph[str] = WeightedGraph( + ["시애틀", "샌프란시스코", "로스앤젤레스", "리버사이드", "피닉스", "시카고", "보스턴", "뉴욕", "애틀랜타", "마이애미", "댈러스", "휴스턴", "디트로이트", "필라델피아", "워싱턴"]) + + city_graph2.add_edge_by_vertices("시애틀", "시카고", 1737) + city_graph2.add_edge_by_vertices("시애틀", "샌프란시스코", 678) + city_graph2.add_edge_by_vertices("샌프란시스코", "리버사이드", 386) + city_graph2.add_edge_by_vertices("샌프란시스코", "로스앤젤레스", 348) + city_graph2.add_edge_by_vertices("로스앤젤레스", "리버사이드", 50) + city_graph2.add_edge_by_vertices("로스앤젤레스", "피닉스", 357) + city_graph2.add_edge_by_vertices("리버사이드", "피닉스", 307) + city_graph2.add_edge_by_vertices("리버사이드", "시카고", 1704) + city_graph2.add_edge_by_vertices("피닉스", "댈러스", 887) + city_graph2.add_edge_by_vertices("피닉스", "휴스턴", 1015) + city_graph2.add_edge_by_vertices("댈러스", "시카고", 805) + city_graph2.add_edge_by_vertices("댈러스", "애틀랜타", 721) + city_graph2.add_edge_by_vertices("댈러스", "휴스턴", 225) + city_graph2.add_edge_by_vertices("휴스턴", "애틀랜타", 702) + city_graph2.add_edge_by_vertices("휴스턴", "마이애미", 968) + city_graph2.add_edge_by_vertices("애틀랜타", "시카고", 588) + city_graph2.add_edge_by_vertices("애틀랜타", "워싱턴", 543) + city_graph2.add_edge_by_vertices("애틀랜타", "마이애미", 604) + city_graph2.add_edge_by_vertices("마이애미", "워싱턴", 923) + city_graph2.add_edge_by_vertices("시카고", "디트로이트", 238) + city_graph2.add_edge_by_vertices("디트로이트", "보스턴", 613) + city_graph2.add_edge_by_vertices("디트로이트", "워싱턴", 396) + city_graph2.add_edge_by_vertices("디트로이트", "뉴욕", 482) + city_graph2.add_edge_by_vertices("보스턴", "뉴욕", 190) + city_graph2.add_edge_by_vertices("뉴욕", "필라델피아", 81) + city_graph2.add_edge_by_vertices("필라델피아", "워싱턴", 123) + + distances, path_dict = dijkstra(city_graph2, "로스앤젤레스") + name_distance: Dict[str, Optional[int]] = distance_array_to_vertex_dict( + city_graph2, distances) + print("로스앤젤레스에서의 거리:") + for key, value in name_distance.items(): + print(f"{key} : {value}") + print("") # 공백 라인 + + print("로스앤젤레스에서 보스턴까지의 최단 경로:") + path: WeightedPath = path_dict_to_path(city_graph2.index_of( + "로스앤젤레스"), city_graph2.index_of("보스턴"), path_dict) + print_weighted_path(city_graph2, path) diff --git a/ch4/edge.py b/ch4/edge.py new file mode 100644 index 0000000..2a187f7 --- /dev/null +++ b/ch4/edge.py @@ -0,0 +1,29 @@ +# edge.py +# From Classic Computer Science Problems in Python Chapter 4 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from dataclasses import dataclass + + +@dataclass +class Edge: + u: int # 정점 u에서 (from) + v: int # 정점 v로 (to) + + def reversed(self) -> Edge: + return Edge(self.v, self.u) + + def __str__(self) -> str: + return f"{self.u} -> {self.v}" diff --git a/ch4/graph.py b/ch4/graph.py new file mode 100644 index 0000000..52f51b8 --- /dev/null +++ b/ch4/graph.py @@ -0,0 +1,135 @@ +# graph.py +# From Classic Computer Science Problems in Python Chapter 4 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import TypeVar, Generic, List, Optional +from edge import Edge + + +V = TypeVar('V') # 그래프 정점(vertice) 타입 + + +class Graph(Generic[V]): + def __init__(self, vertices: List[V] = []) -> None: + self._vertices: List[V] = vertices + self._edges: List[List[Edge]] = [[] for _ in vertices] + + @property + def vertex_count(self) -> int: + return len(self._vertices) # 정점의 수 + + @property + def edge_count(self) -> int: + return sum(map(len, self._edges)) # 에지의 수 + + # 그래프에 정점을 추가하고 인덱스를 반환한다. + def add_vertex(self, vertex: V) -> int: + self._vertices.append(vertex) + self._edges.append([]) # 에지에 빈 리스트를 추가한다. + return self.vertex_count - 1 # 추가된 정점의 인덱스를 반환한다. + + # 무방향(undirected) 그래프이므로 항상 양방향으로 에지를 추가한다. + def add_edge(self, edge: Edge) -> None: + self._edges[edge.u].append(edge) + self._edges[edge.v].append(edge.reversed()) + + # # 정점 인덱스를 사용하여 에지를 추가한다(헬퍼 메서드). + def add_edge_by_indices(self, u: int, v: int) -> None: + edge: Edge = Edge(u, v) + self.add_edge(edge) + + # 정점 인덱스를 참조하여 에지를 추가한다(헬퍼 메서드). + def add_edge_by_vertices(self, first: V, second: V) -> None: + u: int = self._vertices.index(first) + v: int = self._vertices.index(second) + self.add_edge_by_indices(u, v) + + # 특정 인덱스에서 정점을 찾는다. + def vertex_at(self, index: int) -> V: + return self._vertices[index] + + # 정점 인덱스를 찾는다. + def index_of(self, vertex: V) -> int: + return self._vertices.index(vertex) + + # 정점 인덱스에 연결된 이웃 정점을 찾는다. + def neighbors_for_index(self, index: int) -> List[V]: + return list(map(self.vertex_at, [e.v for e in self._edges[index]])) + + # 정점의 이웃 정점을 찾는다(헬퍼 메서드). + def neighbors_for_vertex(self, vertex: V) -> List[V]: + return self.neighbors_for_index(self.index_of(vertex)) + + # 정점 인덱스에 연결된 모든 에지를 반환한다. + def edges_for_index(self, index: int) -> List[Edge]: + return self._edges[index] + + # 정점의 해당 에지를 반환한다(헬퍼 메서드). + def edges_for_vertex(self, vertex: V) -> List[Edge]: + return self.edges_for_index(self.index_of(vertex)) + + # 그래프를 예쁘게 출력한다(pretty-print). + def __str__(self) -> str: + desc: str = "" + for i in range(self.vertex_count): + desc += f"{self.vertex_at(i)} -> {self.neighbors_for_index(i)}\n" + return desc + + +if __name__ == "__main__": + # 기본 그래프 구축 테스트 + city_graph: Graph[str] = Graph(["시애틀", "샌프란시스코", "로스앤젤레스", "리버사이드", "피닉스", "시카고", + "보스턴", "뉴욕", "애틀랜타", "마이애미", "댈러스", "휴스턴", "디트로이트", "필라델피아", "워싱턴"]) + city_graph.add_edge_by_vertices("시애틀", "시카고") + city_graph.add_edge_by_vertices("시애틀", "샌프란시스코") + city_graph.add_edge_by_vertices("샌프란시스코", "리버사이드") + city_graph.add_edge_by_vertices("샌프란시스코", "로스앤젤레스") + city_graph.add_edge_by_vertices("로스앤젤레스", "리버사이드") + city_graph.add_edge_by_vertices("로스앤젤레스", "피닉스") + city_graph.add_edge_by_vertices("리버사이드", "피닉스") + city_graph.add_edge_by_vertices("리버사이드", "시카고") + city_graph.add_edge_by_vertices("피닉스", "댈러스") + city_graph.add_edge_by_vertices("피닉스", "휴스턴") + city_graph.add_edge_by_vertices("댈러스", "시카고") + city_graph.add_edge_by_vertices("댈러스", "애틀랜타") + city_graph.add_edge_by_vertices("댈러스", "휴스턴") + city_graph.add_edge_by_vertices("휴스턴", "애틀랜타") + city_graph.add_edge_by_vertices("휴스턴", "마이애미") + city_graph.add_edge_by_vertices("애틀랜타", "시카고") + city_graph.add_edge_by_vertices("애틀랜타", "워싱턴") + city_graph.add_edge_by_vertices("애틀랜타", "마이애미") + city_graph.add_edge_by_vertices("마이애미", "워싱턴") + city_graph.add_edge_by_vertices("시카고", "디트로이트") + city_graph.add_edge_by_vertices("디트로이트", "보스턴") + city_graph.add_edge_by_vertices("디트로이트", "워싱턴") + city_graph.add_edge_by_vertices("디트로이트", "뉴욕") + city_graph.add_edge_by_vertices("보스턴", "뉴욕") + city_graph.add_edge_by_vertices("뉴욕", "필라델피아") + city_graph.add_edge_by_vertices("필라델피아", "워싱턴") + print(city_graph) + + # city_graph 변수에 2장의 너비 우선 탐색을 재사용한다. + import sys + # 상위 디렉터리에 있는 2장 패키지에 접근한다. + sys.path.insert(0, '..') + from ch2.generic_search import bfs, Node, node_to_path + + bfs_result: Optional[Node[V]] = bfs( + "보스턴", lambda x: x == "마이애미", city_graph.neighbors_for_vertex) + if bfs_result is None: + print("[너비 우선 탐색] 답을 찾을 수 없습니다.") + else: + path: List[V] = node_to_path(bfs_result) + print("보스턴에서 마이애미 최단 경로:") + print(path) diff --git a/ch4/mst.py b/ch4/mst.py new file mode 100644 index 0000000..825fab9 --- /dev/null +++ b/ch4/mst.py @@ -0,0 +1,96 @@ +# mst.py +# From Classic Computer Science Problems in Python Chapter 4 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import TypeVar, List, Optional +from weighted_graph import WeightedGraph +from weighted_edge import WeightedEdge +from priority_queue import PriorityQueue + +V = TypeVar('V') # 그래프 정점(vertice) 타입 +WeightedPath = List[WeightedEdge] # 경로 타입 앨리어스 + + +def total_weight(wp: WeightedPath) -> float: + return sum([e.weight for e in wp]) + + +def mst(wg: WeightedGraph[V], start: int = 0) -> Optional[WeightedPath]: + if start > (wg.vertex_count - 1) or start < 0: + return None + result: WeightedPath = [] # 최소 신장 트리 결과 + pq: PriorityQueue[WeightedEdge] = PriorityQueue() + visited: [bool] = [False] * wg.vertex_count # 방문한 곳 + + def visit(index: int): + visited[index] = True # 방문한 곳을 표시한다. + for edge in wg.edges_for_index(index): + # 해당 정점의 모든 에지를 우선 순위 큐(pq)에 추가한다. + if not visited[edge.v]: + pq.push(edge) + + visit(start) # 첫 번째 정점에서 모든게 시작된다. + + while not pq.empty: # 우선 순위 큐에 에지가 남아있을 때까지 계속 반복한다. + edge = pq.pop() + if visited[edge.v]: + continue # 방문한 곳이면 넘어간다. + result.append(edge) # 최소 가중치의 에지를 결과에 추가한다. + visit(edge.v) # 연결된 에지를 방문한다. + + return result + + +def print_weighted_path(wg: WeightedGraph, wp: WeightedPath) -> None: + for edge in wp: + print(f"{wg.vertex_at(edge.u)} {edge.weight}> {wg.vertex_at(edge.v)}") + print(f"가중치 총합: {total_weight(wp)}") + + +if __name__ == "__main__": + city_graph2: WeightedGraph[str] = WeightedGraph(["시애틀", "샌프란시스코", "로스앤젤레스", "리버사이드", "피닉스", "시카고", + "보스턴", "뉴욕", "애틀랜타", "마이애미", "댈러스", "휴스턴", "디트로이트", "필라델피아", "워싱턴"]) + + city_graph2.add_edge_by_vertices("시애틀", "시카고", 1737) + city_graph2.add_edge_by_vertices("시애틀", "샌프란시스코", 678) + city_graph2.add_edge_by_vertices("샌프란시스코", "리버사이드", 386) + city_graph2.add_edge_by_vertices("샌프란시스코", "로스앤젤레스", 348) + city_graph2.add_edge_by_vertices("로스앤젤레스", "리버사이드", 50) + city_graph2.add_edge_by_vertices("로스앤젤레스", "피닉스", 357) + city_graph2.add_edge_by_vertices("리버사이드", "피닉스", 307) + city_graph2.add_edge_by_vertices("리버사이드", "시카고", 1704) + city_graph2.add_edge_by_vertices("피닉스", "댈러스", 887) + city_graph2.add_edge_by_vertices("피닉스", "휴스턴", 1015) + city_graph2.add_edge_by_vertices("댈러스", "시카고", 805) + city_graph2.add_edge_by_vertices("댈러스", "애틀랜타", 721) + city_graph2.add_edge_by_vertices("댈러스", "휴스턴", 225) + city_graph2.add_edge_by_vertices("휴스턴", "애틀랜타", 702) + city_graph2.add_edge_by_vertices("휴스턴", "마이애미", 968) + city_graph2.add_edge_by_vertices("애틀랜타", "시카고", 588) + city_graph2.add_edge_by_vertices("애틀랜타", "워싱턴", 543) + city_graph2.add_edge_by_vertices("애틀랜타", "마이애미", 604) + city_graph2.add_edge_by_vertices("마이애미", "워싱턴", 923) + city_graph2.add_edge_by_vertices("시카고", "디트로이트", 238) + city_graph2.add_edge_by_vertices("디트로이트", "보스턴", 613) + city_graph2.add_edge_by_vertices("디트로이트", "워싱턴", 396) + city_graph2.add_edge_by_vertices("디트로이트", "뉴욕", 482) + city_graph2.add_edge_by_vertices("보스턴", "뉴욕", 190) + city_graph2.add_edge_by_vertices("뉴욕", "필라델피아", 81) + city_graph2.add_edge_by_vertices("필라델피아", "워싱턴", 123) + + result: Optional[WeightedPath] = mst(city_graph2) + if result is None: + print("[최소 신장 트리] 답을 찾을 수 없습니다.") + else: + print_weighted_path(city_graph2, result) diff --git a/ch4/priority_queue.py b/ch4/priority_queue.py new file mode 100644 index 0000000..a2b45b9 --- /dev/null +++ b/ch4/priority_queue.py @@ -0,0 +1,38 @@ +# priority_queue.py +# From Classic Computer Science Problems in Python Chapter 4 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import TypeVar, Generic, List +from heapq import heappush, heappop + + +T = TypeVar('T') + + +class PriorityQueue(Generic[T]): + def __init__(self) -> None: + self._container: List[T] = [] + + @property + def empty(self) -> bool: + return not self._container # 컨테이너가 비었다면 false가 아니다(=true) + + def push(self, item: T) -> None: + heappush(self._container, item) # 우선순위 push + + def pop(self) -> T: + return heappop(self._container) # 우선순위 pop + + def __repr__(self) -> str: + return repr(self._container) diff --git a/ch4/weighted_edge.py b/ch4/weighted_edge.py new file mode 100644 index 0000000..a51c1e3 --- /dev/null +++ b/ch4/weighted_edge.py @@ -0,0 +1,33 @@ +# weighted_edge.py +# From Classic Computer Science Problems in Python Chapter 4 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from dataclasses import dataclass +from edge import Edge + + +@dataclass +class WeightedEdge(Edge): + weight: float + + def reversed(self) -> WeightedEdge: + return WeightedEdge(self.v, self.u, self.weight) + + # 가장 작은 가중치를 가진 에지를 찾기 위해서, 에지를 정렬할 수 있다. + def __lt__(self, other: WeightedEdge) -> bool: + return self.weight < other.weight + + def __str__(self) -> str: + return f"{self.u} {self.weight}> {self.v}" diff --git a/ch4/weighted_graph.py b/ch4/weighted_graph.py new file mode 100644 index 0000000..c476723 --- /dev/null +++ b/ch4/weighted_graph.py @@ -0,0 +1,81 @@ +# weighted_graph.py +# From Classic Computer Science Problems in Python Chapter 4 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import TypeVar, Generic, List, Tuple +from graph import Graph +from weighted_edge import WeightedEdge + +V = TypeVar('V') # 그래프 정점(vertice) 타입 + + +class WeightedGraph(Generic[V], Graph[V]): + def __init__(self, vertices: List[V] = []) -> None: + self._vertices: List[V] = vertices + self._edges: List[List[WeightedEdge]] = [[] for _ in vertices] + + def add_edge_by_indices(self, u: int, v: int, weight: float) -> None: + edge: WeightedEdge = WeightedEdge(u, v, weight) + self.add_edge(edge) # 슈퍼 클래스 메서드 호출 + + def add_edge_by_vertices(self, first: V, second: V, weight: float) -> None: + u: int = self._vertices.index(first) + v: int = self._vertices.index(second) + self.add_edge_by_indices(u, v, weight) + + def neighbors_for_index_with_weights(self, index: int) -> List[Tuple[V, float]]: + distance_tuples: List[Tuple[V, float]] = [] + for edge in self.edges_for_index(index): + distance_tuples.append((self.vertex_at(edge.v), edge.weight)) + return distance_tuples + + def __str__(self) -> str: + desc: str = "" + for i in range(self.vertex_count): + desc += f"{self.vertex_at(i)} -> {self.neighbors_for_index_with_weights(i)}\n" + return desc + + +if __name__ == "__main__": + city_graph2: WeightedGraph[str] = WeightedGraph( + ["시애틀", "샌프란시스코", "로스앤젤레스", "리버사이드", "피닉스", "시카고", "보스턴", "뉴욕", "애틀랜타", "마이애미", "댈러스", "휴스턴", "디트로이트", "필라델피아", "워싱턴"]) + + city_graph2.add_edge_by_vertices("시애틀", "시카고", 1737) + city_graph2.add_edge_by_vertices("시애틀", "샌프란시스코", 678) + city_graph2.add_edge_by_vertices("샌프란시스코", "리버사이드", 386) + city_graph2.add_edge_by_vertices("샌프란시스코", "로스앤젤레스", 348) + city_graph2.add_edge_by_vertices("로스앤젤레스", "리버사이드", 50) + city_graph2.add_edge_by_vertices("로스앤젤레스", "피닉스", 357) + city_graph2.add_edge_by_vertices("리버사이드", "피닉스", 307) + city_graph2.add_edge_by_vertices("리버사이드", "시카고", 1704) + city_graph2.add_edge_by_vertices("피닉스", "댈러스", 887) + city_graph2.add_edge_by_vertices("피닉스", "휴스턴", 1015) + city_graph2.add_edge_by_vertices("댈러스", "시카고", 805) + city_graph2.add_edge_by_vertices("댈러스", "애틀랜타", 721) + city_graph2.add_edge_by_vertices("댈러스", "휴스턴", 225) + city_graph2.add_edge_by_vertices("휴스턴", "애틀랜타", 702) + city_graph2.add_edge_by_vertices("휴스턴", "마이애미", 968) + city_graph2.add_edge_by_vertices("애틀랜타", "시카고", 588) + city_graph2.add_edge_by_vertices("애틀랜타", "워싱턴", 543) + city_graph2.add_edge_by_vertices("애틀랜타", "마이애미", 604) + city_graph2.add_edge_by_vertices("마이애미", "워싱턴", 923) + city_graph2.add_edge_by_vertices("시카고", "디트로이트", 238) + city_graph2.add_edge_by_vertices("디트로이트", "보스턴", 613) + city_graph2.add_edge_by_vertices("디트로이트", "워싱턴", 396) + city_graph2.add_edge_by_vertices("디트로이트", "뉴욕", 482) + city_graph2.add_edge_by_vertices("보스턴", "뉴욕", 190) + city_graph2.add_edge_by_vertices("뉴욕", "필라델피아", 81) + city_graph2.add_edge_by_vertices("필라델피아", "워싱턴", 123) + + print(city_graph2) diff --git a/ch5/chromosome.py b/ch5/chromosome.py new file mode 100644 index 0000000..5c35f70 --- /dev/null +++ b/ch5/chromosome.py @@ -0,0 +1,40 @@ +# chromosome.py +# From Classic Computer Science Problems in Python Chapter 5 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import TypeVar, Tuple, Type +from abc import ABC, abstractmethod + +T = TypeVar('T', bound='Chromosome') # 자신을 반환하기 위해서 사용한다. + + +# 모든 염색체의 베이스 클래스, 모든 메서드는 오버라이드된다. +class Chromosome(ABC): + @abstractmethod + def fitness(self) -> float: + ... + + @classmethod + @abstractmethod + def random_instance(cls: Type[T]) -> T: + ... + + @abstractmethod + def crossover(self: T, other: T) -> Tuple[T, T]: + ... + + @abstractmethod + def mutate(self) -> None: + ... diff --git a/ch5/genetic_algorithm.py b/ch5/genetic_algorithm.py new file mode 100644 index 0000000..d5f4c4a --- /dev/null +++ b/ch5/genetic_algorithm.py @@ -0,0 +1,94 @@ +# genetic_algorithm.py +# From Classic Computer Science Problems in Python Chapter 5 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import TypeVar, Generic, List, Tuple, Callable +from enum import Enum +from random import choices, random +from heapq import nlargest +from statistics import mean +from chromosome import Chromosome + +C = TypeVar('C', bound=Chromosome) # 염색체 타입 + + +class GeneticAlgorithm(Generic[C]): + SelectionType = Enum("SelectionType", "ROULETTE TOURNAMENT") + + def __init__(self, initial_population: List[C], threshold: float, max_generations: int = 100, mutation_chance: float = 0.01, crossover_chance: float = 0.7, selection_type: SelectionType = SelectionType.TOURNAMENT) -> None: + self._population: List[C] = initial_population + self._threshold: float = threshold + self._max_generations: int = max_generations + self._mutation_chance: float = mutation_chance + self._crossover_chance: float = crossover_chance + self._selection_type: GeneticAlgorithm.SelectionType = selection_type + self._fitness_key: Callable = type(self._population[0]).fitness + + # 두 부모를 선택하기 위해서 룰렛휠(확률 분포)을 사용한다. + # 음수 적합도와 동작하지 않는다. + def _pick_roulette(self, wheel: List[float]) -> Tuple[C, C]: + return tuple(choices(self._population, weights=wheel, k=2)) + + def _pick_roulette(self, wheel: List[float]) -> Tuple[C, C]: + return tuple(choices(self._population, weights=wheel, k=2)) + + # 무작위로 num_participants 만큼 선택한 후, 적합도가 가장 높은 두 염색체를 취한다. + def _pick_tournament(self, num_participants: int) -> Tuple[C, C]: + participants: List[C] = choices(self._population, k=num_participants) + return tuple(nlargest(2, participants, key=self._fitness_key)) + + # 집단을 새로운 세대로 교체한다. + def _reproduce_and_replace(self) -> None: + new_population: List[C] = [] + # 새로운 세대가 채워질 때까지 반복한다. + while len(new_population) < len(self._population): + # parents 중 두 부모를 선택한다. + if self._selection_type == GeneticAlgorithm.SelectionType.ROULETTE: + parents: Tuple[C, C] = self._pick_roulette( + [x.fitness() for x in self._population]) + else: + parents = self._pick_tournament(len(self._population) // 2) + # 두 부모를 크로스오버한다. + if random() < self._crossover_chance: + new_population.extend(parents[0].crossover(parents[1])) + else: + new_population.extend(parents) + # 새 집단의 수가 홀수라면, 이전 집단보다 하나가 더 많으므로 제거한다. + if len(new_population) > len(self._population): + new_population.pop() + self._population = new_population # 새 집단으로 참조를 변경한다. + + # _mutation_chance 확률로 각 개별 염색체를 돌연변이한다. + def _mutate(self) -> None: + for individual in self._population: + if random() < self._mutation_chance: + individual.mutate() + + # max_generations 만큼 유전 알고리즘을 실행하고, + # 최상의 적합도를 가진 개체를 반환한다. + def run(self) -> C: + best: C = max(self._population, key=self._fitness_key) + for generation in range(self._max_generations): + # 임계값을 초과하면, 개체를 바로 반환한다. + if best.fitness() >= self._threshold: + return best + print( + f"세대 {generation} 최상 {best.fitness()} 평균 {mean(map(self._fitness_key, self._population))}") + self._reproduce_and_replace() + self._mutate() + highest: C = max(self._population, key=self._fitness_key) + if highest.fitness() > best.fitness(): + best = highest # 새로운 최상의 개체가 발견됨 + return best # _max_generations에서 최상의 개체를 반환한다. diff --git a/ch5/list_compression.py b/ch5/list_compression.py new file mode 100644 index 0000000..5f28b63 --- /dev/null +++ b/ch5/list_compression.py @@ -0,0 +1,76 @@ +# list_compression.py +# From Classic Computer Science Problems in Python Chapter 5 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import Tuple, List, Any +from chromosome import Chromosome +from genetic_algorithm import GeneticAlgorithm +from random import shuffle, sample +from copy import deepcopy +from zlib import compress +from sys import getsizeof +from pickle import dumps + +# 165 바이트 압축 +PEOPLE: List[str] = ["Michael", "Sarah", "Joshua", "Narine", "David", + "Sajid", "Melanie", "Daniel", "Wei", "Dean", "Brian", "Murat", "Lisa"] + + +class ListCompression(Chromosome): + def __init__(self, lst: List[Any]) -> None: + self.lst: List[Any] = lst + + @property + def bytes_compressed(self) -> int: + return getsizeof(compress(dumps(self.lst))) + + def fitness(self) -> float: + return 1 / self.bytes_compressed + + @classmethod + def random_instance(cls) -> ListCompression: + mylst: List[str] = deepcopy(PEOPLE) + shuffle(mylst) + return ListCompression(mylst) + + def crossover(self, other: ListCompression) -> Tuple[ListCompression, ListCompression]: + child1: ListCompression = deepcopy(self) + child2: ListCompression = deepcopy(other) + idx1, idx2 = sample(range(len(self.lst)), k=2) + l1, l2 = child1.lst[idx1], child2.lst[idx2] + child1.lst[child1.lst.index( + l2)], child1.lst[idx2] = child1.lst[idx2], l2 + child2.lst[child2.lst.index( + l1)], child2.lst[idx1] = child2.lst[idx1], l1 + return child1, child2 + + def mutate(self) -> None: # 두 위치를 스왑한다. + idx1, idx2 = sample(range(len(self.lst)), k=2) + self.lst[idx1], self.lst[idx2] = self.lst[idx2], self.lst[idx1] + + def __str__(self) -> str: + return f"순서: {self.lst} 바이트: {self.bytes_compressed}" + + +if __name__ == "__main__": + initial_population: List[ListCompression] = [ + ListCompression.random_instance() for _ in range(100)] + ga: GeneticAlgorithm[ListCompression] = GeneticAlgorithm(initial_population=initial_population, threshold=1.0, max_generations=100, + mutation_chance=0.2, crossover_chance=0.7, selection_type=GeneticAlgorithm.SelectionType.TOURNAMENT) + result: ListCompression = ga.run() + print(result) + +# 저자가 테스트한 결과 각 세대에 1000 개체를 546번째 세대까지 실행했을 때, 제일 좋은 결과를 얻었다. +# 순서: ['Wei', 'Michael', 'Melanie', 'Daniel', 'Joshua', 'Narine', 'Lisa', 'Dean', 'Brian', 'David', 'Sajid', 'Sarah', 'Murat'] 바이트: 159 diff --git a/ch5/send_more_money2.py b/ch5/send_more_money2.py new file mode 100644 index 0000000..7d23e3f --- /dev/null +++ b/ch5/send_more_money2.py @@ -0,0 +1,86 @@ +# send_more_money2.py +# From Classic Computer Science Problems in Python Chapter 5 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import Tuple, List +from chromosome import Chromosome +from genetic_algorithm import GeneticAlgorithm +from random import shuffle, sample +from copy import deepcopy + + +class SendMoreMoney2(Chromosome): + def __init__(self, letters: List[str]) -> None: + self.letters: List[str] = letters + + def fitness(self) -> float: + s: int = self.letters.index("S") + e: int = self.letters.index("E") + n: int = self.letters.index("N") + d: int = self.letters.index("D") + m: int = self.letters.index("M") + o: int = self.letters.index("O") + r: int = self.letters.index("R") + y: int = self.letters.index("Y") + send: int = s * 1000 + e * 100 + n * 10 + d + more: int = m * 1000 + o * 100 + r * 10 + e + money: int = m * 10000 + o * 1000 + n * 100 + e * 10 + y + difference: int = abs(money - (send + more)) + return 1 / (difference + 1) + + @classmethod + def random_instance(cls) -> SendMoreMoney2: + letters = ["S", "E", "N", "D", "M", "O", "R", "Y", " ", " "] + shuffle(letters) + return SendMoreMoney2(letters) + + def crossover(self, other: SendMoreMoney2) -> Tuple[SendMoreMoney2, SendMoreMoney2]: + child1: SendMoreMoney2 = deepcopy(self) + child2: SendMoreMoney2 = deepcopy(other) + idx1, idx2 = sample(range(len(self.letters)), k=2) + l1, l2 = child1.letters[idx1], child2.letters[idx2] + child1.letters[child1.letters.index( + l2)], child1.letters[idx2] = child1.letters[idx2], l2 + child2.letters[child2.letters.index( + l1)], child2.letters[idx1] = child2.letters[idx1], l1 + return child1, child2 + + def mutate(self) -> None: # 두 문자의 위치를 스왑한다. + idx1, idx2 = sample(range(len(self.letters)), k=2) + self.letters[idx1], self.letters[idx2] = self.letters[idx2], self.letters[idx1] + + def __str__(self) -> str: + s: int = self.letters.index("S") + e: int = self.letters.index("E") + n: int = self.letters.index("N") + d: int = self.letters.index("D") + m: int = self.letters.index("M") + o: int = self.letters.index("O") + r: int = self.letters.index("R") + y: int = self.letters.index("Y") + send: int = s * 1000 + e * 100 + n * 10 + d + more: int = m * 1000 + o * 100 + r * 10 + e + money: int = m * 10000 + o * 1000 + n * 100 + e * 10 + y + difference: int = abs(money - (send + more)) + return f"{send} + {more} = {money} 차이: {difference}" + + +if __name__ == "__main__": + initial_population: List[SendMoreMoney2] = [ + SendMoreMoney2.random_instance() for _ in range(1000)] + ga: GeneticAlgorithm[SendMoreMoney2] = GeneticAlgorithm(initial_population=initial_population, threshold=1.0, + max_generations=1000, mutation_chance=0.2, crossover_chance=0.7, selection_type=GeneticAlgorithm.SelectionType.ROULETTE) + result: SendMoreMoney2 = ga.run() + print(result) diff --git a/ch5/simple_equation.py b/ch5/simple_equation.py new file mode 100644 index 0000000..738be43 --- /dev/null +++ b/ch5/simple_equation.py @@ -0,0 +1,65 @@ +# simple_equation.py +# From Classic Computer Science Problems in Python Chapter 5 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import Tuple, List +from chromosome import Chromosome +from genetic_algorithm import GeneticAlgorithm +from random import randrange, random +from copy import deepcopy + + +class SimpleEquation(Chromosome): + def __init__(self, x: int, y: int) -> None: + self.x: int = x + self.y: int = y + + def fitness(self) -> float: # 6x - x^2 + 4y - y^2 + return 6 * self.x - self.x * self.x + 4 * self.y - self.y * self.y + + @classmethod + def random_instance(cls) -> SimpleEquation: + return SimpleEquation(randrange(100), randrange(100)) + + def crossover(self, other: SimpleEquation) -> Tuple[SimpleEquation, SimpleEquation]: + child1: SimpleEquation = deepcopy(self) + child2: SimpleEquation = deepcopy(other) + child1.y = other.y + child2.y = self.y + return child1, child2 + + def mutate(self) -> None: + if random() > 0.5: # x를 돌연변이 한다. + if random() > 0.5: + self.x += 1 + else: + self.x -= 1 + else: # y를 돌연변이 한다. + if random() > 0.5: + self.y += 1 + else: + self.y -= 1 + + def __str__(self) -> str: + return f"X: {self.x} Y: {self.y} 적합도: {self.fitness()}" + + +if __name__ == "__main__": + initial_population: List[SimpleEquation] = [ + SimpleEquation.random_instance() for _ in range(20)] + ga: GeneticAlgorithm[SimpleEquation] = GeneticAlgorithm( + initial_population=initial_population, threshold=13.0, max_generations=100, mutation_chance=0.1, crossover_chance=0.7) + result: SimpleEquation = ga.run() + print(result) diff --git a/ch6/data_point.py b/ch6/data_point.py new file mode 100644 index 0000000..df5c49c --- /dev/null +++ b/ch6/data_point.py @@ -0,0 +1,41 @@ +# data_point.py +# From Classic Computer Science Problems in Python Chapter 6 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import Iterator, Tuple, List, Iterable +from math import sqrt + + +class DataPoint: + def __init__(self, initial: Iterable[float]) -> None: + self._originals: Tuple[float, ...] = tuple(initial) + self.dimensions: Tuple[float, ...] = tuple(initial) + + @property + def num_dimensions(self) -> int: + return len(self.dimensions) + + def distance(self, other: DataPoint) -> float: + combined: Iterator[Tuple[float, float]] = zip(self.dimensions, other.dimensions) + differences: List[float] = [(x - y) ** 2 for x, y in combined] + return sqrt(sum(differences)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, DataPoint): + return NotImplemented + return self.dimensions == other.dimensions + + def __repr__(self) -> str: + return self._originals.__repr__() diff --git a/ch6/governors.py b/ch6/governors.py new file mode 100644 index 0000000..4c6442f --- /dev/null +++ b/ch6/governors.py @@ -0,0 +1,85 @@ +# governors.py +# From Classic Computer Science Problems in Python Chapter 6 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import List +from data_point import DataPoint +from kmeans import KMeans + + +class Governor(DataPoint): + def __init__(self, longitude: float, age: float, state: str) -> None: + super().__init__([longitude, age]) + self.longitude = longitude + self.age = age + self.state = state + + def __repr__(self) -> str: + return f"{self.state}: (경도: {self.longitude}, 나이: {self.age})" + + +if __name__ == "__main__": + governors: List[Governor] = [Governor(-86.79113, 72, "Alabama"), Governor(-152.404419, 66, "Alaska"), + Governor(-111.431221, 53, + "Arizona"), Governor(-92.373123, 66, "Arkansas"), + Governor(-119.681564, 79, + "California"), Governor(-105.311104, 65, "Colorado"), + Governor(-72.755371, 61, "Connecticut"), Governor(-75.507141, + 61, "Delaware"), + Governor(-81.686783, 64, + "Florida"), Governor(-83.643074, 74, "Georgia"), + Governor(-157.498337, 60, + "Hawaii"), Governor(-114.478828, 75, "Idaho"), + Governor(-88.986137, 60, + "Illinois"), Governor(-86.258278, 49, "Indiana"), + Governor(-93.210526, 57, + "Iowa"), Governor(-96.726486, 60, "Kansas"), + Governor(-84.670067, 50, "Kentucky"), Governor(-91.867805, + 50, "Louisiana"), + Governor(-69.381927, 68, + "Maine"), Governor(-76.802101, 61, "Maryland"), + Governor(-71.530106, 60, + "Massachusetts"), Governor(-84.536095, 58, "Michigan"), + Governor(-93.900192, 70, "Minnesota"), Governor(-89.678696, + 62, "Mississippi"), + Governor(-92.288368, 43, + "Missouri"), Governor(-110.454353, 51, "Montana"), + Governor(-98.268082, 52, + "Nebraska"), Governor(-117.055374, 53, "Nevada"), + Governor(-71.563896, 42, "New Hampshire"), Governor(-74.521011, + 54, "New Jersey"), + Governor(-106.248482, 57, + "New Mexico"), Governor(-74.948051, 59, "New York"), + Governor(-79.806419, 60, "North Carolina"), Governor(-99.784012, + 60, "North Dakota"), + Governor(-82.764915, 65, + "Ohio"), Governor(-96.928917, 62, "Oklahoma"), + Governor(-122.070938, 56, "Oregon"), Governor(-77.209755, + 68, "Pennsylvania"), + Governor(-71.51178, 46, "Rhode Island"), Governor(-80.945007, + 70, "South Carolina"), + Governor(-99.438828, 64, "South Dakota"), Governor(-86.692345, + 58, "Tennessee"), + Governor(-97.563461, 59, + "Texas"), Governor(-111.862434, 70, "Utah"), + Governor(-72.710686, 58, + "Vermont"), Governor(-78.169968, 60, "Virginia"), + Governor(-121.490494, 66, "Washington"), Governor(-80.954453, + 66, "West Virginia"), + Governor(-89.616508, 49, "Wisconsin"), Governor(-107.30249, 55, "Wyoming")] + kmeans: KMeans[Governor] = KMeans(2, governors) + gov_clusters: List[KMeans.Cluster] = kmeans.run() + for index, cluster in enumerate(gov_clusters): + print(f"군집 {index}: {cluster.points}\n") diff --git a/ch6/kmeans.py b/ch6/kmeans.py new file mode 100644 index 0000000..4277504 --- /dev/null +++ b/ch6/kmeans.py @@ -0,0 +1,121 @@ +# kmeans.py +# From Classic Computer Science Problems in Python Chapter 6 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import TypeVar, Generic, List, Sequence +from copy import deepcopy +from functools import partial +from random import uniform +from statistics import mean, pstdev +from dataclasses import dataclass +from data_point import DataPoint + + +def zscores(original: Sequence[float]) -> List[float]: + avg: float = mean(original) + std: float = pstdev(original) + if std == 0: # 변화 차이가 없으면, 모두 0으로 반환된다. + return [0] * len(original) + return [(x - avg) / std for x in original] + + +Point = TypeVar('Point', bound=DataPoint) + + +class KMeans(Generic[Point]): + @dataclass + class Cluster: + points: List[Point] + centroid: DataPoint + + def __init__(self, k: int, points: List[Point]) -> None: + if k < 1: # k-평균은 음수나 0인 군집에 동작하지 않는다. + raise ValueError("k must be >= 1") + self._points: List[Point] = points + self._zscore_normalize() + # 임의의 중심으로 빈 군집을 초기화한다. + self._clusters: List[KMeans.Cluster] = [] + for _ in range(k): + rand_point: DataPoint = self._random_point() + cluster: KMeans.Cluster = KMeans.Cluster([], rand_point) + self._clusters.append(cluster) + + @property + def _centroids(self) -> List[DataPoint]: + return [x.centroid for x in self._clusters] + + def _dimension_slice(self, dimension: int) -> List[float]: + return [x.dimensions[dimension] for x in self._points] + + def _zscore_normalize(self) -> None: + zscored: List[List[float]] = [[] for _ in range(len(self._points))] + for dimension in range(self._points[0].num_dimensions): + dimension_slice: List[float] = self._dimension_slice(dimension) + for index, zscore in enumerate(zscores(dimension_slice)): + zscored[index].append(zscore) + for i in range(len(self._points)): + self._points[i].dimensions = tuple(zscored[i]) + + def _random_point(self) -> DataPoint: + rand_dimensions: List[float] = [] + for dimension in range(self._points[0].num_dimensions): + values: List[float] = self._dimension_slice(dimension) + rand_value: float = uniform(min(values), max(values)) + rand_dimensions.append(rand_value) + return DataPoint(rand_dimensions) + + # 각 포인트에 가장 가까운 군집의 중심을 찾아 해당 군집에 포인트를 할당한다. + def _assign_clusters(self) -> None: + for point in self._points: + closest: DataPoint = min( + self._centroids, key=partial(DataPoint.distance, point)) + idx: int = self._centroids.index(closest) + cluster: KMeans.Cluster = self._clusters[idx] + cluster.points.append(point) + + # 각 군집의 중심을 찾아서 옮긴다. + def _generate_centroids(self) -> None: + for cluster in self._clusters: + if len(cluster.points) == 0: # 포인트가 없으면 같은 중심으로 유지한다. + continue + means: List[float] = [] + for dimension in range(cluster.points[0].num_dimensions): + dimension_slice: List[float] = [ + p.dimensions[dimension] for p in cluster.points] + means.append(mean(dimension_slice)) + cluster.centroid = DataPoint(means) + + def run(self, max_iterations: int = 100) -> List[KMeans.Cluster]: + for iteration in range(max_iterations): + for cluster in self._clusters: # 모든 군집을 비운다. + cluster.points.clear() + self._assign_clusters() # 각 포인트에서 가장 가까운 군집을 찾는다. + old_centroids: List[DataPoint] = deepcopy( + self._centroids) # 중심을 복사한다. + self._generate_centroids() # 새로운 중심을 찾는다. + if old_centroids == self._centroids: # 중심이 이동했는가? + print(f"{iteration}회 반복 후 수렴") + return self._clusters + return self._clusters + + +if __name__ == "__main__": + point1: DataPoint = DataPoint([2.0, 1.0, 1.0]) + point2: DataPoint = DataPoint([2.0, 2.0, 5.0]) + point3: DataPoint = DataPoint([3.0, 1.5, 2.5]) + kmeans_test: KMeans[DataPoint] = KMeans(2, [point1, point2, point3]) + test_clusters: List[KMeans.Cluster] = kmeans_test.run() + for index, cluster in enumerate(test_clusters): + print(f"군집 {index}: {cluster.points}") diff --git a/ch6/mj.py b/ch6/mj.py new file mode 100644 index 0000000..28c71d7 --- /dev/null +++ b/ch6/mj.py @@ -0,0 +1,47 @@ +# mj.py +# From Classic Computer Science Problems in Python Chapter 6 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import List +from data_point import DataPoint +from kmeans import KMeans + + +class Album(DataPoint): + def __init__(self, name: str, year: int, length: float, tracks: float) -> None: + super().__init__([length, tracks]) + self.name = name + self.year = year + self.length = length + self.tracks = tracks + + def __repr__(self) -> str: + return f"{self.name}, {self.year}" + + +if __name__ == "__main__": + albums: List[Album] = [Album("Got to Be There", 1972, 35.45, 10), Album("Ben", 1972, 31.31, 10), + Album("Music & Me", 1973, 32.09, 10), Album( + "Forever, Michael", 1975, 33.36, 10), + Album("Off the Wall", 1979, 42.28, 10), Album( + "Thriller", 1982, 42.19, 9), + Album("Bad", 1987, 48.16, 10), Album( + "Dangerous", 1991, 77.03, 14), + Album("HIStory: Past, Present and Future, Book I", 1995, 148.58, 30), Album("Invincible", 2001, 77.05, 16)] + kmeans: KMeans[Album] = KMeans(2, albums) + clusters: List[KMeans.Cluster] = kmeans.run() + for index, cluster in enumerate(clusters): + print( + f"군집 {index} 평균 길이 {cluster.centroid.dimensions[0]} 평균 트랙 {cluster.centroid.dimensions[1]}: {cluster.points}\n") diff --git a/ch7/iris.csv b/ch7/iris.csv new file mode 100644 index 0000000..2bf7d09 --- /dev/null +++ b/ch7/iris.csv @@ -0,0 +1,150 @@ +5.1,3.5,1.4,0.2,Iris-setosa +4.9,3.0,1.4,0.2,Iris-setosa +4.7,3.2,1.3,0.2,Iris-setosa +4.6,3.1,1.5,0.2,Iris-setosa +5.0,3.6,1.4,0.2,Iris-setosa +5.4,3.9,1.7,0.4,Iris-setosa +4.6,3.4,1.4,0.3,Iris-setosa +5.0,3.4,1.5,0.2,Iris-setosa +4.4,2.9,1.4,0.2,Iris-setosa +4.9,3.1,1.5,0.1,Iris-setosa +5.4,3.7,1.5,0.2,Iris-setosa +4.8,3.4,1.6,0.2,Iris-setosa +4.8,3.0,1.4,0.1,Iris-setosa +4.3,3.0,1.1,0.1,Iris-setosa +5.8,4.0,1.2,0.2,Iris-setosa +5.7,4.4,1.5,0.4,Iris-setosa +5.4,3.9,1.3,0.4,Iris-setosa +5.1,3.5,1.4,0.3,Iris-setosa +5.7,3.8,1.7,0.3,Iris-setosa +5.1,3.8,1.5,0.3,Iris-setosa +5.4,3.4,1.7,0.2,Iris-setosa +5.1,3.7,1.5,0.4,Iris-setosa +4.6,3.6,1.0,0.2,Iris-setosa +5.1,3.3,1.7,0.5,Iris-setosa +4.8,3.4,1.9,0.2,Iris-setosa +5.0,3.0,1.6,0.2,Iris-setosa +5.0,3.4,1.6,0.4,Iris-setosa +5.2,3.5,1.5,0.2,Iris-setosa +5.2,3.4,1.4,0.2,Iris-setosa +4.7,3.2,1.6,0.2,Iris-setosa +4.8,3.1,1.6,0.2,Iris-setosa +5.4,3.4,1.5,0.4,Iris-setosa +5.2,4.1,1.5,0.1,Iris-setosa +5.5,4.2,1.4,0.2,Iris-setosa +4.9,3.1,1.5,0.2,Iris-setosa +5.0,3.2,1.2,0.2,Iris-setosa +5.5,3.5,1.3,0.2,Iris-setosa +4.9,3.6,1.4,0.1,Iris-setosa +4.4,3.0,1.3,0.2,Iris-setosa +5.1,3.4,1.5,0.2,Iris-setosa +5.0,3.5,1.3,0.3,Iris-setosa +4.5,2.3,1.3,0.3,Iris-setosa +4.4,3.2,1.3,0.2,Iris-setosa +5.0,3.5,1.6,0.6,Iris-setosa +5.1,3.8,1.9,0.4,Iris-setosa +4.8,3.0,1.4,0.3,Iris-setosa +5.1,3.8,1.6,0.2,Iris-setosa +4.6,3.2,1.4,0.2,Iris-setosa +5.3,3.7,1.5,0.2,Iris-setosa +5.0,3.3,1.4,0.2,Iris-setosa +7.0,3.2,4.7,1.4,Iris-versicolor +6.4,3.2,4.5,1.5,Iris-versicolor +6.9,3.1,4.9,1.5,Iris-versicolor +5.5,2.3,4.0,1.3,Iris-versicolor +6.5,2.8,4.6,1.5,Iris-versicolor +5.7,2.8,4.5,1.3,Iris-versicolor +6.3,3.3,4.7,1.6,Iris-versicolor +4.9,2.4,3.3,1.0,Iris-versicolor +6.6,2.9,4.6,1.3,Iris-versicolor +5.2,2.7,3.9,1.4,Iris-versicolor +5.0,2.0,3.5,1.0,Iris-versicolor +5.9,3.0,4.2,1.5,Iris-versicolor +6.0,2.2,4.0,1.0,Iris-versicolor +6.1,2.9,4.7,1.4,Iris-versicolor +5.6,2.9,3.6,1.3,Iris-versicolor +6.7,3.1,4.4,1.4,Iris-versicolor +5.6,3.0,4.5,1.5,Iris-versicolor +5.8,2.7,4.1,1.0,Iris-versicolor +6.2,2.2,4.5,1.5,Iris-versicolor +5.6,2.5,3.9,1.1,Iris-versicolor +5.9,3.2,4.8,1.8,Iris-versicolor +6.1,2.8,4.0,1.3,Iris-versicolor +6.3,2.5,4.9,1.5,Iris-versicolor +6.1,2.8,4.7,1.2,Iris-versicolor +6.4,2.9,4.3,1.3,Iris-versicolor +6.6,3.0,4.4,1.4,Iris-versicolor +6.8,2.8,4.8,1.4,Iris-versicolor +6.7,3.0,5.0,1.7,Iris-versicolor +6.0,2.9,4.5,1.5,Iris-versicolor +5.7,2.6,3.5,1.0,Iris-versicolor +5.5,2.4,3.8,1.1,Iris-versicolor +5.5,2.4,3.7,1.0,Iris-versicolor +5.8,2.7,3.9,1.2,Iris-versicolor +6.0,2.7,5.1,1.6,Iris-versicolor +5.4,3.0,4.5,1.5,Iris-versicolor +6.0,3.4,4.5,1.6,Iris-versicolor +6.7,3.1,4.7,1.5,Iris-versicolor +6.3,2.3,4.4,1.3,Iris-versicolor +5.6,3.0,4.1,1.3,Iris-versicolor +5.5,2.5,4.0,1.3,Iris-versicolor +5.5,2.6,4.4,1.2,Iris-versicolor +6.1,3.0,4.6,1.4,Iris-versicolor +5.8,2.6,4.0,1.2,Iris-versicolor +5.0,2.3,3.3,1.0,Iris-versicolor +5.6,2.7,4.2,1.3,Iris-versicolor +5.7,3.0,4.2,1.2,Iris-versicolor +5.7,2.9,4.2,1.3,Iris-versicolor +6.2,2.9,4.3,1.3,Iris-versicolor +5.1,2.5,3.0,1.1,Iris-versicolor +5.7,2.8,4.1,1.3,Iris-versicolor +6.3,3.3,6.0,2.5,Iris-virginica +5.8,2.7,5.1,1.9,Iris-virginica +7.1,3.0,5.9,2.1,Iris-virginica +6.3,2.9,5.6,1.8,Iris-virginica +6.5,3.0,5.8,2.2,Iris-virginica +7.6,3.0,6.6,2.1,Iris-virginica +4.9,2.5,4.5,1.7,Iris-virginica +7.3,2.9,6.3,1.8,Iris-virginica +6.7,2.5,5.8,1.8,Iris-virginica +7.2,3.6,6.1,2.5,Iris-virginica +6.5,3.2,5.1,2.0,Iris-virginica +6.4,2.7,5.3,1.9,Iris-virginica +6.8,3.0,5.5,2.1,Iris-virginica +5.7,2.5,5.0,2.0,Iris-virginica +5.8,2.8,5.1,2.4,Iris-virginica +6.4,3.2,5.3,2.3,Iris-virginica +6.5,3.0,5.5,1.8,Iris-virginica +7.7,3.8,6.7,2.2,Iris-virginica +7.7,2.6,6.9,2.3,Iris-virginica +6.0,2.2,5.0,1.5,Iris-virginica +6.9,3.2,5.7,2.3,Iris-virginica +5.6,2.8,4.9,2.0,Iris-virginica +7.7,2.8,6.7,2.0,Iris-virginica +6.3,2.7,4.9,1.8,Iris-virginica +6.7,3.3,5.7,2.1,Iris-virginica +7.2,3.2,6.0,1.8,Iris-virginica +6.2,2.8,4.8,1.8,Iris-virginica +6.1,3.0,4.9,1.8,Iris-virginica +6.4,2.8,5.6,2.1,Iris-virginica +7.2,3.0,5.8,1.6,Iris-virginica +7.4,2.8,6.1,1.9,Iris-virginica +7.9,3.8,6.4,2.0,Iris-virginica +6.4,2.8,5.6,2.2,Iris-virginica +6.3,2.8,5.1,1.5,Iris-virginica +6.1,2.6,5.6,1.4,Iris-virginica +7.7,3.0,6.1,2.3,Iris-virginica +6.3,3.4,5.6,2.4,Iris-virginica +6.4,3.1,5.5,1.8,Iris-virginica +6.0,3.0,4.8,1.8,Iris-virginica +6.9,3.1,5.4,2.1,Iris-virginica +6.7,3.1,5.6,2.4,Iris-virginica +6.9,3.1,5.1,2.3,Iris-virginica +5.8,2.7,5.1,1.9,Iris-virginica +6.8,3.2,5.9,2.3,Iris-virginica +6.7,3.3,5.7,2.5,Iris-virginica +6.7,3.0,5.2,2.3,Iris-virginica +6.3,2.5,5.0,1.9,Iris-virginica +6.5,3.0,5.2,2.0,Iris-virginica +6.2,3.4,5.4,2.3,Iris-virginica +5.9,3.0,5.1,1.8,Iris-virginica diff --git a/ch7/iris_test.py b/ch7/iris_test.py new file mode 100644 index 0000000..7d04379 --- /dev/null +++ b/ch7/iris_test.py @@ -0,0 +1,64 @@ +# iris_test.py +# From Classic Computer Science Problems in Python Chapter 7 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import csv +from typing import List +from util import normalize_by_feature_scaling +from network import Network +from random import shuffle + +if __name__ == "__main__": + iris_parameters: List[List[float]] = [] + iris_classifications: List[List[float]] = [] + iris_species: List[str] = [] + with open('iris.csv', mode='r') as iris_file: + irises: List = list(csv.reader(iris_file)) + shuffle(irises) # 데이터를 무작위로 섞는다. + for iris in irises: + parameters: List[float] = [float(n) for n in iris[0:4]] + iris_parameters.append(parameters) + species: str = iris[4] + if species == "Iris-setosa": + iris_classifications.append([1.0, 0.0, 0.0]) + elif species == "Iris-versicolor": + iris_classifications.append([0.0, 1.0, 0.0]) + else: + iris_classifications.append([0.0, 0.0, 1.0]) + iris_species.append(species) + normalize_by_feature_scaling(iris_parameters) + + iris_network: Network = Network([4, 6, 3], 0.3) + + def iris_interpret_output(output: List[float]) -> str: + if max(output) == output[0]: + return "Iris-setosa" + elif max(output) == output[1]: + return "Iris-versicolor" + else: + return "Iris-virginica" + + # 데이터셋에서 처음 140개의 붓꽃을 50회 훈련한다. + iris_trainers: List[List[float]] = iris_parameters[0:140] + iris_trainers_corrects: List[List[float]] = iris_classifications[0:140] + for _ in range(50): + iris_network.train(iris_trainers, iris_trainers_corrects) + + # 데이터셋에서 마지막 10개의 붓꽃을 테스트한다. + iris_testers: List[List[float]] = iris_parameters[140:150] + iris_testers_corrects: List[str] = iris_species[140:150] + iris_results = iris_network.validate( + iris_testers, iris_testers_corrects, iris_interpret_output) + print( + f"정확도: {iris_results[0]}/{iris_results[1]} = {iris_results[2] * 100}%") diff --git a/ch7/layer.py b/ch7/layer.py new file mode 100644 index 0000000..7ea1208 --- /dev/null +++ b/ch7/layer.py @@ -0,0 +1,61 @@ +# layer.py +# From Classic Computer Science Problems in Python Chapter 7 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import List, Callable, Optional +from random import random +from neuron import Neuron +from util import dot_product + + +class Layer: + def __init__(self, previous_layer: Optional[Layer], num_neurons: int, learning_rate: float, activation_function: Callable[[float], float], derivative_activation_function: Callable[[float], float]) -> None: + self.previous_layer: Optional[Layer] = previous_layer + self.neurons: List[Neuron] = [] + # 리스트 컴프리헨션을 사용하여 긴 리스트를 생성한다. + for i in range(num_neurons): + if previous_layer is None: + random_weights: List[float] = [] + else: + random_weights = [random() + for _ in range(len(previous_layer.neurons))] + neuron: Neuron = Neuron( + random_weights, learning_rate, activation_function, derivative_activation_function) + self.neurons.append(neuron) + self.output_cache: List[float] = [0.0 for _ in range(num_neurons)] + + def outputs(self, inputs: List[float]) -> List[float]: + if self.previous_layer is None: + self.output_cache = inputs + else: + self.output_cache = [n.output(inputs) for n in self.neurons] + return self.output_cache + + # 출력 레이어에서만 호출된다. + def calculate_deltas_for_output_layer(self, expected: List[float]) -> None: + for n in range(len(self.neurons)): + self.neurons[n].delta = self.neurons[n].derivative_activation_function( + self.neurons[n].output_cache) * (expected[n] - self.output_cache[n]) + + # 출력 레이어에서 호출되지 않는다. + def calculate_deltas_for_hidden_layer(self, next_layer: Layer) -> None: + for index, neuron in enumerate(self.neurons): + next_weights: List[float] = [n.weights[index] + for n in next_layer.neurons] + next_deltas: List[float] = [n.delta for n in next_layer.neurons] + sum_weights_and_deltas: float = dot_product( + next_weights, next_deltas) + neuron.delta = neuron.derivative_activation_function( + neuron.output_cache) * sum_weights_and_deltas diff --git a/ch7/network.py b/ch7/network.py new file mode 100644 index 0000000..d05c715 --- /dev/null +++ b/ch7/network.py @@ -0,0 +1,84 @@ +# network.py +# From Classic Computer Science Problems in Python Chapter 7 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import List, Callable, TypeVar, Tuple +from functools import reduce +from layer import Layer +from util import sigmoid, derivative_sigmoid + +T = TypeVar('T') # 신경망 출력 타입 + + +class Network: + def __init__(self, layer_structure: List[int], learning_rate: float, activation_function: Callable[[float], float] = sigmoid, derivative_activation_function: Callable[[float], float] = derivative_sigmoid) -> None: + if len(layer_structure) < 3: + raise ValueError( + "오류: 최소 3개의 레이어가 필요합니다(입력 레이어, 히든 레이어, 출력 레이어)!") + self.layers: List[Layer] = [] + # 입력 레이어 + input_layer: Layer = Layer( + None, layer_structure[0], learning_rate, activation_function, derivative_activation_function) + self.layers.append(input_layer) + # 히든 레이어와 출력 레이어 + for previous, num_neurons in enumerate(layer_structure[1::]): + next_layer = Layer(self.layers[previous], num_neurons, learning_rate, + activation_function, derivative_activation_function) + self.layers.append(next_layer) + + # 입력 레이어를 첫 번째 레이어로 푸시한 후, + # 첫 번째에서 두 번째, 두 번째에서 세 번째 레이어...로 출력한다. + def outputs(self, input: List[float]) -> List[float]: + return reduce(lambda inputs, layer: layer.outputs(inputs), self.layers, input) + + # 출력 오류와 예상 결과를 비교하여 각 뉴런의 변화를 파악한다. + def backpropagate(self, expected: List[float]) -> None: + # 출력 레이어 뉴런에 대한 델타를 계산한다. + last_layer: int = len(self.layers) - 1 + self.layers[last_layer].calculate_deltas_for_output_layer(expected) + # 히든 레이어에 대한 델타를 역순으로 계산한다. + for l in range(last_layer - 1, 0, -1): + self.layers[l].calculate_deltas_for_hidden_layer( + self.layers[l + 1]) + + # backpropagate() 메서드는 실제로 가중치를 수정하지 않는다. + # 이 메서드는 backpropagate() 메서드에서 계산된 델타를 사용하여 가중치를 변경한다. + def update_weights(self) -> None: + for layer in self.layers[1:]: # 입력 레이어는 제외한다. + for neuron in layer.neurons: + for w in range(len(neuron.weights)): + neuron.weights[w] = neuron.weights[w] + (neuron.learning_rate * ( + layer.previous_layer.output_cache[w]) * neuron.delta) + + # train() 메서드는 많은 입력을 통해 실행된 outputs() 메서드 결과를 사용한다. + # backpropagate() 메서드에 예상값를 입력하고 + # update_weights() 메서드를 호출하여 비교한다. + def train(self, inputs: List[List[float]], expecteds: List[List[float]]) -> None: + for location, xs in enumerate(inputs): + ys: List[float] = expecteds[location] + outs: List[float] = self.outputs(xs) + self.backpropagate(ys) + self.update_weights() + + # validate() 메서드는 분류가 필요한 일반화된 결과에서 + # 정확한 분류 수, 테스트 된 총 샘플 수, 정확한 분류 백분율을 반환한다. + def validate(self, inputs: List[List[float]], expecteds: List[T], interpret_output: Callable[[List[float]], T]) -> Tuple[int, int, float]: + correct: int = 0 + for input, expected in zip(inputs, expecteds): + result: T = interpret_output(self.outputs(input)) + if result == expected: + correct += 1 + percentage: float = correct / len(inputs) + return correct, len(inputs), percentage diff --git a/ch7/neuron.py b/ch7/neuron.py new file mode 100644 index 0000000..4b8e319 --- /dev/null +++ b/ch7/neuron.py @@ -0,0 +1,32 @@ +# neuron.py +# From Classic Computer Science Problems in Python Chapter 7 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List, Callable +from util import dot_product + + +class Neuron: + def __init__(self, weights: List[float], learning_rate: float, activation_function: Callable[[float], float], derivative_activation_function: Callable[[float], float]) -> None: + self.weights: List[float] = weights + self.activation_function: Callable[[float], float] = activation_function + self.derivative_activation_function: Callable[[float], float] = derivative_activation_function + self.learning_rate: float = learning_rate + self.output_cache: float = 0.0 + self.delta: float = 0.0 + + def output(self, inputs: List[float]) -> float: + self.output_cache = dot_product(inputs, self.weights) + return self.activation_function(self.output_cache) + diff --git a/ch7/util.py b/ch7/util.py new file mode 100644 index 0000000..9616dc9 --- /dev/null +++ b/ch7/util.py @@ -0,0 +1,43 @@ +# util.py +# From Classic Computer Science Problems in Python Chapter 7 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import List +from math import exp + + +# 두 벡터의 내적 +def dot_product(xs: List[float], ys: List[float]) -> float: + return sum(x * y for x, y in zip(xs, ys)) + + +# 시그모이드 함수 +def sigmoid(x: float) -> float: + return 1.0 / (1.0 + exp(-x)) + + +def derivative_sigmoid(x: float) -> float: + sig: float = sigmoid(x) + return sig * (1 - sig) + + +# 모든 행 길이가 같고 각 열의 범위(피쳐 스케일링)가 0 - 1이라 가정한다. +def normalize_by_feature_scaling(dataset: List[List[float]]) -> None: + for col_num in range(len(dataset[0])): + column: List[float] = [row[col_num] for row in dataset] + maximum = max(column) + minimum = min(column) + for row_num in range(len(dataset)): + dataset[row_num][col_num] = ( + dataset[row_num][col_num] - minimum) / (maximum - minimum) diff --git a/ch7/wine.csv b/ch7/wine.csv new file mode 100644 index 0000000..a0b3962 --- /dev/null +++ b/ch7/wine.csv @@ -0,0 +1,178 @@ +1,14.23,1.71,2.43,15.6,127,2.8,3.06,.28,2.29,5.64,1.04,3.92,1065 +1,13.2,1.78,2.14,11.2,100,2.65,2.76,.26,1.28,4.38,1.05,3.4,1050 +1,13.16,2.36,2.67,18.6,101,2.8,3.24,.3,2.81,5.68,1.03,3.17,1185 +1,14.37,1.95,2.5,16.8,113,3.85,3.49,.24,2.18,7.8,.86,3.45,1480 +1,13.24,2.59,2.87,21,118,2.8,2.69,.39,1.82,4.32,1.04,2.93,735 +1,14.2,1.76,2.45,15.2,112,3.27,3.39,.34,1.97,6.75,1.05,2.85,1450 +1,14.39,1.87,2.45,14.6,96,2.5,2.52,.3,1.98,5.25,1.02,3.58,1290 +1,14.06,2.15,2.61,17.6,121,2.6,2.51,.31,1.25,5.05,1.06,3.58,1295 +1,14.83,1.64,2.17,14,97,2.8,2.98,.29,1.98,5.2,1.08,2.85,1045 +1,13.86,1.35,2.27,16,98,2.98,3.15,.22,1.85,7.22,1.01,3.55,1045 +1,14.1,2.16,2.3,18,105,2.95,3.32,.22,2.38,5.75,1.25,3.17,1510 +1,14.12,1.48,2.32,16.8,95,2.2,2.43,.26,1.57,5,1.17,2.82,1280 +1,13.75,1.73,2.41,16,89,2.6,2.76,.29,1.81,5.6,1.15,2.9,1320 +1,14.75,1.73,2.39,11.4,91,3.1,3.69,.43,2.81,5.4,1.25,2.73,1150 +1,14.38,1.87,2.38,12,102,3.3,3.64,.29,2.96,7.5,1.2,3,1547 +1,13.63,1.81,2.7,17.2,112,2.85,2.91,.3,1.46,7.3,1.28,2.88,1310 +1,14.3,1.92,2.72,20,120,2.8,3.14,.33,1.97,6.2,1.07,2.65,1280 +1,13.83,1.57,2.62,20,115,2.95,3.4,.4,1.72,6.6,1.13,2.57,1130 +1,14.19,1.59,2.48,16.5,108,3.3,3.93,.32,1.86,8.7,1.23,2.82,1680 +1,13.64,3.1,2.56,15.2,116,2.7,3.03,.17,1.66,5.1,.96,3.36,845 +1,14.06,1.63,2.28,16,126,3,3.17,.24,2.1,5.65,1.09,3.71,780 +1,12.93,3.8,2.65,18.6,102,2.41,2.41,.25,1.98,4.5,1.03,3.52,770 +1,13.71,1.86,2.36,16.6,101,2.61,2.88,.27,1.69,3.8,1.11,4,1035 +1,12.85,1.6,2.52,17.8,95,2.48,2.37,.26,1.46,3.93,1.09,3.63,1015 +1,13.5,1.81,2.61,20,96,2.53,2.61,.28,1.66,3.52,1.12,3.82,845 +1,13.05,2.05,3.22,25,124,2.63,2.68,.47,1.92,3.58,1.13,3.2,830 +1,13.39,1.77,2.62,16.1,93,2.85,2.94,.34,1.45,4.8,.92,3.22,1195 +1,13.3,1.72,2.14,17,94,2.4,2.19,.27,1.35,3.95,1.02,2.77,1285 +1,13.87,1.9,2.8,19.4,107,2.95,2.97,.37,1.76,4.5,1.25,3.4,915 +1,14.02,1.68,2.21,16,96,2.65,2.33,.26,1.98,4.7,1.04,3.59,1035 +1,13.73,1.5,2.7,22.5,101,3,3.25,.29,2.38,5.7,1.19,2.71,1285 +1,13.58,1.66,2.36,19.1,106,2.86,3.19,.22,1.95,6.9,1.09,2.88,1515 +1,13.68,1.83,2.36,17.2,104,2.42,2.69,.42,1.97,3.84,1.23,2.87,990 +1,13.76,1.53,2.7,19.5,132,2.95,2.74,.5,1.35,5.4,1.25,3,1235 +1,13.51,1.8,2.65,19,110,2.35,2.53,.29,1.54,4.2,1.1,2.87,1095 +1,13.48,1.81,2.41,20.5,100,2.7,2.98,.26,1.86,5.1,1.04,3.47,920 +1,13.28,1.64,2.84,15.5,110,2.6,2.68,.34,1.36,4.6,1.09,2.78,880 +1,13.05,1.65,2.55,18,98,2.45,2.43,.29,1.44,4.25,1.12,2.51,1105 +1,13.07,1.5,2.1,15.5,98,2.4,2.64,.28,1.37,3.7,1.18,2.69,1020 +1,14.22,3.99,2.51,13.2,128,3,3.04,.2,2.08,5.1,.89,3.53,760 +1,13.56,1.71,2.31,16.2,117,3.15,3.29,.34,2.34,6.13,.95,3.38,795 +1,13.41,3.84,2.12,18.8,90,2.45,2.68,.27,1.48,4.28,.91,3,1035 +1,13.88,1.89,2.59,15,101,3.25,3.56,.17,1.7,5.43,.88,3.56,1095 +1,13.24,3.98,2.29,17.5,103,2.64,2.63,.32,1.66,4.36,.82,3,680 +1,13.05,1.77,2.1,17,107,3,3,.28,2.03,5.04,.88,3.35,885 +1,14.21,4.04,2.44,18.9,111,2.85,2.65,.3,1.25,5.24,.87,3.33,1080 +1,14.38,3.59,2.28,16,102,3.25,3.17,.27,2.19,4.9,1.04,3.44,1065 +1,13.9,1.68,2.12,16,101,3.1,3.39,.21,2.14,6.1,.91,3.33,985 +1,14.1,2.02,2.4,18.8,103,2.75,2.92,.32,2.38,6.2,1.07,2.75,1060 +1,13.94,1.73,2.27,17.4,108,2.88,3.54,.32,2.08,8.90,1.12,3.1,1260 +1,13.05,1.73,2.04,12.4,92,2.72,3.27,.17,2.91,7.2,1.12,2.91,1150 +1,13.83,1.65,2.6,17.2,94,2.45,2.99,.22,2.29,5.6,1.24,3.37,1265 +1,13.82,1.75,2.42,14,111,3.88,3.74,.32,1.87,7.05,1.01,3.26,1190 +1,13.77,1.9,2.68,17.1,115,3,2.79,.39,1.68,6.3,1.13,2.93,1375 +1,13.74,1.67,2.25,16.4,118,2.6,2.9,.21,1.62,5.85,.92,3.2,1060 +1,13.56,1.73,2.46,20.5,116,2.96,2.78,.2,2.45,6.25,.98,3.03,1120 +1,14.22,1.7,2.3,16.3,118,3.2,3,.26,2.03,6.38,.94,3.31,970 +1,13.29,1.97,2.68,16.8,102,3,3.23,.31,1.66,6,1.07,2.84,1270 +1,13.72,1.43,2.5,16.7,108,3.4,3.67,.19,2.04,6.8,.89,2.87,1285 +2,12.37,.94,1.36,10.6,88,1.98,.57,.28,.42,1.95,1.05,1.82,520 +2,12.33,1.1,2.28,16,101,2.05,1.09,.63,.41,3.27,1.25,1.67,680 +2,12.64,1.36,2.02,16.8,100,2.02,1.41,.53,.62,5.75,.98,1.59,450 +2,13.67,1.25,1.92,18,94,2.1,1.79,.32,.73,3.8,1.23,2.46,630 +2,12.37,1.13,2.16,19,87,3.5,3.1,.19,1.87,4.45,1.22,2.87,420 +2,12.17,1.45,2.53,19,104,1.89,1.75,.45,1.03,2.95,1.45,2.23,355 +2,12.37,1.21,2.56,18.1,98,2.42,2.65,.37,2.08,4.6,1.19,2.3,678 +2,13.11,1.01,1.7,15,78,2.98,3.18,.26,2.28,5.3,1.12,3.18,502 +2,12.37,1.17,1.92,19.6,78,2.11,2,.27,1.04,4.68,1.12,3.48,510 +2,13.34,.94,2.36,17,110,2.53,1.3,.55,.42,3.17,1.02,1.93,750 +2,12.21,1.19,1.75,16.8,151,1.85,1.28,.14,2.5,2.85,1.28,3.07,718 +2,12.29,1.61,2.21,20.4,103,1.1,1.02,.37,1.46,3.05,.906,1.82,870 +2,13.86,1.51,2.67,25,86,2.95,2.86,.21,1.87,3.38,1.36,3.16,410 +2,13.49,1.66,2.24,24,87,1.88,1.84,.27,1.03,3.74,.98,2.78,472 +2,12.99,1.67,2.6,30,139,3.3,2.89,.21,1.96,3.35,1.31,3.5,985 +2,11.96,1.09,2.3,21,101,3.38,2.14,.13,1.65,3.21,.99,3.13,886 +2,11.66,1.88,1.92,16,97,1.61,1.57,.34,1.15,3.8,1.23,2.14,428 +2,13.03,.9,1.71,16,86,1.95,2.03,.24,1.46,4.6,1.19,2.48,392 +2,11.84,2.89,2.23,18,112,1.72,1.32,.43,.95,2.65,.96,2.52,500 +2,12.33,.99,1.95,14.8,136,1.9,1.85,.35,2.76,3.4,1.06,2.31,750 +2,12.7,3.87,2.4,23,101,2.83,2.55,.43,1.95,2.57,1.19,3.13,463 +2,12,.92,2,19,86,2.42,2.26,.3,1.43,2.5,1.38,3.12,278 +2,12.72,1.81,2.2,18.8,86,2.2,2.53,.26,1.77,3.9,1.16,3.14,714 +2,12.08,1.13,2.51,24,78,2,1.58,.4,1.4,2.2,1.31,2.72,630 +2,13.05,3.86,2.32,22.5,85,1.65,1.59,.61,1.62,4.8,.84,2.01,515 +2,11.84,.89,2.58,18,94,2.2,2.21,.22,2.35,3.05,.79,3.08,520 +2,12.67,.98,2.24,18,99,2.2,1.94,.3,1.46,2.62,1.23,3.16,450 +2,12.16,1.61,2.31,22.8,90,1.78,1.69,.43,1.56,2.45,1.33,2.26,495 +2,11.65,1.67,2.62,26,88,1.92,1.61,.4,1.34,2.6,1.36,3.21,562 +2,11.64,2.06,2.46,21.6,84,1.95,1.69,.48,1.35,2.8,1,2.75,680 +2,12.08,1.33,2.3,23.6,70,2.2,1.59,.42,1.38,1.74,1.07,3.21,625 +2,12.08,1.83,2.32,18.5,81,1.6,1.5,.52,1.64,2.4,1.08,2.27,480 +2,12,1.51,2.42,22,86,1.45,1.25,.5,1.63,3.6,1.05,2.65,450 +2,12.69,1.53,2.26,20.7,80,1.38,1.46,.58,1.62,3.05,.96,2.06,495 +2,12.29,2.83,2.22,18,88,2.45,2.25,.25,1.99,2.15,1.15,3.3,290 +2,11.62,1.99,2.28,18,98,3.02,2.26,.17,1.35,3.25,1.16,2.96,345 +2,12.47,1.52,2.2,19,162,2.5,2.27,.32,3.28,2.6,1.16,2.63,937 +2,11.81,2.12,2.74,21.5,134,1.6,.99,.14,1.56,2.5,.95,2.26,625 +2,12.29,1.41,1.98,16,85,2.55,2.5,.29,1.77,2.9,1.23,2.74,428 +2,12.37,1.07,2.1,18.5,88,3.52,3.75,.24,1.95,4.5,1.04,2.77,660 +2,12.29,3.17,2.21,18,88,2.85,2.99,.45,2.81,2.3,1.42,2.83,406 +2,12.08,2.08,1.7,17.5,97,2.23,2.17,.26,1.4,3.3,1.27,2.96,710 +2,12.6,1.34,1.9,18.5,88,1.45,1.36,.29,1.35,2.45,1.04,2.77,562 +2,12.34,2.45,2.46,21,98,2.56,2.11,.34,1.31,2.8,.8,3.38,438 +2,11.82,1.72,1.88,19.5,86,2.5,1.64,.37,1.42,2.06,.94,2.44,415 +2,12.51,1.73,1.98,20.5,85,2.2,1.92,.32,1.48,2.94,1.04,3.57,672 +2,12.42,2.55,2.27,22,90,1.68,1.84,.66,1.42,2.7,.86,3.3,315 +2,12.25,1.73,2.12,19,80,1.65,2.03,.37,1.63,3.4,1,3.17,510 +2,12.72,1.75,2.28,22.5,84,1.38,1.76,.48,1.63,3.3,.88,2.42,488 +2,12.22,1.29,1.94,19,92,2.36,2.04,.39,2.08,2.7,.86,3.02,312 +2,11.61,1.35,2.7,20,94,2.74,2.92,.29,2.49,2.65,.96,3.26,680 +2,11.46,3.74,1.82,19.5,107,3.18,2.58,.24,3.58,2.9,.75,2.81,562 +2,12.52,2.43,2.17,21,88,2.55,2.27,.26,1.22,2,.9,2.78,325 +2,11.76,2.68,2.92,20,103,1.75,2.03,.6,1.05,3.8,1.23,2.5,607 +2,11.41,.74,2.5,21,88,2.48,2.01,.42,1.44,3.08,1.1,2.31,434 +2,12.08,1.39,2.5,22.5,84,2.56,2.29,.43,1.04,2.9,.93,3.19,385 +2,11.03,1.51,2.2,21.5,85,2.46,2.17,.52,2.01,1.9,1.71,2.87,407 +2,11.82,1.47,1.99,20.8,86,1.98,1.6,.3,1.53,1.95,.95,3.33,495 +2,12.42,1.61,2.19,22.5,108,2,2.09,.34,1.61,2.06,1.06,2.96,345 +2,12.77,3.43,1.98,16,80,1.63,1.25,.43,.83,3.4,.7,2.12,372 +2,12,3.43,2,19,87,2,1.64,.37,1.87,1.28,.93,3.05,564 +2,11.45,2.4,2.42,20,96,2.9,2.79,.32,1.83,3.25,.8,3.39,625 +2,11.56,2.05,3.23,28.5,119,3.18,5.08,.47,1.87,6,.93,3.69,465 +2,12.42,4.43,2.73,26.5,102,2.2,2.13,.43,1.71,2.08,.92,3.12,365 +2,13.05,5.8,2.13,21.5,86,2.62,2.65,.3,2.01,2.6,.73,3.1,380 +2,11.87,4.31,2.39,21,82,2.86,3.03,.21,2.91,2.8,.75,3.64,380 +2,12.07,2.16,2.17,21,85,2.6,2.65,.37,1.35,2.76,.86,3.28,378 +2,12.43,1.53,2.29,21.5,86,2.74,3.15,.39,1.77,3.94,.69,2.84,352 +2,11.79,2.13,2.78,28.5,92,2.13,2.24,.58,1.76,3,.97,2.44,466 +2,12.37,1.63,2.3,24.5,88,2.22,2.45,.4,1.9,2.12,.89,2.78,342 +2,12.04,4.3,2.38,22,80,2.1,1.75,.42,1.35,2.6,.79,2.57,580 +3,12.86,1.35,2.32,18,122,1.51,1.25,.21,.94,4.1,.76,1.29,630 +3,12.88,2.99,2.4,20,104,1.3,1.22,.24,.83,5.4,.74,1.42,530 +3,12.81,2.31,2.4,24,98,1.15,1.09,.27,.83,5.7,.66,1.36,560 +3,12.7,3.55,2.36,21.5,106,1.7,1.2,.17,.84,5,.78,1.29,600 +3,12.51,1.24,2.25,17.5,85,2,.58,.6,1.25,5.45,.75,1.51,650 +3,12.6,2.46,2.2,18.5,94,1.62,.66,.63,.94,7.1,.73,1.58,695 +3,12.25,4.72,2.54,21,89,1.38,.47,.53,.8,3.85,.75,1.27,720 +3,12.53,5.51,2.64,25,96,1.79,.6,.63,1.1,5,.82,1.69,515 +3,13.49,3.59,2.19,19.5,88,1.62,.48,.58,.88,5.7,.81,1.82,580 +3,12.84,2.96,2.61,24,101,2.32,.6,.53,.81,4.92,.89,2.15,590 +3,12.93,2.81,2.7,21,96,1.54,.5,.53,.75,4.6,.77,2.31,600 +3,13.36,2.56,2.35,20,89,1.4,.5,.37,.64,5.6,.7,2.47,780 +3,13.52,3.17,2.72,23.5,97,1.55,.52,.5,.55,4.35,.89,2.06,520 +3,13.62,4.95,2.35,20,92,2,.8,.47,1.02,4.4,.91,2.05,550 +3,12.25,3.88,2.2,18.5,112,1.38,.78,.29,1.14,8.21,.65,2,855 +3,13.16,3.57,2.15,21,102,1.5,.55,.43,1.3,4,.6,1.68,830 +3,13.88,5.04,2.23,20,80,.98,.34,.4,.68,4.9,.58,1.33,415 +3,12.87,4.61,2.48,21.5,86,1.7,.65,.47,.86,7.65,.54,1.86,625 +3,13.32,3.24,2.38,21.5,92,1.93,.76,.45,1.25,8.42,.55,1.62,650 +3,13.08,3.9,2.36,21.5,113,1.41,1.39,.34,1.14,9.40,.57,1.33,550 +3,13.5,3.12,2.62,24,123,1.4,1.57,.22,1.25,8.60,.59,1.3,500 +3,12.79,2.67,2.48,22,112,1.48,1.36,.24,1.26,10.8,.48,1.47,480 +3,13.11,1.9,2.75,25.5,116,2.2,1.28,.26,1.56,7.1,.61,1.33,425 +3,13.23,3.3,2.28,18.5,98,1.8,.83,.61,1.87,10.52,.56,1.51,675 +3,12.58,1.29,2.1,20,103,1.48,.58,.53,1.4,7.6,.58,1.55,640 +3,13.17,5.19,2.32,22,93,1.74,.63,.61,1.55,7.9,.6,1.48,725 +3,13.84,4.12,2.38,19.5,89,1.8,.83,.48,1.56,9.01,.57,1.64,480 +3,12.45,3.03,2.64,27,97,1.9,.58,.63,1.14,7.5,.67,1.73,880 +3,14.34,1.68,2.7,25,98,2.8,1.31,.53,2.7,13,.57,1.96,660 +3,13.48,1.67,2.64,22.5,89,2.6,1.1,.52,2.29,11.75,.57,1.78,620 +3,12.36,3.83,2.38,21,88,2.3,.92,.5,1.04,7.65,.56,1.58,520 +3,13.69,3.26,2.54,20,107,1.83,.56,.5,.8,5.88,.96,1.82,680 +3,12.85,3.27,2.58,22,106,1.65,.6,.6,.96,5.58,.87,2.11,570 +3,12.96,3.45,2.35,18.5,106,1.39,.7,.4,.94,5.28,.68,1.75,675 +3,13.78,2.76,2.3,22,90,1.35,.68,.41,1.03,9.58,.7,1.68,615 +3,13.73,4.36,2.26,22.5,88,1.28,.47,.52,1.15,6.62,.78,1.75,520 +3,13.45,3.7,2.6,23,111,1.7,.92,.43,1.46,10.68,.85,1.56,695 +3,12.82,3.37,2.3,19.5,88,1.48,.66,.4,.97,10.26,.72,1.75,685 +3,13.58,2.58,2.69,24.5,105,1.55,.84,.39,1.54,8.66,.74,1.8,750 +3,13.4,4.6,2.86,25,112,1.98,.96,.27,1.11,8.5,.67,1.92,630 +3,12.2,3.03,2.32,19,96,1.25,.49,.4,.73,5.5,.66,1.83,510 +3,12.77,2.39,2.28,19.5,86,1.39,.51,.48,.64,9.899999,.57,1.63,470 +3,14.16,2.51,2.48,20,91,1.68,.7,.44,1.24,9.7,.62,1.71,660 +3,13.71,5.65,2.45,20.5,95,1.68,.61,.52,1.06,7.7,.64,1.74,740 +3,13.4,3.91,2.48,23,102,1.8,.75,.43,1.41,7.3,.7,1.56,750 +3,13.27,4.28,2.26,20,120,1.59,.69,.43,1.35,10.2,.59,1.56,835 +3,13.17,2.59,2.37,20,120,1.65,.68,.53,1.46,9.3,.6,1.62,840 +3,14.13,4.1,2.74,24.5,96,2.05,.76,.56,1.35,9.2,.61,1.6,560 diff --git a/ch7/wine_test.py b/ch7/wine_test.py new file mode 100644 index 0000000..2b4f211 --- /dev/null +++ b/ch7/wine_test.py @@ -0,0 +1,64 @@ +# wine_test.py +# From Classic Computer Science Problems in Python Chapter 7 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import csv +from typing import List +from util import normalize_by_feature_scaling +from network import Network +from random import shuffle + +if __name__ == "__main__": + wine_parameters: List[List[float]] = [] + wine_classifications: List[List[float]] = [] + wine_species: List[int] = [] + with open('wine.csv', mode='r') as wine_file: + wines: List = list(csv.reader(wine_file, quoting=csv.QUOTE_NONNUMERIC)) + shuffle(wines) # 데이터를 무작위로 섞는다. + for wine in wines: + parameters: List[float] = [float(n) for n in wine[1:14]] + wine_parameters.append(parameters) + species: int = int(wine[0]) + if species == 1: + wine_classifications.append([1.0, 0.0, 0.0]) + elif species == 2: + wine_classifications.append([0.0, 1.0, 0.0]) + else: + wine_classifications.append([0.0, 0.0, 1.0]) + wine_species.append(species) + normalize_by_feature_scaling(wine_parameters) + + wine_network: Network = Network([13, 7, 3], 0.9) + + def wine_interpret_output(output: List[float]) -> int: + if max(output) == output[0]: + return 1 + elif max(output) == output[1]: + return 2 + else: + return 3 + + # 처음 150개 와인을 10회 훈련한다. + wine_trainers: List[List[float]] = wine_parameters[0:150] + wine_trainers_corrects: List[List[float]] = wine_classifications[0:150] + for _ in range(10): + wine_network.train(wine_trainers, wine_trainers_corrects) + + # 데이터셋에서 마지막 28개의 와인을 테스트한다. + wine_testers: List[List[float]] = wine_parameters[150:178] + wine_testers_corrects: List[int] = wine_species[150:178] + wine_results = wine_network.validate( + wine_testers, wine_testers_corrects, wine_interpret_output) + print( + f"정확도: {wine_results[0]}/{wine_results[1]} = {wine_results[2] * 100}%") diff --git a/ch8/board.py b/ch8/board.py new file mode 100644 index 0000000..30f9abf --- /dev/null +++ b/ch8/board.py @@ -0,0 +1,55 @@ +# board.py +# From Classic Computer Science Problems in Python Chapter 8 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import NewType, List +from abc import ABC, abstractmethod + +Move = NewType('Move', int) + + +class Piece: + @property + def opposite(self) -> Piece: + raise NotImplementedError("서브 클래스에 의해 구현되어야합니다.") + + +class Board(ABC): + @property + @abstractmethod + def turn(self) -> Piece: + ... + + @abstractmethod + def move(self, location: Move) -> Board: + ... + + @property + @abstractmethod + def legal_moves(self) -> List[Move]: + ... + + @property + @abstractmethod + def is_win(self) -> bool: + ... + + @property + def is_draw(self) -> bool: + return (not self.is_win) and (len(self.legal_moves) == 0) + + @abstractmethod + def evaluate(self, player: Piece) -> float: + ... diff --git a/ch8/connectfour.py b/ch8/connectfour.py new file mode 100644 index 0000000..a3dac44 --- /dev/null +++ b/ch8/connectfour.py @@ -0,0 +1,183 @@ +# connectfour.py +# From Classic Computer Science Problems in Python Chapter 8 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import List, Optional, Tuple +from enum import Enum +from board import Piece, Board, Move + + +class C4Piece(Piece, Enum): + B = "B" + R = "R" + E = " " # 빈 공간 + + @property + def opposite(self) -> C4Piece: + if self == C4Piece.B: + return C4Piece.R + elif self == C4Piece.R: + return C4Piece.B + else: + return C4Piece.E + + def __str__(self) -> str: + return self.value + + +def generate_segments(num_columns: int, num_rows: int, segment_length: int) -> List[List[Tuple[int, int]]]: + segments: List[List[Tuple[int, int]]] = [] + # 수직 세그먼트를 생성한다. + for c in range(num_columns): + for r in range(num_rows - segment_length + 1): + segment: List[Tuple[int, int]] = [] + for t in range(segment_length): + segment.append((c, r + t)) + segments.append(segment) + + # 수평 세그먼트를 생성한다. + for c in range(num_columns - segment_length + 1): + for r in range(num_rows): + segment = [] + for t in range(segment_length): + segment.append((c + t, r)) + segments.append(segment) + + # 왼쪽 아래에서 오른쪽 위 대각선의 세그먼트를 생성한다. + for c in range(num_columns - segment_length + 1): + for r in range(num_rows - segment_length + 1): + segment = [] + for t in range(segment_length): + segment.append((c + t, r + t)) + segments.append(segment) + + # 왼쪽 위에서 오른쪽 아래 대각선의 세그먼트를 생성한다. + for c in range(num_columns - segment_length + 1): + for r in range(segment_length - 1, num_rows): + segment = [] + for t in range(segment_length): + segment.append((c + t, r - t)) + segments.append(segment) + return segments + + +class C4Board(Board): + NUM_ROWS: int = 6 + NUM_COLUMNS: int = 7 + SEGMENT_LENGTH: int = 4 + SEGMENTS: List[List[Tuple[int, int]]] = generate_segments( + NUM_COLUMNS, NUM_ROWS, SEGMENT_LENGTH) + + class Column: + def __init__(self) -> None: + self._container: List[C4Piece] = [] + + @property + def full(self) -> bool: + return len(self._container) == C4Board.NUM_ROWS + + def push(self, item: C4Piece) -> None: + if self.full: + raise OverflowError("격자 열 범위에 벗어날 수 없습니다") + self._container.append(item) + + def __getitem__(self, index: int) -> C4Piece: + if index > len(self._container) - 1: + return C4Piece.E + return self._container[index] + + def __repr__(self) -> str: + return repr(self._container) + + def copy(self) -> C4Board.Column: + temp: C4Board.Column = C4Board.Column() + temp._container = self._container.copy() + return temp + + def __init__(self, position: Optional[List[C4Board.Column]] = None, turn: C4Piece = C4Piece.B) -> None: + if position is None: + self.position: List[C4Board.Column] = [ + C4Board.Column() for _ in range(C4Board.NUM_COLUMNS)] + else: + self.position = position + self._turn: C4Piece = turn + + @property + def turn(self) -> Piece: + return self._turn + + def move(self, location: Move) -> Board: + temp_position: List[C4Board.Column] = self.position.copy() + for c in range(C4Board.NUM_COLUMNS): + temp_position[c] = self.position[c].copy() + temp_position[location].push(self._turn) + return C4Board(temp_position, self._turn.opposite) + + @property + def legal_moves(self) -> List[Move]: + return [Move(c) for c in range(C4Board.NUM_COLUMNS) if not self.position[c].full] + + # Returns the count of black & red pieces in a segment + def _count_segment(self, segment: List[Tuple[int, int]]) -> Tuple[int, int]: + black_count: int = 0 + red_count: int = 0 + for column, row in segment: + if self.position[column][row] == C4Piece.B: + black_count += 1 + elif self.position[column][row] == C4Piece.R: + red_count += 1 + return black_count, red_count + + @property + def is_win(self) -> bool: + for segment in C4Board.SEGMENTS: + black_count, red_count = self._count_segment(segment) + if black_count == 4 or red_count == 4: + return True + return False + + def _evaluate_segment(self, segment: List[Tuple[int, int]], player: Piece) -> float: + black_count, red_count = self._count_segment(segment) + if red_count > 0 and black_count > 0: + return 0 # mixed segments are neutral + count: int = max(red_count, black_count) + score: float = 0 + if count == 2: + score = 1 + elif count == 3: + score = 100 + elif count == 4: + score = 1000000 + color: C4Piece = C4Piece.B + if red_count > black_count: + color = C4Piece.R + if color != player: + return -score + return score + + def evaluate(self, player: Piece) -> float: + total: float = 0 + for segment in C4Board.SEGMENTS: + total += self._evaluate_segment(segment, player) + return total + + def __repr__(self) -> str: + display: str = "" + for r in reversed(range(C4Board.NUM_ROWS)): + display += "|" + for c in range(C4Board.NUM_COLUMNS): + display += f"{self.position[c][r]}" + "|" + display += "\n" + return display diff --git a/ch8/connectfour_ai.py b/ch8/connectfour_ai.py new file mode 100644 index 0000000..942ef0b --- /dev/null +++ b/ch8/connectfour_ai.py @@ -0,0 +1,51 @@ +# tictactoe_ai.py +# From Classic Computer Science Problems in Python Chapter 8 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from minimax import find_best_move +from connectfour import C4Board +from board import Move, Board + +board: Board = C4Board() + + +def get_player_move() -> Move: + player_move: Move = Move(-1) + while player_move not in board.legal_moves: + play: int = int(input("이동할 열 위치를 입력하세요 (0-6): ")) + player_move = Move(play) + return player_move + + +if __name__ == "__main__": + # 메인 게임 루프 + while True: + human_move: Move = get_player_move() + board = board.move(human_move) + if board.is_win: + print("당신이 이겼습니다!") + break + elif board.is_draw: + print("비겼습니다!") + break + computer_move: Move = find_best_move(board, 5) + print(f"컴퓨터가 {computer_move}열을 선택했습니다.") + board = board.move(computer_move) + print(board) + if board.is_win: + print("컴퓨터가 이겼습니다!") + break + elif board.is_draw: + print("비겼습니다!") + break diff --git a/ch8/minimax.py b/ch8/minimax.py new file mode 100644 index 0000000..41c898c --- /dev/null +++ b/ch8/minimax.py @@ -0,0 +1,77 @@ +# minimax.py +# From Classic Computer Science Problems in Python Chapter 8 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from board import Piece, Board, Move + + +# 게임 플레이어의 가능한 최선의 움직임을 찾는다. +def minimax(board: Board, maximizing: bool, original_player: Piece, max_depth: int = 8) -> float: + # 기저 조건 - 게임 종료 위치 또는 최대 깊이에 도달한다. + if board.is_win or board.is_draw or max_depth == 0: + return board.evaluate(original_player) + + # 재귀 조건 - 이익을 극대화하거나 상대방의 이익을 최소화한다. + if maximizing: + best_eval: float = float("-inf") # 낮은 시작 점수 + for move in board.legal_moves: + result: float = minimax(board.move( + move), False, original_player, max_depth - 1) + best_eval = max(result, best_eval) # 가장 높은 평가를 받은 위치로 움직인다. + return best_eval + else: # minimizing (최소화) + worst_eval: float = float("inf") # 높은 시작 점수 + for move in board.legal_moves: + result = minimax(board.move(move), True, + original_player, max_depth - 1) + worst_eval = min(result, worst_eval) # 가장 낮은 평가를 받은 위치로 움직인다. + return worst_eval + + +def alphabeta(board: Board, maximizing: bool, original_player: Piece, max_depth: int = 8, alpha: float = float("-inf"), beta: float = float("inf")) -> float: + # 기저 조건 - 종료 위치 또는 최대 깊이에 도달한다. + if board.is_win or board.is_draw or max_depth == 0: + return board.evaluate(original_player) + + # 재귀 조건 - 자신의 이익을 극대화하거나 상대방의 이익을 극소화한다. + if maximizing: + for move in board.legal_moves: + result: float = alphabeta(board.move( + move), False, original_player, max_depth - 1, alpha, beta) + alpha = max(result, alpha) + if beta <= alpha: + break + return alpha + else: # minimizing + for move in board.legal_moves: + result = alphabeta(board.move(move), True, + original_player, max_depth - 1, alpha, beta) + beta = min(result, beta) + if beta <= alpha: + break + return beta + + +# 최대 깊이(max_depth) 전까지 가장 최선의 움직임을 찾는다. +def find_best_move(board: Board, max_depth: int = 8) -> Move: + best_eval: float = float("-inf") + best_move: Move = Move(-1) + for move in board.legal_moves: + result: float = alphabeta(board.move( + move), False, board.turn, max_depth) + if result > best_eval: + best_eval = result + best_move = move + return best_move diff --git a/ch8/tictactoe.py b/ch8/tictactoe.py new file mode 100644 index 0000000..6a24f94 --- /dev/null +++ b/ch8/tictactoe.py @@ -0,0 +1,83 @@ +# tictactoe.py +# From Classic Computer Science Problems in Python Chapter 8 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations +from typing import List +from enum import Enum +from board import Piece, Board, Move + + +class TTTPiece(Piece, Enum): + X = "X" + O = "O" + E = " " # 빈 공간 + + @property + def opposite(self) -> TTTPiece: + if self == TTTPiece.X: + return TTTPiece.O + elif self == TTTPiece.O: + return TTTPiece.X + else: + return TTTPiece.E + + def __str__(self) -> str: + return self.value + + +class TTTBoard(Board): + def __init__(self, position: List[TTTPiece] = [TTTPiece.E] * 9, turn: TTTPiece = TTTPiece.X) -> None: + self.position: List[TTTPiece] = position + self._turn: TTTPiece = turn + + @property + def turn(self) -> Piece: + return self._turn + + def move(self, location: Move) -> Board: + temp_position: List[TTTPiece] = self.position.copy() + temp_position[location] = self._turn + return TTTBoard(temp_position, self._turn.opposite) + + @property + def legal_moves(self) -> List[Move]: + return [Move(l) for l in range(len(self.position)) if self.position[l] == TTTPiece.E] + + @property + def is_win(self) -> bool: + # 3행과 3열, 2개의 대각선을 확인한다. + return self.position[0] == self.position[1] and self.position[0] == self.position[2] and self.position[0] != TTTPiece.E or \ + self.position[3] == self.position[4] and self.position[3] == self.position[5] and self.position[3] != TTTPiece.E or \ + self.position[6] == self.position[7] and self.position[6] == self.position[8] and self.position[6] != TTTPiece.E or \ + self.position[0] == self.position[3] and self.position[0] == self.position[6] and self.position[0] != TTTPiece.E or \ + self.position[1] == self.position[4] and self.position[1] == self.position[7] and self.position[1] != TTTPiece.E or \ + self.position[2] == self.position[5] and self.position[2] == self.position[8] and self.position[2] != TTTPiece.E or \ + self.position[0] == self.position[4] and self.position[0] == self.position[8] and self.position[0] != TTTPiece.E or \ + self.position[2] == self.position[4] and self.position[2] == self.position[6] and self.position[2] != TTTPiece.E + + def evaluate(self, player: Piece) -> float: + if self.is_win and self.turn == player: + return -1 + elif self.is_win and self.turn != player: + return 1 + else: + return 0 + + def __repr__(self) -> str: + return f"""{self.position[0]}|{self.position[1]}|{self.position[2]} +----- +{self.position[3]}|{self.position[4]}|{self.position[5]} +----- +{self.position[6]}|{self.position[7]}|{self.position[8]}""" diff --git a/ch8/tictactoe_ai.py b/ch8/tictactoe_ai.py new file mode 100644 index 0000000..59be3fb --- /dev/null +++ b/ch8/tictactoe_ai.py @@ -0,0 +1,51 @@ +# tictactoe_ai.py +# From Classic Computer Science Problems in Python Chapter 8 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from minimax import find_best_move +from tictactoe import TTTBoard +from board import Move, Board + +board: Board = TTTBoard() + + +def get_player_move() -> Move: + player_move: Move = Move(-1) + while player_move not in board.legal_moves: + play: int = int(input("이동할 위치를 입력하세요 (0-8): ")) + player_move = Move(play) + return player_move + + +if __name__ == "__main__": + # 메인 게임 루프 + while True: + human_move: Move = get_player_move() + board = board.move(human_move) + if board.is_win: + print("당신이 이겼습니다!") + break + elif board.is_draw: + print("비겼습니다!") + break + computer_move: Move = find_best_move(board) + print(f"컴퓨터가 {computer_move}(으)로 이동했습니다.") + board = board.move(computer_move) + print(board) + if board.is_win: + print("컴퓨터가 이겼습니다!") + break + elif board.is_draw: + print("비겼습니다!") + break diff --git a/ch8/tictactoe_tests.py b/ch8/tictactoe_tests.py new file mode 100644 index 0000000..c0f53a0 --- /dev/null +++ b/ch8/tictactoe_tests.py @@ -0,0 +1,53 @@ +# tictactoe_tests.py +# From Classic Computer Science Problems in Python Chapter 8 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import unittest +from typing import List +from minimax import find_best_move +from tictactoe import TTTPiece, TTTBoard +from board import Move + + +class TTTMinimaxTestCase(unittest.TestCase): + def test_easy_position(self): + # 다음 턴에 X가 이겨야한다. + to_win_easy_position: List[TTTPiece] = [TTTPiece.X, TTTPiece.O, TTTPiece.X, + TTTPiece.X, TTTPiece.E, TTTPiece.O, + TTTPiece.E, TTTPiece.E, TTTPiece.O] + test_board1: TTTBoard = TTTBoard(to_win_easy_position, TTTPiece.X) + answer1: Move = find_best_move(test_board1) + self.assertEqual(answer1, 6) + + def test_block_position(self): + # O의 승리를 막아야한다. + to_block_position: List[TTTPiece] = [TTTPiece.X, TTTPiece.E, TTTPiece.E, + TTTPiece.E, TTTPiece.E, TTTPiece.O, + TTTPiece.E, TTTPiece.X, TTTPiece.O] + test_board2: TTTBoard = TTTBoard(to_block_position, TTTPiece.X) + answer2: Move = find_best_move(test_board2) + self.assertEqual(answer2, 2) + + def test_hard_position(self): + # 남은 두 턴을 고려해서 최선의 이동을 찾는다. + to_win_hard_position: List[TTTPiece] = [TTTPiece.X, TTTPiece.E, TTTPiece.E, + TTTPiece.E, TTTPiece.E, TTTPiece.O, + TTTPiece.O, TTTPiece.X, TTTPiece.E] + test_board3: TTTBoard = TTTBoard(to_win_hard_position, TTTPiece.X) + answer3: Move = find_best_move(test_board3) + self.assertEqual(answer3, 1) + + +if __name__ == '__main__': + unittest.main() diff --git a/ch9/knapsack.py b/ch9/knapsack.py new file mode 100644 index 0000000..14c39af --- /dev/null +++ b/ch9/knapsack.py @@ -0,0 +1,63 @@ +# knapsack.py +# From Classic Computer Science Problems in Python Chapter 9 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import NamedTuple, List + + +class Item(NamedTuple): + name: str + weight: int + value: float + + +def knapsack(items: List[Item], max_capacity: int) -> List[Item]: + # 동적 프로그래밍 표를 작성한다. + table: List[List[float]] = [ + [0.0 for _ in range(max_capacity + 1)] for _ in range(len(items) + 1)] + for i, item in enumerate(items): + for capacity in range(1, max_capacity + 1): + previous_items_value: float = table[i][capacity] + if capacity >= item.weight: # 물건이 용량에 맞는 경우 + value_freeing_weight_for_item: float = table[i][capacity - item.weight] + # 이전 물건보다 더 가치가 있는 경우에만 물건을 넣는다. + table[i + 1][capacity] = max(value_freeing_weight_for_item + + item.value, previous_items_value) + else: # 용량에 맞지 않아서 물건을 넣을 수 없다. + table[i + 1][capacity] = previous_items_value + # 표에서 최상의 결과를 구한다. + solution: List[Item] = [] + capacity = max_capacity + for i in range(len(items), 0, -1): # 거꾸로 반복한다. + # 배낭에 이 물건이 있는가? + if table[i - 1][capacity] != table[i][capacity]: + solution.append(items[i - 1]) + # 용량에서 물건 무게를 뺀다. + capacity -= items[i - 1].weight + return solution + + +if __name__ == "__main__": + items: List[Item] = [Item("TV", 50, 500), + Item("촛대", 2, 300), + Item("오디오", 35, 400), + Item("노트북", 3, 1000), + Item("식량", 15, 50), + Item("옷", 20, 800), + Item("보석", 1, 4000), + Item("책", 100, 300), + Item("프린터", 18, 30), + Item("냉장고", 200, 700), + Item("그림", 10, 1000)] + print(knapsack(items, 75)) diff --git a/ch9/phone_number_mnemonics.py b/ch9/phone_number_mnemonics.py new file mode 100644 index 0000000..768d8e6 --- /dev/null +++ b/ch9/phone_number_mnemonics.py @@ -0,0 +1,42 @@ +# phone_number_mnemonics.py +# From Classic Computer Science Problems in Python Chapter 9 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict, Tuple, Iterable, List +from itertools import product + +phone_mapping: Dict[str, Tuple[str, ...]] = {"1": ("1",), + "2": ("a", "b", "c"), + "3": ("d", "e", "f"), + "4": ("g", "h", "i"), + "5": ("j", "k", "l"), + "6": ("m", "n", "o"), + "7": ("p", "q", "r", "s"), + "8": ("t", "u", "v"), + "9": ("w", "x", "y", "z"), + "0": ("0",)} + + +def possible_mnemonics(phone_number: str) -> Iterable[Tuple[str, ...]]: + letter_tuples: List[Tuple[str, ...]] = [] + for digit in phone_number: + letter_tuples.append(phone_mapping.get(digit, (digit,))) + return product(*letter_tuples) + + +if __name__ == "__main__": + phone_number: str = input("전화번호를 입력해주세요: ") + print("가능한 니모닉 목록: ") + for mnemonic in possible_mnemonics(phone_number): + print("".join(mnemonic)) diff --git a/ch9/tsp.py b/ch9/tsp.py new file mode 100644 index 0000000..27b93b6 --- /dev/null +++ b/ch9/tsp.py @@ -0,0 +1,63 @@ +# tsp.py +# From Classic Computer Science Problems in Python Chapter 9 +# Copyright 2018 David Kopec +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from typing import Dict, List, Iterable, Tuple +from itertools import permutations + +vt_distances: Dict[str, Dict[str, int]] = { + "러틀랜드": + {"벌링턴": 67, + "화이트 리버 정션": 46, + "베닝턴": 55, + "브래틀보로": 75}, + "벌링턴": + {"러틀랜드": 67, + "화이트 리버 정션": 91, + "베닝턴": 122, + "브래틀보로": 153}, + "화이트 리버 정션": + {"러틀랜드": 46, + "벌링턴": 91, + "베닝턴": 98, + "브래틀보로": 65}, + "베닝턴": + {"러틀랜드": 55, + "벌링턴": 122, + "화이트 리버 정션": 98, + "브래틀보로": 40}, + "브래틀보로": + {"러틀랜드": 75, + "벌링턴": 153, + "화이트 리버 정션": 65, + "베닝턴": 40} +} + +vt_cities: Iterable[str] = vt_distances.keys() +city_permutations: Iterable[Tuple[str, ...]] = permutations(vt_cities) +tsp_paths: List[Tuple[str, ...]] = [c + (c[0],) for c in city_permutations] + +if __name__ == "__main__": + best_path: Tuple[str, ...] + min_distance: int = 99999999999 # 높은 숫자로 설정한다. + for path in tsp_paths: + distance: int = 0 + last: str = path[0] + for next in path[1:]: + distance += vt_distances[last][next] + last = next + if distance < min_distance: + min_distance = distance + best_path = path + print(f"최단 경로는 {best_path} 이고, {min_distance} 마일입니다.") From 0928af829f0652cda17230b3e53b1f7964200178 Mon Sep 17 00:00:00 2001 From: Astin CHOI Date: Tue, 3 Dec 2019 07:52:04 +0900 Subject: [PATCH 2/3] readme for koreans --- README.md | 23 +++++++++-------------- _README_old.md | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+), 14 deletions(-) create mode 100644 _README_old.md diff --git a/README.md b/README.md index 8ee2be4..8d19cd4 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,18 @@ -# Classic Computer Science Problems in Python -This repository contains source code to accompany the book *Classic Computer Science Problems in Python* by David Kopec. You will find the source organized by chapter. **As you read the book, each code listing contains a file name that corresponds to a file in this repository.** +## ✨ 고전 컴퓨터 알고리즘 인 파이썬 ✨ +원제: Classic Computer Science Problems in Python -![Classic Computer Science Problems in Python Cover](cover.jpg) +

+ +

-## Get the Book -- [Manning](https://www.manning.com/books/classic-computer-science-problems-in-python) the publisher sells both hard copy and DRM-free eBook editions -- [Amazon](https://amzn.to/2ui96Op) if you buy the hard copy from Amazon, it will come with a way to download the eBook for free from the publisher +* 🛒 [예스24](http://www.yes24.com/Product/Goods/83532292) -## Versioning and Packages -The source code in this repository requires Python 3.7 and installation of the [typing_extensions](https://github.com/python/typing/tree/master/typing_extensions) package. Due to its extensive use of Python 3.7 features (data classes, advanced type hints, etc.), most of the source code will not work with earlier versions of Python. You can install the `typing_extensions` package with `pip3 install typing_extensions` or `pip install typing_extensions` depending on your Python/pip setup. +* 📚 [원서 글](https://freecontent.manning.com/constraint-satisfaction-problems-in-python/) -## Questions about the Book -You can find general questions and descriptive information about the book on the [Classic Computer Science Problems](https://classicproblems.com/) website. Also, feel free to reach out to me on Twitter, [@davekopec](https://twitter.com/davekopec). If you think you found an error in the source code, please open an issue up here on GitHub. +* 위 폴더에서 Chapter**X**로 되어있는게 원서 코드, ch**X**로 되어있는게 번역된 코드입니다. -## Free Content Based on the Book -- [Article: Constraint-Satisfaction Problems in Python](https://freecontent.manning.com/constraint-satisfaction-problems-in-python/) +* 이 코드는 파이썬 3.7 혹은 그 이상의 버전에서 실행가능합니다. 파이썬 3.7의 경우 [typing_extensions](https://github.com/python/typing/tree/master/typing_extensions) 패키지가 필요합니다. 파이썬 3.7에서 확장된 기능(데이터 클래스, 고급 타입 힌트 등) 때문에 이전 버전에는 실행되지 않습니다. `typing_extensions` 패키지 설치의 경우 파이썬 및 pip의 환경설정에 따라서 `pip3 install typing_extensions` 또는 `pip install typing_extensions` 설치가능합니다. ## License All of the source code in this repository is released under the Apache License version 2.0. See `LICENSE`. -## Other Books and Languages -This is the second book in the Classic Computer Science Problems series by David Kopec and published by Manning. It aims to teach classic computer science problems in a Pythonic way. You may also want to checkout the first book in the series, *Classic Computer Science Problems in Swift*, which covers most of the same problems in a more Swifty way. You can check out the repository for that book on [GitHub as well](https://github.com/davecom/ClassicComputerScienceProblemsInSwift). A reader has also reimplemented the first five chapters of the book [in C++](https://github.com/araya-andres/classic_computer_sci). diff --git a/_README_old.md b/_README_old.md new file mode 100644 index 0000000..8ee2be4 --- /dev/null +++ b/_README_old.md @@ -0,0 +1,23 @@ +# Classic Computer Science Problems in Python +This repository contains source code to accompany the book *Classic Computer Science Problems in Python* by David Kopec. You will find the source organized by chapter. **As you read the book, each code listing contains a file name that corresponds to a file in this repository.** + +![Classic Computer Science Problems in Python Cover](cover.jpg) + +## Get the Book +- [Manning](https://www.manning.com/books/classic-computer-science-problems-in-python) the publisher sells both hard copy and DRM-free eBook editions +- [Amazon](https://amzn.to/2ui96Op) if you buy the hard copy from Amazon, it will come with a way to download the eBook for free from the publisher + +## Versioning and Packages +The source code in this repository requires Python 3.7 and installation of the [typing_extensions](https://github.com/python/typing/tree/master/typing_extensions) package. Due to its extensive use of Python 3.7 features (data classes, advanced type hints, etc.), most of the source code will not work with earlier versions of Python. You can install the `typing_extensions` package with `pip3 install typing_extensions` or `pip install typing_extensions` depending on your Python/pip setup. + +## Questions about the Book +You can find general questions and descriptive information about the book on the [Classic Computer Science Problems](https://classicproblems.com/) website. Also, feel free to reach out to me on Twitter, [@davekopec](https://twitter.com/davekopec). If you think you found an error in the source code, please open an issue up here on GitHub. + +## Free Content Based on the Book +- [Article: Constraint-Satisfaction Problems in Python](https://freecontent.manning.com/constraint-satisfaction-problems-in-python/) + +## License +All of the source code in this repository is released under the Apache License version 2.0. See `LICENSE`. + +## Other Books and Languages +This is the second book in the Classic Computer Science Problems series by David Kopec and published by Manning. It aims to teach classic computer science problems in a Pythonic way. You may also want to checkout the first book in the series, *Classic Computer Science Problems in Swift*, which covers most of the same problems in a more Swifty way. You can check out the repository for that book on [GitHub as well](https://github.com/davecom/ClassicComputerScienceProblemsInSwift). A reader has also reimplemented the first five chapters of the book [in C++](https://github.com/araya-andres/classic_computer_sci). From 7a0bc7be803940264426fef935a1c70652d51cab Mon Sep 17 00:00:00 2001 From: Astin CHOI Date: Tue, 3 Dec 2019 07:52:56 +0900 Subject: [PATCH 3/3] readme for Koreans --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8d19cd4..2a1e86e 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ * 📚 [원서 글](https://freecontent.manning.com/constraint-satisfaction-problems-in-python/) -* 위 폴더에서 Chapter**X**로 되어있는게 원서 코드, ch**X**로 되어있는게 번역된 코드입니다. +* 위 폴더에서 Chapter**X**가 원서 코드, ch**X**가 번역된 코드입니다. * 이 코드는 파이썬 3.7 혹은 그 이상의 버전에서 실행가능합니다. 파이썬 3.7의 경우 [typing_extensions](https://github.com/python/typing/tree/master/typing_extensions) 패키지가 필요합니다. 파이썬 3.7에서 확장된 기능(데이터 클래스, 고급 타입 힌트 등) 때문에 이전 버전에는 실행되지 않습니다. `typing_extensions` 패키지 설치의 경우 파이썬 및 pip의 환경설정에 따라서 `pip3 install typing_extensions` 또는 `pip install typing_extensions` 설치가능합니다.