Skip to content

Commit 8c8622f

Browse files
committed
feat(algorithms): new algorithm challenges
1 parent 237b99e commit 8c8622f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+829
-57
lines changed

algorithms/counting/__init__.py

Whitespace-only changes.
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
# Rank Teams by Votes
2+
3+
In a special ranking system, each voter gives a rank from highest to lowest to all teams participating in the competition.
4+
5+
The ordering of teams is decided by who received the most position-one votes. If two or more teams tie in the first
6+
position, we consider the second position to resolve the conflict, if they tie again, we continue this process until the
7+
ties are resolved. If two or more teams are still tied after considering all positions, we rank them alphabetically
8+
based on their team letter.
9+
10+
You are given an array of strings votes which is the votes of all voters in the ranking systems. Sort all teams according
11+
to the ranking system described above.
12+
13+
Return a string of all teams sorted by the ranking system.
14+
15+
## Examples
16+
17+
Example 1:
18+
```text
19+
Input: votes = ["ABC","ACB","ABC","ACB","ACB"]
20+
Output: "ACB"
21+
Explanation:
22+
Team A was ranked first place by 5 voters. No other team was voted as first place, so team A is the first team.
23+
Team B was ranked second by 2 voters and ranked third by 3 voters.
24+
Team C was ranked second by 3 voters and ranked third by 2 voters.
25+
As most of the voters ranked C second, team C is the second team, and team B is the third.
26+
```
27+
28+
Example 2:
29+
```text
30+
Input: votes = ["WXYZ","XYZW"]
31+
Output: "XWYZ"
32+
Explanation:
33+
X is the winner due to the tie-breaking rule. X has the same votes as W for the first position, but X has one vote in
34+
the second position, while W does not have any votes in the second position.
35+
```
36+
37+
Example 3:
38+
39+
```text
40+
Input: votes = ["ZMNAGUEDSJYLBOPHRQICWFXTVK"]
41+
Output: "ZMNAGUEDSJYLBOPHRQICWFXTVK"
42+
Explanation: Only one voter, so their votes are used for the ranking.
43+
```
44+
45+
## Constraints
46+
47+
- 1 <= votes.length <= 1000
48+
- 1 <= votes[i].length <= 26
49+
- votes[i].length == votes[j].length for 0 <= i, j < votes.length.
50+
- votes[i][j] is an English uppercase letter.
51+
- All characters of votes[i] are unique.
52+
- All the characters that occur in votes[0] also occur in votes[j] where 1 <= j < votes.length.
53+
54+
## Hints
55+
56+
- Build array rank where rank[i][j] is the number of votes for team i to be the j-th rank.
57+
- Sort the teams by rank array. if rank array is the same for two or more teams, sort them by the ID in ascending order.
58+
59+
## Topics
60+
61+
- Array
62+
- Hash Table
63+
- String
64+
- Sorting
65+
- Counting
66+
67+
## Solution
68+
69+
The key intuition for solving this problem is to efficiently keep track of the vote counts each team receives for each
70+
position. We create a 2D array for this purpose, where the rows represent the teams and the columns represent the vote
71+
counts for each position. For each team in `votes[i][j]`, we update its vote count at the jth index in the counts array.
72+
After processing all votes, we sort the counts array based on the values in each row, prioritizing the row with the
73+
lowest value at the first index. If there is a tie, the row with the lowest value at the second index takes precedence,
74+
and so on. The final result is constructed using team's name corresponding to each row in the sorted array.
75+
76+
Using the above intuition, the solution can be implemented as follows:
77+
78+
1. Create a 2D array counts with dimensions 26×27 to store vote counts for each team.
79+
- The first dimension (26) corresponds to the teams (A–Z), as there can be a maximum of 26 teams.
80+
- The second dimension (27) holds the vote counts for each rank. Because there can be a maximum of 26 teams, the
81+
maximum rank a team can get is 26 . It includes an additional column to store the team’s name to ensure that we
82+
don’t lose track of which team’s data is in each row after sorting.
83+
2. For each team index t, store the team’s name at counts[t][26]. This keeps track of which team corresponds to each row
84+
in the counts array.
85+
3. Start iterating through each string votes[i] to process the ranking given by each voter.
86+
4. For each character `votes[i][j]` representing a team in the vote, decrement its rank in the counts. This is because each
87+
row in counts denotes the rank of a team; a lower count indicates a higher rank.
88+
5. Sort the counts array based on the values in each row. The sorting is done such that the row with the lowest value at
89+
the first index comes first. If there is a tie, the row with the lowest value at the second index takes precedence,
90+
and so on.
91+
6. Construct the final result by retrieving the team name stored at the last index (counts[i][26]) in the sorted counts.
92+
7. Return the constructed result, which contains all teams sorted according to the specified ranking system.
93+
94+
### Time Complexity
95+
96+
Given the length of the votes is n, the time complexity can be calculates as:
97+
98+
- The time complexity of iterating each character in each string in votes is O(n×26), which is simplified to O(n).
99+
- The time complexity of sorting counts is `O(26×26log(26))`, which is simplified to O(1)
100+
- The time complexity of building the result string is O(26), which is simplified to O(1).
101+
102+
Therefore, the time complexity of the above algorithm is O(n).
103+
104+
### Space Complexity
105+
106+
The algorithm’s space complexity is O(26×27) occupied by counts, which is simplified to O(1).
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
from typing import DefaultDict, List
2+
from collections import defaultdict
3+
4+
5+
def rank_teams_using_hash_map(votes: List[str]) -> str:
6+
"""
7+
Ranks teams given a list of votes.
8+
Args:
9+
votes (List[str]): List of votes.
10+
Returns:
11+
str: Correctly ranked teams.
12+
"""
13+
if not votes:
14+
return ""
15+
if len(votes) == 1:
16+
return votes[0]
17+
18+
# each team's number of votes is equal
19+
votes_per_team = len(votes[0])
20+
# stores a mapping of the team's name to the number of votes per position
21+
# so, e.g. {'A': [5, 0, 0], 'B': [0, 2, 3], ...}, indicates that A received 5 votes for position 1
22+
# and B received 2 votes for position 2 and 3 for position 3, etc.
23+
teams: DefaultDict[str, List[int]] = defaultdict(list)
24+
25+
# Iterate through the votes. Getting the position each voter ranked the given team
26+
for voter in votes:
27+
# For each voter, get where they ranked the given team
28+
for position, team in enumerate(voter):
29+
# If the team already exists in the mapping
30+
if team in teams:
31+
teams[team][position] += 1
32+
else:
33+
# the team has not yet been captured, so, we set the count of votes per team
34+
teams[team] = [0] * votes_per_team
35+
teams[team][position] = 1
36+
37+
sorted_teams = sorted(
38+
teams.items(), key=lambda item: ([-x for x in item[1]], item[0])
39+
)
40+
41+
return "".join(t[0] for t in sorted_teams)
42+
43+
44+
def rank_teams_counting(votes: List[str]) -> str:
45+
counts: List[List[int | str]] = [[0] * 27 for _ in range(26)]
46+
47+
for t in range(26):
48+
counts[t][26] = chr(ord("A") + t)
49+
50+
for i in range(len(votes)):
51+
for j, c in enumerate(votes[i]):
52+
counts[ord(c) - ord("A")][j] -= 1
53+
54+
counts.sort()
55+
56+
res = ""
57+
58+
for i in range(len(votes[0])):
59+
res += counts[i][26]
60+
61+
return res
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import unittest
2+
from typing import List
3+
from parameterized import parameterized
4+
5+
from algorithms.counting.rank_teams_by_votes import (
6+
rank_teams_using_hash_map,
7+
rank_teams_counting,
8+
)
9+
10+
RANK_TEAMS_BY_VOTES_TEST_CASES = [
11+
(["ABC", "ACB", "ABC", "ACB", "ACB"], "ACB"),
12+
(["WXYZ", "XYZW"], "XWYZ"),
13+
(["ZMNAGUEDSJYLBOPHRQICWFXTVK"], "ZMNAGUEDSJYLBOPHRQICWFXTVK"),
14+
]
15+
16+
17+
class RankTeamsTestCase(unittest.TestCase):
18+
@parameterized.expand(RANK_TEAMS_BY_VOTES_TEST_CASES)
19+
def test_rank_teams_hash_map(self, votes: List[str], expected: str):
20+
actual = rank_teams_using_hash_map(votes)
21+
self.assertEqual(expected, actual)
22+
23+
@parameterized.expand(RANK_TEAMS_BY_VOTES_TEST_CASES)
24+
def test_rank_teams_counting(self, votes: List[str], expected: str):
25+
actual = rank_teams_counting(votes)
26+
self.assertEqual(expected, actual)
27+
28+
29+
if __name__ == "__main__":
30+
unittest.main()

