diff --git a/DIRECTORY.md b/DIRECTORY.md index c04945a5..9c577921 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -83,6 +83,8 @@ * Happy Number * [Test Happy Number](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/fast_and_slow/happy_number/test_happy_number.py) * Graphs + * Alien Dictionary + * [Test Alien Dictionary](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/alien_dictionary/test_alien_dictionary.py) * Cat And Mouse * [Test Cat And Mouse](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/cat_and_mouse/test_cat_and_mouse.py) * Course Schedule @@ -106,6 +108,8 @@ * [Union Find](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_islands/union_find.py) * Number Of Provinces * [Test Number Of Provinces](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/number_of_provinces/test_number_of_provinces.py) + * Recipes Supplies + * [Test Find All Possible Recipes](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/recipes_supplies/test_find_all_possible_recipes.py) * Reconstruct Itinerary * [Test Reconstruct Itinerary](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/graphs/reconstruct_itinerary/test_reconstruct_itinerary.py) * Reorder Routes @@ -203,6 +207,11 @@ * Stack * Daily Temperatures * [Test Daily Temperatures](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/stack/daily_temperatures/test_daily_temperatures.py) + * Subsets + * Cascading Subsets + * [Test Cascading Subsets](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/subsets/cascading_subsets/test_cascading_subsets.py) + * Find All Subsets + * [Test Find All Subsets](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/subsets/find_all_subsets/test_find_all_subsets.py) * Taxi Numbers * [Taxi Numbers](https://github.com/BrianLusina/PythonSnips/blob/master/algorithms/taxi_numbers/taxi_numbers.py) * Two Pointers @@ -963,7 +972,6 @@ * [Test Array From Permutation](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_array_from_permutation.py) * [Test Array Pair Sum](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_array_pair_sum.py) * [Test Build Tower](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_build_tower.py) - * [Test Cascading Subsets](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_cascading_subsets.py) * [Test Dynamic Array](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_dynamic_array.py) * [Test Find Unique](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_find_unique.py) * [Test Highest Rank](https://github.com/BrianLusina/PythonSnips/blob/master/tests/datastructures/arrays/test_highest_rank.py) diff --git a/algorithms/graphs/alien_dictionary/README.md b/algorithms/graphs/alien_dictionary/README.md new file mode 100644 index 00000000..3d2d9f56 --- /dev/null +++ b/algorithms/graphs/alien_dictionary/README.md @@ -0,0 +1,289 @@ +# Alien Dictionary + +You are given a list of words written in an alien language, where the words are sorted lexicographically by the rules of +this language. Surprisingly, the aliens also use English lowercase letters, but possibly in a different order. + +Given a list of words written in the alien language, return a string of unique letters sorted in the lexicographical +order of the alien language as derived from the list of words. + +If there’s no solution, that is, no valid lexicographical ordering, you can return an empty string "". + +If multiple valid orderings exist, you may return any of them. + +> Note: A string, a, is considered lexicographically smaller than string b if: +> 1. At the first position where they differ, the character in a comes before the character in b in the alien alphabet. +> 2. If one string is a prefix of the other, the shorter string is considered smaller. + +## Constraints + +- 1 <= `words.length` <= 10^3 +- 1 <= `words[i].length` <= 20 +- All characters in `words[i]` are English lowercase letters + +## Examples + +![Example 1](./images/examples/alien_dictionary_example_1.png) +![Example 2](./images/examples/alien_dictionary_example_2.png) +![Example 3](./images/examples/alien_dictionary_example_3.png) +![Example 4](./images/examples/alien_dictionary_example_4.png) +![Example 5](./images/examples/alien_dictionary_example_5.png) + +## Solution + +We can solve this problem using the topological sort pattern. Topological sort is used to find a linear ordering of +elements that have dependencies on or priority over each other. For example, if A is dependent on B or B has priority +over A, then B is listed before A in topological order. + +Using the list of words, we identify the relative precedence order of the letters in the words and generate a graph to +represent this ordering. To traverse a graph, we can use breadth-first search to find the letters’ order. + +We can essentially map this problem to a graph problem, but before exploring the exact details of the solution, there +are a few things that we need to keep in mind: + +1. The letters within a word don’t tell us anything about the relative order. For example, the word “educative” in the list + doesn’t tell us that the letter “e” is before the letter “d.” + +2. The input can contain words followed by their prefix, such as “educated” and then “educate.” These cases will never result + in a valid alphabet because in a valid alphabet, prefixes are always first. We need to make sure our solution detects + these cases correctly. +3. There can be more than one valid alphabet ordering. It’s fine for our algorithm to return any one of them. +4. The output dictionary must contain all unique letters within the words list, including those that could be in any position + within the ordering. It shouldn’t contain any additional letters that weren’t in the input. + +### Step-by-step solution construction + +For the graph problem, we can break this particular problem into three parts: + +1. Extract the necessary information to identify the dependency rules from the words. For example, in the words + [“patterns”, “interview”], the letter “p” comes before “i.” +2. With the gathered information, we can put these dependency rules into a directed graph with the letters as nodes and + the dependencies (order) as the edges. +3. Lastly, we can sort the graph nodes topologically to generate the letter ordering (dictionary). + +Let’s look at each part in more depth. + +#### Part 1: Identifying the dependencies + +Let’s start with example words and observe the initial ordering through simple reasoning: + +`["mzosr", "mqov", "xxsvq", "xazv", "xazau", "xaqu", "suvzu", "suvxq", "suam", "suax", "rom", "rwx", "rwv"]` + +As in the English language dictionary, where all the words starting with “a” come at the start followed by the words +starting with “b,” “c,” “d,” and so on, we can expect the first letters of each word to be in alphabetical order. + +`["m", "m", "x", "x", "x", "x", "s", "s", "s", "s", "r", "r", "r"]` + +Removing the duplicates, we get the following: + +`["m", "x", "s", "r"]` + +Following the intuition explained above, we can assume that the first letters in the messages are in alphabetical order: + +![Solution 1](./images/solutions/alien_dictionary_solution_1.png) + +Looking at the letters above, we know the relative order of these letters, but we don’t know how these letters fit in +with the rest of the letters. To get more information, we need to look further into our English dictionary analogy. The +word “dirt” comes before “dorm.” This is because we look at the second letter when the first letter is the same. In this +case, “i” comes before “o” in the alphabet. + +We can apply the same logic to our alien words and look at the first two words, “mzsor” and “mqov.” As the first letter +is the same in both words, we look at the second letter. The first word has “z,” and the second one has “q.” Therefore, +we can safely say that “z” comes before “q” in this alien language. We now have two fragments of the letter order: + +![Solution 2](./images/solutions/alien_dictionary_solution_2.png) + +> Note: Notice that we didn’t mention rules such as “m -> a”. This is fine because we can derive this relation from +> “m -> x”, “x -> a”. + +This is it for the first part. Let’s put the pieces that we have in place. + +#### Part 2: Representing the dependencies + +We now have a set of relations mentioning the relative order of the pairs of letters: + +`["z -> q", "m -> x", "x -> a", "x -> v", "x -> s", "z -> x", "v -> a", "s -> r", "o -> w"]` + +Now the question arises, how can we put these relations together? It might be tempting to start chaining all these +together. Let’s look at a few possible chains: + +![Solution 3](./images/solutions/alien_dictionary_solution_3.png) + +We can observe from our chains above that some letters might appear in more than one chain, and putting the chains into +the output list one after the other won’t work. Some of the letters might be duplicated and would result in an invalid +ordering. Let’s try to visualize the relations better with the help of a graph. The nodes are the letters, and an edge +between two letters, “x” and “y” represents that “x” is before “y” in the alien words. + +![Solution 4](./images/solutions/alien_dictionary_solution_4.png) + +#### Part 3: Generating the dictionary + +As we can see from the graph, four of the letters have no incoming arrows. This means that there are no letters that +have to come before any of these four. + +> Remember: There could be multiple valid dictionaries, and if there are, then it’s fine for us to return any of them. + +Therefore, a valid start to the ordering we return would be as follows: +`["o", "m", "u", "z"]` + +We can now remove these letters and edges from the graph because any other letters that required them first will now have +this requirement satisfied. + +![Solution 5](./images/solutions/alien_dictionary_solution_5.png) + +There are now three new letters on this new graph that have no in arrows. We can add these to our output list. + +`["o", "m", "u", "z", "x", "q", "w"]` + +Again, we can remove these from the graph. + +![Solution 6](./images/solutions/alien_dictionary_solution_6.png) + +Then, we add the two new letters with no in arrows. + +`["o", "m", "u", "z", "x", "q", "w", "v", "s"]` +This leaves the following graph: + +![Solution 7](./images/solutions/alien_dictionary_solution_7.png) + +We can place the final two letters in our output list and return the ordering: + +`["o", "m", "u", "z", "x", "q", "w", "v", "s", "a", "r"]` +Let’s now review how we can implement this approach. + +Identifying the dependencies and representing them in the form of a graph is pretty straightforward. We extract the +relations and insert them into an adjacency list: + +![Solution 8](./images/solutions/alien_dictionary_solution_8.png) + +Next, we need to generate the dictionary from the extracted relations: identify the letters (nodes) with no incoming links. +Identifying whether a particular letter (node) has any incoming links or not from our adjacency list format can be a +little complicated. A naive approach is to repeatedly iterate over the adjacency lists of all the other nodes and check +whether or not they contain a link to that particular node. + +This naive method would be fine for our case, but perhaps we can do it more optimally. + +An alternative is to keep two adjacency lists: + +One with the same contents as the one above. +One reversed that shows the incoming links. +This way, every time we traverse an edge, we can remove the corresponding edge from the reversed adjacency list: + +![Solution 9](./images/solutions/alien_dictionary_solution_9.png) + +What if we can do better than this? Instead of tracking the incoming links for all the letters from a particular letter, +we can track the count of how many incoming edges there are. We can keep the in-degree count of all the letters along with +the forward adjacency list. + +> In-degree corresponds to the number of incoming edges of a node. + +It will look like this: + +![Solution 10](./images/solutions/alien_dictionary_solution_10.png) + +Now, we can decrement the in-degree count of a node instead of removing it from the reverse adjacency list. When the in-degree of the node reaches 0, this particular node has no incoming links left. + +We perform BFS on all the letters that are reachable, that is, the in-degree count of the letters is zero. A letter is +only reachable once the letters that need to be before it have been added to the output, result. + +We use a queue to keep track of reachable nodes and perform BFS on them. Initially, we put the letters that have zero +in-degree count. We keep adding the letters to the queue as their in-degree counts become zero. + +We continue this until the queue is empty. Next, we check whether all the letters in the words have been added to the +output or not. This would only happen when some letters still have some incoming edges left, which means there is a cycle. +In this case, we return an empty string. + +> Remember: There can be letters that don’t have any incoming edges. This can result in different orderings for the same +> set of words, and that’s all right. + +### Solution summary + +To recap, the solution to this problem can be divided into the following parts: + +1. Build a graph from the given words and keep track of the in-degrees of alphabets in a dictionary. +2. Add the sources to a result list. +3. Remove the sources and update the in-degrees of their children. If the in-degree of a child becomes 0, it’s the next + source. +4. Repeat until all alphabets are covered. + +### Time Complexity + +There are three parts to the algorithm: + +- Identifying all the relations. +- Putting them into an adjacency list. +- Converting it into a valid alphabet ordering. + +In the worst case, the identification and initialization parts require checking every letter of every word, which is +O(c), where c is the total length of all the words in the input list added together. + +For the generation part, we can recall that a breadth-first search has a cost of O(v+e), where v is the number of vertices +and e is the number of edges. Our algorithm has the same cost as BFS because it visits each edge and node once. + +> Note: A node is visited once all of its edges are visited, unlike the traditional BFS where it’s visited once any edge +> is visited. + +Therefore, determining the cost of our algorithm requires determining how many nodes and edges there are in the graph. + +**Nodes**: We know that there’s one vertex for each unique letter, that is, O(u) vertices, where u is the total number of +unique letters in words. While this is limited to 26 in our case, we still look at how it would impact the complexity if +this weren’t the case. + +**Edges**: We generate each edge in the graph by comparing two adjacent words in the input list. There are n−1 pairs of +adjacent words, and only one edge can be generated from each pair, where n is the total number of words in the input list. +We can again look back at the English dictionary analogy to make sense of this: + +"dirt" +"dorm" + +The only conclusion we can draw is that “i” is before “o.” This is the reason "dirt" appears before "dorm" in an English +dictionary. The solution explains that the remaining letters “rt” and “rm” are irrelevant for determining the alphabetical +ordering. + +> Remember: We only generate rules for adjacent words and don’t add the “implied” rules to the adjacency list. + +So with this, we know that there are at most n−1 edges. + +We can place one additional upper limit on the number of edges since it’s impossible to have more than one edge between +each pair of nodes. With u nodes, this means there can’t be more than u^2 edges. + +Because the number of edges has to be lower than both n−1 and u^2, we know it’s at most the smallest of these two values: +min(u^2 ,n−1). + +We can now substitute the number of nodes and the number of edges in our breadth-first search cost: +- v=u +- e=min(u^2 ,n−1) + +This gives us the following: +> O(v+e) = O(u + min(u^2, n−1)) = O(u + min(u^2 ,n)) + +Finally, we combine the three parts: O(c) for the first two parts and O(u + min(u^2 ,n)) for the third part. Since we +have two independent parts, we can add them and look at the final formula to see whether or not we can identify any +relation between them. Combining them, we get the following: + +> O(c) + O(u + min(u^2, n)) = O(c + u + min(u^2,n)) + +So, what do we know about the relative values of n, c and u? We can deduce that both n, the total number of words, and +u, the total number of unique letters, are smaller than the total number of letters, c, because each word contains at +least one character and there can’t be more unique characters than there are characters. + +We know that c is the biggest of the three, but we don’t know the relation between n and u. + +Let’s simplify our formulation a little since we know that the u bit is insignificant compared to c + +> O(c+u+min(u^2,n))−>O(c+min(u^2 ,n)) + +Let’s now consider two cases to simplify it a little further: + +- If u^2 is smaller than n, then min(u^2,n)=u^2. We have already established that u^2 is smaller than n, which is, in + turn, smaller than c, and so u^2 is definitely less than c. This leaves us with O(c). +- If u^2 is larger than n, then min(u^2,n)=n. Because c>n, we’re left with O(c). + +So in all cases, we know that c>min(u^2 ,n). This gives us a final time complexity of O(c). + +### Space Complexity + +The space complexity is O(1) or O(u+min(u^2, n)). The adjacency list uses O(v+e) memory, which in the worst case is +min(u^2 ,n), as explained in the time complexity analysis. So in total, the adjacency list takes +O(u+min(u^2,n)) space. So, the space complexity for a large number of letters is O(u+min(u^2 ,n)). However, for our use +case, where u is fixed at a maximum of 26, the space complexity is O(1). This is because u is fixed at 26, and the number +of relations is fixed at 26^2, so O(min(26^2,n))=O(26^2)=O(1). diff --git a/algorithms/graphs/alien_dictionary/__init__.py b/algorithms/graphs/alien_dictionary/__init__.py new file mode 100644 index 00000000..eb6c87ac --- /dev/null +++ b/algorithms/graphs/alien_dictionary/__init__.py @@ -0,0 +1,105 @@ +from typing import Deque, DefaultDict, List, Counter, Set +from collections import defaultdict, Counter, deque + + +def alien_order(words: List[str]) -> str: + word_length = len(words) + # Edge cases where the length of the list is 0 and 1. If 0, return an empty string + if word_length == 0: + return "" + + # Graph will store the graph representation of the letters using a dictionary where the key will be a letter and the + # value will be a list representing the neighbors of the key. So, we can move from, say, a to b or c if the graph is + # {a: [b, c]} + graph: DefaultDict[str, Set[str]] = defaultdict(set) + + # This is used to store the number of edges that are incoming to a given node(letter). 0 is default indicating that + # the letter is possibly, the first in the alphabet, based on what order we find from the given list of words. + in_degree: DefaultDict[str, int] = defaultdict(lambda: 0) + + # set every unique character in every word in words to 0 + for word in words: + for char in word: + if not in_degree[char]: + in_degree[char] = 0 + + # Compare the orderings of letters in the supplied words. The comparison is done two words at a time, where the differing + # character between the words is used to determine the ordering of the letters. so, if two words are 'abc' and 'adbc', we + # only check 'b' and 'd'. Since abc comes before adbc, we can conclude that 'b' comes before 'd' and we end the + # comparison there and move to the next two words to compare and do the same matching + for idx in range(word_length - 1): + current_word = words[idx] + next_word = words[idx + 1] + current_word_len = len(current_word) + next_word_len = len(next_word) + + # Prefix check, if the current word is longer than the next word and prefix is a prefix of current word, then return + # ane empty string as that indicates an invalid ordering + if current_word_len > next_word_len and current_word.startswith(next_word): + return "" + + # Check for the differing character in the two words. If the characters differ, the character from the current + # word comes before the character from the next word + for i in range(min(current_word_len, next_word_len)): + char_one = current_word[i] + char_two = next_word[i] + # these are the same characters, so, we move to the next two characters to compare + if char_one != char_two: + # Only increment in_degree if this is a new directed edge + if char_two not in graph[char_one]: + graph[char_one].add(char_two) + # Character two will have an indegree where we increment it by 1, because we can move from character one + # to character two + in_degree[char_two] += 1 + # We don't need to compare the next two characters at this point + break + + # Queue which we will be popping of characters to create a valid alien word from. This will contain all the characters + # with an indegree of 0 initially + queue: Deque[str] = deque([i for i in in_degree if in_degree[i] == 0]) + + # The final word we can create + word = "" + + while queue: + character = queue.popleft() + word += character + + for next_character in graph[character]: + in_degree[next_character] -= 1 + if in_degree[next_character] == 0: + queue.append(next_character) + + return "" if len(word) < len(in_degree.keys()) else word + + +def alien_order_2(words: List[str]) -> str: + adj_list: DefaultDict[str, Set[str]] = defaultdict(set) + counts: Counter[str] = Counter({c: 0 for word in words for c in word}) + + for word1, word2 in zip(words, words[1:]): + for c, d in zip(word1, word2): + if c != d: + if d not in adj_list[c]: + adj_list[c].add(d) + counts[d] += 1 + break + + else: + if len(word2) < len(word1): + return "" + + result: List[str] = [] + sources_queue: Deque[str] = deque([c for c in counts if counts[c] == 0]) + while sources_queue: + c = sources_queue.popleft() + result.append(c) + + for d in adj_list[c]: + counts[d] -= 1 + if counts[d] == 0: + sources_queue.append(d) + + if len(result) < len(counts): + return "" + return "".join(result) diff --git a/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_1.png b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_1.png new file mode 100644 index 00000000..24aabe3f Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_1.png differ diff --git a/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_2.png b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_2.png new file mode 100644 index 00000000..39243421 Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_2.png differ diff --git a/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_3.png b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_3.png new file mode 100644 index 00000000..a41843c7 Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_3.png differ diff --git a/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_4.png b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_4.png new file mode 100644 index 00000000..b24fce96 Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_4.png differ diff --git a/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_5.png b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_5.png new file mode 100644 index 00000000..c3f73058 Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/examples/alien_dictionary_example_5.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_1.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_1.png new file mode 100644 index 00000000..4d4cb7c6 Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_1.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_10.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_10.png new file mode 100644 index 00000000..d6b45208 Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_10.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_2.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_2.png new file mode 100644 index 00000000..1e737549 Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_2.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_3.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_3.png new file mode 100644 index 00000000..cb1d59e9 Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_3.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_4.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_4.png new file mode 100644 index 00000000..8c4a10df Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_4.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_5.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_5.png new file mode 100644 index 00000000..50df258a Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_5.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_6.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_6.png new file mode 100644 index 00000000..e610e2ad Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_6.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_7.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_7.png new file mode 100644 index 00000000..81954979 Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_7.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_8.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_8.png new file mode 100644 index 00000000..d5c3ea5c Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_8.png differ diff --git a/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_9.png b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_9.png new file mode 100644 index 00000000..851901bb Binary files /dev/null and b/algorithms/graphs/alien_dictionary/images/solutions/alien_dictionary_solution_9.png differ diff --git a/algorithms/graphs/alien_dictionary/test_alien_dictionary.py b/algorithms/graphs/alien_dictionary/test_alien_dictionary.py new file mode 100644 index 00000000..f7f8e7a2 --- /dev/null +++ b/algorithms/graphs/alien_dictionary/test_alien_dictionary.py @@ -0,0 +1,77 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.graphs.alien_dictionary import alien_order, alien_order_2 + +ALIEN_DICTIONARY_TEST_CASES = [ + (["ca", "aa", "ab"], "cab"), + (["ac", "ab", "zc", "zb"], "cabz"), + (["baa", "abcd", "abca", "cab", "cad"], "bdac"), + (["mdx", "mars", "avgd", "dkae"], ""), + (["m", "a", "b", "s"], "mabs"), + (["xro", "xma", "per", "prt", "oxh", "olv"], "xaethvlprom"), + (["o", "l", "m", "s"], "olms"), + ( + [ + "mdxok", + "mrolw", + "mroqs", + "kptz", + "klr", + "klon", + "zvef", + "zrsu", + "zzs", + "orm", + "oqt", + ], + "mdxwsptnvefuklrzqo", + ), + ( + [ + "m", + "mx", + "mxe", + "mxer", + "mxerl", + "mxerlo", + "mxerlos", + "mxerlost", + "mxerlostr", + "mxerlostrpq", + "mxerlostrp", + ], + "", + ), + (["wgencorejikhdiwnbhx"], "wgencorjikhdbx"), +] + + +class AlienDictionaryTestCase(unittest.TestCase): + @parameterized.expand(ALIEN_DICTIONARY_TEST_CASES) + def test_alien_order(self, words: List[str], expected: str): + actual = alien_order(words) + self.assertEqual(sorted(expected), sorted(actual)) + + @parameterized.expand(ALIEN_DICTIONARY_TEST_CASES) + def test_alien_order_2(self, words: List[str], expected: str): + actual = alien_order_2(words) + self.assertEqual(sorted(expected), sorted(actual)) + + def is_valid_alien_order(self, words: List[str], order: str) -> bool: + if not order: + return False # or handle expected empty case separately + char_rank = {c: i for i, c in enumerate(order)} + for w1, w2 in zip(words, words[1:]): + for c1, c2 in zip(w1, w2): + if c1 != c2: + if char_rank.get(c1, -1) > char_rank.get(c2, -1): + return False + break + else: + if len(w1) > len(w2): + return False + return True + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/graphs/recipes_supplies/README.md b/algorithms/graphs/recipes_supplies/README.md new file mode 100644 index 00000000..59812b01 --- /dev/null +++ b/algorithms/graphs/recipes_supplies/README.md @@ -0,0 +1,89 @@ +# Find All Possible Recipes from Given Supplies + +You are given information about n different recipes. Each recipe is listed in the array recipes, and its corresponding +ingredients are provided in the 2D array ingredients. The ith recipe, recipes[i], can be prepared if all the necessary +ingredients listed in ingredients[i] are available. Some ingredients might need to be created from other recipes, meaning +ingredients[i] may contain strings that are also in recipes. + +Additionally, you have a string array supplies that contains all the ingredients you initially have, and you have an +infinite supply of each. + +Return a list of all the recipes you can create. The answer can be returned in any order. + +> Note: It is possible for two recipes to list each other as ingredients. However, if these are the only two recipes +> provided, the expected output is an empty list. + +## Constraints + +- `n` == `recipes.length` == `ingredients.length` +- 1 <= `n` <= 100 +- 1 ≤ `ingredients[i].length`, `supplies.length` ≤ 100 +- 1 ≤ `recipes[i].length`, `ingredients[i][j].length`, `supplies[k].length` ≤ 10 +- `recipes[i]`, `ingredients[i][j]`, and `supplies[k]` consist only of lowercase English letters. +- All the combined values of `recipes` and `supplies` are unique. +- Each `ingredients[i]` doesn’t contain any duplicate values. + +## Examples + +![Example 1](images/examples/all_possible_recipes_from_supplies_example_1.png) +![Example 2](images/examples/all_possible_recipes_from_supplies_example_2.png) + +## Solution + +An optimized approach to solve this problem would be to use the topological sort pattern. We need to understand that the +preparation of recipes depends on the availability of ingredients. Some ingredients are available initially, while others +may need to be prepared using other recipes. This dependency between recipes and their ingredients can be represented as +a directed graph where: + +- Each recipe is a node. +- There is a directed edge from node A to node B if the preparation of recipe B requires recipe A. + +Topological sort is an algorithm commonly used to order nodes (tasks) in a directed acyclic graph (DAG) such that for +every directed edge u → v, node u comes before node v in the ordering. In our case, this means that if a recipe A depends +on recipe B, then B should come before A in the order of recipes we can prepare. + +If there are circular dependencies (like two recipes requiring each other), these will be cycles in the graph, and we +cannot prepare either recipe. The goal of using topological sort is to determine the order in which recipes can be prepared, +starting from those that can be made with the initial supplies and progressing through those that depend on previously +prepared recipes. + +Let’s go through the algorithm to see how we will reach the solution: + +1. Initialize data structures: + - `recipe_graph`: This is a dictionary where each key is a recipe and its value is a list of recipes that depend on it. + - `recipe_indegrees`: This is a dictionary that stores the number of prerequisites for each recipe. + - `recipes_queue`: This is a queue that processes recipes, which can be immediately made with the initial supplies. + - `possible_recipes`: This is an array that stores all the recipes that can be created. +2. Build the graph and in-degree count by iterating over each ingredient of each recipe: + - If the ingredient is another recipe, add an edge from that ingredient to the current recipe in `recipe_graph` and + increase `recipe_indegrees` for the current recipe. + - If the ingredient is available in `supplies`, skip it since it’s immediately available. +3. Add all recipes with an in-degree of 0 (no prerequisites) to `recipes_queue`. +4. Process the `recipes_queue` using topological sorting: + - While `recipes_queue` is not empty: + - Pop a recipe from the queue. + - Add it to `possible_recipes` since it can now be made. + - For each recipe that depends on this recipe, reduce its in-degree by 1. + - If any recipe’s in-degree becomes 0, add it to `recipes_queue`. +5. Return the `possible_recipes` list, which contains all the recipes that can be created with the given supplies. + +![Solution 1](./images/solutions/all_possible_recipes_from_supplies_solution_1.png) +![Solution 2](./images/solutions/all_possible_recipes_from_supplies_solution_2.png) +![Solution 3](./images/solutions/all_possible_recipes_from_supplies_solution_3.png) +![Solution 4](./images/solutions/all_possible_recipes_from_supplies_solution_4.png) +![Solution 5](./images/solutions/all_possible_recipes_from_supplies_solution_5.png) +![Solution 6](./images/solutions/all_possible_recipes_from_supplies_solution_6.png) +![Solution 7](./images/solutions/all_possible_recipes_from_supplies_solution_7.png) +![Solution 8](./images/solutions/all_possible_recipes_from_supplies_solution_8.png) +![Solution 9](./images/solutions/all_possible_recipes_from_supplies_solution_9.png) +![Solution 10](./images/solutions/all_possible_recipes_from_supplies_solution_10.png) + +### Time Complexity + +The time complexity of this solution is O(v+e), where v is the vertices of the recipe_graph and e is the edges of the +graph. This is because we traverse each vertex and edge in the graph exactly once during the topological sort, and the +dictionary data structure allows us to access vertices and edges in constant time. + +### Space Complexity + +The space complexity is also O(v+e) as we have to store all the recipes and their respective ingredients. diff --git a/algorithms/graphs/recipes_supplies/__init__.py b/algorithms/graphs/recipes_supplies/__init__.py new file mode 100644 index 00000000..737d0e42 --- /dev/null +++ b/algorithms/graphs/recipes_supplies/__init__.py @@ -0,0 +1,40 @@ +from typing import List, DefaultDict +from collections import defaultdict, deque + + +def find_recipes( + recipes: List[str], ingredients: List[List[str]], supplies: List[str] +) -> List[str]: + # Create a graph to store which recipes depend on which ingredients + recipe_graph: DefaultDict[str, List[str]] = defaultdict(list) + # Create a dictionary to keep track of the number of dependencies (indegree) for each recipe + recipe_in_degrees: DefaultDict[str, int] = defaultdict(int) + + # Initialize a queue with the available supplies + recipe_queue = deque(supplies) + + # Build the graph and indegree dictionary + for idx, recipe in enumerate(recipes): + recipe_ingredients = ingredients[idx] + + for ingredient in recipe_ingredients: + recipe_in_degrees[recipe] += 1 + recipe_graph[ingredient].append(recipe) + + # List to store the possible recipes that can be made + possible_recipes: List[str] = [] + + # Process the queue until it's empty + while recipe_queue: + # Get the next item (ingredient or supply) from the queue + ingredient = recipe_queue.popleft() + + # Reduce the indegree of all recipes that depend on the current item + for recipe in recipe_graph[ingredient]: + recipe_in_degrees[recipe] -= 1 + # If a recipe's indegree reaches 0, all its ingredients are available, so add it to the queue + if recipe_in_degrees[recipe] == 0: + possible_recipes.append(recipe) + recipe_queue.append(recipe) + + return possible_recipes diff --git a/algorithms/graphs/recipes_supplies/images/examples/all_possible_recipes_from_supplies_example_1.png b/algorithms/graphs/recipes_supplies/images/examples/all_possible_recipes_from_supplies_example_1.png new file mode 100644 index 00000000..76dd496e Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/examples/all_possible_recipes_from_supplies_example_1.png differ diff --git a/algorithms/graphs/recipes_supplies/images/examples/all_possible_recipes_from_supplies_example_2.png b/algorithms/graphs/recipes_supplies/images/examples/all_possible_recipes_from_supplies_example_2.png new file mode 100644 index 00000000..fe721b20 Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/examples/all_possible_recipes_from_supplies_example_2.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_1.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_1.png new file mode 100644 index 00000000..2fe6b70a Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_1.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_10.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_10.png new file mode 100644 index 00000000..e9670cbd Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_10.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_2.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_2.png new file mode 100644 index 00000000..0ad6df4d Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_2.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_3.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_3.png new file mode 100644 index 00000000..457e4211 Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_3.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_4.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_4.png new file mode 100644 index 00000000..73c60cba Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_4.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_5.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_5.png new file mode 100644 index 00000000..e5e1f350 Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_5.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_6.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_6.png new file mode 100644 index 00000000..84ef2de6 Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_6.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_7.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_7.png new file mode 100644 index 00000000..db3cbe47 Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_7.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_8.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_8.png new file mode 100644 index 00000000..57866847 Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_8.png differ diff --git a/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_9.png b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_9.png new file mode 100644 index 00000000..f2b2e4b5 Binary files /dev/null and b/algorithms/graphs/recipes_supplies/images/solutions/all_possible_recipes_from_supplies_solution_9.png differ diff --git a/algorithms/graphs/recipes_supplies/test_find_all_possible_recipes.py b/algorithms/graphs/recipes_supplies/test_find_all_possible_recipes.py new file mode 100644 index 00000000..f1a70a4d --- /dev/null +++ b/algorithms/graphs/recipes_supplies/test_find_all_possible_recipes.py @@ -0,0 +1,69 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.graphs.recipes_supplies import find_recipes + +FIND_ALL_POSSIBLE_RECIPES_TEST_CASES = [ + ( + ["tea", "omelette"], + [["milk", "caffeine", "sugar"], ["salt", "egg", "pepper"]], + ["salt", "milk", "egg", "caffeine", "sugar"], + ["tea"], + ), + ( + ["sandwich", "mojito"], + [["cheese", "vegetables", "bread", "salad"], ["rum", "mint", "syrup"]], + ["cheese", "rum", "bread", "salad", "vegetables", "mint", "syrup"], + ["sandwich", "mojito"], + ), + ( + ["bread", "sandwich", "burger"], + [["yeast", "flour"], ["bread", "meat"], ["sandwich", "meat", "bread"]], + ["yeast", "flour", "meat"], + ["bread", "sandwich", "burger"], + ), + ( + ["bread", "sandwich"], + [["yeast", "flour"], ["bread", "meat"]], + ["yeast", "flour", "meat"], + ["bread", "sandwich"], + ), + ( + ["bread"], + [["yeast", "flour"]], + ["yeast", "flour", "corn"], + ["bread"], + ), + ( + ["pasta", "egg", "chicken"], + [["yeast", "flour"], ["pasta", "meat"], ["egg", "meat", "pasta"]], + ["yeast", "flour", "meat"], + ["pasta", "egg", "chicken"], + ), + ( + ["custard", "trifle"], + [ + ["yeast", "flour", "trifle", "bananas", "eggs", "milk"], + ["eggs", "milk", "custard"], + ], + ["eggs", "milk", "yeast", "flour", "corn", "bananas"], + [], + ), +] + + +class FindAllPossibleRecipesTestCase(unittest.TestCase): + @parameterized.expand(FIND_ALL_POSSIBLE_RECIPES_TEST_CASES) + def test_find_recipes( + self, + recipes: List[str], + ingredients: List[List[str]], + supplies: List[str], + expected: List[str], + ): + actual = find_recipes(recipes, ingredients, supplies) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main() diff --git a/algorithms/subsets/__init__.py b/algorithms/subsets/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/datastructures/arrays/cascading_subsets/README.md b/algorithms/subsets/cascading_subsets/README.md similarity index 100% rename from datastructures/arrays/cascading_subsets/README.md rename to algorithms/subsets/cascading_subsets/README.md diff --git a/datastructures/arrays/cascading_subsets/__init__.py b/algorithms/subsets/cascading_subsets/__init__.py similarity index 100% rename from datastructures/arrays/cascading_subsets/__init__.py rename to algorithms/subsets/cascading_subsets/__init__.py diff --git a/tests/datastructures/arrays/test_cascading_subsets.py b/algorithms/subsets/cascading_subsets/test_cascading_subsets.py similarity index 94% rename from tests/datastructures/arrays/test_cascading_subsets.py rename to algorithms/subsets/cascading_subsets/test_cascading_subsets.py index eb5d4d6e..d538abe6 100644 --- a/tests/datastructures/arrays/test_cascading_subsets.py +++ b/algorithms/subsets/cascading_subsets/test_cascading_subsets.py @@ -1,6 +1,6 @@ import unittest -from datastructures.arrays.cascading_subsets import each_cons +from algorithms.subsets.cascading_subsets import each_cons class CascadingSubsetsTestCase(unittest.TestCase): diff --git a/algorithms/subsets/find_all_subsets/README.md b/algorithms/subsets/find_all_subsets/README.md new file mode 100644 index 00000000..0fa1064c --- /dev/null +++ b/algorithms/subsets/find_all_subsets/README.md @@ -0,0 +1,24 @@ +# Subsets + +Given an array of integers, nums, find all possible subsets of nums, including the empty set. + +> Note: The solution set must not contain duplicate subsets. You can return the solution in any order. + +## Constraints + +- 1 <= `nums.length` <= 10 +- -10 <= `nums[i]` <= 10 +- All the numbers in num are unique + +## Examples + +![Example 1](images/examples/find_all_subsets_example_1.png) +![Example 2](images/examples/find_all_subsets_example_2.png) +![Example 3](images/examples/find_all_subsets_example_3.png) +![Example 4](images/examples/find_all_subsets_example_4.png) + +## Topics + +- Bit Manipulation +- Backtracking +- Arrays diff --git a/algorithms/subsets/find_all_subsets/__init__.py b/algorithms/subsets/find_all_subsets/__init__.py new file mode 100644 index 00000000..016f6af7 --- /dev/null +++ b/algorithms/subsets/find_all_subsets/__init__.py @@ -0,0 +1,22 @@ +from typing import List + + +def find_all_subsets(nums: List[int]) -> List[List[int]]: + n = len(nums) + + if n == 0: + return [] + + subsets = [] + + def backtrack(first, curr): + # Add the current subset to the output + subsets.append(curr[:]) + # Generate subsets starting from the current index + for i in range(first, n): + curr.append(nums[i]) + backtrack(i + 1, curr) + curr.pop() + + backtrack(0, []) + return subsets diff --git a/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_1.png b/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_1.png new file mode 100644 index 00000000..3ceb0fc6 Binary files /dev/null and b/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_1.png differ diff --git a/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_2.png b/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_2.png new file mode 100644 index 00000000..ab629154 Binary files /dev/null and b/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_2.png differ diff --git a/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_3.png b/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_3.png new file mode 100644 index 00000000..727d448c Binary files /dev/null and b/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_3.png differ diff --git a/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_4.png b/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_4.png new file mode 100644 index 00000000..9ced6b93 Binary files /dev/null and b/algorithms/subsets/find_all_subsets/images/examples/find_all_subsets_example_4.png differ diff --git a/algorithms/subsets/find_all_subsets/test_find_all_subsets.py b/algorithms/subsets/find_all_subsets/test_find_all_subsets.py new file mode 100644 index 00000000..c237417b --- /dev/null +++ b/algorithms/subsets/find_all_subsets/test_find_all_subsets.py @@ -0,0 +1,47 @@ +import unittest +from typing import List +from parameterized import parameterized +from algorithms.subsets.find_all_subsets import find_all_subsets + +FIND_ALL_SUBSETS_TEST_CASES = [ + ([], []), + ([9], [[], [9]]), + ([1], [[], [1]]), + ([0], [[], [0]]), + ([2, 4], [[], [2], [4], [2, 4]]), + ([1, 2], [[], [1], [2], [1, 2]]), + ([2, 5, 7], [[], [2], [5], [2, 5], [7], [2, 7], [5, 7], [2, 5, 7]]), + ( + [1, 2, 3, 4], + [ + [], + [1], + [2], + [1, 2], + [3], + [1, 3], + [2, 3], + [1, 2, 3], + [4], + [1, 4], + [2, 4], + [1, 2, 4], + [3, 4], + [1, 3, 4], + [2, 3, 4], + [1, 2, 3, 4], + ], + ), + ([3, 6, 9], [[], [3], [3, 6], [3, 6, 9], [3, 9], [6], [6, 9], [9]]), +] + + +class FindAllSubsetsTestCase(unittest.TestCase): + @parameterized.expand(FIND_ALL_SUBSETS_TEST_CASES) + def test_find_all_subsets(self, nums: List[int], expected: List[List[int]]): + actual = find_all_subsets(nums) + self.assertEqual(expected, actual) + + +if __name__ == "__main__": + unittest.main()