diff --git a/graphy.py b/graphy.py new file mode 100644 index 000000000..06839794d --- /dev/null +++ b/graphy.py @@ -0,0 +1,111 @@ + + + +# Singly Linked List +from typing import no_type_check_decorator + + +class ListNode: + def __init__(self, value): + self.value = value + self.next = None + + # self.prev if DLL + +# LL traversal +current = node +while current is not None: + current = current.next + + +class BinarySearchTreeNode: + def __init__(self, value): + self.value = value + self.left = None + self.right = None + +# BST Traversal +## BFT or DFT +### Stack, Queue + +while node is not None: + recurse(self.left) + recurse(self.right) + + +class GraphNode: + def __init__(self, value): + self.value = 'B' + + # options: dictionary, array, set + # B's connections + self.connections = set('A', 'C', 'D') + + # A's connections + self.connections = set('B') + + # D's connections - it's a zero - Billy no mates! + self.connections = set() + + +# outbound vs inbound conections? + +# Grpah terminology for 2-way vs 1-way connections +## undirected graph vs directed graph +## (FB, LinkedIn) vs (instagram, Twitter, TikTok) + +## Graph Traversals + +## DFT, stack +### check every node once, check every connection once + +# make a stack +stack = Stack('A', 'B') +# make a set to track visited +visited = set('A', 'B', 'D', 'C') + +# put the start node into the stack + +# while the stack isn't empty + +## pop off the top of the stack, this is the current node +current_node = 'B' +## check if we have visited this node +### if not, add it to our visited set +### and add each of it's neighbors to our stack +#### started with just A as current node and moved on ^^^^ + +## Time complexity? O(n) +## how many times do we visit each node? once +### how many times did we check each connection? once + +## O(number of nodes + number of connections) +### O(n + m) +## so linear! + +# BFT, Queue +# ---->>>> +q = Queue('D', 'C') + +# make a set to track visited +visited = set('A', 'B') +# enqueue the start node +# while our queue isn't empty +## dequeue from front of line, this is our current node +current_node = 'B' # then move to 'C', add to visited, get neighbors, add to queue +## check if we've visited the node yet +### if not, add to visited +### get it's neighbors, for each add to queue +neighbors = set('C', 'D') +## RIPPLES! A) B) CD) EFG) ')' is the ripple + +# Time complexity? O(n) +## visit every node once, visit every edge once +## O(n + m) +## O(node + edge) +## linear as well! + +# DFT vs. BFT +## same time complexity, each just as fast +## DFT can be done recursively +## BFT can find shorter path diff --git a/projects/adventure/adv.py b/projects/adventure/adv.py index 8bc540b5e..915351249 100644 --- a/projects/adventure/adv.py +++ b/projects/adventure/adv.py @@ -29,6 +29,146 @@ # traversal_path = ['n', 'n'] traversal_path = [] +def traverse(player): + # create a dict for storage + d = {} + # keep track of visited rooms + visited = [] + # get the id of the starting room + starting_room = world.starting_room.id + # create a dict entry for starting room + d[starting_room] = {} + # get starting room exits + exits = world.starting_room.get_exits() + # for each exit, create a dict key for that room with '?' value + for exit in exits: + d[starting_room][exit] = '?' + # choose a random direction to start in + direction = random.choice(exits) + # get next room id + next_room = player.current_room.get_room_in_direction(direction).id + # change the starting room '?' value to room id for that exit + d[starting_room][direction] = next_room + # add both rooms to visited + visited.append([starting_room, next_room]) + # create a stack and queue + stack = [[next_room, direction]] + queue = [] + # while the dictionary does not have as many entries as rooms + while len(d) < len(room_graph): + # while we have a stack to go through + while stack: + # pop off top of stack (curr includes a room + # and the direction it takes to get there) + curr = stack.pop() + # append direction and then travel + player.travel(curr[-1]) + traversal_path.append(curr[-1]) + # get exit rooms + exits = player.current_room.get_exits() + # if room not in dictionary, add it + if player.current_room.id not in d: + d[player.current_room.id] = {} + for exit in exits: + d[player.current_room.id][exit] = '?' + # get list of unvisited rooms (this area could be condensed) + list_of_rooms = {} + for exit in exits: + list_of_rooms[exit] = player.current_room.get_room_in_direction(exit).id + unvisited_exits = [] + unvisited_exits.append([k for k, v in list_of_rooms.items() if v not in visited]) + # if unvisited rooms + try: + unvisited_exits = unvisited_exits[0][0] + # get direction for each unvisited room + for ex in unvisited_exits: + direction = ex[0] + # add the room to visited + visited.append(player.current_room.get_room_in_direction(direction).id) + # add room and direction to stack + stack.append([player.current_room.id, direction]) + # add room and direction to dictionary + d[player.current_room.id][direction] = player.current_room.get_room_in_direction(direction).id + # if no unvisited rooms (this are should be condensed), find an exit with a question mark + except: + # empty the stack + stack = [] + break + # add current room to queue + queue.append([player.current_room.id]) + min_path_length = 999 + # while we have a queue + while queue: + # pop off top path + path = queue.pop(0) + # get the last node in the path + last_in_path = path[-1] + # if we found a room with a question mark + if '?' in d[last_in_path].values(): + # use the path to travel to the next room and add to traversal path and visited + for i, room in enumerate(path[:-1]): + direction = list(d[room].keys())[list(d[room].values()).index(path[i+1])] + traversal_path.append(direction) + player.travel(direction) + visited.append(room) # last one will be appended in stack + # first, find if any unvisited rooms and get list + exits = player.current_room.get_exits() + list_of_rooms = {} + for exit in exits: + list_of_rooms[exit] = player.current_room.get_room_in_direction(exit).id + # see if there are unvisited rooms before choosing one with a '?' + # get list of unvisited rooms + unvisited_exits = [] + unvisited_exits.append([k for k, v in list_of_rooms.items() if v not in visited]) + # try getting unvisited rooms + try: + unvisited_exits = unvisited_exits[0][0] + # if multiple unvisited rooms + if len(unvisited_exits) > 1: + # choose one at random and travel, adding to visited and stack and dict + random_exit = random.choice(unvisited_exits) + chosen_exit_room = player.current_room.get_room_in_direction(random_exit).id + visited.append(chosen_exit_room) + d[player.current_room.id][random_exit] = chosen_exit_room + stack.append([chosen_exit_room, random_exit]) + queue = [] + break + # if one unvisited room + elif len(unvisited_exits) == 1: + #travel there, adding it to visited, stack and dict + exit_room = player.current_room.get_room_in_direction(unvisited_exits).id + visited.append(exit_room) + d[player.current_room.id][unvisited_exits] = exit_room + stack.append([exit_room, unvisited_exits]) + # empty the queue + queue = [] + # if no unvisited rooms + except: + # find rooms whose exits have a value of '? ' + unknown_exits = [key for (key, value) in d[player.current_room.id].items() if value == '?'] + # choose one at random , add it to visited, dict, and stack + random_direction = random.choice(unknown_exits) + chosen = player.current_room.get_room_in_direction(random_direction).id + visited.append(chosen) + d[player.current_room.id][random_direction] = chosen + stack.append([chosen, random_direction]) + # empty the queue + queue = [] + # if this path still contains no rooms with '?' values in its exits + else: + # get exits + exits = world.rooms[last_in_path].get_exits() + # for each exit + for exit in exits: + # create a new path, appending the room leading off the exit + exit_id = world.rooms[last_in_path].get_room_in_direction(exit).id + if exit_id not in path: + new_path = list(path) + new_path.append(exit_id) + queue.append(new_path) + return traversal_path + +traverse(player) # TRAVERSAL TEST @@ -51,12 +191,12 @@ ####### # UNCOMMENT TO WALK AROUND ####### -player.current_room.print_room_description(player) -while True: - cmds = input("-> ").lower().split(" ") - if cmds[0] in ["n", "s", "e", "w"]: - player.travel(cmds[0], True) - elif cmds[0] == "q": - break - else: - print("I did not understand that command.") +#player.current_room.print_room_description(player) +#while True: +# cmds = input("-> ").lower().split(" ") +# if cmds[0] in ["n", "s", "e", "w"]: +# player.travel(cmds[0], True) +# elif cmds[0] == "q": +# break +# else: +# print("I did not understand that command.") diff --git a/projects/ancestor/ancestor.py b/projects/ancestor/ancestor.py index 3bd003098..cffacffb6 100644 --- a/projects/ancestor/ancestor.py +++ b/projects/ancestor/ancestor.py @@ -1,3 +1,47 @@ def earliest_ancestor(ancestors, starting_node): - pass \ No newline at end of file + # create a hash table called vertices to store ancestors + vertices = {} + # add all ancestors to vertices dict + for ancestor in ancestors: + vertices[ancestor[1]] = set() + vertices[ancestor[0]] = set() + # add all ancestors' edges (ancestors) + for ancestor in ancestors: + vertices[ancestor[1]].add(ancestor[0]) + # if starting vertex has no ancestors + if not vertices[starting_node]: + return -1 + # if it has ancestors + else: + # create an empty list for stack + stack = [] + # keep track of depth + depth = {starting_node:0} + # add the starting vertex to the stack + stack.append(starting_node) + # while there are ancestors in the stack + while stack: + # pop off the most recently added vertex + current_vert = stack.pop() + # get a list of its ancestors + edges = vertices[current_vert] + # loop through vertex's edges + for edge in edges: + # track the edge's depth as being one greater than the current node's + depth[edge] = depth[current_vert] + 1 + # add edge to top of the stack + stack.append(edge) + # return the max depth(s) + max_value = max(depth.values()) + # return the minimum key + return min([k for k, v in depth.items() if v == max_value]) + + +if __name__ == '__main__': + + # ancestors + test_ancestors = [(1, 3), (2, 3), (3, 6), (5, 6), (5, 7), (4, 5), + (4, 8), (8, 9), (11, 8), (10, 1)] + + print(earliest_ancestor(test_ancestors, 9)) \ No newline at end of file diff --git a/projects/count_islands.py b/projects/count_islands.py new file mode 100644 index 000000000..cac0089c4 --- /dev/null +++ b/projects/count_islands.py @@ -0,0 +1,122 @@ +""" +Write a function that takes a 2D binary array and returns the number of 1 islands. + +An island consists of 1s that are connected to the north, south, east or west. For example: + +islands = [[0, 1, 0, 1, 0], + [1, 1, 0, 1, 1], + [0, 0, 1, 0, 0], + [1, 0, 1, 0, 0], + [1, 1, 0, 0, 0]] + +island_counter(islands) # returns 4 + +1. Describe the problem in graphs terms +-- What are our nodes? +--- each (x, y) in the array +---- if it's a 1? or we only operate on the 1s + +-- What are our edges? when are nodes connected? +--- connected when a direct neighbor is a 1 + +-- Island in graphs terms? +--- connected component + +2. Build your graph or write getNeighbors + +-- go through the array node by node, then check north, east, south, west +neighbors are like.. index - 1[same place], index[-1 place], index[+1 place], index + 1[same place] + +3. Choose your fighter(s) + +-- How to count the number of connected components? +-- get to each island, traverse the component, then continue from where you were +--- keep track of which nodes have already been traversed + + +-- go through the array node by node, then check north, east, south, west +""" +islands = [[0, 1, 0, 1, 0], + [1, 1, 0, 1, 1], + [0, 0, 1, 0, 0], + [1, 0, 1, 0, 0], + [1, 1, 0, 0, 0]] + +big_islands = [[1, 0, 0, 1, 1, 0, 1, 1, 0, 1], + [0, 0, 1, 1, 0, 1, 0, 0, 0, 0], + [0, 1, 1, 1, 0, 0, 0, 1, 0, 1], + [0, 0, 1, 0, 0, 1, 0, 0, 1, 1], + [0, 0, 1, 1, 0, 1, 0, 1, 1, 0], + [0, 1, 0, 1, 1, 1, 0, 1, 0, 0], + [0, 0, 1, 0, 0, 1, 1, 0, 0, 0], + [1, 0, 1, 1, 0, 0, 0, 1, 1, 0], + [0, 1, 1, 0, 0, 0, 1, 1, 0, 0], + [0, 0, 1, 1, 0, 1, 0, 0, 1, 0]] +# 13 big islands!! + +# getNeighbors goes here +def getNeighbors(row, col, matrix): +# check north, east, south, west +# neighbors are like.. index - 1[same place], index[-1 place], index[+1 place], index + 1[same place] +# What about the edges of the matrix? +# check if these are 1s + + neighbors = [] + + if col >= 1: + west_neighbor = matrix[row][col - 1] + if west_neighbor == 1: + neighbors.append((row, col - 1)) + + if col <= len(matrix) - 2: + east_neighbor = matrix[row][col + 1] + if east_neighbor == 1: + neighbors.append((row, col + 1)) + + if row <= len(matrix) - 2: + south_neighbor = matrix[row + 1][col] + if south_neighbor == 1: + neighbors.append((row + 1, col)) + + if row >= 1: + north_neighbor = matrix[row - 1][col] + if north_neighbor == 1: + neighbors.append((row - 1, col)) + + return neighbors + +# traversal goes here +def dft_recursive(row, col, visited, matrix): + if (row, col) not in visited: + visited.add((row, col)) + + neighbors = getNeighbors(row, col, matrix) + + for neighbor in neighbors: + dft_recursive(neighbor[0], neighbor[1], visited, matrix) + +# count number of connected components +def count_islands(matrix): + visited = set() + + number_of_connected_components = 0 + + # iterate over the matrix + for row in range(len(matrix)): + for col in range(len(matrix)): + node = matrix[row][col] + # if 0 continue, + if node == 0: + continue + # if 1: hold place, traverse and add each to visited, at end go back + elif node == 1: + if (row, col) not in visited: + number_of_connected_components += 1 + + dft_recursive(row, col, visited, matrix) + + return number_of_connected_components + + + +print(count_islands(islands)) # return 4 \ No newline at end of file diff --git a/projects/graph/graph.py b/projects/graph/graph.py index 59fecae4b..2e1febab0 100644 --- a/projects/graph/graph.py +++ b/projects/graph/graph.py @@ -9,54 +9,153 @@ class Graph: def __init__(self): self.vertices = {} + def add_vertex(self, vertex_id): """ Add a vertex to the graph. """ - pass # TODO + # inserting vertex_id into our vertices + self.vertices[vertex_id] = set() def add_edge(self, v1, v2): """ Add a directed edge to the graph. """ - pass # TODO + # assuming the edges are ordered pairs + if v1 in self.vertices and v2 in self.vertices: + self.vertices[v1].add(v2) + else: + raise IndexError("All your vertices are mine, or rather, do not exist.") def get_neighbors(self, vertex_id): """ Get all neighbors (edges) of a vertex. """ - pass # TODO + return self.vertices[vertex_id] def bft(self, starting_vertex): """ Print each vertex in breadth-first order beginning from starting_vertex. """ - pass # TODO + # make a queue + q = Queue() + # make a set to track which nodes we have visited + visited = set() + # enqueue the starting node + q.enqueue(starting_vertex) + # loop while the queue isn't empty + while q.size() > 0: + # dequeue, this is our current node + # check if we've visited yet + current_node = q.dequeue() + # check if we've visited yet + if current_node not in visited: + print(current_node) + ## if not, we go to the node + ### mark as visited == add to visited set + visited.add(current_node) + ### get the neighbors + neighbors = self.get_neighbors() + ### iterate over the neighbors, enqueue them + for neighbor in neighbors: + q.enqueue(neighbor) + def dft(self, starting_vertex): """ Print each vertex in depth-first order beginning from starting_vertex. """ - pass # TODO + # instantiate a stack + stack = Stack() + # track visited verts as a set() + visited = set() + # push on the starting node + stack.push(starting_vertex) + # loop while the stack isn't empty + while stack.size() > 0: + # pop, this is our current node + current_node = stack.pop() + # check if we've yet visited + if current_node not in visited: + print(current_node) + ## if not, we go to the node + ### mark as visited == add to visited set + visited.add(current_node) + ### get the neighbors + neighbors = self.get_neighbors(current_node) + ### iterate over the neighbors, enqueue them + for neighbor in neighbors: + stack.push(neighbor) - def dft_recursive(self, starting_vertex): + + def dft_recursive(self, vertex, visited=set()): """ Print each vertex in depth-first order beginning from starting_vertex. This should be done using recursion. """ - pass # TODO + # if the vertex is not in visited + if vertex not in visited: + # print vertex + print(vertex) + # add vertex to visited + visited.add(vertex) + # get the neighbors + neighbors = self.get_neighbors(vertex) + # if neighbors is none + if len(neighbors) == 0: + # return + return + else: + for neighbor in neighbors: + self.dft_recursive(neighbor, visited) + def bfs(self, starting_vertex, destination_vertex): """ Return a list containing the shortest path from starting_vertex to destination_vertex in breath-first order. + + ENQUEUE A PATH TO THE STARTING NODE + """ - pass # TODO + # instantiate a queue + queue = Queue() + # visited nodes + visited = set() + # queue the starting vertex + queue.enqueue([starting_vertex]) + # while there is a queue + while queue.size() > 0: + # dequeue a path + current_path = queue.dequeue() + # go to the deepest level in the path + current_node = current_path[-1] + # check if we have found our target node + if current_node == destination_vertex: + # then we are done! return + return current_path + # check if we've yet visited + if current_node not in visited: + ## if not, we go to the node + ### mark as visited == add to visited set + visited.add(current_node) + ### get the neighbors + neighbors = self.get_neighbors(current_node) + ### iterate over the neighbors, enqueue the PATH to them + for neighbor in neighbors: + # path_copy = list(current_path) + # path_copy = current_path.copy() + # path_copy = copy.copy(current_path) + # path_copy = current_path[:] + # path_copy.append(neighbor) + path_copy = current_path + [neighbor] + queue.enqueue(path_copy) + def dfs(self, starting_vertex, destination_vertex): """ @@ -64,7 +163,28 @@ def dfs(self, starting_vertex, destination_vertex): starting_vertex to destination_vertex in depth-first order. """ - pass # TODO + # instantiate a stack + stack = Stack() + # push the starting vertex onto the stack + stack.push([starting_vertex]) + # while there is a stack + while stack.size() > 0: + # pop off a path + path = stack.pop() + # go to the deepest level in the path + deepest_vertex = path[-1] + # if the deepest level vertex is the destination vertex + if deepest_vertex == destination_vertex: + # we can stop our search + return path + # for each neighbor + for neighbor in self.get_neighbors(deepest_vertex): + # create a new path (increasing the level of depth in our search) + new_path = list(path) + # where we add a new neighbor (level of depth) for each possibility + new_path.append(neighbor) + # add the new path to the queue + stack.push(new_path) def dfs_recursive(self, starting_vertex, destination_vertex): """ @@ -74,7 +194,24 @@ def dfs_recursive(self, starting_vertex, destination_vertex): This should be done using recursion. """ - pass # TODO + # original starting vertex is not a list + if not isinstance(starting_vertex, list): + starting_vertex = [starting_vertex] + # get the latest node on the path, check if destination node + if starting_vertex[-1] == destination_vertex: + return starting_vertex + # get neighbors of latest node + neighbors = self.get_neighbors(starting_vertex[-1]) + for neighbor in neighbors: + # if neighbor not in starting_vertex + if neighbor not in starting_vertex: + new_path = list(starting_vertex) + # append neighbor + new_path.append(neighbor) + result = self.dfs_recursive(new_path, destination_vertex) + if result is not None: + return result + if __name__ == '__main__': graph = Graph() # Instantiate your graph diff --git a/projects/graph/util.py b/projects/graph/util.py index f757503fb..188c1d2c3 100644 --- a/projects/graph/util.py +++ b/projects/graph/util.py @@ -1,4 +1,4 @@ - +from collections import deque # Note: This Queue class is sub-optimal. Why? class Queue(): def __init__(self): diff --git a/projects/social/social.py b/projects/social/social.py index 8609d8800..b8e16f3c6 100644 --- a/projects/social/social.py +++ b/projects/social/social.py @@ -1,12 +1,31 @@ +import random +from itertools import combinations +from collections import deque + + +# Note: This Queue class is sub-optimal. Why? +class Queue(): + def __init__(self): + self.queue = [] + def enqueue(self, value): + self.queue.append(value) + def dequeue(self): + if self.size() > 0: + return self.queue.pop(0) + else: + return None + def size(self): + return len(self.queue) + class User: def __init__(self, name): self.name = name class SocialGraph: def __init__(self): - self.last_id = 0 - self.users = {} - self.friendships = {} + self.last_id = 0 # current number of users + self.users = {} # your users with their attributes + self.friendships = {} # adjacency list def add_friendship(self, user_id, friend_id): """ @@ -28,6 +47,11 @@ def add_user(self, name): self.users[self.last_id] = User(name) self.friendships[self.last_id] = set() + def fisher_yates_shuffle(self, l): + for i in range(0, len(l)): + random_index = random.randint(i, len(l) - 1) + l[random_index], l[i] = l[i], l[random_index] + def populate_graph(self, num_users, avg_friendships): """ Takes a number of users and an average number of friendships @@ -42,11 +66,56 @@ def populate_graph(self, num_users, avg_friendships): self.last_id = 0 self.users = {} self.friendships = {} - # !!!! IMPLEMENT ME # Add users + for user in range(num_users): + self.add_user(user) # Create friendships + # if 1 is a friend of 2, and 2 is a friend of 1, count this as 2 friendships + total_friendships = avg_friendships * num_users + + # create a list with all possible friendship combinations, + # friendship_combos = [(0, 1), (0, 2), (0, 3), (1, 2), (1, 3), (2, 3)] + friendship_combos = [] + + for user_id in range(1, num_users + 1): + # You can avoid this by only creating friendships where user1 < user2 + for friend_id in range(user_id + 1, num_users + 1): + friendship_combos.append((user_id, friend_id)) + + # shuffle the list, + self.fisher_yates_shuffle(friendship_combos) + # then grab the first N elements from the list + friendships_to_make = friendship_combos[:(total_friendships // 2)] + + for friendship in friendships_to_make: + self.add_friendship(friendship[0], friendship[1]) + + def populate_graph_linear(self, num_users, avg_friendships): + self.last_id = 0 + self.users = {} + self.friendships = {} + # add users + for user in range(num_users): + self.add_user(user) + # create friendships + # if 1 is a friend of 2, and 2 is a friend of 1, + total_friendships = avg_friendships * num_users + friendships_made = 0 + # do this until we have as many as we want + while friendships_made < total_friendships: + # choose two random user ids + first_user = random.randint(1, num_users + 1) + second_user = random.randint(1, num_users + 1) + # try to make the friendship + new_friendship = self.add_friendship(first_user, second_user) + if new_friendship: + friendships_made += 2 + + def get_friends(self, current_friend): + return self.friendships[current_friend] + def get_all_social_paths(self, user_id): """ @@ -56,10 +125,31 @@ def get_all_social_paths(self, user_id): extended network with the shortest friendship path between them. The key is the friend's ID and the value is the path. + + Choose your fighter: BFT """ + q = Queue() visited = {} # Note that this is a dictionary, not a set # !!!! IMPLEMENT ME - return visited + # create a queue + q.enqueue([user_id]) + while q.size() > 0: + # get the next person in line + current_path = q.dequeue() + current_person = current_path[-1] + # check if we've visited them yet + if current_person not in visited: + ## if not, mark as visited + # key: user_id, value: path + visited[current_person] = current_path + ## get their friends (visited their edges) + friends = self.get_friends(current_person) + ## enqueue them + for friend in friends: + friend_path = list(current_path) + # friend_path = [*current_path] + friend_path.append(friend) + q.enqueue(friend_path) if __name__ == '__main__': @@ -68,3 +158,22 @@ def get_all_social_paths(self, user_id): print(sg.friendships) connections = sg.get_all_social_paths(1) print(connections) + + +""" +Questions Section: +Question 1: To create 100 users with an average of 10 friends each, +how many times would you need to call add_friendship()? Why? + +Answer 1: 500 times, because each time you call add_friendship() it +creates a friendship for two keys, as it's bidirectional and thus creates +two edges. + +Question 2: If you create 1000 users with an average of 5 random friends +each, what percentage of other users will be in a particular user's extended +social network? What is the average degree of separation between a user +and those in his/her extended network? + +Answer 2: About 99.5%. There is more room for error in this percentage with +a smaller sample size like 10, where connections tend to vary between 8-10. +""" diff --git a/projects/word_ladders.py b/projects/word_ladders.py new file mode 100644 index 000000000..a847aa720 --- /dev/null +++ b/projects/word_ladders.py @@ -0,0 +1,169 @@ +""" +Solving (Almost) Any Graphs Problem + +0. Spot that you might use a graph to solve it +- Shortest path +- spot that there are things connected to other things + +1. Describe the problem using graphs terminology +- What are my nodes here? +- What are my edges? aka when is there an edge, when is a node connected to another node? When not? + +2. Build your graph, or write getNeighbors + +3. Choose your fighter, traverse your graph + + +Given two words (begin_word and end_word), and a dictionary's word list, +return the shortest transformation sequence from begin_word to end_word, such that: + +Only one letter can be changed at a time. + +Each transformed word must exist in the word list. Note that begin_word is not a transformed word. +Note: + +Return None if there is no such transformation sequence. +All words contain only lowercase alphabetic characters. +You may assume no duplicates in the word list. +You may assume begin_word and end_word are non-empty and are not the same. + + +Sample: +begin_word = "hit" +end_word = "cog" +return: ['hit', 'hot', 'cot', 'cog'] + +begin_word = "sail" +end_word = "boat" +['sail', 'bail', 'boil', 'boll', 'bolt', 'boat'] + +beginWord = "hungry" +endWord = "happy" +None + + +0. Spot that we might use a graph: "shortest transformation sequence" is a lot like shortest path + +1. Describe the problem using graphs terminology +- What are my nodes here? +--- words! if in the word list +--- letters?? + +- What are my edges? aka when is there an edge, when is a node connected to another node? When not? + +--- two words are "connected" if they're just one letter different +---- in other words, all letters same, except one +---- and the same length! + +2. Build your graph, or write getNeighbors + +3. Choose your fighter, traverse your graph +BFS to get shortest +DFS/T to see how far we go +Combine them: DFS to find the target word, then BFS to select the shortest path +""" +class Queue(): + def __init__(self): + self.queue = [] + def enqueue(self, value): + self.queue.append(value) + def dequeue(self): + if self.size() > 0: + return self.queue.pop(0) + else: + return None + def size(self): + return len(self.queue) + +# do it in one go, but not lowercase them: set(f.read().split()) + +# be able to access words +f = open('words.txt', 'r') +giant_list_of_words = f.read().split('\n') +word_set = set() +for word in giant_list_of_words: + word_set.add(word.lower()) + +f.close() + +# build graph, or write getNeighbors +## somehow we need to find all the neighbors of a word... + +# for word in giant_list_of_words: +# if word not in my_graph.vertices: +# my_graph.add_node(word) +# for other_word in giant_list_of_words: +# if is_edge(word, other_word): +# my_graph.add_edge(word, other_word) + +import string +# "sail" +# "aail", "bail", "cail", "dail" +# "sail", "sbil", "scil" +# O(26 * n) +# O( # of letters * Alphabet) +# ["bail", "dail", "sall", "hail", "soil"] + +def getNeighbors(word): + neighbors = [] +# iterate over each letter of the word + for letter in range(len(word)): +## for each letter, swap out a letter of the alphabet + for other_letter in string.ascii_lowercase: + ## swap them out + ### turn it into a list + word_now_a_list = list(word) + word_now_a_list[letter] = other_letter + # turn back into a string + maybe_neighbor = "" + for l in word_now_a_list: + maybe_neighbor += l + +### check if the resulting "word" is in the word list/set + if maybe_neighbor in word_set: +#### if so, it's a neighbor +##### append to a list of neighbors + neighbors.append(maybe_neighbor) + + return neighbors + + +# use BFS +def bfs(start_node, target_node): + q = Queue() + + visited = set() + + q.enqueue([start_node]) + + while q.size() > 0: + + current_path = q.dequeue() + + current_node = current_path[-1] + + if current_node == target_node: + return current_path + + if current_node not in visited: + visited.add(current_node) + + neighbors = getNeighbors(current_node) + + for neighbor in neighbors: + + path_copy = list(current_path) + + path_copy.append(neighbor) + + q.enqueue(path_copy) + +begin_word = "hit" +end_word = "cog" + +print(bfs(begin_word, end_word)) + +begin_word = "sail" +end_word = "boat" + +print(bfs(begin_word, end_word)) \ No newline at end of file