algorithms/dynamic_programming/total_appeal_of_a_string/__init__.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def appeal_sum_dp(s: str) -> int:
1212

1313
for idx, char in enumerate(s):
1414
# Convert character to index (0-25 for a-z)
15-
char_index = ord(char) - ord('a')
15+
char_index = ord(char) - ord("a")
1616
# Calculate new substrings that include current character
1717
# These are all substrings from (last occurrence of char + 1) to current_index
1818
# Each adds 1 to the appeal count if char wasn't in that substring before
@@ -26,6 +26,7 @@ def appeal_sum_dp(s: str) -> int:
2626

2727
return total_appeal_sum
2828

29+
2930
def appeal_sum_hash_table(s: str) -> int:
3031
# Create a hash map to track the last occurrence index of each character
3132
track = defaultdict(lambda: -1)
@@ -47,4 +48,4 @@ def appeal_sum_hash_table(s: str) -> int:
4748
track[c] = i
4849

4950
# Return the total appeal sum
50-
return appeal
51+
return appeal

algorithms/dynamic_programming/total_appeal_of_a_string/test_appeal_of_a_string.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import unittest
22
from parameterized import parameterized
3-
from algorithms.dynamic_programming.total_appeal_of_a_string import appeal_sum_dp, appeal_sum_hash_table
3+
from algorithms.dynamic_programming.total_appeal_of_a_string import (
4+
appeal_sum_dp,
5+
appeal_sum_hash_table,
6+
)
47

