diff --git a/src/my_project/interviews/amazon_high_frequency_23/round_7/k_free_subsets.py b/src/my_project/interviews/amazon_high_frequency_23/round_7/k_free_subsets.py new file mode 100644 index 00000000..09639612 --- /dev/null +++ b/src/my_project/interviews/amazon_high_frequency_23/round_7/k_free_subsets.py @@ -0,0 +1,111 @@ +from typing import List, Union, Collection, Mapping, Optional +from collections import defaultdict + +class Solution: + def countTheNumOfKFreeSubsets(self, nums: List[int], k: int) -> int: + """ + Count k-Free subsets using dynamic programming. + + Approach: + 1. Group elements by (num % k) to find independent groups + 2. Within each group, sort and build chains where elements differ by k + 3. For each chain, use House Robber DP to count valid subsets + 4. Multiply results across all independent chains + + Time: O(n log n), Space: O(n) + """ + # Group numbers by their remainder when divided by k + groups = defaultdict(list) + for num in nums: + groups[num % k].append(num) + + res = 1 + + # Process each group independently + for group in groups.values(): + group.sort() + + # Build chains within this group + i = 0 + while i < len(group): + chain = [group[i]] + j = i + 1 + + # Build chain where each element is exactly k more than previous + while j < len(group) and group[j] == chain[-1] + k: + chain.append(group[j]) + j += 1 + + # House Robber DP for this chain + m = len(chain) + if m == 1: + chain_res = 2 # {} or {chain[0]} + else: + take = 1 # Take first element + skip = 1 # Skip first element + + for idx in range(1, m): + new_take = skip # Can only take current if we skipped previous + new_skip = take + skip # Can skip current regardless + take, skip = new_take, new_skip + + chain_res = take + skip + + res *= chain_res + i = j + + return res + + + + + +''' +Detailed Algorithm Explanation +Part 1: Why Group by num % k? +Two numbers can have a difference of exactly k only if they have the same remainder when divided by k. + +Mathematical proof: + +If a - b = k, then a = b + k +Therefore: a % k = (b + k) % k = b % k +Example: nums = [2, 3, 5, 8], k = 5 + +num | num % 5 | group +----|---------|------- +2 | 2 | Group A +3 | 3 | Group B +5 | 0 | Group C +8 | 3 | Group B + + +Why this matters: Elements from different groups can never differ by k, so they're independent. We can combine any subset from Group A with any subset from Group B. + +Part 2: Building Chains +Within each group, we sort and find chains where consecutive elements differ by exactly k. + +Example with Group B: [3, 8] + +Sorted: [3, 8] +Check: 8 - 3 = 5 ✓ +Chain: 3 → 8 + +Another example: nums = [1, 6, 11, 21], k = 5 (all have remainder 1) + +Sorted: [1, 6, 11, 21] +Check: 6-1=5 ✓, 11-6=5 ✓, 21-11=10 ✗ +Chains: [1 → 6 → 11], [21] + + +Part 3: House Robber DP - The Core Logic +For a chain like [3 → 8], we can't pick both 3 and 8 (they differ by k). This is the House Robber problem: count all subsets where we don't pick adjacent elements. + +DP State Variables +take = number of valid subsets that INCLUDE the current element +skip = number of valid subsets that EXCLUDE the current element + +DP Transitions +new_take = skip # To take current, we MUST have skipped previous +new_skip = take + skip # To skip current, we can take or skip previous + +''' \ No newline at end of file diff --git a/src/my_project/interviews/amazon_high_frequency_23/round_7/longest_common_subsequence.py b/src/my_project/interviews/amazon_high_frequency_23/round_7/longest_common_subsequence.py new file mode 100644 index 00000000..3459fc0d --- /dev/null +++ b/src/my_project/interviews/amazon_high_frequency_23/round_7/longest_common_subsequence.py @@ -0,0 +1,21 @@ +from typing import List, Union, Collection, Mapping, Optional + +class Solution: + def longestCommonSubsequence(self, text1: str, text2: str) -> int: + + # Make a grid of 0's with len(text2) + 1 columns + # and len(text1) + 1 rows. + len_1 = len(text1) + len_2 = len(text2) + dp_grid = [[0]*(len_2+1) for _ in range(len_1+1)] + + # Iterate up each column, starting from the last one. + for j in reversed(range(len_2)): + for i in reversed(range(len_1)): + if text1[i] == text2[j]: + dp_grid[i][j] = dp_grid[i+1][j+1] + 1 + else: + dp_grid[i][j] = max(dp_grid[i+1][j], dp_grid[i][j+1]) + + # The original problem's answer is in dp_grid[0][0]. Return it. + return dp_grid[0][0] \ No newline at end of file diff --git a/src/my_project/interviews/google_top_exercises/round_1/28_alien_dictionary.py b/src/my_project/interviews/google_top_exercises/round_1/28_alien_dictionary.py new file mode 100644 index 00000000..c141f2c2 --- /dev/null +++ b/src/my_project/interviews/google_top_exercises/round_1/28_alien_dictionary.py @@ -0,0 +1,90 @@ +from typing import List +from collections import deque, defaultdict + +class Solution: + def alienOrder(self, words: List[str]) -> str: + """Topological sort (Kahn's BFS) over letters. + + Each adjacent pair of words gives at most one ordering fact: the first + position where they differ tells us c1 comes before c2. Everything after + that position is unconstrained, so we stop comparing there. The invalid + case is a prefix violation - ["abc", "ab"] - since a proper prefix must + sort first no matter what the alphabet is. + + Time O(C) where C is total characters, space O(1) - at most 26 nodes and + 26 * 26 edges. + """ + + adj = {c: set() for word in words for c in word} + indegree = {c: 0 for c in adj} + + for first, second in zip(words, words[1:]): + for c1, c2 in zip(first, second): + if c1 != c2: + if c2 not in adj[c1]: + adj[c1].add(c2) + indegree[c2] += 1 + break + else: + # No differing character: second must not be a strict prefix + if len(second) < len(first): + return "" + + q = deque([c for c in indegree if indegree[c] == 0]) + order = [] + + while q: + c = q.popleft() + order.append(c) + + for nxt in adj[c]: + indegree[nxt] -= 1 + if indegree[nxt] == 0: + q.append(nxt) + + # A leftover letter means it sits on a cycle + return "".join(order) if len(order) == len(adj) else "" + + + def alienOrder_DFS(self, words: List[str]) -> str: + """Same graph, post-order DFS with cycle detection. + + visiting[c] is True while c is on the current recursion stack (cycle) and + False once it is fully processed. Post-order emits a letter only after all + of its successors, so the reversed result is the topological order. + """ + + adj = defaultdict(set) + letters = {c for word in words for c in word} + + for first, second in zip(words, words[1:]): + for c1, c2 in zip(first, second): + if c1 != c2: + adj[c1].add(c2) + break + else: + if len(second) < len(first): + return "" + + visiting = {} + order = [] + + def dfs(c: str) -> bool: + """Returns True if a cycle is reached from c.""" + if c in visiting: + return visiting[c] + + visiting[c] = True + for nxt in adj[c]: + if dfs(nxt): + return True + + visiting[c] = False + order.append(c) + return False + + for c in letters: + if dfs(c): + return "" + + return "".join(reversed(order))