Skip to content

Commit a0ba8ae

Browse files
authored
Merge pull request #1718 from ivanpenaloza/august02
adding updats
2 parents 9fab6af + 1912e2b commit a0ba8ae

3 files changed

Lines changed: 222 additions & 0 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from typing import List, Union, Collection, Mapping, Optional
2+
from collections import defaultdict
3+
4+
class Solution:
5+
def countTheNumOfKFreeSubsets(self, nums: List[int], k: int) -> int:
6+
"""
7+
Count k-Free subsets using dynamic programming.
8+
9+
Approach:
10+
1. Group elements by (num % k) to find independent groups
11+
2. Within each group, sort and build chains where elements differ by k
12+
3. For each chain, use House Robber DP to count valid subsets
13+
4. Multiply results across all independent chains
14+
15+
Time: O(n log n), Space: O(n)
16+
"""
17+
# Group numbers by their remainder when divided by k
18+
groups = defaultdict(list)
19+
for num in nums:
20+
groups[num % k].append(num)
21+
22+
res = 1
23+
24+
# Process each group independently
25+
for group in groups.values():
26+
group.sort()
27+
28+
# Build chains within this group
29+
i = 0
30+
while i < len(group):
31+
chain = [group[i]]
32+
j = i + 1
33+
34+
# Build chain where each element is exactly k more than previous
35+
while j < len(group) and group[j] == chain[-1] + k:
36+
chain.append(group[j])
37+
j += 1
38+
39+
# House Robber DP for this chain
40+
m = len(chain)
41+
if m == 1:
42+
chain_res = 2 # {} or {chain[0]}
43+
else:
44+
take = 1 # Take first element
45+
skip = 1 # Skip first element
46+
47+
for idx in range(1, m):
48+
new_take = skip # Can only take current if we skipped previous
49+
new_skip = take + skip # Can skip current regardless
50+
take, skip = new_take, new_skip
51+
52+
chain_res = take + skip
53+
54+
res *= chain_res
55+
i = j
56+
57+
return res
58+
59+
60+
61+
62+
63+
'''
64+
Detailed Algorithm Explanation
65+
Part 1: Why Group by num % k?
66+
Two numbers can have a difference of exactly k only if they have the same remainder when divided by k.
67+
68+
Mathematical proof:
69+
70+
If a - b = k, then a = b + k
71+
Therefore: a % k = (b + k) % k = b % k
72+
Example: nums = [2, 3, 5, 8], k = 5
73+
74+
num | num % 5 | group
75+
----|---------|-------
76+
2 | 2 | Group A
77+
3 | 3 | Group B
78+
5 | 0 | Group C
79+
8 | 3 | Group B
80+
81+
82+
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.
83+
84+
Part 2: Building Chains
85+
Within each group, we sort and find chains where consecutive elements differ by exactly k.
86+
87+
Example with Group B: [3, 8]
88+
89+
Sorted: [3, 8]
90+
Check: 8 - 3 = 5 ✓
91+
Chain: 3 → 8
92+
93+
Another example: nums = [1, 6, 11, 21], k = 5 (all have remainder 1)
94+
95+
Sorted: [1, 6, 11, 21]
96+
Check: 6-1=5 ✓, 11-6=5 ✓, 21-11=10 ✗
97+
Chains: [1 → 6 → 11], [21]
98+
99+
100+
Part 3: House Robber DP - The Core Logic
101+
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.
102+
103+
DP State Variables
104+
take = number of valid subsets that INCLUDE the current element
105+
skip = number of valid subsets that EXCLUDE the current element
106+
107+
DP Transitions
108+
new_take = skip # To take current, we MUST have skipped previous
109+
new_skip = take + skip # To skip current, we can take or skip previous
110+
111+
'''
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from typing import List, Union, Collection, Mapping, Optional
2+
3+
class Solution:
4+
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
5+
6+
# Make a grid of 0's with len(text2) + 1 columns
7+
# and len(text1) + 1 rows.
8+
len_1 = len(text1)
9+
len_2 = len(text2)
10+
dp_grid = [[0]*(len_2+1) for _ in range(len_1+1)]
11+
12+
# Iterate up each column, starting from the last one.
13+
for j in reversed(range(len_2)):
14+
for i in reversed(range(len_1)):
15+
if text1[i] == text2[j]:
16+
dp_grid[i][j] = dp_grid[i+1][j+1] + 1
17+
else:
18+
dp_grid[i][j] = max(dp_grid[i+1][j], dp_grid[i][j+1])
19+
20+
# The original problem's answer is in dp_grid[0][0]. Return it.
21+
return dp_grid[0][0]
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
from typing import List
2+
from collections import deque, defaultdict
3+
4+
class Solution:
5+
def alienOrder(self, words: List[str]) -> str:
6+
"""Topological sort (Kahn's BFS) over letters.
7+
8+
Each adjacent pair of words gives at most one ordering fact: the first
9+
position where they differ tells us c1 comes before c2. Everything after
10+
that position is unconstrained, so we stop comparing there. The invalid
11+
case is a prefix violation - ["abc", "ab"] - since a proper prefix must
12+
sort first no matter what the alphabet is.
13+
14+
Time O(C) where C is total characters, space O(1) - at most 26 nodes and
15+
26 * 26 edges.
16+
"""
17+
18+
adj = {c: set() for word in words for c in word}
19+
indegree = {c: 0 for c in adj}
20+
21+
for first, second in zip(words, words[1:]):
22+
for c1, c2 in zip(first, second):
23+
if c1 != c2:
24+
if c2 not in adj[c1]:
25+
adj[c1].add(c2)
26+
indegree[c2] += 1
27+
break
28+
else:
29+
# No differing character: second must not be a strict prefix
30+
if len(second) < len(first):
31+
return ""
32+
33+
q = deque([c for c in indegree if indegree[c] == 0])
34+
order = []
35+
36+
while q:
37+
c = q.popleft()
38+
order.append(c)
39+
40+
for nxt in adj[c]:
41+
indegree[nxt] -= 1
42+
if indegree[nxt] == 0:
43+
q.append(nxt)
44+
45+
# A leftover letter means it sits on a cycle
46+
return "".join(order) if len(order) == len(adj) else ""
47+
48+
49+
def alienOrder_DFS(self, words: List[str]) -> str:
50+
"""Same graph, post-order DFS with cycle detection.
51+
52+
visiting[c] is True while c is on the current recursion stack (cycle) and
53+
False once it is fully processed. Post-order emits a letter only after all
54+
of its successors, so the reversed result is the topological order.
55+
"""
56+
57+
adj = defaultdict(set)
58+
letters = {c for word in words for c in word}
59+
60+
for first, second in zip(words, words[1:]):
61+
for c1, c2 in zip(first, second):
62+
if c1 != c2:
63+
adj[c1].add(c2)
64+
break
65+
else:
66+
if len(second) < len(first):
67+
return ""
68+
69+
visiting = {}
70+
order = []
71+
72+
def dfs(c: str) -> bool:
73+
"""Returns True if a cycle is reached from c."""
74+
if c in visiting:
75+
return visiting[c]
76+
77+
visiting[c] = True
78+
for nxt in adj[c]:
79+
if dfs(nxt):
80+
return True
81+
82+
visiting[c] = False
83+
order.append(c)
84+
return False
85+
86+
for c in letters:
87+
if dfs(c):
88+
return ""
89+
90+
return "".join(reversed(order))

0 commit comments

Comments
 (0)