58
APPEAL_SUM_TEST_CASES = [
69
("abbca", 28),
@@ -12,6 +15,7 @@
1215
("hippopotamus", 279),
1316
]
1417

18+
1519
class AppealSumTestCase(unittest.TestCase):
1620
@parameterized.expand(APPEAL_SUM_TEST_CASES)
1721
def test_appeal_sum_dp(self, s: str, expected: int):
@@ -23,5 +27,6 @@ def test_appeal_sum_hash_table(self, s: str, expected: int):
2327
actual = appeal_sum_hash_table(s)
2428
self.assertEqual(expected, actual)
2529

26-
if __name__ == '__main__':
30+
31+
if __name__ == "__main__":
2732
unittest.main()

algorithms/graphs/number_of_islands/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,58 @@ Input: grid = [
3333
["0","0","0","1","1"]
3434
]
3535
Output: 3
36+
37+
---
38+
# Number of Distinct Islands
39+
40+
Given an m x n binary matrix where 1 represents land and 0 represents water. An island is a group of connected 1s that
41+
are adjacent horizontally or vertically. Two islands are considered the same if one matches the other without rotating
42+
or flipping. The task is to return the number of distinct islands.
43+
44+
## Constraints
45+
46+
- m == grid.length
47+
- n == grid[i].length
48+
- 1≤ m, n ≤ 100
49+
- grid[i][j] is either 0 or 1.
50+
51+
## Examples
52+
53+
![Example 1](./images/examples/number_of_distinct_islands_example_1.png)
54+
![Example 2](./images/examples/number_of_distinct_islands_example_2.png)
55+
![Example 3](./images/examples/number_of_distinct_islands_example_3.png)
56+
![Example 4](./images/examples/number_of_distinct_islands_example_4.png)
57+
58+
### Solution
59+
60+
This algorithm is designed to find the number of distinct islands in a grid, where each island is a group of connected
61+
1s. The idea is to treat the grid as a map and perform a depth-first search(DFS) to explore every piece of land. During
62+
DFS, the shape of the island is stored by the relative positions of land cells to the initial starting point of the
63+
island. This means the coordinates are normalized so that two islands can be compared even in different locations on the
64+
grid. These shapes are stored in the dictionary as keys, and the count of islands having the same shape is stored as a
65+
value. The result is the number of keys in the dictionary, which represents the distinct islands.
66+
Here’s the step-by-step implementation of the solution:
67+
- Initialize a set to track visited land cells and a dictionary to store the shapes of islands.
68+
- Loop through every cell in the grid. For each unvisited cell containing 1:
69+
- Call DFS on the cell.
70+
- Explore all connected land cells recursively in all four directions (up, down, left, right) until the entire island
71+
is mapped.
72+
- Normalize every land cell included in an island by subtracting the coordinates of that land cell from the coordinates
73+
of the first cell in the respective island.
74+
75+
- Once DFS completes exploring an island, the shape is stored in the dictionary. The key in this dictionary represents
76+
the shape, and the value is a count of the islands having the same shape.
77+
- After processing the entire grid, return the dictionary as the number of distinct island shapes is the number of unique
78+
keys in the dictionary.
79+
80+
### Time Complexity
81+
82+
The time complexity of this solution is O(m*n), where n is the number of rows and m is the number of columns in the grid.
83+
This is because the DFS visits every cell exactly once, and each cell is processed in constant time as it checks the
84+
surrounding cells for potential land connections. The total time to explore all islands is proportional to the grid size.
85+
86+
### Space Complexity
87+
88+
The space complexity of this solution is O(m*n), where n is the number of rows and m is the number of columns. This is
89+
because the set and dictionary store up to m*n entries in the worst case, where every cell is a land cell. Additionally,
90+
the recursion depth of the DFS can reach m*n if the grid is entirely land.

0 commit comments

Comments
 (0)