Introduction π
Welcome to the NeetCode Solutions book! π This book compiles solutions to the NeetCodeΒ 150 list of problems.
I created this mdbook to facilitate my quick revision and I'm sharing it because I found it incredibly useful. ππ§
Feel free to connect with me on LinkedIn:
- LinkedIn πΌ
Credits π
Special thanks to:
- NeetCode.io - For the amazing problem list and explanations π―
- NeetCode Solutions Repository - For comprehensive solution implementations π»
Arrays and Hashing π
This section contains problems belonging to the Arrays and Hashing category.
Problems
- π’ Easy: Contains Duplicate
- π’ Easy: Valid Anagram
- π’ Easy: Two Sum
- π‘ Medium: Group Anagrams
- π‘ Medium: Top K Frequent Elements
- π‘ Medium: Product of Array Except Self
- π‘ Medium: Valid Sudoku
- π‘ Medium: Encode and Decode Strings
- π‘ Medium: Longest Consecutive Sequence
Contains Duplicate π§
LeetCode Link: Contains Duplicate
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 217 - Contains Duplicate
Intuition: To solve this problem, we can utilize the property of a hash set. By storing each element encountered in the set and checking for collisions, we can efficiently determine if any duplicates exist in the array. If a collision occurs, it indicates the presence of a duplicate element.
Approach:
- Initialize an empty hash set.
- Iterate through each element num in the input array nums:
- If num is already present in the hash set, return true as we have found a duplicate.
- Otherwise, add num to the hash set.
- If no duplicates are found after iterating through all elements, return false.
β Time Complexity: The time complexity of this approach is O(n), where n is the size of the input array nums. This is because we iterate through the array once and perform constant-time operations for each element.
πΎ Space Complexity: The space complexity is O(n), as the hash set can potentially store all elements of the input array.
Solutions π‘
Cpp π»
class Solution {
public:
bool containsDuplicate(vector<int> &nums) {
unordered_set<int> seen;
for (int num : nums) {
if (seen.count(num) > 0) {
return true; // Duplicate found
}
seen.insert(num);
}
return false; // No duplicates found
}
};
/*
auto init = [](){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
return 0;
}();
class Solution {
public:
bool containsDuplicate(vector<int>& nums) {
size_t count = nums.size();
// Sort using LSD Radix
Sort(reinterpret_cast<unsigned int*>(&(nums[0])), count);
--count;
for(size_t i = 0; i < count; ++i)
if(nums[i] == nums[i+1])
return true;
return false;
}
private:
void Sort(unsigned int* start, size_t len) {
unsigned int* buffer = new unsigned int[len];
LSDRadix(start, buffer, len);
delete[] buffer;
}
void LSDRadix(unsigned int* input, unsigned int* buffer, size_t len) {
for(int bits = 0; bits < 16; bits += 8) {
size_t count[256] = {0};
for(int x = 0; x < len; ++x)
++count[(input[x] >> bits) & 0xff];
for(int x = 0; x < 255; ++x)
count[x + 1] += count[x];
for(int x = len - 1; x >= 0; --x)
buffer[--count[(input[x] >> bits) & 0xff]] = input[x];
unsigned int* temp = input;
input = buffer;
buffer = temp;
}
}
};
*/
Python π
class Solution:
def containsDuplicate(self, nums: List[int]) -> bool:
hashset = set()
for n in nums:
if n in hashset:
return True
hashset.add(n)
return False
Valid Anagram π§
LeetCode Link: Valid Anagram
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 242 - Valid Anagram
Description: Given two strings s and t, return true if t is an anagram of s, and false otherwise.
Intuition: To determine if two strings are valid anagrams, we can compare the counts of each character in both strings. If the counts of all characters are equal, the strings are valid anagrams.
Approach:
- If the lengths of the two input strings s and t are not equal, return false.
- Initialize an array of size 26 to store the counts of each character.
- Iterate through each character in string s and increment its count in the array.
- Iterate through each character in string t and decrement its count in the array.
- If any count in the array is not zero, return false as the characters do not match.
- Return true if all counts in the array are zero.
β Time Complexity: The time complexity of this approach is O(n), where n is the length of the input strings s and t. This is because we iterate through both strings once to update the counts of each character.
πΎ Space Complexity: The space complexity is O(1), as we use a fixed-size array of size 26 to store the counts.
Solutions π‘
Cpp π»
auto init = []() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
return 0;
}
();
class Solution {
public:
bool isAnagram(string s, string t) {
if (s.length() != t.length()) {
return false;
}
int count[26] = {0};
for (char ch : s) {
count[ch - 'a']++;
}
for (char ch : t) {
count[ch - 'a']--;
}
for (int i = 0; i < 26; i++) {
if (count[i] != 0) {
return false;
}
}
return true;
}
};
// class Solution {
// public:
// bool isAnagram(string s, string t) {
// if(s.size() != t.size())
// return false;
// sort(s.begin(), s.end());
// sort(t.begin(), t.end());
// return s == t;
// }
// };
Python π
class Solution:
def isAnagram(self, s: str, t: str) -> bool:
if len(s) != len(t):
return False
char_frequency = {}
# Build character frequency map for string s
for char in s:
char_frequency[char] = char_frequency.get(char, 0) + 1
# Compare with string t
for char in t:
if char not in char_frequency or char_frequency[char] == 0:
return False
char_frequency[char] -= 1
return True
Two Sum π§
LeetCode Link: Two Sum
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 1 - Two Sum
Description: Given an array of integers nums and an integer target, return indices of the two numbers such that they add up to target.
Intuition: To find the pair of numbers that add up to the target, we can utilize a hash map. By iterating through the array and checking if the complement (target - current number) exists in the hash map, we can efficiently find the desired pair.
Approach:
- Initialize an empty hash map to store the elements and their indices.
- Iterate through each element num and its index in the input array nums:
- Calculate the complement as target - num.
- If the complement exists in the hash map, return the indices [map[complement], index].
- Otherwise, add the current element num and its index to the hash map.
- If no solution is found, return an empty vector or handle it as per the problem's requirement.
β Time Complexity: The time complexity of this approach is O(n), where n is the size of the input array nums. This is because we iterate through the array once and perform constant-time operations for each element.
πΎ Space Complexity: The space complexity is O(n), as the hash map can potentially store all elements of the input array.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> twoSum(vector<int> &nums, int target) {
unordered_map<int, int> numMap;
for (int i = 0; i < nums.size(); i++) {
int complement = target - nums[i];
if (numMap.count(complement) > 0) {
return {numMap[complement], i};
}
numMap[nums[i]] = i;
}
return {}; // Handle no solution as per problem's requirement
}
};
// class Solution {
// public:
// vector<int> twoSum(vector<int>& nums, int target) {
// unordered_map<int, int> M;
// vector<int> res;
// for(int i = 0; i < nums.size(); i++) {
// int compliment = target - nums[i];
// // If we find the compliment
// if(M.find(compliment) != M.end()) {
// // returning i and index of compliment
// res.push_back(M[compliment]);
// res.push_back(i);
// }
// else // insert pair
// M.insert( {nums[i], i});
// }
// return res;
// }
// };
Python π
class Solution:
def twoSum(self, nums: List[int], target: int) -> List[int]:
prevMap = {} # val -> index
for i, n in enumerate(nums):
diff = target - n
if diff in prevMap:
return [prevMap[diff], i]
prevMap[n] = i
Group Anagrams π§
LeetCode Link: Group Anagrams
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 49 - Group Anagrams
Description: Given an array of strings strs, group the anagrams together. You can return the answer in any order.
Intuition: An anagram is a word formed by rearranging the letters of another word. To group anagrams together, we can utilize a hash map. By sorting each string and using the sorted string as a key in the hash map, we can efficiently group the anagrams.
Approach:
- Initialize an empty hash map to store the groups of anagrams.
- Iterate through each string str in the input array strs:
- Sort the characters of str to create a sorted string.
- If the sorted string is already present in the hash map, add str to its corresponding group.
- Otherwise, create a new group in the hash map with the sorted string as the key and str as the initial value.
- Return the values of the hash map, which represent the grouped anagrams.
β Time Complexity: The time complexity of this approach is O(n * k * log k), where n is the size of the input array strs and k is the maximum length of a string in strs. This is because we iterate through the array and sort each string, which takes O(k * log k) time for each string.
πΎ Space Complexity: The space complexity is O(n * k), as we store all the strings in the hash map, where n is the number of groups and k is the maximum length of a string.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string> &strs) {
// To store the sorted string and all its anagrams
unordered_map<string, vector<string>> M;
/*
Consider example 1 : strs = ["eat","tea","tan","ate","nat","bat"]
After the below opeartion of for loop map will contain -
aet -- eat, tea, ate
ant -- tan, nat
abt -- bat
*/
for (int i = 0; i < strs.size(); i++) {
string T = strs[i];
sort(T.begin(), T.end());
M[T].push_back(strs[i]);
}
// Now pushing all the anagrams(vector<string>) of one word, one by one, into result vector
vector< vector<string> > result;
for (auto i : M) {
result.push_back(i.second);
}
return result;
}
};
Python π
class Solution:
def groupAnagrams(self, strs: List[str]) -> List[List[str]]:
anagrams_map = {}
for word in strs:
sorted_word = "".join(sorted(word))
if sorted_word in anagrams_map:
anagrams_map[sorted_word].append(word)
else:
anagrams_map[sorted_word] = [word]
return list(anagrams_map.values())
Top K Frequent Elements π§
LeetCode Link: Top K Frequent Elements
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 347 - Top K Frequent Elements
Description: Given an integer array nums and an integer k, return the k most frequent elements. You may return the answer in any order.
Intuition: To find the k most frequent elements, we can utilize a combination of a hash map and bucket sort. By counting the frequencies of elements using the hash map and using bucket sort to group elements by their frequencies, we can efficiently find the k most frequent elements.
Approach:
- Initialize an empty hash map, m, to store the frequencies of elements.
- Iterate through each element num in the input array nums:
- Increment the frequency count of num in the hash map.
- Create a vector of vectors, buckets, to act as buckets for grouping elements based on their frequencies.
- Iterate through the key-value pairs in the hash map:
- Place each key (element) in the corresponding bucket based on its frequency.
- Create an empty vector, result, to store the k most frequent elements.
- Iterate from the highest bucket index down to 0:
- If the result vector size reaches k, break the loop.
- If the current bucket is not empty, append its elements to the result vector.
- Return the result vector, which represents the k most frequent elements.
β Time Complexity: The time complexity of this approach is O(n), where n is the size of the input array nums. This is because we iterate through the array once to count the frequencies and place elements in the buckets.
πΎ Space Complexity: The space complexity is O(n), as we store the frequencies of elements in the hash map and the bucket vectors.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> topKFrequent(vector<int> &nums, int k) {
int n = nums.size();
unordered_map<int, int> m;
for (int i = 0; i < n; i++) {
m[nums[i]]++;
}
vector<vector<int>> buckets(n + 1);
for (auto it = m.begin(); it != m.end(); it++) {
buckets[it->second].push_back(it->first);
}
vector<int> result;
for (int i = n; i >= 0; i--) {
if (result.size() >= k) {
break;
}
if (!buckets[i].empty()) {
result.insert(result.end(), buckets[i].begin(), buckets[i].end());
}
}
return result;
}
};
/*
! Using Priority Queue
Problem: LeetCode 347 - Top K Frequent Elements
Description:
Given an integer array nums and an integer k, return the k most frequent elements. You may return the answer in any order.
Intuition:
To find the k most frequent elements, we can utilize a combination of a hash map and a priority queue (min-heap). By counting the frequencies of elements using the hash map and maintaining a min-heap of size k, we can efficiently find the k most frequent elements.
Approach:
1. Initialize an empty hash map to store the frequencies of elements.
2. Iterate through each element num in the input array nums:
- Increment the frequency count of num in the hash map.
3. Initialize a min-heap to store the k most frequent elements based on their frequencies.
4. Iterate through the elements and frequencies in the hash map:
- Push the current element into the min-heap.
- If the size of the min-heap exceeds k, remove the smallest element (with the lowest frequency).
5. Return the elements in the min-heap, which represent the k most frequent elements.
Time Complexity:
The time complexity of this approach is O(n log k), where n is the size of the input array nums.
This is because we iterate through the array once to count the frequencies and perform log k operations for each element insertion and removal in the min-heap.
Space Complexity:
The space complexity is O(n), as we store the frequencies of elements in the hash map and the k most frequent elements in the min-heap.
*/
/*
class Solution {
public:
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> freqMap;
for (int num : nums) {
freqMap[num]++;
}
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> minHeap;
for (auto& kvp : freqMap) {
minHeap.push({kvp.second, kvp.first});
if (minHeap.size() > k) {
minHeap.pop();
}
}
vector<int> result;
while (!minHeap.empty()) {
result.push_back(minHeap.top().second);
minHeap.pop();
}
return result;
}
};
*/
// ! O(n log n) solution
/*
class Solution {
public:
// Approach - First make a frequency map normally, then insert key value pairs(frequency first, value second) in vector and sort in descending order
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> M;
vector< pair<int, int> > arr;
vector<int> result;
for(int i = 0; i < nums.size(); i++) {
// it reaches end of map if it didn't find the element
if(M.find(nums[i]) == M.end())
M[nums[i]] = 1;
else
M[nums[i]]++;
}
for(auto i : M) {
arr.push_back( make_pair(i.second, i.first) );
}
sort(arr.rbegin(), arr.rend());
for(int i = 0; i < k; i++) {
result.push_back(arr[i].second);
}
return result;
}
};
*/
Python π
import heapq
class Solution:
def topKFrequent(self, nums: List[int], k: int) -> List[int]:
frequency_map = {}
for num in nums:
frequency_map[num] = frequency_map.get(num, 0) + 1
min_heap = []
for num, frequency in frequency_map.items():
heapq.heappush(min_heap, (frequency, num))
if len(min_heap) > k:
heapq.heappop(min_heap)
return [num for frequency, num in min_heap]
Product of Array Except Self π§
LeetCode Link: Product of Array Except Self
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 238 - Product of Array Except Self
Description: Given an integer array nums, return an array output such that output[i] is equal to the product of all the elements of nums except nums[i].
Intuition: To find the product of all elements except self, we can utilize prefix and suffix products. By calculating the prefix product from the left and the suffix product from the right, we can obtain the desired result efficiently.
Approach:
- Initialize an output array of the same size as the input array nums.
- Calculate the prefix product from the left side of the array and store it in the output array.
- Calculate the suffix product from the right side of the array and multiply it with the corresponding element in the output array.
- Return the resulting output array.
β Time Complexity: The time complexity of this approach is O(n), where n is the size of the input array nums. This is because we iterate through the array twice, once for calculating the prefix product and once for calculating the suffix product.
πΎ Space Complexity: The space complexity is O(1), as we are reusing the input array for storing the output.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> productExceptSelf(vector<int> &nums) {
int n = nums.size();
vector<int> output(n, 1);
// Calculate prefix product
int prefix = 1;
for (int i = 0; i < n; i++) {
output[i] *= prefix;
prefix *= nums[i];
}
// Calculate suffix product and multiply with prefix product stored in the output array
int suffix = 1;
for (int i = n - 1; i >= 0; i--) {
output[i] *= suffix;
suffix *= nums[i];
}
return output;
}
};
// class Solution {
// public:
// vector<int> productExceptSelf(vector<int>& nums) {
// vector<int> res(nums.size(), 1);
// // First, compute the prefix product and store in res
// // res[i] = product of elements in nums from index 0, 1, ... to i - 1
// for (int i = 0; i < nums.size() - 1; i++) {
// res[i+1] = nums[i] * res[i];
// }
// // Second, compute the final result
// int suffixProduct = 1;
// for(int j = nums.size()-1; j > 0; j--) {
// suffixProduct *= nums[j];
// res[j-1] *= suffixProduct;
// }
// return res;
// }
// };
Python π
class Solution:
def productExceptSelf(self, nums: List[int]) -> List[int]:
n = len(nums)
result = [1] * n
# Calculate the left product of each element
left_product = 1
for i in range(n):
result[i] *= left_product
left_product *= nums[i]
# Calculate the right product of each element and update the result list
right_product = 1
for i in range(n - 1, -1, -1):
result[i] *= right_product
right_product *= nums[i]
return result
Valid Sudoku π§
LeetCode Link: Valid Sudoku
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 36 - Valid Sudoku
Description: Given a 9 x 9 Sudoku board, determine if it is a valid Sudoku. The board is only partially filled, and each digit from 1 to 9 must appear exactly once in each row, column, and 3 x 3 sub-grid.
Intuition: To check if a Sudoku board is valid, we need to verify that each digit appears exactly once in each row, column, and 3 x 3 sub-grid. We can use three separate 2D arrays to keep track of the digits already used in each row, column, and sub-grid.
Approach:
- Initialize three 2D arrays, usedRows, usedCols, and usedSubgrids, with all values set to 0.
- Iterate through each cell in the Sudoku board:
- If the current cell is not empty:
- Convert the character to an integer and subtract 1 to get the corresponding number index.
- Calculate the sub-grid index based on the current cell's position.
- Check if the number is already used in the current row, column, or sub-grid by looking at the corresponding index in the usedRows, usedCols, and usedSubgrids arrays.
- If the number is already used, return false as the Sudoku board is not valid.
- Mark the number as used in the current row, column, and sub-grid by setting the corresponding index to 1 in the usedRows, usedCols, and usedSubgrids arrays.
- If all cells pass the checks, return true as the Sudoku board is valid.
β Time Complexity: The time complexity of this approach is O(1) since the Sudoku board has a fixed size of 9 x 9, and the iteration is constant.
πΎ Space Complexity: The space complexity is O(1) since the arrays used for tracking the used digits (usedRows, usedCols, and usedSubgrids) have a fixed size of 9 x 9.
Solutions π‘
Cpp π»
class Solution {
public:
bool isValidSudoku(vector<vector<char>> &board) {
int usedRows[9][9] = {0};
int usedCols[9][9] = {0};
int usedSubgrids[9][9] = {0};
for (int i = 0; i < board.size(); ++i) {
for (int j = 0; j < board[i].size(); ++j) {
if (board[i][j] != '.') {
int num = board[i][j] - '0' - 1;
int subgridIndex = (i / 3) * 3 + j / 3;
if (usedRows[i][num] || usedCols[j][num] || usedSubgrids[subgridIndex][num]) {
return false;
}
usedRows[i][num] = usedCols[j][num] = usedSubgrids[subgridIndex][num] = 1;
}
}
}
return true;
}
};
Python π
class Solution:
def isValidSudoku(self, board: List[List[str]]) -> bool:
seen = set()
for i in range(9):
for j in range(9):
if board[i][j] != ".":
num = board[i][j]
if (
(i, num) in seen
or (num, j) in seen
or (i // 3, j // 3, num) in seen
):
return False
seen.add((i, num))
seen.add((num, j))
seen.add((i // 3, j // 3, num))
return True
Encode and Decode Strings π§
LeetCode Link: Encode and Decode Strings
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 271 - Encode and Decode Strings
Description: Design an algorithm to encode a list of strings to a string and decode the string to the original list of strings. The encoded string should be as compact as possible.
Intuition: To encode and decode a list of strings, we need a way to separate individual strings and handle any special characters. One approach is to use a delimiter to separate the strings, and escape any occurrences of the delimiter within the strings.
Approach: Encoding:
- Iterate through the input list of strings:
- For each string, append its length followed by a delimiter (e.g., '#') to the encoded string.
- Append the actual string content to the encoded string.
- Return the encoded string.
Decoding:
- Initialize an empty result list of strings.
- While the encoded string is not empty:
- Extract the length of the next string from the encoded string until the delimiter.
- Convert the length string to an integer.
- Extract the next substring of length characters from the encoded string.
- Append the substring to the result list.
- Return the result list.
β Time Complexity: The time complexity for encoding is O(n), where n is the total number of characters in all the strings. For decoding, the time complexity is also O(n), as we need to iterate through the encoded string to extract and reconstruct the original strings.
πΎ Space Complexity: The space complexity is O(n), where n is the total number of characters in all the strings. This is because we need to store the encoded string, which contains all the characters from the input strings.
Solutions π‘
Cpp π»
class Codec {
public:
// Encodes a list of strings to a single string.
string encode(vector<string> &strs) {
string encodedStr;
for (const string &str : strs) {
encodedStr += to_string(str.length()) + "#" + str;
}
return encodedStr;
}
// Decodes a single string to a list of strings.
vector<string> decode(string s) {
vector<string> decodedStrs;
int i = 0;
while (i < s.length()) {
int delimiterIndex = s.find("#", i);
int strLength = stoi(s.substr(i, delimiterIndex - i));
decodedStrs.push_back(s.substr(delimiterIndex + 1, strLength));
i = delimiterIndex + strLength + 2;
}
return decodedStrs;
}
};
Python π
class Codec:
def encode(self, strs: List[str]) -> str:
encoded = ""
for s in strs:
encoded += str(len(s)) + "#" + s
return encoded
def decode(self, s: str) -> List[str]:
decoded = []
i = 0
while i < len(s):
delimiter_pos = s.find("#", i)
size = int(s[i:delimiter_pos])
start_pos = delimiter_pos + 1
end_pos = start_pos + size
decoded.append(s[start_pos:end_pos])
i = end_pos
return decoded
Longest Consecutive Sequence π§
LeetCode Link: Longest Consecutive Sequence
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 128 - Longest Consecutive Sequence
Description: Given an unsorted array of integers nums, return the length of the longest consecutive elements sequence.
Intuition: To find the longest consecutive sequence, we can sort the array in ascending order and then iterate through it to count the longest consecutive sequence. However, this approach would have a time complexity of O(n log n) due to the sorting operation. We can instead use a HashSet to efficiently find consecutive elements.
Approach:
- Create a HashSet
numSet
and insert all the numbers from the input arraynums
. - Initialize a variable
maxLength
to store the maximum length of consecutive elements. - Iterate through each number
num
in thenumSet
:
- Check if the current number
num - 1
is not present in thenumSet
. If true, it means the current number is the start of a consecutive sequence. - Initialize a variable
length
to 1 to count the current consecutive sequence length. - Iterate through consecutive numbers starting from the current number
num + 1
until a number is not present in thenumSet
, incrementing thelength
accordingly. - Update
maxLength
with the maximum value betweenmaxLength
andlength
.
- Return
maxLength
, which represents the length of the longest consecutive sequence.
β Time Complexity:
The time complexity of this approach is O(n), where n is the number of elements in the input array. It is determined by the iteration through the numSet
, which contains all the unique numbers.
πΎ Space Complexity:
The space complexity is O(n) as we need to store all the numbers from the input array in the numSet
.
Solutions π‘
Cpp π»
class Solution {
public:
int longestConsecutive(vector<int> &nums) {
unordered_set<int> numSet(nums.begin(), nums.end());
int maxLength = 0;
for (int num : numSet) {
// Check if the current number is the start of a consecutive sequence
if (numSet.count(num - 1) == 0) {
int length = 1;
// Iterate through consecutive numbers starting from the current number
while (numSet.count(num + length) != 0) {
length++;
}
maxLength = max(maxLength, length);
}
}
return maxLength;
}
};
// auto init = [](){
// ios::sync_with_stdio(false);
// cin.tie(0);
// cout.tie(0);
// return 0;
// }();
// class Solution {
// public:
// int longestConsecutive(vector<int>& nums) {
// if(nums.empty())
// return 0;
// else if(nums.size() == 1)
// return 1;
// sort(nums.begin(),nums.end());
// int ans = 1;
// int cnt = 1;
// for(int i = 0; i < nums.size(); i++) {
// if(i > 0 && nums[i-1]+1 == nums[i]) {
// ++cnt;
// ans = max(ans, cnt);
// }
// else if(i > 0 && nums[i-1] == nums[i])
// continue;
// else
// cnt = 1;
// }
// return ans;
// }
// };
/*
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
if(nums.size() == 0)
return 0;
// Using set because its sorted and takes O(log N) time
set<int> S;
int Max = 0, local_Max = 0;
for(int i = 0; i < nums.size(); i++)
S.insert(nums[i]);
vector<int> V(S.begin(), S.end());
for(int i = 1; i < V.size(); i++) {
if(V[i] - V[i-1] == 1) {
local_Max++;
if(Max < local_Max)
Max = local_Max;
}
else
local_Max = 0;
}
return Max + 1;
}
};
*/
Python π
class Solution:
def longestConsecutive(self, nums: List[int]) -> int:
num_set = set(nums)
max_length = 0
for num in num_set:
if num - 1 not in num_set:
current_num = num
current_length = 1
while current_num + 1 in num_set:
current_num += 1
current_length += 1
max_length = max(max_length, current_length)
return max_length
Two Pointers π
This section contains problems belonging to the Two Pointers category.
Problems
- π’ Easy: Valid Palindrome
- π‘ Medium: Two Sum II - Input Array Is Sorted
- π‘ Medium: 3Sum
- π‘ Medium: Container With Most Water
- π΄ Hard: Trapping Rain Water
Valid Palindrome π§
LeetCode Link: Valid Palindrome
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 125 - Valid Palindrome
Description: Given a string s, determine if it is a palindrome, considering only alphanumeric characters and ignoring cases.
Intuition: To check if a string is a palindrome, we need to compare characters from both ends of the string. We can ignore non-alphanumeric characters and treat uppercase and lowercase letters as the same.
Approach:
- Initialize two pointers,
left
pointing to the start of the string, andright
pointing to the end of the string. - Iterate while
left
is less thanright
:
- Skip non-alphanumeric characters by incrementing
left
or decrementingright
if the character at that position is not alphanumeric. - Convert both characters to lowercase and compare them. If they are not equal, return false.
- Increment
left
and decrementright
to move to the next pair of characters.
- If the loop completes without finding any non-matching characters, return true.
β Time Complexity: The time complexity is O(n), where n is the length of the input string. We iterate through the string once to check if it is a valid palindrome.
πΎ Space Complexity: The space complexity is O(1) since we are using a constant amount of space to store the pointers and perform the comparison.
Solutions π‘
Cpp π»
class Solution {
public:
bool isPalindrome(string s) {
int left = 0, right = s.length() - 1;
while (left < right) {
// isalnum -> Is alphabet or number
if (!isalnum(s[left])) {
left++;
continue;
}
if (!isalnum(s[right])) {
right--;
continue;
}
if (tolower(s[left]) != tolower(s[right])) {
return false;
}
left++;
right--;
}
return true;
}
};
Python π
class Solution:
def isPalindrome(self, s: str) -> bool:
left, right = 0, len(s) - 1
while left < right:
while left < right and not s[left].isalnum():
left += 1
while left < right and not s[right].isalnum():
right -= 1
if s[left].lower() != s[right].lower():
return False
left += 1
right -= 1
return True
Two Sum II - Input Array Is Sorted π§
LeetCode Link: Two Sum II - Input Array Is Sorted
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 167 - Two Sum II - Input array is sorted
Description: Given an array of integers numbers that is already sorted in non-decreasing order, find two numbers such that they add up to a specific target number. Return the indices of the two numbers (1-indexed) as an integer array answer of size 2, where 1 <= answer[0] < answer[1] <= numbers.length. You may assume that each input would have exactly one solution and you may not use the same element twice.
Intuition: Since the input array is sorted, we can use a two-pointer approach to find the two numbers that sum up to the target. By maintaining two pointers, one pointing to the start of the array and the other pointing to the end, we can narrow down the search space based on the comparison of the current sum with the target.
Approach:
- Initialize two pointers,
left
pointing to the start of the array (index 0) andright
pointing to the end of the array. - Iterate while
left
is less thanright
:
- Calculate the current sum of the elements at
left
andright
. - If the sum is equal to the target, return the indices (
left + 1
andright + 1
) as the answer. - If the sum is less than the target, increment
left
to consider a larger element. - If the sum is greater than the target, decrement
right
to consider a smaller element.
- If no solution is found, return an empty vector.
β Time Complexity: The time complexity is O(n), where n is the number of elements in the input array. In the worst case, we iterate through the entire array once.
πΎ Space Complexity: The space complexity is O(1) as we are using a constant amount of space to store the pointers and perform the comparison.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> twoSum(vector<int> &numbers, int target) {
int left = 0, right = numbers.size() - 1;
while (left < right) {
int sum = numbers[left] + numbers[right];
if (sum == target) {
return {left + 1, right + 1};
} else if (sum < target) {
left++;
} else {
right--;
}
}
return {};
}
};
Python π
class Solution:
def twoSum(self, numbers: List[int], target: int) -> List[int]:
left, right = 0, len(numbers) - 1
while left < right:
current_sum = numbers[left] + numbers[right]
if current_sum == target:
return [left + 1, right + 1]
elif current_sum < target:
left += 1
else:
right -= 1
# No solution found
return [-1, -1]
3Sum π§
LeetCode Link: 3Sum
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 15 - 3Sum
Description: Given an array nums of n integers, find all unique triplets in the array which gives the sum of zero. The solution set must not contain duplicate triplets.
Intuition: To find unique triplets that sum up to zero, we can leverage the two-pointer approach. By sorting the array and iterating through each element, we can convert the problem into finding two numbers that sum up to the negation of the current element.
Approach:
- Sort the input array
nums
in non-decreasing order. - Iterate through each element
nums[i]
from 0 to n-3 (exclusive):
- If
i > 0
andnums[i]
is equal tonums[i-1]
, skip the current iteration to avoid duplicate triplets. - Initialize two pointers,
left
pointing to the element next tonums[i]
, andright
pointing to the last element of the array. - Iterate while
left
is less thanright
: - Calculate the current sum of the elements
nums[i]
,nums[left]
, andnums[right]
. - If the sum is equal to zero, add the triplet
[nums[i], nums[left], nums[right]]
to the result. - Increment
left
and decrementright
to search for the next pair of elements. - Skip any duplicate values of
nums[left]
andnums[right]
to avoid duplicate triplets. - If the sum is less than zero, increment
left
. - If the sum is greater than zero, decrement
right
.
- Return the result, which contains all unique triplets that sum up to zero.
β Time Complexity: The time complexity is O(n^2), where n is the number of elements in the input array. Sorting the array takes O(nlogn) time, and iterating through the array with two pointers takes O(n^2) time.
πΎ Space Complexity: The space complexity is O(1) as we are using a constant amount of space to store the pointers and the result.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<int>> threeSum(vector<int> &nums) {
vector<vector<int>> result;
int n = nums.size();
sort(nums.begin(), nums.end());
for (int i = 0; i < n - 2; i++) {
if (i > 0 && nums[i] == nums[i - 1]) {
continue;
}
int left = i + 1, right = n - 1;
while (left < right) {
int sum = nums[i] + nums[left] + nums[right];
if (sum == 0) {
result.push_back({nums[i], nums[left], nums[right]});
left++;
right--;
while (left < right && nums[left] == nums[left - 1]) {
left++;
}
while (left < right && nums[right] == nums[right + 1]) {
right--;
}
} else if (sum < 0) {
left++;
} else {
right--;
}
}
}
return result;
}
};
Python π
class Solution:
def threeSum(self, nums: List[int]) -> List[List[int]]:
nums.sort()
result = []
n = len(nums)
for i in range(n - 2):
if i > 0 and nums[i] == nums[i - 1]:
continue
left, right = i + 1, n - 1
while left < right:
current_sum = nums[i] + nums[left] + nums[right]
if current_sum == 0:
result.append([nums[i], nums[left], nums[right]])
# Skip duplicates
while left < right and nums[left] == nums[left + 1]:
left += 1
while left < right and nums[right] == nums[right - 1]:
right -= 1
left += 1
right -= 1
elif current_sum < 0:
left += 1
else:
right -= 1
return result
Container With Most Water π§
LeetCode Link: Container With Most Water
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 11 - Container With Most Water
Description: Given n non-negative integers a1, a2, ..., an, where each represents a point at coordinate (i, ai). n vertical lines are drawn such that the two endpoints of the line i is at (i, ai) and (i, 0). Find two lines, which, together with the x-axis, forms a container, such that the container contains the most water.
Intuition: To maximize the container's area, we need to find two vertical lines that enclose the most water. The area is determined by the height of the shorter line and the distance between the lines. We can start with the maximum width and move the pointers inward, always choosing the next height that is greater than the current one.
Approach:
- Initialize two pointers,
left
pointing to the start of the array (index 0), andright
pointing to the end of the array. - Initialize a variable
maxArea
to store the maximum container area. - Iterate while
left
is less thanright
:
- Calculate the current container area as the minimum height between
height[left]
andheight[right]
multiplied by the width (right - left
). - Update
maxArea
with the maximum value betweenmaxArea
and the current area. - Move the pointer with the smaller height inward, as moving the pointer with the greater height does not increase the area.
- Return
maxArea
, which represents the maximum container area.
β Time Complexity: The time complexity is O(n), where n is the number of elements in the input array. We only need to iterate through the array once from both ends.
πΎ Space Complexity: The space complexity is O(1) as we are using a constant amount of space to store the pointers and the maximum area.
Solutions π‘
Cpp π»
class Solution {
public:
int maxArea(vector<int> &height) {
int left = 0, right = height.size() - 1;
int maxArea = 0;
while (left < right) {
int currArea = min(height[left], height[right]) * (right - left);
maxArea = max(maxArea, currArea);
if (height[left] < height[right]) {
left++;
} else {
right--;
}
}
return maxArea;
}
};
// class Solution {
// public:
// int maxArea(vector<int>& height) {
// int leftPointer = 0, rightPointer = height.size() - 1;
// int result = -1, currResult = 0;
// while(leftPointer < rightPointer){
// int dist = rightPointer - leftPointer;
// currResult = min(height[leftPointer], height[rightPointer]) * dist;
// result = max(result, currResult);
// if( height[leftPointer] > height[rightPointer] )
// rightPointer--;
// else
// leftPointer++;
// }
// return result;
// }
// };
/*
class Solution {
public:
int maxArea(vector<int>& height) {
int water = 0;
int i = 0, j = height.size() - 1;
while (i < j) {
int h = min(height[i], height[j]);
water = max(water, (j - i) * h);
while(height[i] <= h && i < j)
i++;
while(height[j] <= h && i < j)
j--;
}
return water;
}
};
*/
Python π
class Solution:
def maxArea(self, height: List[int]) -> int:
left, right = 0, len(height) - 1
max_area = 0
while left < right:
current_area = min(height[left], height[right]) * (right - left)
max_area = max(max_area, current_area)
if height[left] < height[right]:
left += 1
else:
right -= 1
return max_area
Trapping Rain Water π§
LeetCode Link: Trapping Rain Water
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 42 - Trapping Rain Water
Description: Given n non-negative integers representing an elevation map where the width of each bar is 1, compute how much water it can trap after raining.
Intuition: To determine the amount of water that can be trapped, we need to consider the height of each bar and the width between the bars. The amount of water trapped at a particular position depends on the minimum height of the tallest bars on its left and right sides minus the elevation.
Approach:
- Initialize two pointers,
left
pointing to the start of the array (index 0), andright
pointing to the end of the array. - Initialize variables
leftMax
andrightMax
to keep track of the maximum heights encountered on the left and right sides. - Initialize a variable
water
to store the total trapped water. - Iterate while
left
is less thanright
:
- If the height at
left
is less than or equal to the height atright
: - Update
leftMax
with the maximum value betweenleftMax
and the current height atleft
. - Calculate the amount of water that can be trapped at
left
by subtracting the height atleft
fromleftMax
. - Add the calculated water to the total
water
variable. - Increment
left
. - If the height at
left
is greater than the height atright
: - Update
rightMax
with the maximum value betweenrightMax
and the current height atright
. - Calculate the amount of water that can be trapped at
right
by subtracting the height atright
fromrightMax
. - Add the calculated water to the total
water
variable. - Decrement
right
.
- Return the total trapped
water
.
β Time Complexity: The time complexity is O(n), where n is the number of elements in the input array. We iterate through the array once from both ends.
πΎ Space Complexity: The space complexity is O(1) as we are using a constant amount of space to store the pointers and variables.
Solutions π‘
Cpp π»
class Solution {
public:
int trap(vector<int> &height) {
int left = 0, right = height.size() - 1;
int leftMax = 0, rightMax = 0;
int water = 0;
while (left < right) {
if (height[left] <= height[right]) {
leftMax = max(leftMax, height[left]);
water += leftMax - height[left];
left++;
} else {
rightMax = max(rightMax, height[right]);
water += rightMax - height[right];
right--;
}
}
return water;
}
};
/*
Solution -
The key is we can calculate the amount of water at any given index by
-> taking minimum of (max of left nd right so far)
-> that will give us water in between them
-> then subtract the height to remove the elevation
SO make two array to store max height so far from left and right
and then just calculate, min(maxleft, maxright) - height[i];
*/
// class Solution {
// public:
// int trap(vector<int>& height) {
// int heightSize = height.size();
// vector<int> leftMax(heightSize), rightMax(heightSize);
// int result = 0;
// // Filling the left and right max arrays
// for(int i = 1, j = heightSize - 2; i < heightSize, j >= 0; i++, j--) {
// if(leftMax[i-1] <= height[i-1])
// leftMax[i] = height[i-1];
// else
// leftMax[i] = leftMax[i-1];
// if(rightMax[j+1] <= height[j+1])
// rightMax[j] = height[j+1];
// else
// rightMax[j] = rightMax[j+1];
// }
// for(int i = 0; i < heightSize; i++) {
// int temp = min(leftMax[i], rightMax[i]) - height[i];
// if(temp < 0)
// temp = 0;
// result += temp;
// }
// return result;
// }
// };
/*
class Solution {
public:
int trap(vector<int>& H) {
int n = H.size(), mx = 0, ans = 0;
int idx = max_element(begin(H), end(H)) - begin(H);
for(int i = 0; i < idx; i++) {
ans += max(0, mx - H[i]);
mx = max(mx, H[i]);
}
mx = 0;
for(int i = n - 1; i > idx; i--) {
ans += max(0, mx - H[i]);
mx = max(mx, H[i]);
}
return ans;
}
};
*/
Python π
class Solution:
def trap(self, height: List[int]) -> int:
left, right = 0, len(height) - 1
max_left, max_right = 0, 0
trapped_water = 0
while left < right:
if height[left] <= height[right]:
if height[left] >= max_left:
max_left = height[left]
else:
trapped_water += max_left - height[left]
left += 1
else:
if height[right] >= max_right:
max_right = height[right]
else:
trapped_water += max_right - height[right]
right -= 1
return trapped_water
Sliding Window π
This section contains problems belonging to the Sliding Window category.
Problems
- π’ Easy: Best Time to Buy and Sell Stock
- π‘ Medium: Longest Substring Without Repeating Characters
- π‘ Medium: Longest Repeating Character Replacement
- π‘ Medium: Permutation in String
- π΄ Hard: Minimum Window Substring
- π΄ Hard: Sliding Window Maximum
Best Time to Buy and Sell Stock π§
LeetCode Link: Best Time to Buy and Sell Stock
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 121 - Best Time to Buy and Sell Stock
Description:
You are given an array prices
where prices[i]
is the price of a given stock on the i-th day.
You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock.
Return the maximum profit you can achieve from this transaction. If you cannot achieve any profit, return 0.
Intuition: To maximize the profit, we need to find the largest difference between any two prices, where the lower price comes before the higher price. We can track the minimum price seen so far and calculate the maximum profit by subtracting the minimum price from each subsequent price.
Approach:
- Initialize variables
minPrice
andmaxProfit
.
- Set
minPrice
to the maximum possible value andmaxProfit
to 0.
- Iterate through each price in the
prices
array:
- Update
minPrice
to the minimum value betweenminPrice
and the current price. - Calculate the potential profit by subtracting
minPrice
from the current price. - Update
maxProfit
to the maximum value betweenmaxProfit
and the potential profit.
- Return
maxProfit
, which represents the maximum profit achievable.
β Time Complexity: The time complexity is O(n), where n is the number of elements in the input array. We iterate through the array once to calculate the maximum profit.
πΎ Space Complexity: The space complexity is O(1) as we are using a constant amount of space to store the variables.
Solutions π‘
Cpp π»
class Solution {
public:
int maxProfit(vector<int> &prices) {
int minPrice = INT_MAX;
int maxProfit = 0;
for (int price : prices) {
minPrice = min(minPrice, price);
maxProfit = max(maxProfit, price - minPrice);
}
return maxProfit;
}
};
// -> Calculate LeastSoFar
// -> Measure profit, if greater than result, update result
// class Solution {
// public:
// int maxProfit(vector<int>& prices) {
// int leastSoFar = 100000, profit = 0, result = 0;
// for(int i = 0; i < prices.size(); i++) {
// if(prices[i] < leastSoFar)
// leastSoFar = prices[i];
// profit = prices[i] - leastSoFar;
// if(result < profit)
// result = profit;
// }
// return result;
// }
// };
/*
class Solution {
public:
int maxProfit(vector<int>& prices) {
ios_base::sync_with_stdio(0);
cout.tie(0);
int profit = 0;
int minbuy = prices[0];
for(int x: prices) {
int diff = x - minbuy;
profit = max(profit, diff);
minbuy = min(minbuy, x);
}
return profit;
}
};
*/
Python π
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices:
return 0
min_price = float("inf")
max_profit = 0
for price in prices:
min_price = min(min_price, price)
max_profit = max(max_profit, price - min_price)
return max_profit
Longest Substring Without Repeating Characters π§
LeetCode Link: Longest Substring Without Repeating Characters
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 3 - Longest Substring Without Repeating Characters
Description:
Given a string s
, find the length of the longest substring without repeating characters.
Intuition: To find the longest substring without repeating characters, we can use a sliding window approach. We can maintain a window that contains unique characters and keep expanding it until we encounter a repeating character. At each step, we update the maximum length of the non-repeating substring.
Approach:
- Initialize variables
left
andright
to represent the left and right pointers of the sliding window.
- Set both
left
andright
to 0 initially.
- Initialize a variable
maxLen
to store the maximum length of the non-repeating substring. - Initialize a set or map to keep track of the unique characters in the window.
- Iterate while
right
is less than the length of the string:
- Check if the character at
right
is already present in the set/map: - If it is present, remove the character at
left
from the set/map and incrementleft
. - If it is not present, add the character at
right
to the set/map, calculate the current length of the non-repeating substring asright - left + 1
, and updatemaxLen
if necessary. - Increment
right
to expand the window.
- Return
maxLen
, which represents the length of the longest substring without repeating characters.
β Time Complexity: The time complexity is O(n), where n is the length of the input string. We iterate through the string once with the sliding window approach.
πΎ Space Complexity: The space complexity is O(min(n, m)), where n is the length of the input string and m is the size of the character set. In the worst case, the character set can be as large as the input string, but it can also be limited by the number of distinct characters.
Solutions π‘
Cpp π»
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int left = 0, right = 0;
int maxLen = 0;
unordered_set<char> charSet;
while (right < s.length()) {
if (charSet.count(s[right])) {
charSet.erase(s[left]);
left++;
} else {
charSet.insert(s[right]);
maxLen = max(maxLen, right - left + 1);
right++;
}
}
return maxLen;
}
};
/*
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int n = s.length();
int ans = 0;
vector<int> M(256, -1);
int start = 0, end = 0;
while(end < n) {
if(M[s[end]] != -1) {
start = max(start, M[s[end]] + 1);
}
M[s[end]] = end;
ans = max(ans, end-start + 1);
end++;
}
return ans;
}
};
*/
Python π
class Solution:
def lengthOfLongestSubstring(self, s: str) -> int:
left, right = 0, 0
max_length = 0
unique_chars = set()
while right < len(s):
if s[right] not in unique_chars:
unique_chars.add(s[right])
max_length = max(max_length, right - left + 1)
right += 1
else:
unique_chars.remove(s[left])
left += 1
return max_length
Longest Repeating Character Replacement π§
LeetCode Link: Longest Repeating Character Replacement
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 424 - Longest Repeating Character Replacement
Description:
Given a string s
that consists of only uppercase English letters, you can perform at most k
operations on that string.
In one operation, you can choose any character of the string and change it to any other uppercase English letter.
Find the length of the longest substring containing repeating letters you can get after performing the above operations.
Intuition: To find the longest substring with repeating characters, we can use a sliding window approach. We can maintain a window that contains the most frequent character and update it as we expand the window. The maximum length of the substring with repeating characters can be obtained by considering the count of the most frequent character within the window.
Approach:
- Initialize variables
maxCount
andmaxLength
to track the maximum count of a character and the maximum length of the substring with repeating characters. - Initialize a vector
count
to keep track of the count of each character. - Initialize variables
left
andright
to represent the left and right pointers of the sliding window.
- Set both
left
andright
to 0 initially.
- Iterate while
right
is less than the length of the string:
- Increment the count of the character at
right
in thecount
vector. - Update
maxCount
with the maximum value betweenmaxCount
and the current count. - Calculate the length of the current substring as
right - left + 1
. - If the length of the current substring minus
maxCount
is greater thank
: - Decrement the count of the character at
left
in thecount
vector. - Increment
left
to shrink the window. - Update
maxLength
with the maximum value betweenmaxLength
and the current substring length. - Increment
right
to expand the window.
- Return
maxLength
, which represents the length of the longest substring with repeating characters that can be obtained.
β Time Complexity: The time complexity is O(n), where n is the length of the input string. We iterate through the string once with the sliding window approach.
πΎ Space Complexity:
The space complexity is O(1) as we are using a fixed-size vector count
to store the count of characters.
Solutions π‘
Cpp π»
class Solution {
public:
int characterReplacement(string s, int k) {
vector<int> count(26, 0);
int left = 0, right = 0;
int maxCount = 0, maxLength = 0;
while (right < s.length()) {
count[s[right] - 'A']++;
maxCount = max(maxCount, count[s[right] - 'A']);
if ((right - left + 1) - maxCount > k) {
count[s[left] - 'A']--;
left++;
}
maxLength = max(maxLength, right - left + 1);
right++;
}
return maxLength;
}
};
// -> Have two pointer L and R start at 0
// -> Keep incrementing R until size of the window(R-L) minus
// the most frequent element is lesser than or equal to k.
// i.e Window Size - Most Frequent Element count <= K
// Because K is the max number of changes we can make
// -> If Window Size - Most Frequent Element count > K, then increment L
// class Solution {
// public:
// int characterReplacement(string s, int k) {
// int L = 0, R = 0;
// int maxFreq = 0, maxLength = 0;
// unordered_map<char, int> M; //For storing chars and their frequency
// while(R < s.size()) {
// // int winLen = R - L + 1; // Sliding window length
// M[s[R]]++; // Updating frequency in Map
// maxFreq = max(maxFreq, M[s[R]]); // if curr freq is greater than maxFreq, update
// if(R-L+1 - maxFreq > k) {
// M[s[L]]--; // Decrementing count of L'th element because we're gonna move L
// L++;
// }
// // Technically we will never need to reduce the maxFreq,
// // because answer will be set around around it
// // We can adjust the length, so basically the ans is maxFreq + K
// maxLength = max(maxLength, R-L+1);
// R++; //Moving R
// }
// return maxLength;
// }
// };
/*
class Solution {
public:
int characterReplacement(string s, int k) {
if(s.size() == 1)
return 1;
int i{}, j{}, res{}, maxCount{0};
vector<int> a(26, 0);
while(j < s.size()) {
a[s[j] - 'A']++;
if(maxCount < a[s[j]-'A'])
maxCount = a[s[j]-'A'];
if((j - i + 1) - maxCount > k){
a[s[i]-'A']--;
i++;
}
res = max(res, j-i+1);
j++;
}
return res;
}
};
*/
Python π
class Solution:
def characterReplacement(self, s: str, k: int) -> int:
left, right = 0, 0
max_length = 0
char_freq = {}
max_freq = 0
while right < len(s):
char_freq[s[right]] = char_freq.get(s[right], 0) + 1
max_freq = max(max_freq, char_freq[s[right]])
if (right - left + 1) - max_freq > k:
char_freq[s[left]] -= 1
left += 1
max_length = max(max_length, right - left + 1)
right += 1
return max_length
Permutation in String π§
LeetCode Link: Permutation in String
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 567 - Permutation in String
Description:
Given two strings s1
and s2
, return true
if s2
contains the permutation of s1
.
In other words, one of the first string's permutations is the substring of the second string.
Intuition:
To check if s2
contains a permutation of s1
, we can use a sliding window approach.
- We initialize two frequency maps,
freq1
andfreq2
, to store the frequencies of characters ins1
and the sliding window ofs2
. - If the frequencies in
freq1
andfreq2
are equal, then it meanss2
contains a permutation ofs1
. - By sliding the window through
s2
, we can keep track of the frequencies of characters in the current window and compare them with the frequencies infreq1
.
Approach:
- We start by checking if the length of
s1
is greater thans2
. If it is, then it's not possible fors2
to contain a permutation ofs1
, so we returnfalse
. - We initialize two arrays,
freq1
andfreq2
, to store the frequencies of characters ins1
and the sliding window ofs2
.
- Both arrays should have a size of 26 to represent the lowercase English letters.
- We iterate through the first
s1.size()
characters ofs2
to initialize the frequencies in both arrays. - We initialize two pointers,
L
andR
, representing the left and right ends of the window, respectively. - If the frequencies in
freq1
andfreq2
are equal, we returntrue
becauses2
contains a permutation ofs1
. - We iterate from index
s1.size()
tos2.size()
using the sliding window approach:
- If the frequency of the character at index
L
infreq2
is 1, we remove it fromfreq2
since we will incrementL
later. - Otherwise, we decrement the frequency of the character at index
L
infreq2
since we will incrementL
. - We increment the frequency of the character at index
R
infreq2
since we have added a new character to the window. - We increment
L
to slide the window to the right. - If the frequencies in
freq1
andfreq2
are equal, we returntrue
.
- If no permutation is found after iterating through
s2
, we returnfalse
.
β Time Complexity:
The time complexity is O(n), where n is the length of s2
. We iterate through s2
once using the sliding window approach.
πΎ Space Complexity:
The space complexity is O(1) as both freq1
and freq2
have a fixed size of 26, representing lowercase English letters.
Solutions π‘
Cpp π»
class Solution {
public:
bool checkInclusion(string s1, string s2) {
if (s1.size() > s2.size()) {
return false;
}
vector<int> freq1(26, 0);
vector<int> freq2(26, 0);
for (int i = 0; i < s1.size(); i++) {
freq1[s1[i] - 'a']++;
freq2[s2[i] - 'a']++;
}
int L = 0;
if (freq1 == freq2) {
return true;
}
for (int R = s1.size(); R < s2.size(); R++) {
if (freq2[s2[L] - 'a'] == 1) {
freq2[s2[L] - 'a'] = 0;
} else {
freq2[s2[L] - 'a']--;
}
freq2[s2[R] - 'a']++;
L++;
if (freq1 == freq2) {
return true;
}
}
return false;
}
};
Python π
class Solution:
def checkInclusion(self, s1: str, s2: str) -> bool:
if len(s1) > len(s2):
return False
char_freq_s1 = {}
for char in s1:
char_freq_s1[char] = char_freq_s1.get(char, 0) + 1
left, right = 0, 0
char_freq_temp = {}
while right < len(s2):
char_freq_temp[s2[right]] = char_freq_temp.get(s2[right], 0) + 1
if right - left + 1 == len(s1):
if char_freq_temp == char_freq_s1:
return True
char_freq_temp[s2[left]] -= 1
if char_freq_temp[s2[left]] == 0:
del char_freq_temp[s2[left]]
left += 1
right += 1
return False
Minimum Window Substring π§
LeetCode Link: Minimum Window Substring
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 76 - Minimum Window Substring
Description:
Given two strings s
and t
, return the minimum window in s
that contains all the characters of t
.
If there is no such window in s
that covers all characters in t
, return an empty string.
Intuition:
To find the minimum window substring, we can use the sliding window technique.
The idea is to maintain two pointers, left
and right
, to create a window in s
.
By expanding and contracting the window, we can find the minimum window substring that contains all the characters of t
.
Approach:
- We start by initializing two pointers,
left
andright
, to the first index ofs
. - We initialize two frequency maps,
targetFreq
andwindowFreq
, to store the frequencies of characters int
and the current window ofs
, respectively. - We initialize variables,
count
andminLen
, to keep track of the count of characters int
that are present in the current window and the minimum window length found so far. - We iterate through
s
using theright
pointer:
- Increment the frequency of the character at
s[right]
inwindowFreq
. - If the frequency of the character at
s[right]
inwindowFreq
is less than or equal to the frequency of the same character intargetFreq
, increment thecount
. - If
count
is equal to the length oft
, it means we have found a valid window that contains all the characters oft
. - Update
minLen
if the current window length is smaller. - Move the
left
pointer to contract the window: - Decrement the frequency of the character at
s[left]
inwindowFreq
. - If the frequency of the character at
s[left]
inwindowFreq
becomes less than the frequency intargetFreq
, decrement thecount
. - Move the
left
pointer to the right to search for the next valid window.
- Repeat steps 4 and 5 until the
right
pointer reaches the end ofs
. - Return the minimum window substring found based on
minLen
. If no valid window is found, return an empty string.
β Time Complexity:
The time complexity is O(n), where n is the length of s
. We iterate through s
once using the sliding window approach.
πΎ Space Complexity:
The space complexity is O(1) as both targetFreq
and windowFreq
have a fixed size of 128 to represent ASCII characters.
Solutions π‘
Cpp π»
class Solution {
public:
string minWindow(string s, string t) {
vector<int> targetFreq(128, 0);
vector<int> windowFreq(128, 0);
for (char ch : t) {
targetFreq[ch]++;
}
int left = 0;
int right = 0;
int count = 0;
int minLen = INT_MAX;
int minStart = 0;
while (right < s.length()) {
windowFreq[s[right]]++;
if (windowFreq[s[right]] <= targetFreq[s[right]]) {
count++;
}
while (count == t.length()) {
if (right - left + 1 < minLen) {
minLen = right - left + 1;
minStart = left;
}
windowFreq[s[left]]--;
if (windowFreq[s[left]] < targetFreq[s[left]]) {
count--;
}
left++;
}
right++;
}
return (minLen == INT_MAX) ? "" : s.substr(minStart, minLen);
}
};
// class Solution {
// public:
// string minWindow(string s, string t) {
// if(t.length() > s.length())
// return "";
// unordered_map<char, int> M;
// for(int i = 0; i < t.size(); i++)
// M[t[i]]++;
// int L = 0, R = 0;
// int count = t.size(); // No. of chars in t that need to be n S
// int minLength = INT_MAX;
// int minStart = 0; // We have to return string, so this variable is for storing teh starting index
// while(R < s.size()) {
// if(M[s[R]] > 0) // If we find an element from T(which we put in M)
// count--;
// // We will decrease the value here also, since count only counts unique elements
// // Elements in M will decrease till 0(which then decreases count)
// // and elements that dont exist in M will be -ve
// M[s[R]]--;
// R++;
// while(count == 0) {
// if(R-L < minLength) {
// minLength = R-L;
// minStart = L;
// }
// M[s[L]]++; // Updating the value in Map
// // If the current L element is from T, then increase count
// if(M[s[L]] > 0) {
// count++;
// }
// L++;
// }
// }
// // If the ans is empty string, minLength(= MAX_INT) will not be changed
// if (minLength != INT_MAX) {
// return s.substr(minStart, minLength);
// }
// return "";
// }
// };
/*
class Solution {
public:
string minWindow(string s, string t) {
int cnt[128] = {}, diff = t.size();
for(int i = 0; i < t.size(); i++)
cnt[t[i]]++;
int left = 0, right = 0, idx = 0, size = 2e5;
while(right < s.size()) {
if(cnt[s[right++]]-- > 0)
diff--;
while(diff == 0) {
if(right-left < size)
size = right-(idx = left);
if(cnt[s[left++]]++ == 0)
diff++;
}
}
return (size == 2e5) ? "" : s.substr(idx, size);
}
};
*/
Python π
class Solution:
def minWindow(self, s: str, t: str) -> str:
if not s or not t:
return ""
char_freq_t = {}
for char in t:
char_freq_t[char] = char_freq_t.get(char, 0) + 1
left, right = 0, 0
char_freq_temp = {}
required_chars = len(char_freq_t)
formed_chars = 0
min_length = float("inf")
min_window = ""
while right < len(s):
char_freq_temp[s[right]] = char_freq_temp.get(s[right], 0) + 1
if (
s[right] in char_freq_t
and char_freq_temp[s[right]] == char_freq_t[s[right]]
):
formed_chars += 1
while left <= right and formed_chars == required_chars:
if right - left + 1 < min_length:
min_length = right - left + 1
min_window = s[left : right + 1]
char_freq_temp[s[left]] -= 1
if (
s[left] in char_freq_t
and char_freq_temp[s[left]] < char_freq_t[s[left]]
):
formed_chars -= 1
left += 1
right += 1
return min_window
Sliding Window Maximum π§
LeetCode Link: Sliding Window Maximum
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 239 - Sliding Window Maximum
Description:
Given an array of integers nums
and an integer k
, return the maximum sliding window of size k
in the array.
Intuition:
To find the maximum sliding window of size k
, we can use a sliding window and a deque (double-ended queue).
The deque will store the indices of elements in decreasing order of their values.
By maintaining this order, the maximum element in the window will always be at the front of the deque.
Approach:
- We start by initializing an empty deque to store the indices of elements.
- We iterate through the array
nums
from left to right:
- Remove indices from the back of the deque if the corresponding elements are smaller than the current element.
- Add the current element's index to the back of the deque.
- If the index at the front of the deque is outside the current window, remove it.
- If the current index is greater than or equal to
k - 1
(i.e., the window size), it means we have processedk
elements and can start collecting the maximum values. - Add the maximum value (which is the element at the front of the deque) to the result vector.
- Return the result vector.
β Time Complexity:
The time complexity is O(n), where n is the length of the input array nums
. We iterate through nums
once.
πΎ Space Complexity:
The space complexity is O(k), where k is the size of the sliding window. The deque stores at most k
indices.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> maxSlidingWindow(vector<int> &nums, int k) {
vector<int> result;
deque<int> indices;
for (int i = 0; i < nums.size(); i++) {
// Remove indices from the back of the deque if the corresponding elements are smaller than the current element
while (!indices.empty() && nums[indices.back()] < nums[i]) {
indices.pop_back();
}
indices.push_back(i);
// Remove the index at the front of the deque if it is outside the current window
if (indices.front() <= i - k) {
indices.pop_front();
}
// Add the maximum value to the result if the current index is in the window
if (i >= k - 1) {
result.push_back(nums[indices.front()]);
}
}
return result;
}
};
/*
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int offset = 1e4;
std::ios_base::sync_with_stdio(false); cin.tie(0); cout.tie(0);
int mp[(int)(2 * 1e4 + 1)] = {0};
int maxval = INT_MIN;
for(int i = 0; i < k; i++) {
mp[offset + nums[i]]++;
maxval = max(maxval, nums[i]);
}
vector<int> ans;
ans.push_back(maxval);
for(int j = k; j < nums.size(); j++) {
mp[offset + nums[j - k]]--;
mp[offset + nums[j]]++;
maxval = max(maxval, nums[j]);
int mpos = maxval + offset;
while(mp[mpos] == 0) {
mpos--;
}
maxval = mpos - offset;
ans.push_back(maxval);
}
return ans;
}
};
*/
Python π
from collections import deque
class Solution:
def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]:
if not nums or k <= 0:
return []
result = []
window = deque()
for i, num in enumerate(nums):
while window and nums[window[-1]] < num:
window.pop()
window.append(i)
if i - window[0] >= k:
window.popleft()
if i >= k - 1:
result.append(nums[window[0]])
return result
Stack π
This section contains problems belonging to the Stack category.
Problems
- π’ Easy: Valid Parentheses
- π‘ Medium: Min Stack
- π‘ Medium: Evaluate Reverse Polish Notation
- π‘ Medium: Generate Parentheses
- π‘ Medium: Daily Temperatures
- π‘ Medium: Car Fleet
- π΄ Hard: Largest Rectangle In Histogram
Valid Parentheses π§
LeetCode Link: Valid Parentheses
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 20 - Valid Parentheses
Description:
Given a string s
containing just the characters '(', ')', '{', '}', '[' and ']', determine if the input string is valid.
Intuition: To determine if a string of parentheses is valid, we can utilize a stack data structure. We iterate through the characters of the string and perform the following steps:
- If we encounter a closing bracket, we check if the stack is empty or if the top of the stack does not correspond to the current closing bracket. If either condition is true, the string is not valid.
- If we encounter an opening bracket, we push it onto the stack. After iterating through all the characters, if the stack is empty, the string is valid; otherwise, it is not.
Approach:
- Initialize an empty stack to store opening brackets.
- Iterate through each character in the string
s
:
- If the current character is a closing bracket, check if the stack is empty or if the top of the stack does not match the current closing bracket. Return false if either condition is true.
- If the current character is an opening bracket, push it onto the stack.
- After iterating through all characters, check if the stack is empty:
- If the stack is empty, return true, indicating that all brackets have been matched.
- If the stack is not empty, return false, indicating unmatched brackets.
- The function returns true if all brackets are matched; otherwise, it returns false.
β Time Complexity:
The time complexity is O(n), where n is the length of the input string s
. We iterate through each character once.
πΎ Space Complexity:
The space complexity is O(n), where n is the length of the input string s
. In the worst case, the stack may store all opening brackets.
Solutions π‘
Cpp π»
class Solution {
public:
bool isValid(string s) {
stack<char> bracketStack;
for (char ch : s) {
if (ch == ')' || ch == '}' || ch == ']') {
if (bracketStack.empty() || !isMatchingPair(bracketStack.top(), ch)) {
return false;
}
bracketStack.pop();
} else {
bracketStack.push(ch);
}
}
return bracketStack.empty();
}
private:
bool isMatchingPair(char opening, char closing) {
return (opening == '(' && closing == ')') ||
(opening == '{' && closing == '}') ||
(opening == '[' && closing == ']');
}
};
// class Solution {
// public:
// bool isValid(string s) {
// stack<char> stag;
// for(int i = 0; i < s.size(); i++) {
// if(s[i] == ')' || s[i] == '}' || s[i] == ']') {
// if(stag.empty())
// return false;
// if(s[i] == ')' && stag.top() != '(')
// return false;
// if(s[i] == '}' && stag.top() != '{')
// return false;
// if(s[i] == ']' && stag.top() != '[')
// return false;
// stag.pop();
// }
// else {
// stag.push(s[i]);
// }
// }
// if (!stag.empty()) {
// return false;
// }
// return true;
// }
// };
Python π
class Solution:
def isValid(self, s: str) -> bool:
stack = []
parentheses_map = {")": "(", "}": "{", "]": "["}
for char in s:
if char in parentheses_map.values():
stack.append(char)
elif char in parentheses_map:
if not stack or stack[-1] != parentheses_map[char]:
return False
stack.pop()
else:
return False
return not stack
Min Stack π§
LeetCode Link: Min Stack
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 155 - Min Stack
Description: Design a stack that supports push, pop, top, and retrieving the minimum element in constant time.
Intuition: To efficiently retrieve the minimum element in constant time, we need to keep track of the minimum value at each step. We can achieve this by using an additional stack to store the minimum values.
Approach:
- Initialize two stacks: a main stack to store the actual values and a minimum stack to store the minimum values.
- When pushing a value, check if it is smaller than or equal to the top of the minimum stack. If it is, push the value onto both stacks.
- If it is larger, push the value only onto the main stack.
- When popping a value, check if the top of the main stack is equal to the top of the minimum stack.
- If they are equal, pop both values from both stacks.
- If they are not equal, pop only from the main stack.
- When retrieving the top element, simply return the top of the main stack.
- When retrieving the minimum element, simply return the top of the minimum stack.
- The implementation ensures that the minimum stack always has the minimum value at the top, reflecting the minimum value at each step.
β Time Complexity: All operations - push, pop, top, and getMin - have a time complexity of O(1). They are all performed in constant time.
πΎ Space Complexity: The space complexity is O(n), where n is the number of elements pushed into the stack. The two stacks store the same number of elements as the main stack.
Solutions π‘
Cpp π»
class MinStack {
private:
std::stack<int> mainStack;
std::stack<int> minStack;
public:
MinStack() {
}
void push(int val) {
mainStack.push(val);
if (minStack.empty() || val <= minStack.top()) {
minStack.push(val);
}
}
void pop() {
if (mainStack.top() == minStack.top()) {
minStack.pop();
}
mainStack.pop();
}
int top() {
return mainStack.top();
}
int getMin() {
return minStack.top();
}
};
Python π
class MinStack:
def __init__(self):
self.stack = []
self.min_stack = []
def push(self, val: int) -> None:
self.stack.append(val)
if not self.min_stack or val <= self.min_stack[-1]:
self.min_stack.append(val)
def pop(self) -> None:
if self.stack:
if self.stack[-1] == self.min_stack[-1]:
self.min_stack.pop()
self.stack.pop()
def top(self) -> int:
if self.stack:
return self.stack[-1]
def getMin(self) -> int:
if self.min_stack:
return self.min_stack[-1]
Evaluate Reverse Polish Notation π§
LeetCode Link: Evaluate Reverse Polish Notation
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 150 - Evaluate Reverse Polish Notation
Description: Evaluate the value of an arithmetic expression in Reverse Polish Notation (RPN). Valid operators are +, -, *, and /. Each operand may be an integer or another expression.
Intuition: Reverse Polish Notation (RPN) eliminates the need for parentheses in arithmetic expressions by using a postfix notation. To evaluate an RPN expression, we can utilize a stack to perform the necessary operations.
Approach:
- Initialize an empty stack.
- Iterate through each token in the given list:
- If the token is an operator ('+', '-', '*', '/'), pop the top two values from the stack, perform the corresponding operation, and push the result back to the stack.
- If the token is an operand (integer), convert it to an integer and push it onto the stack.
- After iterating through all tokens, the stack will contain the final result.
- Pop the result from the stack and return it.
β Time Complexity: The time complexity is O(n), where n is the number of tokens in the input list. We iterate through each token once.
πΎ Space Complexity: The space complexity is O(n), where n is the number of tokens in the input list. In the worst case, the stack may store all the operands.
Solutions π‘
Cpp π»
class Solution {
public:
int evalRPN(std::vector<std::string> &tokens) {
std::stack<int> stack;
for (const auto &token : tokens) {
if (isOperator(token)) {
int operand2 = stack.top();
stack.pop();
int operand1 = stack.top();
stack.pop();
int result = performOperation(operand1, operand2, token);
stack.push(result);
} else {
stack.push(std::stoi(token));
}
}
return stack.top();
}
private:
bool isOperator(const std::string &token) {
return token == "+" || token == "-" || token == "*" || token == "/";
}
int performOperation(int operand1, int operand2, const std::string &operatorStr) {
if (operatorStr == "+") {
return operand1 + operand2;
} else if (operatorStr == "-") {
return operand1 - operand2;
} else if (operatorStr == "*") {
return operand1 * operand2;
} else if (operatorStr == "/") {
return operand1 / operand2;
}
return 0; // Invalid operator
}
};
Python π
class Solution:
def evalRPN(self, tokens: List[str]) -> int:
stack = []
for token in tokens:
if token.isdigit() or (token[0] == "-" and token[1:].isdigit()):
stack.append(int(token))
else:
num2 = stack.pop()
num1 = stack.pop()
if token == "+":
stack.append(num1 + num2)
elif token == "-":
stack.append(num1 - num2)
elif token == "*":
stack.append(num1 * num2)
elif token == "/":
stack.append(int(num1 / num2))
return stack[0]
Generate Parentheses π§
LeetCode Link: Generate Parentheses
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 22 - Generate Parentheses
Description: Given an integer n, generate all combinations of well-formed parentheses of length 2n.
Intuition: To generate all combinations of well-formed parentheses, we can use a backtracking approach. At each step, we have two choices: either to place an opening parenthesis or a closing parenthesis.
Approach:
- Initialize an empty result vector to store all valid combinations.
- Define a helper function, backtrack, that takes the current combination, the count of opening parentheses, the count of closing parentheses, and the result vector.
- In the backtrack function:
- If the length of the current combination is equal to 2n, add it to the result vector.
- If the count of opening parentheses is less than n, recursively call the backtrack function with the current combination appended with an opening parenthesis and an incremented count of opening parentheses.
- If the count of closing parentheses is less than the count of opening parentheses, recursively call the backtrack function with the current combination appended with a closing parenthesis and an incremented count of closing parentheses.
- Call the backtrack function initially with an empty combination, 0 opening parentheses, 0 closing parentheses, and the result vector.
- Return the result vector containing all valid combinations.
β Time Complexity: The time complexity is O(4^n / sqrt(n)), as there are Catalan numbers of well-formed parentheses combinations.
πΎ Space Complexity: The space complexity is O(4^n / sqrt(n)), as there can be a total of 4^n / sqrt(n) combinations generated.
Solutions π‘
Cpp π»
class Solution {
public:
vector<string> generateParenthesis(int n) {
vector<string> result;
generate(n, 0, 0, "", result);
return result;
}
private:
void generate(int n, int open, int close, string str, vector<string> &result) {
if (open == n && close == n) {
result.push_back(str);
return;
}
if (open < n) {
generate(n, open + 1, close, str + '(', result);
}
if (open > close) {
generate(n, open, close + 1, str + ')', result);
}
}
};
Python π
class Solution:
def generateParenthesis(self, n: int) -> List[str]:
def backtrack(s, open_count, close_count):
if len(s) == 2 * n:
result.append(s)
return
if open_count < n:
backtrack(s + "(", open_count + 1, close_count)
if close_count < open_count:
backtrack(s + ")", open_count, close_count + 1)
result = []
backtrack("", 0, 0)
return result
Daily Temperatures π§
LeetCode Link: Daily Temperatures
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 739 - Daily Temperatures
Description: Given a list of daily temperatures, produce a list that, for each day in the input, tells you how many days you would have to wait until a warmer temperature. If there is no future day with a warmer temperature, put 0 instead.
Intuition: To determine the number of days until a warmer temperature, we can use a stack to keep track of the previous temperatures. By iterating through the temperatures in reverse order, we can compare each temperature with the temperatures in the stack to find the next warmer day.
Approach:
- Initialize an empty stack to store the indices of temperatures.
- Initialize a result vector with 0s of the same size as the input temperatures.
- Iterate through the temperatures in reverse order:
- While the stack is not empty and the current temperature is greater than the temperature at the index at the top of the stack:
- Pop the index from the stack and calculate the number of days until a warmer temperature (current index - popped index).
- Update the result vector at the popped index with the number of days.
- Push the current index onto the stack.
- Return the result vector.
β Time Complexity: The time complexity is O(n), where n is the number of temperatures. We iterate through the temperatures once.
πΎ Space Complexity: The space complexity is O(n), where n is the number of temperatures. In the worst case, the stack may store all the indices of the temperatures.
Solutions π‘
Cpp π»
#include<bits/stdc++.h>
using namespace std;
class Solution {
public:
vector<int> dailyTemperatures(vector<int> &temperatures) {
int n = temperatures.size();
vector<int> result(n, 0);
stack<int> stack;
for (int i = n - 1; i >= 0; --i) {
while (!stack.empty() && temperatures[i] >= temperatures[stack.top()]) {
stack.pop();
}
if (!stack.empty()) {
result[i] = stack.top() - i;
}
stack.push(i);
}
return result;
}
};
Python π
class Solution:
def dailyTemperatures(self, temperatures: List[int]) -> List[int]:
stack = []
result = [0] * len(temperatures)
for i in range(len(temperatures) - 1, -1, -1):
while stack and temperatures[i] >= temperatures[stack[-1]]:
stack.pop()
if stack:
result[i] = stack[-1] - i
stack.append(i)
return result
Car Fleet π§
LeetCode Link: Car Fleet
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 853 - Car Fleet
Description: N cars are going to the same destination along a one-lane road. The destination is target miles away. Each car i has a constant speed speed[i] (in miles per hour), and initial position position[i] miles towards the target along the road.
A car can never pass another car ahead of it, but it can catch up to it and drive bumper to bumper at the same speed. The distance between these two cars is ignored - they are assumed to have the same position.
A car fleet is some non-empty set of cars driving at the same position and same speed. Note that a single car is also a car fleet.
If a car catches up to a car fleet right at the destination point, it will still be considered as one car fleet.
Intuition: To determine the number of car fleets that reach the destination, we can simulate the car movement and track the time it takes for each car to reach the destination.
Approach:
- Create a vector of pairs to store the positions and speeds of the cars.
- Sort the vector of pairs based on the positions in descending order.
- Initialize the count of car fleets to 0.
- Iterate through each car in the sorted vector:
- Calculate the time it takes for the car to reach the destination (target - position) / speed.
- If the current car's time is greater than the previous car's time (car fleet is not possible), increment the count of car fleets.
- Update the previous car's time with the maximum of the current car's time and the previous car's time.
- Return the count of car fleets.
β Time Complexity: The time complexity is O(n log n), where n is the number of cars. Sorting the vector of pairs takes O(n log n) time.
πΎ Space Complexity: The space complexity is O(n), where n is the number of cars. We store the positions and speeds of the cars in a vector of pairs.
Solutions π‘
Cpp π»
#include<bits/stdc++.h>
using namespace std;
class Solution {
public:
int carFleet(int target, vector<int> &position, vector<int> &speed) {
int n = position.size();
vector<pair<int, int>> cars;
// Create vector of pairs to store positions and speeds
for (int i = 0; i < n; ++i) {
cars.push_back({position[i], speed[i]});
}
// Sort the vector of pairs based on positions in descending order
sort(cars.begin(), cars.end(), [](const pair<int, int> &a, const pair<int, int> &b) {
return a.first > b.first;
});
int count = 0;
double prevTime = 0.0;
for (int i = 0; i < n; ++i) {
double currTime = static_cast<double>(target - cars[i].first) / cars[i].second;
if (currTime > prevTime) {
count++;
prevTime = currTime;
}
}
return count;
}
};
Python π
class Solution:
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
cars = sorted(zip(position, speed), reverse=True)
fleets = 0
prev_time = -1.0
for pos, spd in cars:
time = (target - pos) / spd
if time > prev_time:
fleets += 1
prev_time = time
return fleets
Largest Rectangle In Histogram π§
LeetCode Link: Largest Rectangle In Histogram
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 84 - Largest Rectangle in Histogram
Description: Given an array of integers heights representing the histogram's bar height where the width of each bar is 1, return the area of the largest rectangle in the histogram.
Intuition: To find the largest rectangle in a histogram, we can utilize a stack to keep track of the indices of increasing heights. By iterating through the histogram, we can calculate the area of each rectangle formed by the heights.
Approach:
- Initialize a stack to store the indices of increasing heights.
- Initialize the maximum area to 0.
- Iterate through each bar in the histogram:
- While the stack is not empty and the current bar's height is less than the height at the index at the top of the stack:
- Pop the index from the stack and calculate the area of the rectangle formed by the popped bar.
- Update the maximum area if the calculated area is greater.
- Push the current index onto the stack.
- After iterating through all bars, there might be remaining bars in the stack. Process them similarly to step 3 to calculate the areas and update the maximum area.
- Return the maximum area.
β Time Complexity: The time complexity is O(n), where n is the number of bars in the histogram. We iterate through each bar once.
πΎ Space Complexity: The space complexity is O(n), where n is the number of bars in the histogram. In the worst case, all bars are stored in the stack.
Solutions π‘
Cpp π»
#include<bits/stdc++.h>
using namespace std;
class Solution {
public:
int largestRectangleArea(vector<int> &heights) {
int n = heights.size();
stack<int> stack;
int maxArea = 0;
for (int i = 0; i <= n; ++i) {
while (!stack.empty() && (i == n || heights[i] < heights[stack.top()])) {
int height = heights[stack.top()];
stack.pop();
int width = stack.empty() ? i : i - stack.top() - 1;
maxArea = max(maxArea, height * width);
}
stack.push(i);
}
return maxArea;
}
};
Python π
class Solution:
def largestRectangleArea(self, heights: List[int]) -> int:
stack = []
max_area = 0
for i in range(len(heights)):
while stack and heights[i] < heights[stack[-1]]:
height = heights[stack.pop()]
width = i if not stack else i - stack[-1] - 1
max_area = max(max_area, height * width)
stack.append(i)
while stack:
height = heights[stack.pop()]
width = len(heights) if not stack else len(heights) - stack[-1] - 1
max_area = max(max_area, height * width)
return max_area
Binary Search π
This section contains problems belonging to the Binary Search category.
Problems
- π’ Easy: Binary Search
- π‘ Medium: Search a 2D Matrix
- π‘ Medium: Koko Eating Bananas
- π‘ Medium: Find Minimum In Rotated Sorted Array
- π‘ Medium: Search In Rotated Sorted Array
- π‘ Medium: Time Based Key Value Store
- π΄ Hard: Median of Two Sorted Arrays
Binary Search π§
LeetCode Link: Binary Search
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 704 - Binary Search
Description: Given a sorted (in ascending order) integer array nums of n elements and a target value, write a function to search target in nums. If target exists, then return its index, otherwise return -1.
Intuition: Binary search is a widely used algorithm to efficiently search for a target element in a sorted array. It works by repeatedly dividing the search space in half until the target element is found or determined to be not present.
Approach:
- Initialize left and right pointers to the start and end of the array.
- While the left pointer is less than or equal to the right pointer:
- Calculate the middle index as (left + right) / 2.
- If the middle element is equal to the target, return the middle index.
- If the middle element is greater than the target, update the right pointer to middle - 1.
- If the middle element is less than the target, update the left pointer to middle + 1.
- If the target is not found after the while loop, return -1.
β Time Complexity: The time complexity of binary search is O(log n), where n is the number of elements in the array. At each step, the search space is divided in half.
πΎ Space Complexity: The space complexity is O(1), as the algorithm uses a constant amount of extra space to store the left and right pointers.
Solutions π‘
Cpp π»
class Solution {
public:
int search(vector<int> &nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int mid = left + (right - left) / 2;
if (nums[mid] == target) {
return mid;
} else if (nums[mid] > target) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1;
}
};
Python π
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
elif nums[mid] < target:
left = mid + 1
else:
right = mid - 1
return -1
Search a 2D Matrix π§
LeetCode Link: Search a 2D Matrix
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 74 - Search a 2D Matrix
Description: Write an efficient algorithm that searches for a value in an m x n matrix. This matrix has the following properties:
- Integers in each row are sorted in ascending order from left to right.
- Integers in each column are sorted in ascending order from top to bottom.
Intuition: To search for a value efficiently in the given matrix, we can utilize the sorted property of the matrix and perform binary search on both rows and columns. By narrowing down the search space, we can quickly find the target value or determine that it does not exist.
Approach:
- Initialize the number of rows (m) and columns (n) in the matrix.
- Perform a binary search on the rows:
- Set the left pointer to 0 and the right pointer to m - 1.
- While the left pointer is less than or equal to the right pointer:
- Calculate the middle row index as (left + right) / 2.
- If the target value is found in the middle row, return true.
- If the target value is less than the first element of the middle row, update the right pointer to middle - 1.
- If the target value is greater than the last element of the middle row, update the left pointer to middle + 1.
- Perform a binary search on the columns:
- Set the top pointer to 0 and the bottom pointer to n - 1.
- While the top pointer is less than or equal to the bottom pointer:
- Calculate the middle column index as (top + bottom) / 2.
- If the target value is found in the middle column, return true.
- If the target value is less than the first element of the middle column, update the bottom pointer to middle - 1.
- If the target value is greater than the last element of the middle column, update the top pointer to middle + 1.
- If the target value is not found after both binary searches, return false.
β Time Complexity: The time complexity is O(log m + log n), where m is the number of rows and n is the number of columns in the matrix. Binary search is performed on both rows and columns.
πΎ Space Complexity: The space complexity is O(1), as the algorithm uses a constant amount of extra space.
Solutions π‘
Cpp π»
class Solution {
public:
bool searchMatrix(std::vector<std::vector<int>> &matrix, int target) {
int m = matrix.size();
int n = matrix[0].size();
int leftRow = 0;
int rightRow = m - 1;
int topCol = 0;
int bottomCol = n - 1;
while (leftRow <= rightRow) {
int midRow = leftRow + (rightRow - leftRow) / 2;
if (matrix[midRow][0] <= target && target <= matrix[midRow][n - 1]) {
while (topCol <= bottomCol) {
int midCol = topCol + (bottomCol - topCol) / 2;
if (matrix[midRow][midCol] == target) {
return true;
} else if (matrix[midRow][midCol] < target) {
topCol = midCol + 1;
} else {
bottomCol = midCol - 1;
}
}
break;
} else if (matrix[midRow][0] > target) {
rightRow = midRow - 1;
} else {
leftRow = midRow + 1;
}
}
return false;
}
};
Python π
class Solution:
def searchMatrix(self, matrix: List[List[int]], target: int) -> bool:
if not matrix or not matrix[0]:
return False
rows, cols = len(matrix), len(matrix[0])
left, right = 0, rows * cols - 1
while left <= right:
mid = left + (right - left) // 2
num = matrix[mid // cols][mid % cols]
if num == target:
return True
elif num < target:
left = mid + 1
else:
right = mid - 1
return False
Koko Eating Bananas π§
LeetCode Link: Koko Eating Bananas
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 875 - Koko Eating Bananas
Description: Koko loves to eat bananas. There are n piles of bananas, the ith pile has piles[i] bananas. The guards have gone and will come back in h hours. Koko can decide her bananas-per-hour eating speed of k. Each hour, she chooses some pile of bananas and eats k bananas from that pile. If the pile has less than k bananas, she eats all of them instead and will not eat any more bananas during this hour. Koko likes to eat slowly but still wants to finish eating all the bananas before the guards come back. Return the minimum integer k such that she can eat all the bananas within h hours.
Intuition: To find the minimum eating speed that allows Koko to eat all the bananas within h hours, we can use binary search. By defining the search space and adjusting the speed based on the time taken to eat bananas, we can find the optimal speed.
Approach:
- Set the left and right pointers to 1 and the maximum number of bananas in piles, respectively.
- While the left pointer is less than the right pointer:
- Calculate the middle value as (left + right) / 2, which represents the eating speed.
- Initialize the total hours as 0.
- Iterate through each pile of bananas:
- Calculate the time taken to eat the pile as ceil(piles[i] / (double)middle), which represents the number of hours needed to eat the bananas in the pile.
- Add the time taken to the total hours.
- If the total hours is less than or equal to h, update the right pointer to middle to search for a lower eating speed.
- If the total hours is greater than h, update the left pointer to middle + 1 to search for a higher eating speed.
- Return the left pointer, which represents the minimum eating speed.
β Time Complexity: The time complexity is O(n log m), where n is the number of piles of bananas and m is the maximum number of bananas in piles. Binary search is performed on the search space of eating speeds.
πΎ Space Complexity: The space complexity is O(1), as the algorithm uses a constant amount of extra space.
Solutions π‘
Cpp π»
#include <cmath>
class Solution {
public:
int minEatingSpeed(vector<int> &piles, int h) {
int left = 1;
int right = *max_element(piles.begin(), piles.end());
while (left < right) {
int middle = left + (right - left) / 2;
int totalHours = 0;
for (int bananas : piles) {
// handling integer overflow by using ceil to round up the division result
// and ensuring the result is accurate by casting bananas to double before performing division
totalHours += ceil(static_cast<double>(bananas) / middle);
}
if (totalHours <= h) {
right = middle;
} else {
left = middle + 1;
}
}
return left;
}
};
/*
class Solution {
public:
// Helper function to check if a given eating speed works within the given time constraint
bool isSpeedFeasible(vector<int>& piles, int k, int h) {
double hours = 0;
for (int bananas : piles) {
hours += ceil(static_cast<double>(bananas) / k);
}
return hours <= h;
}
int minEatingSpeed(vector<int>& piles, int h) {
int start = 1;
int end = *max_element(piles.begin(), piles.end());
int bestSpeed = end;
while (start <= end) {
int mid = start + (end - start) / 2;
if (isSpeedFeasible(piles, mid, h)) {
bestSpeed = mid;
end = mid - 1;
} else {
start = mid + 1;
}
}
return bestSpeed;
}
};
*/
Python π
class Solution:
def minEatingSpeed(self, piles: List[int], h: int) -> int:
left, right = 1, max(piles)
while left < right:
mid = left + (right - left) // 2
hours = sum((pile + mid - 1) // mid for pile in piles)
if hours > h:
left = mid + 1
else:
right = mid
return left
Find Minimum In Rotated Sorted Array π§
LeetCode Link: Find Minimum In Rotated Sorted Array
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 153 - Find Minimum in Rotated Sorted Array
Description: Suppose an array of length n is sorted in ascending order, rotated at some pivot unknown to you beforehand. You are given a target value to search for. If found in the array, return its index, otherwise return -1.
Intuition: To find the minimum element in the rotated sorted array, we can utilize the property that the minimum element is the only element that is smaller than its adjacent elements. By performing a modified version of binary search, we can efficiently locate the minimum element.
Approach:
- Initialize left and right pointers to the start and end of the array.
- While the left pointer is less than the right pointer:
- Calculate the middle index as (left + right) / 2.
- If the middle element is greater than the last element, the minimum element is on the right side of the middle. Update the left pointer to middle + 1.
- If the middle element is less than or equal to the last element, the minimum element is on the left side of the middle. Update the right pointer to middle.
- Return the element at the left pointer, which represents the minimum element.
β Time Complexity: The time complexity is O(log n), where n is the length of the array. At each step, the search space is divided in half.
πΎ Space Complexity: The space complexity is O(1), as the algorithm uses a constant amount of extra space.
Solutions π‘
Cpp π»
class Solution {
public:
int findMin(vector<int> &nums) {
int left = 0; // Initialize the left pointer to the start of the array
int right = nums.size() - 1; // Initialize the right pointer to the end of the array
while (left < right) { // Perform binary search until left pointer is less than right pointer
int middle = left + (right - left) / 2; // Calculate the middle index
if (nums[middle] > nums[right]) {
// If the middle element is greater than the last element,
// it means the rotation point is on the right side of the middle.
// Update the left pointer to middle + 1 to search in the right half.
left = middle + 1;
} else {
// If the middle element is less than or equal to the last element,
// it means the rotation point is on the left side of the middle or the middle element itself.
// Update the right pointer to middle to search in the left half.
right = middle;
}
}
return nums[left]; // Return the element at the left pointer, which represents the minimum element.
}
};
// class Solution {
// public:
// int findMin(vector<int>& nums) {
// int low = 0;
// int high = nums.size() - 1;
// // not low <= high since not searching for target
// while (low < high) {
// int mid = low + (high - low) / 2;
// // ex. [3,4,5,6,7,8,9,1,2], mid = 4, high = 8
// // nums[mid] > nums[high], min must be right
// if (nums[mid] > nums[high]) {
// // never consider mid bc know for sure not min
// low = mid + 1;
// // ex. [8,9,1,2,3,4,5,6,7], mid = 4, high = 8
// // nums[mid] <= nums[right], min must be left
// } else {
// // consider mid still bc could be min
// high = mid;
// }
// }
// // low lands on correct value, never disqualified mins
// return nums[low];
// }
// };
Python π
class Solution:
def findMin(self, nums: List[int]) -> int:
left, right = 0, len(nums) - 1
while left < right:
mid = left + (right - left) // 2
if nums[mid] > nums[right]:
left = mid + 1
else:
right = mid
return nums[left]
Search In Rotated Sorted Array π§
LeetCode Link: Search In Rotated Sorted Array
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 33 - Search in Rotated Sorted Array
Description: Suppose an array sorted in ascending order is rotated at some pivot unknown to you beforehand. You are given a target value to search for. If found in the array, return its index, otherwise return -1. You may assume no duplicate exists in the array.
Intuition: To search for a target value efficiently in the rotated sorted array, we can utilize a modified version of binary search. By comparing the target value with the array elements and adjusting the search space based on the rotation point, we can find the target value or determine its absence.
Approach:
- Initialize the left and right pointers to the start and end of the array.
- While the left pointer is less than or equal to the right pointer:
- Calculate the middle index as (left + right) / 2.
- If the middle element is equal to the target value, return the middle index.
- If the left half of the array is sorted:
- If the target value is within the range of the left half, update the right pointer to middle - 1.
- Otherwise, update the left pointer to middle + 1.
- If the right half of the array is sorted:
- If the target value is within the range of the right half, update the left pointer to middle + 1.
- Otherwise, update the right pointer to middle - 1.
- If the target value is not found after the while loop, return -1.
β Time Complexity: The time complexity is O(log n), where n is the size of the array. At each step, the search space is divided in half.
πΎ Space Complexity: The space complexity is O(1), as the algorithm uses a constant amount of extra space.
Solutions π‘
Cpp π»
class Solution {
public:
int search(vector<int> &nums, int target) {
int left = 0;
int right = nums.size() - 1;
while (left <= right) {
int middle = left + (right - left) / 2;
if (nums[middle] == target) { // If middle element is equal to the target value, return the middle index
return middle;
}
if (nums[left] <= nums[middle]) { // If left half of the array is sorted
if (target >= nums[left] && target < nums[middle]) { // If target value is within the range of the left half
right = middle - 1; // Update the right pointer to search in the left half
} else {
left = middle + 1; // Update the left pointer to search in the right half
}
} else { // If right half of the array is sorted
if (target > nums[middle] && target <= nums[right]) { // If target value is within the range of the right half
left = middle + 1; // Update the left pointer to search in the right half
} else {
right = middle - 1; // Update the right pointer to search in the left half
}
}
}
return -1; // Target value not found, return -1
}
};
Python π
class Solution:
def search(self, nums: List[int], target: int) -> int:
left, right = 0, len(nums) - 1
while left <= right:
mid = left + (right - left) // 2
if nums[mid] == target:
return mid
if nums[left] <= nums[mid]:
if nums[left] <= target < nums[mid]:
right = mid - 1
else:
left = mid + 1
else:
if nums[mid] < target <= nums[right]:
left = mid + 1
else:
right = mid - 1
return -1
Time Based Key Value Store π§
LeetCode Link: Time Based Key Value Store
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 981 - Time Based Key-Value Store
Description: Design a time-based key-value data structure that can store multiple values for the same key and retrieve the value of a key at a certain time. Implement the TimeMap class:
- TimeMap() Initializes the object of the data structure.
- void set(String key, String value, int timestamp) Stores the key, value pair in the data structure.
- String get(String key, int timestamp) Returns a value such that set(key, value, timestamp_prev) was called previously, with timestamp_prev <= timestamp. If there are multiple such values, it returns the value associated with the largest timestamp_prev. If there are no values, it returns an empty string ("").
Intuition: To implement the TimeMap, we can utilize a hashmap data structure to store the key-value pairs. Each key will map to a list of timestamps and corresponding values. By using binary search within the list of timestamps, we can efficiently retrieve the value associated with a given key and timestamp.
Approach:
- Implement the TimeMap class.
- Create a hashmap, where each key maps to a list of pairs containing timestamps and values.
- Implement the set function:
- If the key does not exist in the hashmap, create a new list with the first pair of (timestamp, value).
- If the key already exists, append the new pair of (timestamp, value) to the existing list.
- Implement the get function:
- If the key does not exist in the hashmap, return an empty string.
- If the key exists, perform binary search within the list of pairs:
- If the timestamp at the middle index is equal to the target timestamp, return the corresponding value.
- If the timestamp at the middle index is greater than the target timestamp, update the right pointer to middle - 1 and continue searching in the left half.
- If the timestamp at the middle index is less than the target timestamp, update the left pointer to middle + 1 and continue searching in the right half.
- If the binary search does not find an exact match, return the value at the index of the right pointer.
β Time Complexity: The time complexity of set is O(1), as it only involves hashmap operations. The time complexity of get is O(log n), where n is the number of entries for a given key in the hashmap. The binary search is performed within the list of timestamps.
πΎ Space Complexity: The space complexity is O(n), where n is the total number of entries in the hashmap. Each entry occupies space for the key and a list of timestamp-value pairs.
Solutions π‘
Cpp π»
class TimeMap {
private:
unordered_map<string, vector<pair<int, string>>> data; // Hashmap to store key-value pairs
public:
TimeMap() {}
void set(string key, string value, int timestamp) {
data[key].emplace_back(timestamp, value); // Append the pair (timestamp, value) to the list for the key
}
string get(string key, int timestamp) {
if (data.find(key) == data.end()) {
return ""; // Key does not exist, return empty string
}
const vector<pair<int, string>> &entries = data[key]; // Get the list of entries for the key
int left = 0;
int right = entries.size() - 1;
while (left <= right) { // Perform binary search
int middle = left + (right - left) / 2; // Calculate the middle index
if (entries[middle].first == timestamp) {
return entries[middle].second; // Exact match found, return the value
} else if (entries[middle].first > timestamp) {
right = middle - 1; // Target timestamp is in the left half, update the right pointer
} else {
left = middle + 1; // Target timestamp is in the right half, update the left pointer
}
}
if (right >= 0) {
return entries[right].second; // No exact match found, return the value at the right pointer index
}
return ""; // No value found, return empty string
}
};
Python π
class TimeMap:
def __init__(self):
self.data = defaultdict(list)
def set(self, key: str, value: str, timestamp: int) -> None:
self.data[key].append((timestamp, value))
def get(self, key: str, timestamp: int) -> str:
values = self.data[key]
left, right = 0, len(values) - 1
while left <= right:
mid = left + (right - left) // 2
if values[mid][0] == timestamp:
return values[mid][1]
elif values[mid][0] < timestamp:
left = mid + 1
else:
right = mid - 1
if right >= 0:
return values[right][1]
return ""
Median of Two Sorted Arrays π§
LeetCode Link: Median of Two Sorted Arrays
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 4 - Median of Two Sorted Arrays
Description: Given two sorted arrays nums1 and nums2 of size m and n respectively, return the median of the two sorted arrays.
Intuition: To find the median of two sorted arrays, we need to divide the combined array into two equal halves. The median is the middle element or the average of the two middle elements, depending on whether the total number of elements is odd or even. Since the arrays are sorted, we can leverage binary search and partitioning to find the median efficiently.
Approach:
- Ensure that nums1 is the smaller array. If not, swap nums1 and nums2.
- Use binary search to find the correct partitioning point in the smaller array (nums1).
- The partition point i divides nums1 into two parts: elements before i form the left half, and elements after i form the right half.
- Calculate the partition point j in nums2 based on i, such that j = (m + n + 1) / 2 - i, where m is the size of nums1 and n is the size of nums2.
- Check if the partitioning is valid by comparing the maximum element of the left half (maxLeft) with the minimum element of the right half (minRight).
- If the partitioning is valid, calculate the median based on the array sizes and the partition points:
- If the total number of elements (m + n) is odd, the median is maxLeft.
- If the total number of elements is even, the median is the average of maxLeft and minRight.
- If the partitioning is not valid, adjust the partition points using binary search until a valid partitioning is found.
- Return the median.
β Time Complexity: The time complexity of the approach is O(log(min(m, n))), where m and n are the sizes of the input arrays. The binary search is performed on the smaller array.
πΎ Space Complexity: The space complexity is O(1) as the approach uses only a constant amount of extra space.
Solutions π‘
Cpp π»
class Solution {
public:
double findMedianSortedArrays(vector<int> &nums1, vector<int> &nums2) {
// Ensure nums1 is the smaller array
if (nums1.size() > nums2.size()) {
nums1.swap(nums2);
}
int m = nums1.size();
int n = nums2.size();
int left = 0;
int right = m;
int halfLen = (m + n + 1) / 2;
while (left <= right) {
int i = left + (right - left) / 2; // Partition point in nums1
int j = halfLen - i; // Partition point in nums2
if (i < m && nums2[j - 1] > nums1[i]) {
// i is too small, increase it to the right half
left = i + 1;
} else if (i > 0 && nums1[i - 1] > nums2[j]) {
// i is too large, decrease it to the left half
right = i - 1;
} else {
// Found the correct partitioning
int maxLeft = 0;
if (i == 0) {
maxLeft = nums2[j - 1];
} else if (j == 0) {
maxLeft = nums1[i - 1];
} else {
maxLeft = max(nums1[i - 1], nums2[j - 1]);
}
if ((m + n) % 2 == 1) {
return maxLeft; // Odd number of elements, median is the max of the left half
}
int minRight = 0;
if (i == m) {
minRight = nums2[j];
} else if (j == n) {
minRight = nums1[i];
} else {
minRight = min(nums1[i], nums2[j]);
}
return (maxLeft + minRight) / 2.0; // Even number of elements, median is the average of max left and min right
}
}
// Should never reach this point
return 0.0;
}
};
// class Solution {
// public:
// double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
// if (nums1.size() > nums2.size()) {
// // if nums1 is bigger we swap it because we're gonna assume nums1 is smaller
// return findMedianSortedArrays(nums2, nums1);
// }
// int m = nums1.size();
// int n = nums2.size();
// double Answer = 0.0;
// // Now that nums1 is smaller
// int L = 0, R = m;
// while(L <= R) {
// // Mid of nums1 & nums2
// int MidM = L + (R-L)/2;
// int MidN = (m+n+1)/2 - MidM;
// int a = (MidM > 0) ? nums1[MidM - 1] : INT_MIN;
// int b = (MidM < m) ? nums1[MidM] : INT_MAX;
// int c = (MidN > 0) ? nums2[MidN - 1] : INT_MIN;
// int d = (MidN < n) ? nums2[MidN] : INT_MAX;
// // If both halves are correctly sorted
// if(a <= d && c <= b) {
// // Checking if the merged array has even elements or odd
// if( (m + n) % 2 == 0)
// Answer = (max(a, c) + min(b, d)) / 2.0;
// else
// Answer = max(a, c);
// break;
// }
// // If not correctly sorted and right part of nums2 is lesser than left part of nums1
// else if(d < a)
// R = MidM - 1;
// // If not correctly sorted and right side of nums1 is lesser than left part of nums2 i.e. c < b
// else
// L = MidM + 1;
// }
// return Answer;
// }
// };
Python π
class Solution:
def findMedianSortedArrays(self, nums1: List[int], nums2: List[int]) -> float:
if len(nums1) > len(nums2):
nums1, nums2 = nums2, nums1
m, n = len(nums1), len(nums2)
low, high = 0, m
while low <= high:
partition1 = (low + high) // 2
partition2 = (m + n + 1) // 2 - partition1
maxLeft1 = float("-inf") if partition1 == 0 else nums1[partition1 - 1]
minRight1 = float("inf") if partition1 == m else nums1[partition1]
maxLeft2 = float("-inf") if partition2 == 0 else nums2[partition2 - 1]
minRight2 = float("inf") if partition2 == n else nums2[partition2]
if maxLeft1 <= minRight2 and maxLeft2 <= minRight1:
if (m + n) % 2 == 0:
return (max(maxLeft1, maxLeft2) + min(minRight1, minRight2)) / 2
else:
return max(maxLeft1, maxLeft2)
elif maxLeft1 > minRight2:
high = partition1 - 1
else:
low = partition1 + 1
raise ValueError("Input arrays are not sorted.")
Linked List π
This section contains problems belonging to the Linked List category.
Problems
- π’ Easy: Reverse Linked List
- π’ Easy: Merge Two Sorted Lists
- π‘ Medium: Reorder List
- π‘ Medium: Remove Nth Node From End of List
- βͺ Unknown: Copy List With Random Pointer
- π‘ Medium: Add Two Numbers
- π’ Easy: Linked List Cycle
- π‘ Medium: Find The Duplicate Number
- π‘ Medium: LRU Cache
- π΄ Hard: Merge K Sorted Lists
- π΄ Hard: Reverse Nodes In K Group
Reverse Linked List π§
LeetCode Link: Reverse Linked List
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 206 - Reverse Linked List
Description: Reverse a singly linked list.
Intuition: To reverse a linked list, we need to change the direction of each node's "next" pointer. We can do this iteratively by keeping track of three nodes: current, previous, and next. We start with current pointing to the head of the original list, previous as nullptr (since there is no node before the head), and next as the next node of the current node. During each iteration, we update the "next" pointer of the current node to point to the previous node. Then, we move the previous node to be the current node and the current node to be the next node. We repeat this process until the end of the original list is reached.
Approach:
- Create three pointers: current, previous, and next.
- Initialize current to the head of the original list and previous as nullptr.
- While the current node is not nullptr: a. Update the "next" pointer of the current node to point to the previous node. b. Move previous to be the current node and current to be the next node.
- At the end of the loop, the previous node will be the new head of the reversed list.
β Time Complexity: The time complexity is O(N), where N is the number of nodes in the linked list. We visit each node once during the reversal process.
πΎ Space Complexity: The space complexity is O(1) since we are using a constant amount of extra space for the three pointers.
Solutions π‘
Cpp π»
class Solution {
public:
ListNode *reverseList(ListNode *head) {
ListNode *current = head;
ListNode *previous = nullptr;
ListNode *next;
while (current) {
next = current->next;
current->next = previous;
previous = current;
current = next;
}
return previous;
}
};
Python π
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseList(self, head: ListNode) -> ListNode:
prev = None
current = head
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
return prev
Merge Two Sorted Lists π§
LeetCode Link: Merge Two Sorted Lists
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 21 - Merge Two Sorted Lists
Description: Merge two sorted linked lists and return it as a new sorted list. The new list should be made by splicing together the nodes of the first two lists.
Intuition: To merge two sorted linked lists, we can use a simple approach where we compare the values of the nodes at the head of both lists. We create a dummy node to serve as the new merged list's head. Then, we compare the values of the current nodes from both lists. Whichever node's value is smaller, we append that node to the merged list and move its pointer to the next node. We repeat this process until we have traversed both input lists completely.
Approach:
- Create a dummy node as the merged list's head.
- Initialize two pointers, "curr" and "dummy," both pointing to the dummy node.
- While both input lists are not empty: a. Compare the values of the current nodes from both lists. b. Append the node with the smaller value to the merged list. c. Move the pointer of the merged list and the pointer of the selected node's list to their next nodes.
- If any of the input lists still has nodes remaining, append the rest of that list to the merged list.
- Return the merged list's head, which is the next node of the dummy node.
β Time Complexity: The time complexity is O(N+M), where N and M are the number of nodes in the input lists, as we need to visit each node once.
πΎ Space Complexity: The space complexity is O(1), as we only use a constant amount of extra space for the pointers.
Solutions π‘
Cpp π»
class Solution {
public:
ListNode *mergeTwoLists(ListNode *l1, ListNode *l2) {
ListNode *dummy = new ListNode(0);
ListNode *curr = dummy;
while (l1 && l2) {
if (l1->val < l2->val) {
curr->next = l1;
l1 = l1->next;
} else {
curr->next = l2;
l2 = l2->next;
}
curr = curr->next;
}
if (l1) {
curr->next = l1;
} else {
curr->next = l2;
}
return dummy->next;
}
};
Python π
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def mergeTwoLists(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode()
current = dummy
while l1 and l2:
if l1.val < l2.val:
current.next = l1
l1 = l1.next
else:
current.next = l2
l2 = l2.next
current = current.next
if l1:
current.next = l1
elif l2:
current.next = l2
return dummy.next
Reorder List π§
LeetCode Link: Reorder List
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 143 - Reorder List
Description: Given a singly linked list L: L0 -> L1 -> ... -> Ln-1 -> Ln, reorder it to: L0 -> Ln -> L1 -> Ln-1 -> L2 -> Ln-2 -> ...
You may not modify the values in the list's nodes, only nodes itself may be changed.
Intuition: To reorder the linked list, we can divide the problem into three main steps:
- Find the middle of the list using the slow and fast pointer approach.
- Reverse the second half of the list.
- Merge the first half and the reversed second half together to form the reordered list.
Approach:
- Use the slow and fast pointer approach to find the middle of the list.
- Reverse the second half of the list.
- Merge the first half and the reversed second half together to form the reordered list.
β Time Complexity: The time complexity is O(N), where N is the number of nodes in the linked list, as we need to traverse the list twice (once to find the middle and once to reverse the second half).
πΎ Space Complexity: The space complexity is O(1) as we are using constant extra space for the pointers.
Solutions π‘
Cpp π»
class Solution {
public:
void reorderList(ListNode *head) {
if (!head || !head->next || !head->next->next) {
return;
}
// Step 1: Find the middle of the list
ListNode *slow = head;
ListNode *fast = head;
while (fast->next && fast->next->next) {
slow = slow->next;
fast = fast->next->next;
}
// Step 2: Reverse the second half of the list
ListNode *prev = nullptr;
ListNode *curr = slow->next;
slow->next = nullptr;
while (curr) {
ListNode *nextNode = curr->next;
curr->next = prev;
prev = curr;
curr = nextNode;
}
// Step 3: Merge the first half and the reversed second half
ListNode *first = head;
ListNode *second = prev;
while (second) {
ListNode *nextFirst = first->next;
ListNode *nextSecond = second->next;
first->next = second;
second->next = nextFirst;
first = nextFirst;
second = nextSecond;
}
}
};
Python π
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reorderList(self, head: ListNode) -> None:
if not head or not head.next or not head.next.next:
return
# Find the middle of the list
slow, fast = head, head
while fast.next and fast.next.next:
slow = slow.next
fast = fast.next.next
# Reverse the second half of the list
prev, current = None, slow.next
slow.next = None
while current:
next_node = current.next
current.next = prev
prev = current
current = next_node
# Merge the two halves alternately
p1, p2 = head, prev
while p2:
next_p1, next_p2 = p1.next, p2.next
p1.next = p2
p2.next = next_p1
p1, p2 = next_p1, next_p2
Remove Nth Node From End of List π§
LeetCode Link: Remove Nth Node From End of List
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 19 - Remove Nth Node From End of List
Description: Given the head of a linked list, remove the nth node from the end of the list and return its head.
Intuition: To remove the nth node from the end of the list, we can use the two-pointer approach. We will use two pointers, first and second, with a gap of n nodes between them. When the second pointer reaches the end of the list, the first pointer will be pointing to the nth node from the end. We can then remove that node.
Approach:
- Create a dummy node and set it as the head of the list to handle cases where we need to remove the head.
- Initialize both first and second pointers to the dummy node.
- Move the second pointer n+1 times to create a gap of n nodes between first and second.
- Move both first and second pointers simultaneously until the second pointer reaches the end.
- Remove the nth node by updating the next pointer of the previous node to skip the nth node.
β Time Complexity: The time complexity is O(N), where N is the number of nodes in the linked list, as we need to traverse the list once to find the nth node from the end.
πΎ Space Complexity: The space complexity is O(1) as we are using constant extra space for the pointers.
Solutions π‘
Cpp π»
class Solution {
public:
ListNode *removeNthFromEnd(ListNode *head, int n) {
ListNode *dummy = new ListNode(0);
dummy->next = head;
ListNode *first = dummy;
ListNode *second = dummy;
// Move second pointer n+1 steps ahead
for (int i = 0; i <= n; i++) {
second = second->next;
}
// Move both pointers simultaneously until second reaches the end
while (second) {
first = first->next;
second = second->next;
}
// Remove the nth node by updating the next pointer of the previous node
ListNode *temp = first->next;
first->next = first->next->next;
delete temp;
return dummy->next;
}
};
Python π
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def removeNthFromEnd(self, head: ListNode, n: int) -> ListNode:
dummy = ListNode(0)
dummy.next = head
fast = slow = dummy
# Move 'fast' n nodes ahead
for _ in range(n):
fast = fast.next
# Move both pointers until 'fast' reaches the end
while fast.next:
fast = fast.next
slow = slow.next
# Remove the nth node from the end
slow.next = slow.next.next
return dummy.next
Copy List With Random Pointer π§
LeetCode Link: Copy List With Random Pointer
Difficulty: Unknown
Problem Explanation π
Problem: LeetCode 138 - Copy List with Random Pointer
Description: A linked list is given such that each node contains an additional random pointer that could point to any node in the list or null. Return a deep copy of the list.
Intuition: To create a deep copy of the given linked list, we can use a hashmap to keep track of the mapping between the original nodes and their copies. We will traverse the original list, create a copy of each node, and store the mapping in the hashmap. Then we will traverse the list again to connect the copied nodes with their corresponding random pointers.
Approach:
- Traverse the original list, create a copy of each node, and store the mapping in the hashmap.
- Traverse the original list again, and for each node, connect its copy with the corresponding random pointer using the hashmap.
β Time Complexity: The time complexity is O(N), where N is the number of nodes in the linked list, as we need to traverse the list twice.
πΎ Space Complexity: The space complexity is O(N) as we use extra space to store the hashmap, where N is the number of nodes in the linked list.
Solutions π‘
Cpp π»
class Solution {
public:
Node *copyRandomList(Node *head) {
if (!head) {
return nullptr;
}
unordered_map<Node *, Node *> nodeMap;
Node *current = head;
// Traverse the original list and create a copy of each node
while (current) {
nodeMap[current] = new Node(current->val);
current = current->next;
}
current = head;
// Traverse the original list again to connect the copied nodes with their random pointers
while (current) {
nodeMap[current]->next = nodeMap[current->next];
nodeMap[current]->random = nodeMap[current->random];
current = current->next;
}
return nodeMap[head];
}
};
Python π
# Definition for a Node.
# class Node:
# def __init__(self, x: int, next: 'Node' = None, random: 'Node' = None):
# self.val = int(x)
# self.next = next
# self.random = random
class Solution:
def copyRandomList(self, head: "Node") -> "Node":
if not head:
return None
# Step 1: Duplicate nodes and insert them in between the original nodes
current = head
while current:
duplicate = Node(current.val)
duplicate.next = current.next
current.next = duplicate
current = duplicate.next
# Step 2: Update random pointers for the duplicate nodes
current = head
while current:
if current.random:
current.next.random = current.random.next
current = current.next.next
# Step 3: Split the combined list into two separate lists
original = head
duplicate_head = head.next
current = duplicate_head
while original:
original.next = original.next.next
if current.next:
current.next = current.next.next
original = original.next
current = current.next
return duplicate_head
Add Two Numbers π§
LeetCode Link: Add Two Numbers
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 2 - Add Two Numbers
Description: You are given two non-empty linked lists representing two non-negative integers. The digits are stored in reverse order, and each of their nodes contains a single digit. Add the two numbers and return the sum as a linked list.
Intuition: We can traverse the two linked lists simultaneously, adding the digits at the same position and keeping track of the carry. As we move forward, we create a new node for the sum and update the carry for the next iteration. If any linked list has more digits left, we continue the process until both lists are fully traversed.
Approach:
- Initialize a dummy node to the head of the result list.
- Initialize variables
carry
andsum
to 0. - Traverse both linked lists simultaneously until both are fully traversed.
- At each step, compute the sum of digits and the carry for the next iteration.
- Create a new node with the sum and attach it to the result list.
- Move the current pointers of both input lists to the next nodes.
- If any list has remaining digits, continue adding them to the result.
- Return the head of the result list after skipping the dummy node.
β Time Complexity: The time complexity is O(max(N, M)), where N and M are the number of nodes in the input linked lists. We traverse both lists once.
πΎ Space Complexity: The space complexity is O(max(N, M)), where N and M are the number of nodes in the input linked lists. The extra space is used to store the result list.
Solutions π‘
Cpp π»
class Solution {
public:
ListNode *addTwoNumbers(ListNode *l1, ListNode *l2) {
ListNode *dummy = new ListNode(0);
ListNode *current = dummy;
int carry = 0;
while (l1 || l2) {
int sum = carry;
if (l1) {
sum += l1->val;
l1 = l1->next;
}
if (l2) {
sum += l2->val;
l2 = l2->next;
}
carry = sum / 10;
current->next = new ListNode(sum % 10);
current = current->next;
}
if (carry) {
current->next = new ListNode(carry);
}
return dummy->next;
}
};
Python π
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def addTwoNumbers(self, l1: ListNode, l2: ListNode) -> ListNode:
dummy = ListNode()
current = dummy
carry = 0
while l1 or l2:
val1 = l1.val if l1 else 0
val2 = l2.val if l2 else 0
total = val1 + val2 + carry
carry = total // 10
digit = total % 10
current.next = ListNode(digit)
current = current.next
if l1:
l1 = l1.next
if l2:
l2 = l2.next
if carry:
current.next = ListNode(carry)
return dummy.next
Linked List Cycle π§
LeetCode Link: Linked List Cycle
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 141 - Linked List Cycle
Description: Given a linked list, determine if it has a cycle in it. To represent a cycle in the given linked list, we use an integer pos, which represents the position (0-indexed) in the linked list where the tail connects to. If pos is -1, there is no cycle in the linked list.
Intuition: To detect if there is a cycle in a linked list, we can use the two-pointer technique. We maintain two pointers, slow and fast. The slow pointer moves one step at a time, while the fast pointer moves two steps at a time. If there is a cycle in the linked list, the fast pointer will eventually catch up to the slow pointer, and they will meet.
Approach:
- Initialize two pointers, slow and fast, to the head of the linked list.
- Move the slow pointer one step and the fast pointer two steps at a time.
- If there is a cycle, the fast pointer will eventually catch up to the slow pointer.
- If the fast pointer becomes null, there is no cycle in the linked list.
- Return true if the two pointers meet, otherwise return false.
β Time Complexity: The time complexity is O(n), where n is the number of nodes in the linked list. In the worst case, the fast pointer traverses the linked list twice.
πΎ Space Complexity: The space complexity is O(1) since we only use two pointers to detect the cycle.
Solutions π‘
Cpp π»
class Solution {
public:
bool hasCycle(ListNode *head) {
ListNode *slow = head;
ListNode *fast = head;
while (fast && fast->next) {
slow = slow->next;
fast = fast->next->next;
if (slow == fast) {
return true;
}
}
return false;
}
};
Python π
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, x):
# self.val = x
# self.next = None
class Solution:
def hasCycle(self, head: ListNode) -> bool:
if not head or not head.next:
return False
slow = head
fast = head.next
while slow != fast:
if not fast or not fast.next:
return False
slow = slow.next
fast = fast.next.next
return True
Find The Duplicate Number π§
LeetCode Link: Find The Duplicate Number
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 287 - Find the Duplicate Number
Description: Given an array nums containing n + 1 integers where each integer is between 1 and n (inclusive), prove that at least one duplicate number must exist in the array. Assume that there is only one duplicate number, find the duplicate one.
Intuition: To find the duplicate number, we can use the concept of cycle detection in a linked list. Since the numbers in the array are between 1 and n, we can treat the array as a linked list, where each element points to the value at its index. If there is a duplicate number, it means there is a cycle in the linked list.
Approach:
- Use the two-pointer technique to detect the cycle in the array.
- Initialize slow and fast pointers to the first element (index 0) of the array.
- Move the slow pointer one step at a time and the fast pointer two steps at a time.
- When the two pointers meet, reset one of the pointers to the first element (index 0).
- Move both pointers one step at a time until they meet again. The meeting point is the duplicate number.
β Time Complexity: The time complexity is O(n), where n is the length of the array. The loop to detect the cycle takes at most n steps.
πΎ Space Complexity: The space complexity is O(1) since we are not using any additional data structures.
Note: The given array is assumed to have at least one duplicate number, and the duplicate number will always exist.
Solutions π‘
Cpp π»
class Solution {
public:
int findDuplicate(vector<int> &nums) {
int slow = nums[0]; // Tortoise
int fast = nums[0]; // Hare
// Step 1: Detect the cycle
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
// Step 2: Find the entrance to the cycle (duplicate number)
slow = nums[0];
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
};
Python π
class Solution:
def findDuplicate(self, nums: List[int]) -> int:
slow = nums[0]
fast = nums[0]
# Move slow and fast pointers
while True:
slow = nums[slow]
fast = nums[nums[fast]]
if slow == fast:
break
# Find the entrance of the cycle
slow = nums[0]
while slow != fast:
slow = nums[slow]
fast = nums[fast]
return slow
LRU Cache π§
LeetCode Link: LRU Cache
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 146 - LRU Cache
Description: Design a data structure that follows the constraints of a Least Recently Used (LRU) cache. Implement the LRUCache class:
- LRUCache(int capacity) Initialize the LRU cache with positive size capacity.
- int get(int key) Return the value of the key if the key exists, otherwise return -1.
- void put(int key, int value) Update the value of the key if the key exists. Otherwise, add the key-value pair to the cache. If the number of keys exceeds the capacity from this operation, evict the least recently used key.
Intuition: To efficiently implement an LRU cache, we can use a combination of a hashmap and a doubly linked list. The hashmap will store the key-value pairs, and the doubly linked list will maintain the order of the elements based on their usage.
Approach:
- We use a hashmap to store the key-value pairs for quick access.
- We use a doubly linked list to maintain the order of the elements based on their usage.
- When getting a value from the cache, we first check if the key exists in the hashmap. If it does, we update the usage order in the doubly linked list by moving the corresponding node to the front (most recently used).
- When putting a key-value pair into the cache, we check if the key already exists in the hashmap. If it does, we update the value and move the corresponding node to the front (most recently used). If it doesn't, we add a new node to the front of the doubly linked list and update the hashmap. If the number of keys exceeds the capacity, we remove the least recently used node from the back of the doubly linked list and also remove the corresponding key from the hashmap.
β Time Complexity: Both get and put operations have a time complexity of O(1) since all operations (insert, delete, and access) on the doubly linked list are constant time.
πΎ Space Complexity: The space complexity is O(capacity) to store the key-value pairs and doubly linked list nodes.
Solutions π‘
Cpp π»
class LRUCache {
private:
// Structure to represent a doubly linked list node
struct ListNode {
int key; // The key of the cache item
int value; // The value of the cache item
ListNode *prev; // Pointer to the previous node in the list
ListNode *next; // Pointer to the next node in the list
ListNode(int k, int v) : key(k), value(v), prev(nullptr), next(nullptr) {}
};
int capacity; // Maximum capacity of the LRUCache
unordered_map<int, ListNode *> hashmap; // Hashmap to store key-node mappings
ListNode *head; // Dummy head node for the doubly linked list
ListNode *tail; // Dummy tail node for the doubly linked list
// Helper function to add a node to the front of the linked list
void addToFront(ListNode *node) {
node->next = head->next;
node->prev = head;
head->next->prev = node;
head->next = node;
}
// Helper function to remove a node from the linked list
void removeFromList(ListNode *node) {
node->prev->next = node->next;
node->next->prev = node->prev;
}
public:
// Constructor to initialize the LRUCache with a given capacity
LRUCache(int capacity) {
this->capacity = capacity;
head = new ListNode(-1, -1); // Initialize dummy head with key and value -1
tail = new ListNode(-1, -1); // Initialize dummy tail with key and value -1
head->next = tail; // Connect head to tail initially (empty list)
tail->prev = head; // Connect tail to head initially (empty list)
}
// Function to get the value of the given key from the cache
int get(int key) {
if (hashmap.find(key) != hashmap.end()) {
ListNode *node = hashmap[key];
removeFromList(node); // Move the accessed node to the front
addToFront(node);
return node->value; // Return the value associated with the key
}
return -1; // Key not found, return -1
}
// Function to put a key-value pair in the cache
void put(int key, int value) {
if (hashmap.find(key) != hashmap.end()) {
ListNode *node = hashmap[key];
removeFromList(node); // Remove the existing node from the list
node->value = value; // Update the value
addToFront(node); // Move the updated node to the front
} else {
if (hashmap.size() >= capacity) {
ListNode *removedNode = tail->prev;
removeFromList(removedNode); // Remove the last node (least recently used)
hashmap.erase(removedNode->key); // Remove the key from the hashmap
delete removedNode; // Free the memory of the removed node
}
ListNode *newNode = new ListNode(key, value); // Create a new node for the key-value pair
addToFront(newNode); // Add the new node to the front of the list
hashmap[key] = newNode; // Add the key-node mapping to the hashmap
}
}
// Destructor to properly deallocate memory for the ListNode objects
~LRUCache() {
ListNode *curr = head;
while (curr) {
ListNode *temp = curr;
curr = curr->next;
delete temp;
}
}
};
Python π
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = {}
self.order = DoublyLinkedList()
def get(self, key: int) -> int:
if key in self.cache:
self.order.move_to_front(key)
return self.cache[key]
return -1
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache[key] = value
self.order.move_to_front(key)
else:
if len(self.cache) >= self.capacity:
removed_key = self.order.remove_last()
del self.cache[removed_key]
self.cache[key] = value
self.order.add_to_front(key)
class DoublyLinkedList:
def __init__(self):
self.head = ListNode()
self.tail = ListNode()
self.head.next = self.tail
self.tail.prev = self.head
self.nodes = {}
def add_to_front(self, key):
node = ListNode(key)
node.next = self.head.next
node.prev = self.head
self.head.next.prev = node
self.head.next = node
self.nodes[key] = node
def move_to_front(self, key):
node = self.nodes[key]
node.prev.next = node.next
node.next.prev = node.prev
self.add_to_front(key)
def remove_last(self):
node = self.tail.prev
node.prev.next = self.tail
self.tail.prev = node.prev
del self.nodes[node.key]
return node.key
class ListNode:
def __init__(self, key=None):
self.key = key
self.prev = None
self.next = None
"""
from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.cache = OrderedDict()
self.capacity = capacity
def get(self, key: int) -> int:
if key in self.cache:
self.cache.move_to_end(key)
return self.cache[key]
else:
return -1
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache.move_to_end(key)
elif self.capacity <= 0:
_ = self.cache.popitem(False)
else:
self.capacity = max(0, self.capacity - 1)
self.cache[key] = value
"""
# Your LRUCache object will be instantiated and called as such:
# obj = LRUCache(capacity)
# param_1 = obj.get(key)
# obj.put(key,value)
Merge K Sorted Lists π§
LeetCode Link: Merge K Sorted Lists
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 23 - Merge k Sorted Lists
Description: You are given an array of k linked-lists, each linked-list is sorted in ascending order. Merge all the linked-lists into one sorted linked-list and return it.
Intuition: To merge k sorted lists, we can use a priority queue (min heap). We start by pushing the head nodes of all k linked lists into the priority queue. Then, while the priority queue is not empty, we pop the node with the minimum value, append it to the result list, and push its next node (if exists) back into the priority queue. This way, we always pick the smallest element to add to the result list, which ensures that the final list is sorted.
Approach:
- Create a struct ListNode to represent the nodes of the linked list.
- Create a custom comparator function for the priority queue to compare ListNode pointers based on their values.
- Initialize a priority queue (min heap) using the custom comparator.
- Push the head nodes of all k linked lists into the priority queue.
- Initialize a dummy ListNode to build the merged list.
- While the priority queue is not empty, pop the node with the minimum value, append it to the dummy list, and push its next node (if exists) back into the priority queue.
- Finally, return the next node of the dummy list, which will be the head of the merged sorted list.
β Time Complexity: The overall time complexity is O(n*log(k)), where n is the total number of nodes and k is the number of linked lists. The priority queue operations take O(log(k)) time, and we do this for all n nodes.
πΎ Space Complexity: The space complexity is O(k), as we store the head nodes of all k linked lists in the priority queue.
Solutions π‘
Cpp π»
struct ListNodeComparator {
bool operator()(const ListNode *a, const ListNode *b) const {
return a->val > b->val;
}
};
class Solution {
public:
ListNode *mergeKLists(vector<ListNode *> &lists) {
priority_queue<ListNode *, vector<ListNode *>, ListNodeComparator> pq;
for (ListNode *head : lists) {
if (head) {
pq.push(head);
}
}
ListNode dummy(0);
ListNode *current = &dummy;
while (!pq.empty()) {
ListNode *smallest = pq.top();
pq.pop();
current->next = smallest;
current = current->next;
if (smallest->next) {
pq.push(smallest->next);
}
}
return dummy.next;
}
};
Python π
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
import heapq
class Solution:
def mergeKLists(self, lists: List[ListNode]) -> ListNode:
min_heap = []
for i, l in enumerate(lists):
if l:
heapq.heappush(min_heap, (l.val, i))
dummy = ListNode()
current = dummy
while min_heap:
val, idx = heapq.heappop(min_heap)
current.next = ListNode(val)
current = current.next
if lists[idx].next:
heapq.heappush(min_heap, (lists[idx].next.val, idx))
lists[idx] = lists[idx].next
return dummy.next
Reverse Nodes In K Group π§
LeetCode Link: Reverse Nodes In K Group
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 25 - Reverse Nodes in k-Group
Description: Given a linked list, reverse the nodes of a linked list k at a time and return its modified list. k is a positive integer and is less than or equal to the length of the linked list. If the number of nodes is not a multiple of k, then the remaining nodes should remain as it is.
Intuition: To reverse the nodes in k-groups, we can use a recursive approach. We start by finding the k+1-th node in the linked list, as it will be the new head of the reversed k-group. Then we reverse the current k-group and connect it to the next reversed k-group. We repeat this process until we reach the end of the linked list.
Approach:
- Create a struct ListNode to represent the nodes of the linked list.
- Implement a helper function to reverse a k-group of nodes, which takes the head and tail of the group as input and returns the new head of the reversed group.
- Initialize a dummy ListNode to build the final result list.
- Create a pointer current to iterate through the linked list.
- Find the k+1-th node from the current node.
- If the k+1-th node exists, reverse the current k-group and update the pointers accordingly.
- Append the reversed k-group to the dummy list and move the current pointer to the k+1-th node.
- Repeat steps 5 to 7 until we reach the end of the linked list.
- Finally, return the next node of the dummy list, which will be the head of the modified list.
β Time Complexity: The time complexity is O(n), where n is the number of nodes in the linked list. We visit each node once during the reversal process.
πΎ Space Complexity: The space complexity is O(1), as we use only a constant amount of extra space throughout the process.
Solutions π‘
Cpp π»
class Solution {
public:
ListNode *reverseKGroup(ListNode *head, int k) {
ListNode *dummy = new ListNode(0);
dummy->next = head;
ListNode *current = dummy;
while (current) {
ListNode *groupStart = current->next;
ListNode *groupEnd = current;
// Find k+1-th node
for (int i = 0; i < k && groupEnd; i++) {
groupEnd = groupEnd->next;
}
if (!groupEnd) {
break; // Remaining nodes are less than k
}
ListNode *nextGroup = groupEnd->next;
// Reverse the current k-group
reverseGroup(groupStart, groupEnd);
// Connect reversed k-group to the previous group
current->next = groupEnd;
groupStart->next = nextGroup;
// Move the current pointer to the next group
current = groupStart;
}
return dummy->next;
}
private:
// Helper function to reverse a k-group of nodes
void reverseGroup(ListNode *head, ListNode *tail) {
ListNode *prev = nullptr;
ListNode *current = head;
while (current != tail) {
ListNode *nextNode = current->next;
current->next = prev;
prev = current;
current = nextNode;
}
tail->next = prev;
}
};
Python π
# Definition for singly-linked list.
# class ListNode:
# def __init__(self, val=0, next=None):
# self.val = val
# self.next = next
class Solution:
def reverseKGroup(self, head: ListNode, k: int) -> ListNode:
if not head or k == 1:
return head
# Count the number of nodes in the list
count = 0
current = head
while current:
count += 1
current = current.next
if count < k:
return head
# Reverse the first k nodes
prev, current = None, head
for _ in range(k):
next_node = current.next
current.next = prev
prev = current
current = next_node
# Recursively reverse the remaining part of the list
head.next = self.reverseKGroup(current, k)
return prev
Trees π
This section contains problems belonging to the Trees category.
Problems
- π’ Easy: Invert Binary Tree
- π’ Easy: Maximum Depth of Binary Tree
- π’ Easy: Diameter of Binary Tree
- π’ Easy: Balanced Binary Tree
- π’ Easy: Same Tree
- π’ Easy: Subtree of Another Tree
- π‘ Medium: Lowest Common Ancestor of a Binary Search Tree
- π‘ Medium: Binary Tree Level Order Traversal
- π‘ Medium: Binary Tree Right Side View
- π‘ Medium: Count Good Nodes In Binary Tree
- π‘ Medium: Validate Binary Search Tree
- π‘ Medium: Kth Smallest Element In a BST
- π‘ Medium: Construct Binary Tree from Preorder and Inorder Traversal
- π΄ Hard: Binary Tree Maximum Path Sum
- π΄ Hard: Serialize and Deserialize Binary Tree
Invert Binary Tree π§
LeetCode Link: Invert Binary Tree
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 226 - Invert Binary Tree
Description: Given the root of a binary tree, invert the tree and return its root.
Intuition: To invert a binary tree, we need to swap the left and right subtrees of each node. This can be done recursively, starting from the root node and swapping the children of each node.
Approach:
- Implement a recursive function to invert the binary tree.
- If the root is null, return null.
- Swap the left and right children of the root node.
- Recursively invert the left and right subtrees.
- Return the modified root.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the binary tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode *invertTree(TreeNode *root) {
// Base case: if the root is null, return null
if (root == nullptr) {
return nullptr;
}
// Swap the left and right children of the root node
swap(root->left, root->right);
// Recursively invert the left and right subtrees
invertTree(root->left);
invertTree(root->right);
return root;
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def invertTree(self, root: TreeNode) -> TreeNode:
if not root:
return None
# Swap left and right subtrees
root.left, root.right = root.right, root.left
# Recursively invert left and right subtrees
self.invertTree(root.left)
self.invertTree(root.right)
return root
Maximum Depth of Binary Tree π§
LeetCode Link: Maximum Depth of Binary Tree
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 104 - Maximum Depth of Binary Tree
Description: Given the root of a binary tree, return its maximum depth.
Intuition: The maximum depth of a binary tree is the number of edges in the longest path from the root node to any leaf node. We can find the maximum depth by recursively traversing the tree and keeping track of the depth at each level.
Approach:
- Implement a recursive function to find the maximum depth of the binary tree.
- If the root is null, return 0.
- Recursively calculate the maximum depth of the left subtree.
- Recursively calculate the maximum depth of the right subtree.
- Return the maximum depth among the left and right subtrees, plus 1 for the current level.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the binary tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode *root) {
// Base case: if the root is null, return 0
if (root == nullptr) {
return 0;
}
// Recursively calculate the maximum depth of the left and right subtrees
int leftDepth = maxDepth(root->left);
int rightDepth = maxDepth(root->right);
// Return the maximum depth among the left and right subtrees, plus 1 for the current level
return max(leftDepth, rightDepth) + 1;
}
};
// class Solution {
// public:
// int maxDepth(TreeNode* root) {
// if(!root)
// return 0;
// return 1 + max(maxDepth(root->left), maxDepth(root->right));
// }
// };
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def maxDepth(self, root: TreeNode) -> int:
if not root:
return 0
left_depth = self.maxDepth(root.left)
right_depth = self.maxDepth(root.right)
return max(left_depth, right_depth) + 1
Diameter of Binary Tree π§
LeetCode Link: Diameter of Binary Tree
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 543 - Diameter of Binary Tree
Description: Given the root of a binary tree, return the length of the diameter of the tree. The diameter of a binary tree is defined as the length of the longest path between any two nodes in the tree. This path may or may not pass through the root.
Intuition: To find the diameter of a binary tree, we need to determine the length of the longest path between any two nodes. This can be achieved by calculating the maximum depth of each node's left and right subtrees and summing them up. We can use a recursive approach to traverse the tree and update the diameter as we go.
Approach:
- Implement a recursive function to find the diameter of the binary tree.
- If the root is null, return 0 as the diameter.
- Recursively calculate the maximum depth of the left subtree.
- Recursively calculate the maximum depth of the right subtree.
- Update the diameter by taking the maximum of the current diameter and the sum of the maximum depths of the left and right subtrees.
- Return the maximum depth among the left and right subtrees, plus 1 for the current level.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the binary tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
// Definition for a binary tree node
// struct TreeNode {
// int val;
// TreeNode* left;
// TreeNode* right;
// TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
// };
class Solution {
public:
int diameterOfBinaryTree(TreeNode *root) {
int diameter = 0;
maxDepth(root, diameter);
return diameter;
}
private:
// Depth First Search
int maxDepth(TreeNode *root, int &diameter) {
// Base case: if the root is null, return 0
if (root == nullptr) {
return 0;
}
// Recursively calculate the maximum depth of the left and right subtrees
int leftDepth = maxDepth(root->left, diameter);
int rightDepth = maxDepth(root->right, diameter);
// Update the diameter by taking the maximum of the current diameter and the sum of the maximum depths of the left and right subtrees
diameter = max(diameter, leftDepth + rightDepth);
// Return the maximum depth among the left and right subtrees, plus 1 for the current level
return max(leftDepth, rightDepth) + 1;
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def diameterOfBinaryTree(self, root: TreeNode) -> int:
def height(node):
if not node:
return 0
left_height = height(node.left)
right_height = height(node.right)
self.diameter = max(self.diameter, left_height + right_height)
return max(left_height, right_height) + 1
self.diameter = 0
height(root)
return self.diameter
Balanced Binary Tree π§
LeetCode Link: Balanced Binary Tree
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 110 - Balanced Binary Tree
Description: Given a binary tree, determine if it is height-balanced. A height-balanced binary tree is defined as a binary tree in which the left and right subtrees' heights differ by at most one.
Intuition: To determine if a binary tree is height-balanced, we need to check if the heights of its left and right subtrees differ by at most one. We can calculate the height of each subtree recursively and compare the heights at each level to check for balance.
Approach:
- Implement a recursive function to check if the binary tree is height-balanced.
- If the root is null, return true as it is considered height-balanced.
- Recursively calculate the height of the left subtree.
- Recursively calculate the height of the right subtree.
- Check if the heights of the left and right subtrees differ by more than one. If so, return false.
- If the heights are balanced, return true if both the left and right subtrees are also balanced; otherwise, return false.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the binary tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool isBalanced(TreeNode *root) {
return checkHeight(root) != -1;
}
private:
int checkHeight(TreeNode *root) {
// Base case: if the root is null, return 0
if (root == nullptr) {
return 0;
}
// Recursively calculate the height of the left and right subtrees
int leftHeight = checkHeight(root->left);
int rightHeight = checkHeight(root->right);
// If the left or right subtree is not balanced, return -1
if (leftHeight == -1 || rightHeight == -1) {
return -1;
}
// If the heights of the left and right subtrees differ by more than one, return -1
if (abs(leftHeight - rightHeight) > 1) {
return -1;
}
// Return the maximum height between the left and right subtrees, plus 1 for the current level
return max(leftHeight, rightHeight) + 1;
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isBalanced(self, root: TreeNode) -> bool:
def height(node):
if not node:
return 0
left_height = height(node.left)
right_height = height(node.right)
if abs(left_height - right_height) > 1:
return float("inf") # Indicate imbalance
return max(left_height, right_height) + 1
return height(root) != float("inf")
Same Tree π§
LeetCode Link: Same Tree
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 100 - Same Tree
Description: Given the roots of two binary trees p and q, write a function to check if they are the same or not. Two binary trees are considered the same if they are structurally identical, and the nodes have the same value.
Intuition: To determine if two binary trees are the same, we need to compare their structures and values. We can use a recursive approach to check if the current nodes of both trees are equal and if their left and right subtrees are also equal.
Approach:
- Implement a recursive function to check if two binary trees are the same.
- If both roots are null, return true as they are considered the same.
- If one of the roots is null and the other is not, or if the values of the roots are different, return false.
- Recursively check if the left subtrees of both trees are the same.
- Recursively check if the right subtrees of both trees are the same.
- Return the logical AND of the above checks.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the binary tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool isSameTree(TreeNode *p, TreeNode *q) {
// Base case: if both roots are null, return true
if (p == nullptr && q == nullptr) {
return true;
}
// Check if either root is null or their values are different
if (p == nullptr || q == nullptr || p->val != q->val) {
return false;
}
// Recursively check if the left subtrees and right subtrees are the same
return isSameTree(p->left, q->left) && isSameTree(p->right, q->right);
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isSameTree(self, p: TreeNode, q: TreeNode) -> bool:
if not p and not q:
return True
if not p or not q or p.val != q.val:
return False
return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)
Subtree of Another Tree π§
LeetCode Link: Subtree of Another Tree
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 572 - Subtree of Another Tree
Description:
Given the roots of two binary trees, s
and t
, check if t
is a subtree of s
.
A subtree of a tree is a tree consisting of a node in s
and all of its descendants.
The trees s
and t
have the same structure, and all the values of the nodes in t
are also present in s
.
Intuition:
To check if t
is a subtree of s
, we can perform a depth-first search (DFS) on s
to find a node with the same value as the root of t
.
Once we find a matching node, we can recursively check if the subtree rooted at that node is identical to t
.
Approach:
- Implement a recursive function to check if
t
is a subtree ofs
. - If
s
is null, return false ast
cannot be a subtree of an empty tree. - Check if the current node in
s
is identical tot
. If so, check if the subtree rooted at this node is identical tot
. - If the subtree is identical, return true.
- If not, recursively check if
t
is a subtree of the left subtree or the right subtree ofs
. - Return the logical OR of the above checks.
β Time Complexity:
The time complexity of the approach is O(m * n), where m and n are the number of nodes in s
and t
, respectively. In the worst case, we may have to compare each node of s
with t
.
πΎ Space Complexity:
The space complexity is O(max(m, n)), where m and n are the number of nodes in s
and t
, respectively. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool isSubtree(TreeNode *s, TreeNode *t) {
// Base case: if `s` is null, return false as `t` cannot be a subtree of an empty tree
if (s == nullptr) {
return false;
}
// Check if the current node in `s` is identical to `t`, and if the subtree rooted at this node is identical to `t`
if (isIdentical(s, t)) {
return true;
}
// Recursively check if `t` is a subtree of the left subtree or the right subtree of `s`
return isSubtree(s->left, t) || isSubtree(s->right, t);
}
private:
bool isIdentical(TreeNode *s, TreeNode *t) {
// Base cases: if either `s` or `t` is null, return true only if both are null
if (s == nullptr && t == nullptr) {
return true;
}
if (s == nullptr || t == nullptr) {
return false;
}
// Check if the current nodes have the same value and recursively check if their left and right subtrees are identical
return (s->val == t->val) && isIdentical(s->left, t->left) && isIdentical(s->right, t->right);
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isSubtree(self, s: TreeNode, t: TreeNode) -> bool:
if not s:
return False
if self.isSameTree(s, t):
return True
return self.isSubtree(s.left, t) or self.isSubtree(s.right, t)
def isSameTree(self, p, q):
if not p and not q:
return True
if not p or not q or p.val != q.val:
return False
return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right)
Lowest Common Ancestor of a Binary Search Tree π§
LeetCode Link: Lowest Common Ancestor of a Binary Search Tree
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 235 - Lowest Common Ancestor of a Binary Search Tree
Description: Given a binary search tree (BST), find the lowest common ancestor (LCA) of two given nodes in the BST. According to the definition of LCA on Wikipedia: "The lowest common ancestor is defined between two nodes p and q as the lowest node in T that has both p and q as descendants (where we allow a node to be a descendant of itself)."
Intuition: The key property of a binary search tree (BST) is that for every node, its left subtree contains values smaller than the node's value, and its right subtree contains values greater than the node's value. To find the lowest common ancestor (LCA) of two nodes in a BST, we can take advantage of this property to navigate through the tree and determine the LCA based on the values of the nodes.
Approach:
- Start from the root of the BST.
- If both
p
andq
are smaller than the current node, the LCA lies in the left subtree. Recurse on the left subtree. - If both
p
andq
are greater than the current node, the LCA lies in the right subtree. Recurse on the right subtree. - If neither of the above conditions is true, then the current node is the LCA.
β Time Complexity: The time complexity of the approach is O(h), where h is the height of the BST. In the worst case, we need to traverse the entire height of the BST.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the BST. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode *lowestCommonAncestor(TreeNode *root, TreeNode *p, TreeNode *q) {
// If both `p` and `q` are smaller than the current node, recurse on the left subtree
if (p->val < root->val && q->val < root->val) {
return lowestCommonAncestor(root->left, p, q);
}
// If both `p` and `q` are greater than the current node, recurse on the right subtree
else if (p->val > root->val && q->val > root->val) {
return lowestCommonAncestor(root->right, p, q);
}
// If neither of the above conditions is true, return the current node as the LCA
else {
return root;
}
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def lowestCommonAncestor(
self, root: TreeNode, p: TreeNode, q: TreeNode
) -> TreeNode:
if root.val > p.val and root.val > q.val:
return self.lowestCommonAncestor(root.left, p, q)
elif root.val < p.val and root.val < q.val:
return self.lowestCommonAncestor(root.right, p, q)
else:
return root
Binary Tree Level Order Traversal π§
LeetCode Link: Binary Tree Level Order Traversal
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 102 - Binary Tree Level Order Traversal
Description: Given the root of a binary tree, return the level order traversal of its nodes' values. (i.e., from left to right, level by level).
Intuition: To perform a level order traversal of a binary tree, we can utilize a breadth-first search (BFS) algorithm. By visiting the nodes in a breadth-first manner, we can easily track the nodes at each level and store their values.
Approach:
- Create a result vector to store the level order traversal.
- Create a queue to perform the BFS.
- Enqueue the root node into the queue.
- While the queue is not empty:
- Get the current size of the queue to represent the number of nodes at the current level.
- Create a level vector to store the values of the nodes at the current level.
- Iterate through the nodes at the current level:
- Dequeue a node from the queue.
- Add the value of the dequeued node to the level vector.
- Enqueue the left and right children of the dequeued node, if they exist.
- Add the level vector to the result vector.
- Return the result vector.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once during the BFS.
πΎ Space Complexity: The space complexity is O(m), where m is the maximum number of nodes at any level in the binary tree. This is the space used by the queue and the result vector.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode *root) {
vector<vector<int>> result;
if (root == nullptr) {
return result;
}
queue<TreeNode *> q;
q.push(root);
while (!q.empty()) {
int levelSize = q.size();
vector<int> level;
for (int i = 0; i < levelSize; i++) {
// Dequeue a node from the queue
TreeNode *node = q.front();
q.pop();
// Add the value of the dequeued node to the level vector
level.push_back(node->val);
// Enqueue the left and right children of the dequeued node, if they exist
if (node->left != nullptr) {
q.push(node->left);
}
if (node->right != nullptr) {
q.push(node->right);
}
}
// Add the level vector to the result vector
result.push_back(level);
}
return result;
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if not root:
return []
result = []
queue = [root]
while queue:
level = []
next_level = []
for node in queue:
level.append(node.val)
if node.left:
next_level.append(node.left)
if node.right:
next_level.append(node.right)
result.append(level)
queue = next_level
return result
Binary Tree Right Side View π§
LeetCode Link: Binary Tree Right Side View
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 199 - Binary Tree Right Side View
Description: Given a binary tree, imagine yourself standing on the right side of it, return the values of the nodes you can see ordered from top to bottom.
Intuition: To obtain the right side view of a binary tree, we can perform a level order traversal and keep track of the last node at each level. Since we traverse the tree level by level, the last node we encounter at each level from left to right will be visible from the right side.
Approach:
- Create a vector,
result
, to store the right side view of the binary tree. - Create an empty queue of
TreeNode*
to perform the level order traversal. - Enqueue the root node into the queue.
- While the queue is not empty:
- Get the current size of the queue to represent the number of nodes at the current level.
- Create a variable,
lastValue
, to store the value of the last node at the current level. - Iterate through the nodes at the current level:
- Dequeue a node from the queue.
- Update
lastValue
with the value of the dequeued node. - Enqueue the left and right children of the dequeued node, if they exist.
- Add
lastValue
to theresult
vector.
- Return the
result
vector containing the right side view.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once during the level order traversal.
πΎ Space Complexity:
The space complexity is O(m), where m is the maximum number of nodes at any level in the binary tree. This is the space used by the queue and the result
vector.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
vector<int> rightSideView(TreeNode *root) {
vector<int> result;
if (root == nullptr) {
return result;
}
queue<TreeNode *> q;
q.push(root);
while (!q.empty()) {
int levelSize = q.size();
int lastValue;
for (int i = 0; i < levelSize; i++) {
TreeNode *node = q.front();
q.pop();
lastValue = node->val;
if (node->left != nullptr) {
q.push(node->left);
}
if (node->right != nullptr) {
q.push(node->right);
}
}
result.push_back(lastValue);
}
return result;
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def rightSideView(self, root: TreeNode) -> List[int]:
if not root:
return []
result = []
queue = [root]
while queue:
level_size = len(queue)
for i in range(level_size):
node = queue.pop(0)
if i == level_size - 1:
result.append(node.val)
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
return result
Count Good Nodes In Binary Tree π§
LeetCode Link: Count Good Nodes In Binary Tree
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 1448 - Count Good Nodes in Binary Tree
Description: Given a binary tree root, a node X in the tree is named good if in the path from root to X there are no nodes with a value greater than X. Return the number of good nodes in the binary tree.
Intuition: To count the number of good nodes in a binary tree, we can perform a depth-first search (DFS) and keep track of the maximum value seen so far. For each node, if its value is greater than or equal to the maximum value seen so far, we increment the count of good nodes.
Approach:
- Create a helper function,
countGoodNodesHelper
, to perform the DFS traversal and count the good nodes. - Initialize a count variable to keep track of the number of good nodes.
- Start the DFS traversal from the root node with the initial maximum value as negative infinity.
- In the
countGoodNodesHelper
function:
- Check if the current node is nullptr. If so, return.
- Update the maximum value seen so far to be the maximum of the current node's value and the current maximum value.
- If the current node's value is greater than or equal to the maximum value seen so far, increment the count of good nodes.
- Recursively call the
countGoodNodesHelper
function for the left and right children of the current node.
- Return the count of good nodes.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once during the DFS traversal.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the binary tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int goodNodes(TreeNode *root) {
return countGoodNodesHelper(root, INT_MIN);
}
private:
int countGoodNodesHelper(TreeNode *node, int maxSoFar) {
if (node == nullptr) {
return 0;
}
int count = 0;
if (node->val >= maxSoFar) {
count++;
}
int newMax = max(node->val, maxSoFar);
count += countGoodNodesHelper(node->left, newMax);
count += countGoodNodesHelper(node->right, newMax);
return count;
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def goodNodes(self, root: TreeNode) -> int:
def dfs(node, max_val):
if not node:
return 0
if node.val >= max_val:
max_val = node.val
count = 1
else:
count = 0
count += dfs(node.left, max_val)
count += dfs(node.right, max_val)
return count
return dfs(root, float("-inf"))
Validate Binary Search Tree π§
LeetCode Link: Validate Binary Search Tree
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 98 - Validate Binary Search Tree
Description: Given the root of a binary tree, determine if it is a valid binary search tree (BST).
Intuition: A binary search tree (BST) is a binary tree in which the value of each node is greater than all the values in its left subtree and less than all the values in its right subtree. To validate a BST, we can perform an in-order traversal and check if the values are in ascending order.
Approach:
- Initialize a previous value to store the last visited value during the in-order traversal.
- Create a helper function,
isValidBSTHelper
, to perform the in-order traversal and validate the BST. - In the
isValidBSTHelper
function:
- Check if the current node is
nullptr
. If so, return true since it does not violate the BST property. - Recursively call the
isValidBSTHelper
function for the left subtree. If it returns false, return false. - Check if the current node's value is less than or equal to the previous value. If so, return false.
- Update the previous value to be the current node's value.
- Recursively call the
isValidBSTHelper
function for the right subtree. If it returns false, return false.
- Return true if the entire tree has been traversed without any violations.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once during the in-order traversal.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the binary tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
bool isValidBST(TreeNode *root) {
long long prev = LLONG_MIN; // Use long long to handle edge case with INT_MIN
return isValidBSTHelper(root, prev);
}
private:
bool isValidBSTHelper(TreeNode *node, long long &prev) {
if (node == nullptr) {
return true;
}
if (!isValidBSTHelper(node->left, prev)) {
return false;
}
if (node->val <= prev) {
return false;
}
prev = node->val;
return isValidBSTHelper(node->right, prev);
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def isValidBST(self, root: TreeNode) -> bool:
def inorder_traversal(node, prev):
if not node:
return True
if not inorder_traversal(node.left, prev):
return False
if prev[0] is not None and node.val <= prev[0]:
return False
prev[0] = node.val
return inorder_traversal(node.right, prev)
prev = [None]
return inorder_traversal(root, prev)
Kth Smallest Element In a BST π§
LeetCode Link: Kth Smallest Element In a BST
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 230 - Kth Smallest Element in a BST
Description: Given the root of a binary search tree (BST), return the kth smallest element in the BST.
Intuition: In a binary search tree (BST), the left subtree contains smaller elements, and the right subtree contains larger elements. To find the kth smallest element, we can perform an in-order traversal and keep track of the count of visited nodes.
Approach:
- Create a helper function,
kthSmallestHelper
, to perform the in-order traversal and find the kth smallest element. - Initialize a count variable to keep track of the number of visited nodes.
- Start the in-order traversal from the root node.
- In the
kthSmallestHelper
function:
- Check if the current node is
nullptr
. If so, return -1 to indicate an invalid result. - Recursively call the
kthSmallestHelper
function for the left subtree. If the result is not -1, return the result. - Increment the count of visited nodes. If the count equals k, return the current node's value.
- Recursively call the
kthSmallestHelper
function for the right subtree. If the result is not -1, return the result.
- Return -1 if the kth smallest element is not found.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary search tree. We visit each node once during the in-order traversal.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the binary search tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int kthSmallest(TreeNode *root, int k) {
int count = 0;
return kthSmallestHelper(root, k, count);
}
private:
int kthSmallestHelper(TreeNode *node, int k, int &count) {
if (node == nullptr) {
return -1;
}
int result = kthSmallestHelper(node->left, k, count);
if (result != -1) {
return result;
}
count++;
if (count == k) {
return node->val;
}
return kthSmallestHelper(node->right, k, count);
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def kthSmallest(self, root: TreeNode, k: int) -> int:
def inorder_traversal(node):
if not node:
return []
left = inorder_traversal(node.left)
right = inorder_traversal(node.right)
return left + [node.val] + right
inorder_values = inorder_traversal(root)
return inorder_values[k - 1]
Construct Binary Tree from Preorder and Inorder Traversal π§
LeetCode Link: Construct Binary Tree from Preorder and Inorder Traversal
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 105 - Construct Binary Tree from Preorder and Inorder Traversal
Description:
Given two integer arrays preorder
and inorder
representing the preorder and inorder traversal of a binary tree, construct the binary tree.
Intuition: In a preorder traversal, the root node is visited first, followed by the left subtree and then the right subtree. In an inorder traversal, the left subtree is visited first, followed by the root node and then the right subtree. We can utilize these properties to construct the binary tree.
Approach:
- Create a helper function,
buildTreeHelper
, to construct the binary tree recursively. - In the
buildTreeHelper
function:
- Check if the preorder array is empty. If so, return
nullptr
. - Extract the root value from the preorder array and create a new node.
- Find the index of the root value in the inorder array.
- Split the inorder array into left and right subtrees based on the root index.
- Recursively call the
buildTreeHelper
function for the left subtree using the corresponding sections of the preorder and inorder arrays. - Recursively call the
buildTreeHelper
function for the right subtree using the corresponding sections of the preorder and inorder arrays. - Assign the left and right subtrees to the root node.
- Return the root node.
- Call the
buildTreeHelper
function with the entire preorder and inorder arrays. - Return the constructed binary tree.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once during the construction.
πΎ Space Complexity: The space complexity is O(n), where n is the number of nodes in the binary tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode *buildTree(vector<int> &preorder, vector<int> &inorder) {
return buildTreeHelper(preorder, inorder, 0, 0, inorder.size() - 1);
}
private:
TreeNode *buildTreeHelper(vector<int> &preorder, vector<int> &inorder, int preStart, int inStart, int inEnd) {
if (preStart >= preorder.size() || inStart > inEnd) {
return nullptr;
}
int rootVal = preorder[preStart];
TreeNode *root = new TreeNode(rootVal);
int rootIndex;
for (int i = inStart; i <= inEnd; i++) {
if (inorder[i] == rootVal) {
rootIndex = i;
break;
}
}
int leftSize = rootIndex - inStart;
root->left = buildTreeHelper(preorder, inorder, preStart + 1, inStart, rootIndex - 1);
root->right = buildTreeHelper(preorder, inorder, preStart + leftSize + 1, rootIndex + 1, inEnd);
return root;
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
if not preorder or not inorder:
return None
root_val = preorder.pop(0)
root = TreeNode(root_val)
root_index = inorder.index(root_val)
root.left = self.buildTree(preorder, inorder[:root_index])
root.right = self.buildTree(preorder, inorder[root_index + 1 :])
return root
Binary Tree Maximum Path Sum π§
LeetCode Link: Binary Tree Maximum Path Sum
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 124 - Binary Tree Maximum Path Sum
Description: Given a non-empty binary tree, find the maximum path sum. A path is defined as any sequence of nodes from some starting node to any node in the tree along the parent-child connections. The path must contain at least one node and does not need to go through the root.
Intuition: The maximum path sum can be found by considering the maximum sum path for each node in the binary tree. A path can include nodes from the left subtree, the current node, and nodes from the right subtree. We need to keep track of the maximum sum encountered so far as we traverse the tree.
Approach:
- Create a helper function,
maxPathSumHelper
, to calculate the maximum path sum for each node. - In the
maxPathSumHelper
function:
- Check if the current node is
nullptr
. If so, return 0 to represent an empty path. - Recursively call the
maxPathSumHelper
function for the left subtree and store the result inleftSum
. - Recursively call the
maxPathSumHelper
function for the right subtree and store the result inrightSum
. - Calculate the maximum sum path that includes the current node:
- Calculate
maxChildSum
as the maximum betweenleftSum
andrightSum
, or 0 if they are negative. - Calculate
maxSum
as the maximum betweenleftSum + rightSum + node->val
andnode->val
. - Update the maximum sum encountered so far by comparing
maxSum
with the current maximum. - Return the maximum sum path that includes the current node by adding
node->val
tomaxChildSum
.
- Call the
maxPathSumHelper
function with the root node. - Return the maximum sum encountered during the traversal.
β Time Complexity: The time complexity of the approach is O(n), where n is the number of nodes in the binary tree. We visit each node once during the traversal.
πΎ Space Complexity: The space complexity is O(h), where h is the height of the binary tree. This is the space used by the recursive call stack.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
int maxPathSum(TreeNode *root) {
int maxSum = INT_MIN;
maxPathSumHelper(root, maxSum);
return maxSum;
}
private:
int maxPathSumHelper(TreeNode *node, int &maxSum) {
if (node == nullptr) {
return 0;
}
// Recursively calculate the maximum path sum for the left and right subtrees
int leftSum = maxPathSumHelper(node->left, maxSum);
int rightSum = maxPathSumHelper(node->right, maxSum);
// Calculate the maximum sum path that includes the current node
int maxChildSum = max(max(leftSum, rightSum), 0);
int maxSumWithNode = max(maxChildSum + node->val, leftSum + rightSum + node->val);
maxSum = max(maxSum, maxSumWithNode);
// Return the maximum sum path that includes the current node by adding it to the maximum child sum
return max(maxChildSum + node->val, node->val);
}
};
Python π
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution:
def maxPathSum(self, root: TreeNode) -> int:
def maxPathSumHelper(node):
if not node:
return 0
left_sum = max(0, maxPathSumHelper(node.left))
right_sum = max(0, maxPathSumHelper(node.right))
self.max_sum = max(self.max_sum, left_sum + right_sum + node.val)
return max(left_sum, right_sum) + node.val
self.max_sum = float("-inf")
maxPathSumHelper(root)
return self.max_sum
Serialize and Deserialize Binary Tree π§
LeetCode Link: Serialize and Deserialize Binary Tree
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 297 - Serialize and Deserialize Binary Tree
Description: Design an algorithm to serialize and deserialize a binary tree. Serialize means to encode a tree structure into a string representation, and deserialize means to decode the string representation back into a binary tree structure. The encoded string should be as compact as possible.
Intuition: To serialize a binary tree, we can perform a pre-order traversal and encode the tree structure into a string. We use a special character to represent nullptr or empty nodes. During deserialization, we can use the encoded string to reconstruct the binary tree.
Approach:
- Serialization:
- Perform a pre-order traversal of the binary tree.
- When encountering a non-empty node, append its value to the serialized string, followed by a separator.
- When encountering a nullptr or empty node, append a special character (e.g., 'N') to represent it.
- Use a separator character (e.g., ',') to separate the values in the serialized string.
- Deserialization:
- Split the serialized string by the separator to obtain an array of values.
- Create a queue to hold the values from the array.
- Recursively build the binary tree using a helper function:
- If the queue is empty or the current value is the special character, return nullptr.
- Create a new node with the current value.
- Pop the queue to move to the next value.
- Set the left child of the current node by recursively calling the helper function.
- Set the right child of the current node by recursively calling the helper function.
- Return the current node.
- Call the helper function with the queue to build the binary tree.
- Return the root of the binary tree.
β Time Complexity:
- Serialization: O(n), where n is the number of nodes in the binary tree. We perform a pre-order traversal to serialize the tree.
- Deserialization: O(n), where n is the number of nodes in the binary tree. We iterate through the serialized string to deserialize and reconstruct the tree.
πΎ Space Complexity:
- Serialization: O(n), where n is the number of nodes in the binary tree. The serialized string requires space proportional to the number of nodes.
- Deserialization: O(n), where n is the number of nodes in the binary tree. We use a queue to store the values during deserialization.
Solutions π‘
Cpp π»
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Codec {
public:
string serialize(TreeNode *root) {
string serialized = "";
PreOrder(root, serialized);
return serialized;
}
TreeNode *deserialize(string data) {
queue<string> Q;
split(data, Q);
return MakeTree(Q);
}
private:
void PreOrder(TreeNode *root, string &str) {
if (!root) {
str.push_back('N');
str.push_back(',');
return;
}
str += to_string(root->val) + ",";
PreOrder(root->left, str);
PreOrder(root->right, str);
}
TreeNode *MakeTree(queue<string> &Q) {
string S = Q.front();
Q.pop();
if (S == "N") {
return nullptr;
}
// stoi -> string to integer
TreeNode *root = new TreeNode(stoi(S));
root->left = MakeTree(Q);
root->right = MakeTree(Q);
return root;
}
void split(const string &data, queue<string> &Q) {
size_t start = 0;
size_t pos = data.find(",");
while (pos != string::npos) {
Q.push(data.substr(start, pos - start));
start = pos + 1;
pos = data.find(",", start);
}
}
};
// class Codec {
// public:
// // Encodes a tree to a single string.
// string serialize(TreeNode* root) {
// string serialized = "";
// PreOrder(root, serialized);
// return serialized;
// }
// // Decodes your encoded data to tree.
// TreeNode* deserialize(string data) {
// queue<string> Q;
// string S;
// for(int i = 0; i < data.size(); i++) {
// // If it's a comma, push string into queue and reset string
// if(data[i] == ',') {
// Q.push(S);
// S = "";
// // Continuing so we go to next iteration without pushing comma
// continue;
// }
// // pushing back char to string if its not a comma
// S.push_back(data[i]);
// }
// // Making tree after decoding
// return MakeTree(Q);
// }
// private:
// // Function to make string using pre-order traversal
// void PreOrder(TreeNode* root, string &str) {
// // If root is null then insert N into string
// if(!root) {
// str.push_back('N');
// str.push_back(',');
// return;
// }
// // Converting int to string and appending
// // Another way of appending
// str += to_string(root->val) + ",";
// PreOrder(root->left, str);
// PreOrder(root->right, str);
// }
// TreeNode* MakeTree(queue<string> &Q) {
// string S = Q.front();
// Q.pop();
// if(S == "N")
// return NULL;
// // stoi -> string to integer
// TreeNode* root = new TreeNode(stoi(S));
// root->left = MakeTree(Q);
// root->right = MakeTree(Q);
// return root;
// }
// };
Python π
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Codec:
def serialize(self, root):
"""Encodes a tree to a single string.
:type root: TreeNode
:rtype: str
"""
def preorder(node):
if not node:
return "None,"
return str(node.val) + "," + preorder(node.left) + preorder(node.right)
return preorder(root)
def deserialize(self, data):
"""Decodes your encoded data to tree.
:type data: str
:rtype: TreeNode
"""
def build_tree(values):
if values[0] == "None":
values.pop(0)
return None
root = TreeNode(int(values.pop(0)))
root.left = build_tree(values)
root.right = build_tree(values)
return root
values = data.split(",")
return build_tree(values[:-1])
# Your Codec object will be instantiated and called as such:
# ser = Codec()
# deser = Codec()
# ans = deser.deserialize(ser.serialize(root))
Tries π
This section contains problems belonging to the Tries category.
Problems
- π‘ Medium: Implement Trie Prefix Tree
- π‘ Medium: Design Add and Search Words Data Structure
- π΄ Hard: Word Search II
Implement Trie Prefix Tree π§
LeetCode Link: Implement Trie Prefix Tree
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 208 - Implement Trie (Prefix Tree)
Description: Implement a trie with insert, search, and startsWith methods.
Intuition: A trie, also known as a prefix tree, is a tree-like data structure that stores a set of strings. Each node in the trie represents a prefix or a complete word. The trie allows efficient insertion, search, and prefix matching operations.
Approach:
- TrieNode:
- Define a TrieNode class that represents each node in the trie.
- Each TrieNode has an array of pointers to child nodes, representing the 26 lowercase letters of the English alphabet.
- Each TrieNode also has a boolean flag to indicate if it represents a complete word.
- Trie:
- Define a Trie class that contains the root of the trie.
- Implement the insert method to insert a word into the trie:
- Start from the root and iterate over each character in the word.
- For each character, check if the corresponding child node exists. If not, create a new node and link it to the current node.
- Move to the child node and repeat the process for the next character.
- After iterating through all characters, mark the last node as a complete word.
- Implement the search method to search for a word in the trie:
- Start from the root and iterate over each character in the word.
- For each character, check if the corresponding child node exists. If not, the word is not in the trie.
- Move to the child node and repeat the process for the next character.
- After iterating through all characters, check if the last node represents a complete word.
- Implement the startsWith method to check if there is any word in the trie that starts with the given prefix:
- Start from the root and iterate over each character in the prefix.
- For each character, check if the corresponding child node exists. If not, there are no words with the given prefix.
- Move to the child node and repeat the process for the next character.
- After iterating through all characters, return true, indicating that there are words with the given prefix.
β Time Complexity:
- Insert: O(m), where m is the length of the word being inserted.
- Search: O(m), where m is the length of the word being searched.
- StartsWith: O(m), where m is the length of the prefix being checked.
πΎ Space Complexity:
- The space complexity is O(n*m), where n is the number of words inserted into the trie and m is the average length of the words.
Solutions π‘
Cpp π»
class TrieNode {
public:
bool isWord;
TrieNode *children[26];
TrieNode() {
isWord = false;
for (int i = 0; i < 26; i++) {
children[i] = nullptr;
}
}
};
class Trie {
private:
TrieNode *root;
public:
Trie() {
root = new TrieNode();
}
void insert(string word) {
TrieNode *node = root;
for (char c : word) {
int index = c - 'a';
if (!node->children[index]) {
node->children[index] = new TrieNode();
}
node = node->children[index];
}
node->isWord = true;
}
bool search(string word) {
TrieNode *node = root;
for (char c : word) {
int index = c - 'a';
if (!node->children[index]) {
return false;
}
node = node->children[index];
}
return node->isWord;
}
bool startsWith(string prefix) {
TrieNode *node = root;
for (char c : prefix) {
int index = c - 'a';
if (!node->children[index]) {
return false;
}
node = node->children[index];
}
return true;
}
};
/*
class Trie {
private:
// Defining TrieNode Datatype
struct TrieNode {
// Can have 26 diffenet childen because thats the amt of alphabets
TrieNode *child[26];
// To check where the word ends
bool isWord;
//Constructor to initialise values
TrieNode() {
isWord = false;
for (auto &c : child)
c = nullptr;
}
};
// Root pointer
TrieNode *root;
public:
Trie() {
root = new TrieNode();
}
void insert(string word) {
// Pointer to point the character we insert in trie
TrieNode* current = root;
for(auto i: word) {
// index of the character i.e. a = 0, z = 25
int index = i - 'a';
if(current->child[index] == NULL)
current->child[index] = new TrieNode();
// Pointing current to the character we just inserted
current = current->child[index];
}
current->isWord = true;
}
bool search(string word) {
TrieNode *current = root;
for(auto i: word) {
int index = i - 'a';
// If the child isn't there, return false. Else keep going.
if(current->child[index] == NULL)
return false;
current = current->child[index];
}
// Will return true if it's a word
return current->isWord;
}
// Same code as search but here we dont need to check if its a word
bool startsWith(string prefix) {
TrieNode *current = root;
for(auto i: prefix) {
int index = i - 'a';
// If the child isn't there, return false. Else keep going.
if(current->child[index] == NULL)
return false;
current = current->child[index];
}
return true;
}
};
*/
Python π
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class Trie:
def __init__(self):
self.root = TrieNode()
def insert(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
def search(self, word: str) -> bool:
node = self.root
for char in word:
if char not in node.children:
return False
node = node.children[char]
return node.is_end
def startsWith(self, prefix: str) -> bool:
node = self.root
for char in prefix:
if char not in node.children:
return False
node = node.children[char]
return True
# Your Trie object will be instantiated and called as such:
# obj = Trie()
# obj.insert(word)
# param_2 = obj.search(word)
# param_3 = obj.startsWith(prefix)
Design Add and Search Words Data Structure π§
LeetCode Link: Design Add and Search Words Data Structure
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 211 - Design Add and Search Words Data Structure
Description: Design a data structure that supports adding new words and finding if a string matches any previously added string. The word may contain only lowercase alphabets '.' or be an empty string.
Intuition: To solve this problem, we can use a Trie data structure to store the words. The Trie allows efficient insertion and search operations. For the '.' character, we need to consider all possible characters at that position.
Approach:
- TrieNode:
- Define a TrieNode class that represents each node in the Trie.
- Each TrieNode has an array of pointers to child nodes, representing the lowercase alphabets and the '.' character.
- Each TrieNode also has a boolean flag to indicate if it represents a complete word.
- WordDictionary:
- Define a WordDictionary class that contains the root of the Trie.
- Implement the addWord method to add a word to the Trie:
- Start from the root and iterate over each character in the word.
- For each character, check if the corresponding child node exists. If not, create a new node and link it to the current node.
- Move to the child node and repeat the process for the next character.
- After iterating through all characters, mark the last node as a complete word.
- Implement the search method to search for a word in the Trie:
- Start from the root and iterate over each character in the word.
- For each character, check if the corresponding child node exists. If not, return false.
- Move to the child node and repeat the process for the next character.
- After iterating through all characters, check if the last node represents a complete word.
- Implement the searchWithWildcard method to search for a word with wildcard characters ('.') in the Trie:
- Use a recursive approach to search for the word.
- If the current character is a wildcard ('.'), iterate over all possible child nodes and recursively search for the remaining word.
- If the current character is not a wildcard, check if the corresponding child node exists and recursively search for the remaining word.
- Return true if any of the recursive searches return true.
- Return false if no matching word is found.
β Time Complexity:
- Adding a word: O(m), where m is the length of the word being added.
- Searching a word: O(m), where m is the length of the word being searched.
- Searching a word with wildcard: O(n*m), where n is the number of words in the Trie and m is the length of the word being searched.
πΎ Space Complexity:
- The space complexity is O(n*m), where n is the number of words added to the Trie and m is the average length of the words.
Solutions π‘
Cpp π»
class TrieNode {
public:
bool isWord;
TrieNode *children[26];
TrieNode() {
isWord = false;
for (int i = 0; i < 26; i++) {
children[i] = nullptr;
}
}
};
class WordDictionary {
private:
TrieNode *root;
public:
WordDictionary() {
root = new TrieNode();
}
void addWord(string word) {
TrieNode *node = root;
for (char c : word) {
int index = c - 'a';
if (!node->children[index]) {
node->children[index] = new TrieNode();
}
node = node->children[index];
}
node->isWord = true;
}
bool search(string word) {
return searchHelper(word, root, 0);
}
bool searchHelper(string word, TrieNode *node, int index) {
if (index == word.length()) {
return node->isWord;
}
char c = word[index];
if (c != '.') {
int childIndex = c - 'a';
if (node->children[childIndex]) {
return searchHelper(word, node->children[childIndex], index + 1);
} else {
return false;
}
} else {
for (int i = 0; i < 26; i++) {
if (node->children[i] && searchHelper(word, node->children[i], index + 1)) {
return true;
}
}
return false;
}
}
};
/*
class WordDictionary {
private:
struct TrieNode {
TrieNode* child[26];
bool isWord;
TrieNode() {
for(auto &i: child)
i = nullptr;
isWord = false;
}
};
TrieNode* root;
bool searchInNode(string& word, int i, TrieNode* node) {
if (node == NULL)
return false;
if (i == word.size())
return node->isWord;
// if its an alphabet and not .
if (word[i] != '.')
return searchInNode(word, i + 1, node->child[word[i] - 'a']);
// If the current character is a dot, we need to check all children of the current node
// recursively by skipping over the dot character and moving to the next character of the word
for (int j = 0; j < 26; j++)
if (searchInNode(word, i + 1, node->child[j]))
return true;
return false;
}
public:
WordDictionary() {
root = new TrieNode();
}
void addWord(string word) {
TrieNode *current = root;
for(auto c: word) {
int i = c - 'a';
if(!current->child[i])
current->child[i] = new TrieNode();
current = current->child[i];
}
current->isWord = true;
}
bool search(string word) {
TrieNode* node = root;
return searchInNode(word, 0, node);
}
};
*/
Python π
class TrieNode:
def __init__(self):
self.children = {}
self.is_end = False
class WordDictionary:
def __init__(self):
self.root = TrieNode()
def addWord(self, word: str) -> None:
node = self.root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.is_end = True
def search(self, word: str) -> bool:
def search_in_node(node, word):
for i, char in enumerate(word):
if char not in node.children:
if char == ".":
for child in node.children:
if search_in_node(node.children[child], word[i + 1 :]):
return True
return False
else:
node = node.children[char]
return node.is_end
return search_in_node(self.root, word)
# Your WordDictionary object will be instantiated and called as such:
# obj = WordDictionary()
# obj.addWord(word)
# param_2 = obj.search(word)
# class WordDictionary:
# def __init__(self):
# self.word_set = set()
# def addWord(self, word: str) -> None:
# self.word_set.add(word)
# for i in range(len(word)):
# # Add all possible variations with a '.' in each position
# self.word_set.add(word[:i] + '.' + word[i + 1:])
# def search(self, word: str) -> bool:
# if word in self.word_set:
# return True
# # Check if the word contains a '.'
# if '.' not in word:
# return False
# # Split the word into two parts at the first occurrence of '.'
# first_part, rest_part = word.split('.', 1)
# # Iterate over lowercase letters and create variations to search
# for char in 'abcdefghijklmnopqrstuvwxyz':
# new_word = first_part + char + rest_part
# if new_word in self.word_set:
# return True
# return False
Word Search II π§
LeetCode Link: Word Search II
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 212 - Word Search II
Description: Given an m x n board of characters and a list of words, find all words in the board. Each word must be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.
Intuition: To find all the words in the board, we can use the Trie data structure to efficiently search for each word. We perform a depth-first search (DFS) starting from each cell on the board, checking if the current sequence of characters forms a valid word in the Trie.
Approach:
- TrieNode:
- Define a TrieNode class that represents each node in the Trie.
- Each TrieNode has an unordered_map to store the child nodes, representing the lowercase alphabets.
- Each TrieNode also has a boolean flag to indicate if it represents a complete word.
- Build the Trie:
- Construct a Trie by inserting each word from the given list into the Trie.
- DFS Search:
- Perform a depth-first search (DFS) starting from each cell on the board.
- At each cell, check if the current character exists in the Trie and move to the corresponding child node.
- Mark the current cell as visited.
- If the current node represents a complete word, add it to the result.
- Recursively explore the neighboring cells (up, down, left, right).
- Backtrack by unmarking the current cell and removing the last character from the current sequence.
- Word Search II:
- Initialize an empty vector to store the found words.
- Iterate through each cell on the board and perform the DFS search.
- Return the found words as the result.
β Time Complexity:
- Building the Trie: O(m), where m is the total number of characters in all words.
- DFS Search: O((m*n)*3^l), where m and n are the dimensions of the board and l is the average length of the words.
πΎ Space Complexity:
- The space complexity is O(m), where m is the total number of characters in all words (used for constructing the Trie).
- The space complexity of the recursive stack for DFS is O(l), where l is the maximum length of the words.
Solutions π‘
Cpp π»
class TrieNode {
public:
bool isWord;
unordered_map<char, TrieNode *> children; // Map to store the child nodes
TrieNode() {
isWord = false;
}
};
class Solution {
public:
vector<string> findWords(vector<vector<char>> &board, vector<string> &words) {
TrieNode *root = buildTrie(words); // Build the Trie
int rows = board.size();
int cols = board[0].size();
vector<string> result;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
string currentWord = ""; // Initialize the current word for each cell
dfs(board, i, j, root, currentWord, result); // Perform DFS search
}
}
return result;
}
private:
TrieNode *buildTrie(vector<string> &words) {
TrieNode *root = new TrieNode();
for (string &word : words) {
TrieNode *node = root;
for (char c : word) {
if (node->children.find(c) == node->children.end()) {
node->children[c] = new TrieNode();
}
node = node->children[c];
}
node->isWord = true;
}
return root;
}
void dfs(vector<vector<char>> &board, int row, int col, TrieNode *node, string ¤tWord, vector<string> &result) {
if (row < 0 || row >= board.size() || col < 0 || col >= board[0].size() || board[row][col] == '#') {
return;
}
char c = board[row][col];
if (node->children.find(c) == node->children.end()) {
return;
}
node = node->children[c];
currentWord += c;
if (node->isWord) {
result.push_back(currentWord); // Add the found word to the result
node->isWord = false; // Mark the word as visited
}
board[row][col] = '#'; // Mark the current cell as visited
// Explore the neighboring cells (up, down, left, right)
dfs(board, row - 1, col, node, currentWord, result);
dfs(board, row + 1, col, node, currentWord, result);
dfs(board, row, col - 1, node, currentWord, result);
dfs(board, row, col + 1, node, currentWord, result);
board[row][col] = c; // Backtrack: unmark the current cell
currentWord.pop_back(); // Remove the current character from the current word
}
};
Python π
from collections import Counter
from itertools import chain, product
from typing import List
class TrieNode:
def __init__(self):
self.children = {} # Store child nodes for each character
self.refcnt = 0 # Count of references to this node
self.is_word = False # Flag to indicate if a complete word ends at this node
self.is_rev = False # Flag to indicate if a word should be reversed
class Trie:
def __init__(self):
self.root = TrieNode() # Initialize the root of the trie
def insert(self, word, rev):
node = self.root
for c in word:
node = node.children.setdefault(c, TrieNode())
node.refcnt += 1
node.is_word = True
node.is_rev = rev
def remove(self, word):
node = self.root
for i, c in enumerate(word):
parent = node
node = node.children[c]
if node.refcnt == 1:
path = [(parent, c)]
for c in word[i + 1 :]:
path.append((node, c))
node = node.children[c]
for parent, c in path:
parent.children.pop(c)
return
node.refcnt -= 1
node.is_word = False
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
res = []
n, m = len(board), len(board[0])
trie = Trie()
# Count characters on the board
boardcnt = Counter(chain(*board))
# Insert words into trie with appropriate orientation
for w, wrdcnt in ((w, Counter(w)) for w in words):
if any(wrdcnt[c] > boardcnt[c] for c in wrdcnt):
continue # Skip if the word cannot be formed from the board
if wrdcnt[w[0]] < wrdcnt[w[-1]]:
trie.insert(w, False)
else:
trie.insert(w[::-1], True)
def dfs(r, c, parent) -> None:
if not (node := parent.children.get(board[r][c])):
return
path.append(board[r][c])
board[r][c] = "#" # Mark visited cell
if node.is_word:
word = "".join(path)
res.append(word[::-1] if node.is_rev else word)
trie.remove(word)
# Explore neighboring cells
if r > 0:
dfs(r - 1, c, node)
if r < n - 1:
dfs(r + 1, c, node)
if c > 0:
dfs(r, c - 1, node)
if c < m - 1:
dfs(r, c + 1, node)
board[r][c] = path.pop() # Backtrack and unmark cell
path = []
for r, c in product(range(n), range(m)):
dfs(r, c, trie.root)
return res
Heap Priority Queues π
This section contains problems belonging to the Heap Priority Queues category.
Problems
- π’ Easy: Kth Largest Element in a Stream
- π’ Easy: Last Stone Weight
- π‘ Medium: K Closest Points to Origin
- π‘ Medium: Kth Largest Element in an Array
- π‘ Medium: Task Scheduler
- π‘ Medium: Design Twitter
- π΄ Hard: Find Median from Data Stream
Kth Largest Element in a Stream π§
LeetCode Link: Kth Largest Element in a Stream
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 703 - Kth Largest Element in a Stream
Description: Design a class to find the kth largest element in a stream. Note that it is the kth largest element in the sorted order, not the kth distinct element.
Intuition: To find the kth largest element in a stream efficiently, we can use a min-heap of size k. As new elements are added to the stream, we compare them with the root of the min-heap. If the new element is larger than the root, we replace the root with the new element and perform heapify to maintain the heap property.
Approach:
- Implement a class KthLargest with the following members:
- A min-heap to store the k largest elements. Use a priority_queue in C++ with the smallest element on top.
- A variable k to store the value of k.
- In the constructor of KthLargest:
- Initialize the variable k.
- Iterate through the given vector of integers and add each element to the min-heap.
- If the size of the min-heap exceeds k, remove the smallest element from the heap.
- In the add function:
- If the size of the min-heap is less than k, simply add the new element to the heap.
- If the new element is larger than the root of the min-heap, replace the root with the new element and perform heapify.
- Return the value of the root of the min-heap, which represents the kth largest element.
β Time Complexity:
- Construction: O(n*log(k)), where n is the number of elements in the input vector.
- Adding an element: O(log(k)), as we need to perform heapify after adding an element.
πΎ Space Complexity:
- The space complexity is O(k), as we store the k largest elements in the min-heap.
Solutions π‘
Cpp π»
// static int pr = []() {
// std::ios::sync_with_stdio(false);
// cin.tie(NULL);
// return 0;
// }();
class KthLargest {
private:
int k;
priority_queue<int, vector<int>, greater<int>> minHeap; // Min-heap to store the k largest elements
public:
KthLargest(int k, vector<int> &nums) {
this->k = k;
for (int num : nums) {
minHeap.push(num);
// removing elements till k elements remain
if (minHeap.size() > k) {
minHeap.pop();
}
}
}
int add(int val) {
minHeap.push(val);
if (minHeap.size() > k) {
minHeap.pop();
}
return minHeap.top();
}
};
Python π
import heapq
class KthLargest:
def __init__(self, k: int, nums: List[int]):
self.min_heap = []
self.k = k
for num in nums:
self.add(num)
def add(self, val: int) -> int:
heapq.heappush(self.min_heap, val)
if len(self.min_heap) > self.k:
heapq.heappop(self.min_heap)
return self.min_heap[0]
# Your KthLargest object will be instantiated and called as such:
# obj = KthLargest(k, nums)
# param_1 = obj.add(val)
Last Stone Weight π§
LeetCode Link: Last Stone Weight
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 1046 - Last Stone Weight
Description:
You are given an array stones
where stones[i]
represents the weight of the ith stone.
In each turn, you choose the two heaviest stones and smash them together.
If the stones have the same weight, they both get destroyed, and if they have different weights, the heavier stone gets destroyed and the lighter stone's weight is reduced by the difference.
When only one stone remains, return its weight. If no stones remain, return 0.
Intuition: To find the last stone weight, we can use a max-heap to keep track of the heaviest stones. In each turn, we remove the two largest stones from the heap, calculate their difference, and add the result back to the heap. We repeat this process until the heap contains only one stone.
Approach:
- Implement a function
lastStoneWeight
that takes the input arraystones
as a parameter. - Create a max-heap using
priority_queue<int>
in C++ to store the stone weights. The largest element will always be at the top of the heap. - Populate the max-heap with the elements from the
stones
array. - While the heap has more than one stone:
- Remove the two largest stones from the heap using
pop()
. - Calculate the difference between the two stones.
- If the difference is non-zero, add it back to the heap using
push()
.
- After the loop ends, the heap will contain only one stone.
- If the heap is empty, return 0 as no stones remain.
- If the heap is not empty, return the top element of the heap, which represents the last stone weight.
β Time Complexity:
- Building the max-heap: O(n), where n is the number of elements in the input array.
- Performing heap operations: O(log(n)) per operation.
- Overall time complexity: O(n*log(n)), where n is the number of elements in the input array.
πΎ Space Complexity:
- The space complexity is O(n), where n is the number of elements in the input array, for storing the max-heap.
Solutions π‘
Cpp π»
class Solution {
public:
int lastStoneWeight(vector<int> &stones) {
priority_queue<int> maxHeap; // Max-heap to store the stone weights
// Populate the max-heap with the elements from the 'stones' array
for (int stone : stones) {
maxHeap.push(stone);
}
while (maxHeap.size() > 1) {
int stone1 = maxHeap.top(); // Get the heaviest stone
maxHeap.pop();
int stone2 = maxHeap.top(); // Get the second heaviest stone
maxHeap.pop();
int diff = stone1 - stone2; // Calculate the difference
if (diff > 0) {
maxHeap.push(diff); // Add the difference back to the heap
}
}
if (maxHeap.empty()) {
return 0; // No stones remain
}
return maxHeap.top(); // Return the last stone weight
}
};
Python π
import heapq
class Solution:
def lastStoneWeight(self, stones: List[int]) -> int:
max_heap = [-stone for stone in stones] # Use negative values for max-heap
heapq.heapify(max_heap)
while len(max_heap) > 1:
x = -heapq.heappop(max_heap) # Extract the largest stone
y = -heapq.heappop(max_heap) # Extract the second largest stone
if x != y:
heapq.heappush(max_heap, -(x - y)) # Push the remaining weight
return -max_heap[0] if max_heap else 0
K Closest Points to Origin π§
LeetCode Link: K Closest Points to Origin
Difficulty: Medium
Problem Explanation π
Problem: K Closest Points to Origin
Description: Given an array points representing coordinates of N points on a 2D plane, and an integer k, you need to return the k closest points to the origin (0, 0).
Intuition: To find the K closest points to the origin, we can utilize the concept of a priority queue (min-heap). We iterate through the points and calculate the distance of each point from the origin. We add the points to the priority queue, and if the size of the queue exceeds K, we remove the farthest point. In the end, the priority queue will contain the K closest points to the origin. Approach:
1.Create a vector distances to store pairs of distances and indices of points. 2.Iterate through each point in the points vector and calculate its distance from the origin using the formula distance = x^2 + y^2, where (x, y) are the coordinates of the point. 3. Store each distance along with its corresponding index in the distances vector. 4. Use the nth_element function to find the k-th smallest distance in the distances vector. This function partially sorts the vector, placing the k-th smallest element in its correct position. Elements before it are smaller or equal, while elements after it are greater. 5. Create a result vector result to store the k closest points. 6. Iterate through the first k elements of the distances vector. For each element, retrieve the index and use it to access the corresponding point in the points vector. Add this point to the result vector. 7. Return the result vector containing the k closest points to the origin.
β Time Complexity: The time complexity of this approach is O(N log k), where N is the number of points. Calculating the distance for each point takes O(N), and the nth_element function takes O(N log k).
πΎ Space Complexity: The space complexity is O(N), where N is the number of points. We use the distances vector to store the distances and indices of points
Solutions π‘
Cpp π»
/*
* Syntax for PQ: priority_queue<data_type, container, comparator> ds;
* Data_type(mandatory) : datatype that we are going to store in priority_queue. (int, float, or even any custom datatype)
* Container(optional) : Container is passed as an underlying container to store the elements. It needs to satisfy some properties, therefore it can be either vector<datatype> or deque<datatype>.
* Comparator(optional) : Comparator decides the ordering of elements.
*/
class Solution {
public:
vector<vector<int>> kClosest(vector<vector<int>> &points, int k) {
// Create a vector to store the distances and indices of points
vector<pair<int, int>> distances(points.size());
// Calculate the distance of each point from the origin and store it along with its index
for (int i = 0; i < points.size(); i++) {
int distance = points[i][0] * points[i][0] + points[i][1] * points[i][1];
distances[i] = make_pair(distance, i);
}
// Find the k-th smallest distance using nth_element
nth_element(distances.begin(), distances.begin() + k - 1, distances.end());
// Create a result vector to store the k closest points
vector<vector<int>> result(k);
// Add the k closest points to the result vector
for (int i = 0; i < k; i++) {
int index = distances[i].second;
result[i] = move(points[index]);
}
return result;
}
};
Python π
import heapq
class Solution:
def kClosest(self, points: List[List[int]], k: int) -> List[List[int]]:
def distance(point):
return point[0] ** 2 + point[1] ** 2
min_heap = [(distance(point), point) for point in points]
heapq.heapify(min_heap)
result = []
for _ in range(k):
result.append(heapq.heappop(min_heap)[1])
return result
Kth Largest Element in an Array π§
LeetCode Link: Kth Largest Element in an Array
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 215 - Kth Largest Element in an Array
Description:
Given an integer array nums
and an integer k
, return the kth largest element in the array.
Note that it is the kth largest element in the sorted order, not the kth distinct element.
Intuition: The code uses the Quickselect algorithm to find the kth largest element in the array efficiently. Quickselect is a variation of the QuickSort algorithm and works by partitioning the array based on a chosen pivot element. Instead of sorting both sides of the partition like in QuickSort, Quickselect only focuses on the side of the partition that contains the desired kth largest element.
Approach:
- The code implements the Quickselect algorithm to efficiently find the kth largest element.
- Quickselect is a variation of the QuickSort algorithm that focuses on the desired kth largest element during partitioning.
- The
quickselect
function is a helper function that takes the input arraynums
, start indexl
, end indexr
, and the value ofk
as parameters.
- It returns the kth largest element in the array by recursively partitioning the array.
- Base case:
- If the start index
l
is greater than or equal to the end indexr
, it means we have found the kth largest element, so we return the element at indexl
.
- Partitioning:
- The code chooses the pivot element as
nums[l + r >> 1]
, which is the element at the middle index betweenl
andr
. - It initializes two pointers,
i
andj
, wherei
starts froml - 1
andj
starts fromr + 1
. - The code enters a loop where it increments
i
until it finds an element greater than the pivot and decrementsj
until it finds an element smaller than the pivot. - If
i
is less thanj
, it means there are elements on the wrong side of the partition, so it swapsnums[i]
andnums[j]
.
- After the loop:
- The code calculates the size of the smaller partition as
sl = j - l + 1
.
- Recursion:
- If
k
is less than or equal tosl
, it means the desired element is in the smaller partition, so it recursively callsquickselect
on that partition. - If
k
is greater thansl
, it means the desired element is in the larger partition, so it recursively callsquickselect
on that partition, adjustingk
by subtractingsl
.
- The
findKthLargest
function is the main function that takes the input arraynums
and integerk
as parameters.
- It disables synchronization between C and C++ standard streams for faster I/O.
- It returns the result of the
quickselect
function called withnums
, 0 as the start index,nums.size() - 1
as the end index, andk
.
β Time Complexity:
- On average, the Quickselect algorithm has a time complexity of O(n), where n is the number of elements in the input array.
- However, in the worst case, Quickselect can have a time complexity of O(n^2) if the pivot selection is unbalanced.
- Overall, the average time complexity of Quickselect is O(n), making it an efficient algorithm for finding the kth largest element.
πΎ Space Complexity:
- The space complexity is O(log n) for the recursive call stack of the
quickselect
function, where n is the number of elements in the input array. - In addition to the input array, the algorithm uses a constant amount of extra space.
- Therefore, the overall space complexity is O(log n).
Solutions π‘
Cpp π»
class Solution {
public:
int quickselect(vector<int> &nums, int left, int right, int k) {
// Base case: if the left and right indices are the same, return the element at that index
if (left >= right) {
return nums[left];
}
// Choose a pivot element
int pivot = nums[left + (right - left) / 2];
// Initialize two pointers, one from the left and one from the right
int i = left - 1;
int j = right + 1;
// Partition the array around the pivot
while (i < j) {
// Find the first element greater than the pivot from the left side
while (nums[++i] > pivot);
// Find the first element smaller than the pivot from the right side
while (nums[--j] < pivot);
// Swap the elements if the pointers haven't crossed each other
if (i < j) {
swap(nums[i], nums[j]);
}
}
// Calculate the size of the smaller partition
int smallerPartitionSize = j - left + 1;
// If the desired element is in the smaller partition, recursively call quickselect on that partition
if (k <= smallerPartitionSize) {
return quickselect(nums, left, j, k);
}
// If the desired element is in the larger partition, recursively call quickselect on that partition
return quickselect(nums, j + 1, right, k - smallerPartitionSize);
}
int findKthLargest(vector<int> &nums, int k) {
// Disable synchronization between C and C++ standard streams for faster I/O
cin.tie(0);
ios::sync_with_stdio(false);
// Call the quickselect algorithm to find the kth largest element
return quickselect(nums, 0, nums.size() - 1, k);
}
};
Python π
import heapq
class Solution:
def findKthLargest(self, nums: List[int], k: int) -> int:
min_heap = []
for num in nums:
heapq.heappush(min_heap, num)
if len(min_heap) > k:
heapq.heappop(min_heap)
return min_heap[0]
Task Scheduler π§
LeetCode Link: Task Scheduler
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 621 - Task Scheduler
Description: Given a characters array tasks, representing the tasks a CPU needs to do, where each character represents a different task. Tasks could be done in any order. Each task is done in one unit of time. For each unit of time, the CPU could complete either one task or just be idle. However, there is a non-negative integer n that represents the cooldown period between two same tasks (the same task cannot be executed in adjacent units of time). Return the least number of units of times that the CPU will take to finish all the given tasks.
Intuition: To minimize the overall time, we need to arrange the tasks in such a way that the maximum number of occurrences of any task is spread apart by the cooldown period. We can then fill the gaps with idle cycles if needed.
Approach:
- Count the frequency of each task and store it in a frequency array.
- Sort the frequency array in descending order.
- Find the maximum frequency
maxFreq
. - Calculate the number of idle cycles required:
- Subtract 1 from
maxFreq
to exclude the last occurrence of the most frequent task (as it doesn't need an idle cycle after it). - Multiply
maxFreq - 1
byn
to get the number of slots occupied by the most frequent task and its cooldown periods. - Subtract this value from the total number of tasks to get the number of remaining idle cycles.
- Calculate the minimum number of time units required:
- Add the total number of tasks to the number of idle cycles calculated in step 4.
- Return the maximum of this value and the length of the tasks array.
β Time Complexity: The time complexity is O(n log n), where n is the number of tasks. This is due to the sorting operation on the frequency array.
πΎ Space Complexity: The space complexity is O(1) since the frequency array has a fixed size of 26 (assuming tasks only contain uppercase letters).
Solutions π‘
Cpp π»
class Solution {
public:
int leastInterval(vector<char> &tasks, int n) {
// Count the frequency of each task
vector<int> frequency(26, 0);
for (char task : tasks) {
frequency[task - 'A']++;
}
// Sort the frequency array in descending order
sort(frequency.rbegin(), frequency.rend());
// Find the maximum frequency
int maxFreq = frequency[0];
// Calculate the number of idle cycles required
int idleCycles = (maxFreq - 1) * n;
// Subtract the remaining tasks from the idle cycles
for (int i = 1; i < frequency.size(); i++) {
idleCycles -= min(frequency[i], maxFreq - 1);
}
// Calculate the minimum number of time units required
int minTime = tasks.size() + max(0, idleCycles);
return minTime;
}
};
Python π
import heapq
from collections import Counter
class Solution:
def leastInterval(self, tasks: List[str], n: int) -> int:
task_counts = Counter(tasks)
max_heap = [-count for count in task_counts.values()]
heapq.heapify(max_heap)
cooldown = 0
while max_heap:
temp = []
for _ in range(n + 1):
if max_heap:
temp.append(heapq.heappop(max_heap) + 1)
for count in temp:
if count < 0:
heapq.heappush(max_heap, count)
if max_heap:
cooldown += n + 1
else:
cooldown += len(temp)
return cooldown
Design Twitter π§
LeetCode Link: Design Twitter
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 355 - Design Twitter
Description: Design a simplified version of Twitter where users can post tweets, follow/unfollow another user, and see the 10 most recent tweets in the user's news feed.
Intuition: To design Twitter, we need to efficiently handle the following operations:
- Posting a tweet: We need to store the tweets along with their timestamps.
- Following/Unfollowing a user: We need to maintain a data structure to track the followers and followees of each user.
- Retrieving the news feed: We need to combine the tweets from the user's own timeline along with the tweets from the users they follow.
Approach:
- Implement the
User
class to store the user's information, including their tweets and the users they follow. - Use an unordered_map to store the user ID as the key and the corresponding
User
object as the value. - Implement the
Tweet
class to store the tweet's information, including the tweet ID and the timestamp. - Use a deque to store the tweets in the user's timeline, with the most recent tweet at the front.
- To post a tweet:
- Get the
User
object corresponding to the user ID. - Create a new
Tweet
object with the tweet ID and the current timestamp. - Add the tweet to the user's timeline and update the timestamp.
- To follow a user:
- Get the
User
objects corresponding to the follower and followee IDs. - Add the followee ID to the follower's list of followees.
- To unfollow a user:
- Get the
User
objects corresponding to the follower and followee IDs. - Remove the followee ID from the follower's list of followees.
- To retrieve the news feed:
- Get the
User
object corresponding to the user ID. - Create a priority_queue to store the tweets from the user's timeline and the timelines of the users they follow.
- Iterate over the tweets in the user's timeline and add them to the priority queue.
- Iterate over the followees of the user and add their tweets to the priority queue.
- Pop the top 10 tweets from the priority queue and return them in reverse order.
β Time Complexity: The time complexity of posting a tweet, following/unfollowing a user, and retrieving the news feed is O(log n), where n is the number of tweets. This is because we use a priority queue to retrieve the top 10 tweets in the news feed.
πΎ Space Complexity: The space complexity is O(m + n), where m is the number of users and n is the number of tweets. We store the user information in the unordered_map and the tweets in the deque.
Solutions π‘
Cpp π»
class Twitter {
private:
struct Tweet {
int tweetId;
int timestamp;
Tweet(int id, int time) : tweetId(id), timestamp(time) {}
};
unordered_map<int, vector<Tweet>> userTweets; // Store tweets of each user
unordered_map<int, unordered_set<int>> userFollowees; // Store followees of each user
int time; // Keep track of the timestamp
public:
Twitter() {
time = 0;
}
void postTweet(int userId, int tweetId) {
userTweets[userId].emplace_back(tweetId, time++); // Add the tweet with the current timestamp
}
void follow(int followerId, int followeeId) {
userFollowees[followerId].insert(followeeId); // Add followee to the follower's set of followees
}
void unfollow(int followerId, int followeeId) {
userFollowees[followerId].erase(followeeId); // Remove followee from the follower's set of followees
}
vector<int> getNewsFeed(int userId) {
vector<int> newsFeed;
priority_queue<pair<int, int>> pq; // Use a priority queue to sort tweets by timestamp (max-heap)
// Add the user's own tweets to the priority queue
for (const auto &tweet : userTweets[userId]) {
pq.push({ tweet.timestamp, tweet.tweetId });
}
// Add tweets from the user's followees to the priority queue
for (int followeeId : userFollowees[userId]) {
for (const auto &tweet : userTweets[followeeId]) {
pq.push({ tweet.timestamp, tweet.tweetId });
}
}
// Retrieve the top 10 tweets from the priority queue
while (!pq.empty() && newsFeed.size() < 10) {
newsFeed.push_back(pq.top().second); // Add the tweet ID to the news feed
pq.pop();
}
return newsFeed;
}
};
Python π
import heapq
class Tweet:
def __init__(self, tweet_id, timestamp):
self.tweet_id = tweet_id
self.timestamp = timestamp
class Twitter:
def __init__(self):
self.user_tweets = {} # User ID -> List of Tweet objects
self.user_followees = {} # User ID -> Set of followees
self.timestamp = 0
def postTweet(self, userId: int, tweetId: int) -> None:
self.timestamp += 1
if userId not in self.user_tweets:
self.user_tweets[userId] = []
self.user_tweets[userId].append(Tweet(tweetId, self.timestamp))
def getNewsFeed(self, userId: int) -> List[int]:
tweets = []
if userId in self.user_tweets:
tweets.extend(self.user_tweets[userId])
if userId in self.user_followees:
for followee in self.user_followees[userId]:
if followee in self.user_tweets:
tweets.extend(self.user_tweets[followee])
tweets.sort(key=lambda x: x.timestamp, reverse=True)
return [tweet.tweet_id for tweet in tweets[:10]]
def follow(self, followerId: int, followeeId: int) -> None:
if followerId != followeeId:
if followerId not in self.user_followees:
self.user_followees[followerId] = set()
self.user_followees[followerId].add(followeeId)
def unfollow(self, followerId: int, followeeId: int) -> None:
if (
followerId in self.user_followees
and followeeId in self.user_followees[followerId]
):
self.user_followees[followerId].remove(followeeId)
# Your Twitter object will be instantiated and called as such:
# obj = Twitter()
# obj.postTweet(userId,tweetId)
# param_2 = obj.getNewsFeed(userId)
# obj.follow(followerId,followeeId)
# obj.unfollow(followerId,followeeId)
Find Median from Data Stream π§
LeetCode Link: Find Median from Data Stream
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 295 - Find Median from Data Stream
Description: Design a data structure that supports adding integers to the structure and finding the median of the current elements. The median is the middle value in an ordered integer list. If the size of the list is even, there is no middle value. So the median is the average of the two middle values.
Intuition: To efficiently find the median of a data stream, we can use two heaps: a max-heap to store the smaller half of the elements, and a min-heap to store the larger half of the elements. The median will either be the root of the max-heap (if the heaps have equal size) or the average of the roots of both heaps (if the max-heap has one more element than the min-heap).
Approach:
- Use two priority_queues (heaps) - a max-heap (
maxHeap
) and a min-heap (minHeap
).
- The max-heap (
maxHeap
) stores the smaller half of the elements. - The min-heap (
minHeap
) stores the larger half of the elements.
- Maintain the following conditions:
- The size of the max-heap is either equal to or one more than the size of the min-heap.
- The root of the max-heap is smaller than or equal to the root of the min-heap.
- When adding a new element:
- If the max-heap is empty or the element is less than the root of the max-heap, push the element into the max-heap.
- Otherwise, push the element into the min-heap.
- Balance the heaps by moving the root of the max-heap to the min-heap if the sizes are not balanced.
- To find the median:
- If the size of the max-heap is greater than the min-heap, return the root of the max-heap.
- Otherwise, return the average of the roots of both heaps.
β Time Complexity: Adding an element and finding the median both have a time complexity of O(log n), where n is the number of elements in the data stream. This is due to the heap operations.
πΎ Space Complexity: The space complexity is O(n), where n is the number of elements in the data stream. This is because we store the elements in the heaps.
Solutions π‘
Cpp π»
class MedianFinder {
private:
priority_queue<int> maxHeap; // Max-heap to store the smaller half of the elements
priority_queue<int, vector<int>, greater<int>> minHeap; // Min-heap to store the larger half of the elements
public:
void addNum(int num) {
if (maxHeap.empty() || num <= maxHeap.top()) {
maxHeap.push(num);
} else {
minHeap.push(num);
}
// Balance the heaps
if (maxHeap.size() > minHeap.size() + 1) {
minHeap.push(maxHeap.top());
maxHeap.pop();
} else if (maxHeap.size() < minHeap.size()) {
maxHeap.push(minHeap.top());
minHeap.pop();
}
}
double findMedian() {
if (maxHeap.size() > minHeap.size()) {
return maxHeap.top();
} else {
return (maxHeap.top() + minHeap.top()) / 2.0;
}
}
};
Python π
import heapq
class MedianFinder:
def __init__(self):
self.min_heap = [] # To store larger elements
self.max_heap = [] # To store smaller elements
def addNum(self, num: int) -> None:
if not self.max_heap or num <= -self.max_heap[0]:
heapq.heappush(self.max_heap, -num)
else:
heapq.heappush(self.min_heap, num)
# Balance the heaps
if len(self.max_heap) > len(self.min_heap) + 1:
heapq.heappush(self.min_heap, -heapq.heappop(self.max_heap))
elif len(self.min_heap) > len(self.max_heap):
heapq.heappush(self.max_heap, -heapq.heappop(self.min_heap))
def findMedian(self) -> float:
if len(self.max_heap) == len(self.min_heap):
return (-self.max_heap[0] + self.min_heap[0]) / 2
else:
return -self.max_heap[0]
Backtracking π
This section contains problems belonging to the Backtracking category.
Problems
- π‘ Medium: Subsets
- π‘ Medium: Combination Sum
- π‘ Medium: Permutations
- π‘ Medium: Subsets II
- π‘ Medium: Combination Sum II
- π‘ Medium: Word Search
- π‘ Medium: Palindrome Partitioning
- π‘ Medium: Letter Combinations of a Phone Number
- π΄ Hard: N Queens
Subsets π§
LeetCode Link: Subsets
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 78 - Subsets
Description: Given an integer array nums of unique elements, return all possible subsets (the power set). The solution set must not contain duplicate subsets. Return the solution in any order.
Intuition: To find all possible subsets of a given array, we can use a backtracking approach. We start with an empty subset and gradually add elements to it, generating all possible combinations.
Approach:
- Initialize an empty vector
subset
to store the current subset. - Initialize an empty vector
result
to store all subsets. - Define a helper function
generateSubsets
:
- If the index is equal to the size of the input array
nums
, add the current subset to theresult
vector. - Otherwise:
- Include the current element at the current index in the subset.
- Recursively call
generateSubsets
with the next index. - Exclude the current element from the subset.
- Recursively call
generateSubsets
with the next index.
- Call the
generateSubsets
function with the initial index 0. - Return the
result
vector containing all possible subsets.
β Time Complexity:
The time complexity is O(2^n), where n is the size of the input array nums
. This is because there are 2^n possible subsets, and we generate all of them.
πΎ Space Complexity:
The space complexity is O(n), where n is the size of the input array nums
. This is because we store the subsets in the result
vector.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<int>> subsets(vector<int> &nums) {
vector<vector<int>> result;
vector<int> subset;
generateSubsets(nums, 0, subset, result);
return result;
}
private:
void generateSubsets(const vector<int> &nums, int index, vector<int> &subset, vector<vector<int>> &result) {
// Base case: If we have processed all elements, add the current subset to the result
if (index == nums.size()) {
result.push_back(subset);
return;
}
subset.push_back(nums[index]); // Include the current element
generateSubsets(nums, index + 1, subset, result); // Recursively call with the next index
subset.pop_back(); // Exclude the current element
generateSubsets(nums, index + 1, subset, result); // Recursively call with the next index
}
};
Python π
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
def backtrack(start, subset):
subsets.append(subset[:]) # Append a copy of the current subset
for i in range(start, len(nums)):
subset.append(nums[i])
backtrack(i + 1, subset)
subset.pop() # Backtrack
subsets = []
backtrack(0, [])
return subsets
Combination Sum π§
LeetCode Link: Combination Sum
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 39 - Combination Sum
Description: Given an array of distinct integers candidates and a target integer target, return a list of all unique combinations of candidates where the chosen numbers sum to target. You may return the combinations in any order. The same number may be chosen from candidates an unlimited number of times. Note: The solution set must not contain duplicate combinations.
Intuition: To find all unique combinations that sum to the target, we can use a backtracking approach. The idea is to generate all possible combinations by trying out different candidate elements at each step. We explore each candidate, including it in the current combination if its value does not exceed the remaining target. By backtracking, we generate all valid combinations that sum up to the target.
Approach:
- Sort the input array
candidates
to handle duplicates and generate combinations in a non-decreasing order. - Initialize an empty vector
combination
to store the current combination. - Initialize an empty vector
result
to store all valid combinations. - Define a helper function
backtrack
:
- If the remaining target is equal to 0, add the current combination to the
result
vector. - Otherwise:
- Iterate through the candidates from the current index to the end:
- If the current candidate is greater than the remaining target, break the loop (as the remaining candidates will be larger and will not fit).
- Include the current candidate in the combination.
- Recursively call
backtrack
with the updated remaining target (target - current candidate) and the same index (to allow choosing the same candidate multiple times). - Exclude the current candidate from the combination (backtrack).
- Call the
backtrack
function with the initial target and index 0. - Return the
result
vector containing all valid combinations.
β Time Complexity: The time complexity is determined by the number of valid combinations. In the worst case, the algorithm explores all possible combinations. The time complexity can be approximated as O(N^target), where N is the number of candidates and target is the given target integer.
πΎ Space Complexity:
The space complexity is determined by the recursion stack and the result
vector that stores all valid combinations. The space complexity is O(target) to store the recursion stack, and the result
vector may contain multiple combinations.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<int>> combinationSum(vector<int> &candidates, int target) {
vector<vector<int>> result;
vector<int> combination;
sort(candidates.begin(), candidates.end()); // Sort the candidates in ascending order
backtrack(candidates, target, 0, combination, result); // Call the backtrack function
return result;
}
private:
// Backtracking function to find combinations that sum up to the target
void backtrack(const vector<int> &candidates, int target, int index, vector<int> &combination, vector<vector<int>> &result) {
// Base case: if the target is reached, add the current combination to the result
if (target == 0) {
result.push_back(combination);
return;
}
// Iterate through the candidates starting from the given index
for (int i = index; i < candidates.size(); ++i) {
// If the current candidate is greater than the target, skip the remaining candidates
if (candidates[i] > target) {
break;
}
// Include the current candidate in the combination
combination.push_back(candidates[i]);
// Recursively call the backtrack function with the updated target and the same index
backtrack(candidates, target - candidates[i], i, combination, result);
// Exclude the current candidate from the combination (backtrack)
combination.pop_back();
}
}
};
Python π
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
def backtrack(start, target, combination):
if target == 0:
result.append(
combination[:]
) # Append a copy of the current combination
return
for i in range(start, len(candidates)):
if candidates[i] > target:
continue # Skip if the candidate is too large
combination.append(candidates[i])
backtrack(i, target - candidates[i], combination)
combination.pop() # Backtrack
result = []
backtrack(0, target, [])
return result
Permutations π§
LeetCode Link: Permutations
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 46 - Permutations
Description: Given an array nums of distinct integers, return all possible permutations of the elements in nums. You can return the answer in any order.
Intuition: To find all possible permutations of a given array, we can use a backtracking approach. The idea is to generate all possible arrangements by trying out different elements at each step. We explore each element, including it in the current permutation if it hasn't been used before. By backtracking, we generate all valid permutations.
Approach:
- Initialize an empty vector
permutation
to store the current permutation. - Initialize an empty vector
result
to store all valid permutations. - Define a helper function
backtrack
:
- If the size of the current permutation is equal to the size of the input array
nums
, add the current permutation to theresult
vector. - Otherwise:
- Iterate through the elements in the input array
nums
: - If the current element is already in the permutation, skip it to avoid duplicates.
- Include the current element in the permutation.
- Recursively call
backtrack
with the updated permutation. - Exclude the current element from the permutation (backtrack).
- Call the
backtrack
function with an empty permutation. - Return the
result
vector containing all valid permutations.
β Time Complexity:
The time complexity is O(N!), where N is the size of the input array nums
. This is because there are N! possible permutations, and we generate all of them.
πΎ Space Complexity:
The space complexity is O(N), where N is the size of the input array nums
. This is because we store the permutations in the result
vector.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<int>> permute(vector<int> &nums) {
vector<vector<int>> result;
vector<int> permutation;
vector<bool> used(nums.size(), false); // Track used elements to avoid duplicates
backtrack(nums, used, permutation, result);
return result;
}
private:
void backtrack(const vector<int> &nums, vector<bool> &used, vector<int> &permutation, vector<vector<int>> &result) {
if (permutation.size() == nums.size()) {
result.push_back(permutation);
return;
}
for (int i = 0; i < nums.size(); ++i) {
if (used[i]) {
continue; // Skip already used elements
}
used[i] = true; // Mark the current element as used
permutation.push_back(nums[i]); // Include the current element in the permutation
backtrack(nums, used, permutation, result); // Recursively call with updated permutation
permutation.pop_back(); // Exclude the current element (backtrack)
used[i] = false; // Mark the current element as unused for other permutations
}
}
};
// class Solution {
// public:
// vector<vector<int>> permute(vector<int>& nums) {
// vector<vector<int>> result;
// backtrack(result, nums, 0);
// return result;
// }
// private:
// void backtrack(vector<vector<int>> &result, vector<int> &nums, int start) {
// if(start == nums.size()) {
// result.push_back(nums);
// return;
// }
// for(int i = start; i < nums.size(); i++) {
// // swapping allows us to explore all possible permutations by considering different elements at the current position.
// // since start is zero we will swap indices, [0, 1], 0, 2], ... , [0, n-1]
// swap(nums[i], nums[start]);
// backtrack(result, nums, start+1);
// // Once we come out of the recursion, we backtrack by swapping back the elements at indices start and i. This is necessary to restore the original order of elements and avoid duplicates.
// swap(nums[i], nums[start]);
// }
// }
// };
Python π
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
def backtrack(start):
if start == len(nums) - 1:
permutations.append(nums[:]) # Append a copy of the current permutation
for i in range(start, len(nums)):
nums[start], nums[i] = nums[i], nums[start] # Swap elements
backtrack(start + 1)
nums[start], nums[i] = nums[i], nums[start] # Backtrack
permutations = []
backtrack(0)
return permutations
Subsets II π§
LeetCode Link: Subsets II
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 90 - Subsets II
Description: Given an integer array nums that may contain duplicates, return all possible subsets (the power set). The solution set must not contain duplicate subsets. Return the solution in any order.
Intuition: To find all possible subsets of a given array that may contain duplicates, we can use a backtracking approach. The idea is to generate all possible combinations by trying out different elements at each step, while avoiding duplicate subsets. We sort the array to handle duplicates and skip adding duplicate elements to the current subset.
Approach:
- Sort the input array
nums
to handle duplicates and generate combinations in a non-decreasing order. - Initialize an empty vector
subset
to store the current subset. - Initialize an empty vector
result
to store all subsets. - Define a helper function
backtrack
:
- Add the current subset to the
result
vector. - Iterate through the elements in the input array
nums
: - If the current element is a duplicate (not the first occurrence), skip it to avoid duplicate subsets.
- Include the current element in the subset.
- Recursively call
backtrack
with the next index. - Exclude the current element from the subset.
- Call the
backtrack
function with the initial index 0. - Return the
result
vector containing all subsets.
β Time Complexity:
The time complexity is O(2^n), where n is the size of the input array nums
. This is because there are 2^n possible subsets, and we generate all of them.
πΎ Space Complexity:
The space complexity is O(n), where n is the size of the input array nums
. This is because we store the subsets in the result
vector.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<int>> subsetsWithDup(vector<int> &nums) {
vector<vector<int>> result;
vector<int> subset;
sort(nums.begin(), nums.end()); // Sort the array to handle duplicates
backtrack(nums, 0, subset, result);
return result;
}
private:
void backtrack(const vector<int> &nums, int index, vector<int> &subset, vector<vector<int>> &result) {
result.push_back(subset); // Add the current subset to the result vector
for (int i = index; i < nums.size(); ++i) {
if (i > index && nums[i] == nums[i - 1]) {
continue; // Skip duplicate elements to avoid duplicate subsets
}
subset.push_back(nums[i]); // Include the current element in the subset
backtrack(nums, i + 1, subset, result); // Recursively call with the next index
subset.pop_back(); // Exclude the current element (backtrack)
}
}
};
Python π
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
def backtrack(start, subset):
subsets.append(subset[:]) # Append a copy of the current subset
for i in range(start, len(nums)):
if i > start and nums[i] == nums[i - 1]:
continue # Skip duplicates at the same depth level
subset.append(nums[i])
backtrack(i + 1, subset)
subset.pop() # Backtrack
nums.sort() # Sort the input to handle duplicates
subsets = []
backtrack(0, [])
return subsets
Combination Sum II π§
LeetCode Link: Combination Sum II
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 40 - Combination Sum II
Description: Given a collection of candidate numbers (candidates) and a target number (target), find all unique combinations in candidates where the candidate numbers sum to target. Each number in candidates may only be used once in the combination. Note: The solution set must not contain duplicate combinations.
Intuition: To find all unique combinations that sum to the target, we can use a backtracking approach. The idea is to generate all possible combinations by trying out different candidate elements at each step. However, we need to skip duplicate combinations to avoid duplicates in the solution set.
Approach:
- Sort the input array
candidates
to handle duplicates and generate combinations in a non-decreasing order. - Initialize an empty vector
combination
to store the current combination. - Initialize an empty vector
result
to store all valid combinations. - Define a helper function
backtrack
:
- If the current sum is equal to the target, add the current combination to the
result
vector. - Otherwise:
- Iterate through the candidates from the current index to the end:
- If the current candidate is greater than the remaining target, break the loop (as the remaining candidates will be larger and won't fit).
- If the current candidate is a duplicate (not the first occurrence), skip it to avoid duplicate combinations.
- Include the current candidate in the combination.
- Recursively call
backtrack
with the updated sum and the next index. - Exclude the current candidate from the combination.
- Call the
backtrack
function with the initial sum of 0 and index 0. - Return the
result
vector containing all valid combinations.
β Time Complexity:
The time complexity is O(2^n), where n is the size of the input array candidates
. This is because there are 2^n possible combinations, and we generate all of them.
πΎ Space Complexity:
The space complexity is O(n), where n is the size of the input array candidates
. This is because we store the combinations in the result
vector.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<int>> combinationSum2(vector<int> &candidates, int target) {
vector<vector<int>> result;
vector<int> combination;
sort(candidates.begin(), candidates.end()); // Sort the candidates
backtrack(candidates, target, 0, combination, result);
return result;
}
private:
void backtrack(const vector<int> &candidates, int target, int index, vector<int> &combination, vector<vector<int>> &result) {
if (target == 0) {
result.push_back(combination);
return;
}
for (int i = index; i < candidates.size(); ++i) {
if (candidates[i] > target) {
break; // Skip remaining candidates since they will be larger and won't fit
}
if (i > index && candidates[i] == candidates[i - 1]) {
continue; // Skip duplicate candidates to avoid duplicate combinations
}
combination.push_back(candidates[i]); // Include the current candidate
backtrack(candidates, target - candidates[i], i + 1, combination, result); // Recursively call with updated target and next index
combination.pop_back(); // Exclude the current candidate (backtrack)
}
}
};
Python π
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
def backtrack(start, target, combination):
if target == 0:
result.append(
combination[:]
) # Append a copy of the current combination
return
for i in range(start, len(candidates)):
if i > start and candidates[i] == candidates[i - 1]:
continue # Skip duplicates at the same depth level
if candidates[i] > target:
continue # Skip if the candidate is too large
combination.append(candidates[i])
backtrack(i + 1, target - candidates[i], combination)
combination.pop() # Backtrack
candidates.sort() # Sort the input to handle duplicates
result = []
backtrack(0, target, [])
return result
Word Search π§
LeetCode Link: Word Search
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 79 - Word Search
Description: Given an m x n grid of characters board and a string word, return true if word exists in the grid. The word can be constructed from letters of sequentially adjacent cells, where adjacent cells are horizontally or vertically neighboring. The same letter cell may not be used more than once.
Intuition: To determine if a given word exists in the grid, we can use a backtracking approach. We start from each cell and explore all possible paths to find the word. At each step, we check if the current cell matches the current character of the word. If it does, we continue the search in the neighboring cells until the word is found or all paths have been explored.
Approach:
- Iterate through each cell in the grid:
- If the current cell matches the first character of the word, call the
backtrack
function.
- Define a helper function
backtrack
:
- If the index of the current character equals the length of the word, return true (the entire word has been found).
- If the current cell is out of bounds or does not match the current character, return false.
- Mark the current cell as visited (e.g., change its value to a special character to indicate it has been used).
- Recursively call
backtrack
for the neighboring cells (up, down, left, right) with the next character of the word. - Restore the original value of the current cell (backtrack).
- Return the result obtained from the
backtrack
function.
β Time Complexity: The time complexity is O(M * N * 4^L), where M is the number of rows, N is the number of columns, and L is the length of the word. In the worst case, we explore all possible paths from each cell.
πΎ Space Complexity: The space complexity is O(L), where L is the length of the word. This is the maximum depth of the recursion stack.
Solutions π‘
Cpp π»
class Solution {
public:
bool exist(vector<vector<char>> &board, string word) {
int m = board.size();
int n = board[0].size();
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == word[0] && backtrack(board, word, i, j, 0)) {
return true;
}
}
}
return false;
}
private:
bool backtrack(vector<vector<char>> &board, const string &word, int row, int col, int index) {
if (index == word.length()) {
return true; // The entire word has been found
}
int m = board.size();
int n = board[0].size();
if (row < 0 || row >= m || col < 0 || col >= n || board[row][col] != word[index]) {
return false; // Out of bounds or the current cell does not match the current character
}
char temp = board[row][col];
board[row][col] = '#'; // Mark the current cell as visited
// Recursively call for the neighboring cells
bool found = backtrack(board, word, row - 1, col, index + 1) ||
backtrack(board, word, row + 1, col, index + 1) ||
backtrack(board, word, row, col - 1, index + 1) ||
backtrack(board, word, row, col + 1, index + 1);
board[row][col] = temp; // Mark the current cell as unvisited (backtrack)
return found;
}
};
Python π
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
def dfs(row, col, index):
if index == len(word):
return True
if (
row < 0
or row >= len(board)
or col < 0
or col >= len(board[0])
or board[row][col] != word[index]
):
return False
original_char = board[row][col]
board[row][col] = "#" # Mark the cell as visited
found = (
dfs(row + 1, col, index + 1)
or dfs(row - 1, col, index + 1)
or dfs(row, col + 1, index + 1)
or dfs(row, col - 1, index + 1)
)
board[row][col] = original_char # Backtrack
return found
for row in range(len(board)):
for col in range(len(board[0])):
if board[row][col] == word[0] and dfs(row, col, 0):
return True
return False
Palindrome Partitioning π§
LeetCode Link: Palindrome Partitioning
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 131 - Palindrome Partitioning
Description: Given a string s, partition s such that every substring of the partition is a palindrome. Return all possible palindrome partitioning of s. A palindrome string is a string that reads the same backward as forward.
Intuition: To find all possible palindrome partitioning of a given string, we can use a backtracking approach. The idea is to generate all possible partitions by trying out different substrings at each step. We check if the current substring is a palindrome and continue the search for the remaining part of the string. By backtracking, we generate all valid palindrome partitionings.
Approach:
- Initialize an empty vector
partition
to store the current partition. - Initialize an empty vector
result
to store all valid partitions. - Define a helper function
backtrack
:
- If the start index is equal to the length of the string, add the current partition to the
result
vector. - Otherwise:
- Iterate through the substring starting from the current index:
- If the substring is a palindrome:
- Include the substring in the partition.
- Recursively call
backtrack
with the updated start index (next index after the substring). - Exclude the substring from the partition (backtrack).
- Call the
backtrack
function with the initial start index of 0. - Return the
result
vector containing all valid partitions.
β Time Complexity:
The time complexity is O(N * 2^N), where N is the length of the string s
. This is because there can be up to 2^N possible palindrome partitions, and for each partition, we check if each substring is a palindrome.
πΎ Space Complexity:
The space complexity is O(N) for the recursion stack, where N is the length of the string s
. The result
vector may contain multiple partitions, but the overall space complexity is dominated by the recursion stack.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<string>> partition(string s) {
vector<vector<string>> result;
vector<string> partition;
backtrack(s, 0, partition, result); // Call the backtrack function to generate all valid partitions
return result;
}
private:
// Depth First Search
void backtrack(const string &s, int start, vector<string> &partition, vector<vector<string>> &result) {
if (start == s.length()) {
result.push_back(partition); // Add the current partition to the result
return;
}
for (int i = start; i < s.length(); ++i) {
if (isPalindrome(s, start, i)) { // Check if the substring is a palindrome
partition.push_back(s.substr(start, i - start + 1)); // Include the palindrome substring in the partition
backtrack(s, i + 1, partition, result); // Recursively call with the next index
partition.pop_back(); // Exclude the last added substring (backtrack)
}
}
}
bool isPalindrome(const string &s, int start, int end) {
while (start < end) {
if (s[start] != s[end]) { // If characters don't match, it's not a palindrome
return false;
}
++start;
--end;
}
return true; // All characters matched, it's a palindrome
}
};
Python π
class Solution:
def partition(self, s: str) -> List[List[str]]:
def is_palindrome(sub):
return sub == sub[::-1]
def backtrack(start, partition):
if start == len(s):
result.append(partition[:]) # Append a copy of the current partition
return
for end in range(start + 1, len(s) + 1):
sub = s[start:end]
if is_palindrome(sub):
partition.append(sub)
backtrack(end, partition)
partition.pop() # Backtrack
result = []
backtrack(0, [])
return result
Letter Combinations of a Phone Number π§
LeetCode Link: Letter Combinations of a Phone Number
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 17 - Letter Combinations of a Phone Number
Description: Given a string containing digits from 2-9 inclusive, return all possible letter combinations that the number could represent. Return the answer in any order.
Intuition: To find all possible letter combinations of a given phone number, we can use a backtracking approach. The idea is to generate all possible combinations by trying out different letters at each step, based on the digit mapping on a phone keypad.
Approach:
- Define a mapping from each digit to the corresponding letters on a phone keypad.
- Initialize an empty vector
combinations
to store the current combination. - Initialize an empty vector
result
to store all valid combinations. - Define a helper function
backtrack
:
- If the index is equal to the length of the input digits, add the current combination to the
result
vector. - Otherwise:
- Get the letters corresponding to the current digit.
- Iterate through the letters:
- Include the current letter in the combination.
- Recursively call
backtrack
with the updated index. - Exclude the current letter from the combination (backtrack).
- Call the
backtrack
function with the initial index of 0. - Return the
result
vector containing all valid combinations.
β Time Complexity: The time complexity is O(3^N * 4^M), where N is the number of digits that map to 3 letters (e.g., 2, 3, 4, 5, 6, 8) and M is the number of digits that map to 4 letters (e.g., 7, 9). For each digit, there can be up to 3 mappings (3^N) or 4 mappings (4^M), and we generate all possible combinations.
πΎ Space Complexity: The space complexity is O(N + M), where N is the number of digits that map to 3 letters and M is the number of digits that map to 4 letters. This is the space used to store the combinations.
Solutions π‘
Cpp π»
class Solution {
public:
vector<string> letterCombinations(string digits) {
vector<string> result;
if (digits.empty()) {
return result; // If the input is empty, return an empty result
}
vector<string> mapping = {
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
};
string combination; // Store the current combination
backtrack(digits, mapping, 0, combination, result); // Call the backtrack function to generate all valid combinations
return result;
}
private:
// Depth First Search (DFS) Backtracking function
void backtrack(const string &digits, const vector<string> &mapping, int index, string &combination, vector<string> &result) {
if (index == digits.length()) {
result.push_back(combination); // Add the current combination to the result
return;
}
string letters = mapping[digits[index] - '0']; // Get the corresponding letters for the current digit
for (char letter : letters) {
combination.push_back(letter); // Add the letter to the current combination
backtrack(digits, mapping, index + 1, combination, result); // Recursively call with the next index
combination.pop_back(); // Remove the last added letter (backtrack)
}
}
};
/*
// Breadth First Search((BFS)
class Solution {
public:
vector<string> letterCombinations(string digits) {
vector<string> result;
if (digits.empty()) {
return result;
}
vector<string> mapping = {
"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"
};
queue<string> combinations;
combinations.push("");
while (!combinations.empty()) {
string curr = combinations.front();
combinations.pop();
if (curr.length() == digits.length()) {
result.push_back(curr);
continue;
}
string letters = mapping[digits[curr.length()] - '0'];
for (char letter : letters) {
combinations.push(curr + letter);
}
}
return result;
}
};
*/
/*
class Solution {
public:
vector<string> letterCombinations(string digits) {
if(digits.size() == 0) return vector<string>{};
vector<string> ans;
string curr;
backtrack(curr, 0, digits, ans);
return ans;
}
private:
unordered_map<char, vector<char>> dig2char = {
{'2', {'a', 'b', 'c'}},
{'3', {'d', 'e', 'f'}},
{'4', {'g', 'h', 'i'}},
{'5', {'j', 'k', 'l'}},
{'6', {'m', 'n', 'o'}},
{'7', {'p', 'q', 'r', 's'}},
{'8', {'t', 'u', 'v'}},
{'9', {'w', 'x', 'y', 'z'}}
};
void backtrack(string & curr, int index, string & digits, vector<string> & ans) {
if(curr.size() == digits.size()) {
ans.push_back(curr);
return;
}
vector<char> possibleLetters = dig2char[digits[index]];
for(char c: possibleLetters) {
curr.push_back(c);
backtrack(curr, index + 1, digits, ans);
curr.pop_back();
}
}
};
*/
Python π
class Solution:
def letterCombinations(self, digits: str) -> List[str]:
if not digits:
return []
phone_mapping = {
"2": "abc",
"3": "def",
"4": "ghi",
"5": "jkl",
"6": "mno",
"7": "pqrs",
"8": "tuv",
"9": "wxyz",
}
def backtrack(index, combination):
if index == len(digits):
combinations.append(combination)
return
digit = digits[index]
letters = phone_mapping[digit]
for letter in letters:
backtrack(index + 1, combination + letter)
combinations = []
backtrack(0, "")
return combinations
N Queens π§
LeetCode Link: N Queens
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 51 - N-Queens
Description: The n-queens puzzle is the problem of placing n queens on an n x n chessboard such that no two queens attack each other. Given an integer n, return all distinct solutions to the n-queens puzzle. Each solution contains a distinct board configuration of the n-queens' placement, where 'Q' and '.' both indicate a queen and an empty space, respectively.
Intuition: The N-Queens problem can be solved using backtracking. The idea is to place queens on the board row by row, ensuring that no two queens attack each other. We can use a recursive approach to explore all possible placements and backtrack when conflicts arise.
Approach:
- Define a vector of vectors
board
to represent the chessboard. - Define a vector
queens
to store the column index of the queens placed in each row. - Define a helper function
backtrack
:
- If the current row is equal to
n
, it means all queens have been placed successfully. Add the currentboard
configuration to theresult
vector. - Otherwise:
- Iterate through the columns from 0 to
n
: - Check if placing a queen at the current position (row, col) is valid (i.e., no conflicts with previously placed queens).
- If it is valid, mark the current position on the
board
as a queen ('Q') and add the current column toqueens
. - Recursively call
backtrack
for the next row. - Remove the queen from the
board
and backtrack by removing the last queen fromqueens
.
- Call the
backtrack
function with the initial row 0. - Return the
result
vector containing all distinct board configurations.
β Time Complexity: The time complexity is O(N!), where N is the size of the chessboard (n x n). This is because there are N! possible placements for the queens.
πΎ Space Complexity:
The space complexity is O(N), where N is the size of the chessboard (n x n). This is because we store the board
, queens
, and the result
vector.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> result;
vector<string> board(n, string(n, '.')); // Initialize the board with empty spaces
vector<int> queens; // Column indices of the queens in each row
backtrack(n, 0, board, queens, result); // Call the backtrack function to generate all valid solutions
return result;
}
private:
// Backtracking function to generate all valid solutions
void backtrack(int n, int row, vector<string> &board, vector<int> &queens, vector<vector<string>> &result) {
if (row == n) {
result.push_back(board); // Add the current valid solution to the result
return;
}
for (int col = 0; col < n; ++col) {
if (isValidPlacement(row, col, queens)) {
board[row][col] = 'Q'; // Place the queen at the current position
queens.push_back(col); // Store the column index of the queen in the current row
// Recursively call for the next row
backtrack(n, row + 1, board, queens, result);
queens.pop_back(); // Remove the last queen from the current row
board[row][col] = '.'; // Restore the empty space
}
}
}
// Function to check if placing a queen at the current position is valid
bool isValidPlacement(int row, int col, const vector<int> &queens) {
for (int i = 0; i < queens.size(); ++i) {
int rowDiff = abs(row - i);
int colDiff = abs(col - queens[i]);
if (rowDiff == 0 || colDiff == 0 || rowDiff == colDiff) {
return false; // Found a queen in the same row, same column, or diagonal
}
}
return true; // No conflicting queens found, placement is valid
}
};
Python π
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
def is_safe(row, col):
# Check for conflicts with previous rows
for prev_row in range(row):
if board[prev_row][col] == "Q":
return False
if (
col - (row - prev_row) >= 0
and board[prev_row][col - (row - prev_row)] == "Q"
):
return False
if (
col + (row - prev_row) < n
and board[prev_row][col + (row - prev_row)] == "Q"
):
return False
return True
def place_queen(row):
if row == n:
result.append(["".join(row) for row in board])
return
for col in range(n):
if is_safe(row, col):
board[row][col] = "Q"
place_queen(row + 1)
board[row][col] = "."
board = [["." for _ in range(n)] for _ in range(n)]
result = []
place_queen(0)
return result
# class Solution:
# def solveNQueens(self, n: int) -> List[List[str]]:
# result = [] # List to store solutions
# board = [['.'] * n for _ in range(n)] # Chessboard representation
# left_diagonal = [False] * (2 * n - 1) # Left diagonals availability
# right_diagonal = [False] * (2 * n - 1) # Right diagonals availability
# column = [False] * n # Columns availability
# def backtrack(row):
# if row == n:
# solution = ["".join(row) for row in board] # Convert the board to a solution format
# result.append(solution)
# return
# for col in range(n):
# # Check if placing a queen in the current position is valid
# if column[col] or left_diagonal[row - col] or right_diagonal[row + col]:
# continue
# # Place a queen and mark unavailable positions
# board[row][col] = 'Q'
# column[col] = left_diagonal[row - col] = right_diagonal[row + col] = True
# # Move to the next row
# backtrack(row + 1)
# # Backtrack: Reset the board and availability
# board[row][col] = '.'
# column[col] = left_diagonal[row - col] = right_diagonal[row + col] = False
# backtrack(0) # Start the backtracking process from the first row
# return result
Graphs π
This section contains problems belonging to the Graphs category.
Problems
- π‘ Medium: Number of Islands
- π‘ Medium: Clone Graph
- π‘ Medium: Max Area of Island
- π‘ Medium: Pacific Atlantic Water Flow
- π‘ Medium: Surrounded Regions
- π‘ Medium: Rotting Oranges
- π‘ Medium: Walls and Gates
- π‘ Medium: Course Schedule
- π‘ Medium: Course Schedule II
- π‘ Medium: Redundant Connection
- π‘ Medium: Number of Connected Components in an Undirected Graph
- π‘ Medium: Graph Valid Tree
- π΄ Hard: Word Ladder
Number of Islands π§
LeetCode Link: Number of Islands
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 200 - Number of Islands
Description: Given an m x n grid containing '1's (land) and '0's (water), return the number of islands. An island is surrounded by water and is formed by connecting adjacent lands horizontally or vertically. You may assume all four edges of the grid are all surrounded by water.
Intuition: To find the number of islands in the grid, we can use a depth-first search (DFS) or breadth-first search (BFS) approach. The idea is to traverse the grid and whenever we encounter a land ('1'), we explore its neighboring cells and mark them as visited to avoid counting them again.
Approach:
- Iterate through each cell in the grid:
- If the current cell is a land ('1'), increment the count of islands and call the
dfs
orbfs
function to explore its neighboring cells.
- Define a helper function
dfs
orbfs
:
- Check if the current cell is out of bounds or is not a land ('1'). If so, return.
- Mark the current cell as visited by changing its value to '0'.
- Recursively call
dfs
orbfs
for the neighboring cells (up, down, left, right).
- Return the count of islands.
β Time Complexity: The time complexity is O(m * n), where m is the number of rows and n is the number of columns in the grid. We visit each cell once.
πΎ Space Complexity: The space complexity is O(min(m, n)), where m is the number of rows and n is the number of columns in the grid. This is the maximum depth of the recursion stack or the maximum number of cells in the queue during the BFS traversal.
Solutions π‘
Cpp π»
class Solution {
public:
int numIslands(vector<vector<char>> &grid) {
if (grid.empty()) {
return 0;
}
int m = grid.size(); // Number of rows in the grid
int n = grid[0].size(); // Number of columns in the grid
int count = 0; // Counter for the number of islands
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '1') { // Found a new island
++count;
dfs(grid, i, j); // Explore the island using DFS
// bfs(grid, i, j); // Alternatively, we can use BFS to explore the island
}
}
}
return count;
}
private:
void dfs(vector<vector<char>> &grid, int row, int col) {
if (row < 0 || row >= grid.size() || col < 0 || col >= grid[0].size() || grid[row][col] != '1') {
return; // Out of bounds or already visited cell
}
grid[row][col] = '0'; // Mark the current cell as visited
// Recursively call DFS for the neighboring cells (up, down, left, right)
dfs(grid, row - 1, col); // Up
dfs(grid, row + 1, col); // Down
dfs(grid, row, col - 1); // Left
dfs(grid, row, col + 1); // Right
}
void bfs(vector<vector<char>> &grid, int row, int col) {
int m = grid.size(); // Number of rows in the grid
int n = grid[0].size(); // Number of columns in the grid
queue<pair<int, int>> q; // Queue to store cell positions
q.push({row, col}); // Start with the current cell
grid[row][col] = '0'; // Mark the current cell as visited
vector<pair<int, int>> directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}}; // Possible directions (up, down, left, right)
while (!q.empty()) {
int r = q.front().first; // Row of the current cell
int c = q.front().second; // Column of the current cell
q.pop();
// Explore the neighboring cells
for (const auto &dir : directions) {
int nr = r + dir.first; // Row of the neighboring cell
int nc = c + dir.second; // Column of the neighboring cell
// Check if the neighboring cell is within the grid and is unvisited
if (nr >= 0 && nr < m && nc >= 0 && nc < n && grid[nr][nc] == '1') {
q.push({nr, nc}); // Add the neighboring cell to the queue
grid[nr][nc] = '0'; // Mark the neighboring cell as visited
}
}
}
}
};
Python π
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
if not grid:
return 0
rows, cols = len(grid), len(grid[0])
count = 0
def dfs(row, col):
if (
row < 0
or row >= rows
or col < 0
or col >= cols
or grid[row][col] == "0"
):
return
grid[row][col] = "0" # Mark the cell as visited
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
for dr, dc in directions:
dfs(row + dr, col + dc)
for i in range(rows):
for j in range(cols):
if grid[i][j] == "1":
count += 1
dfs(i, j)
return count
Clone Graph π§
LeetCode Link: Clone Graph
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 133 - Clone Graph
Description: Given a reference of a node in a connected undirected graph. Return a deep copy (clone) of the graph. Each node in the graph contains a value (int) and a list (List[Node]) of its neighbors.
Intuition: To clone a graph, we can use a depth-first search (DFS) or breadth-first search (BFS) approach. The idea is to traverse the original graph and create a copy of each node and its neighbors. We can store the visited nodes in a map to ensure that we don't create duplicate copies.
Approach:
- Define a helper function
cloneNode
:
- Create a copy of the current node.
- Iterate through the neighbors of the current node:
- If a neighbor is not visited, recursively call
cloneNode
to create a copy of the neighbor and add it to the neighbors of the current node. - If a neighbor is already visited, add the corresponding copy from the map to the neighbors of the current node.
- Create an empty map to store the copies of the nodes.
- Call the
cloneNode
function with the given reference node. - Return the copy of the reference node.
β Time Complexity: The time complexity is O(V + E), where V is the number of nodes (vertices) and E is the number of edges in the graph. We visit each node and each edge once.
πΎ Space Complexity: The space complexity is O(V), where V is the number of nodes (vertices) in the graph. This is the space used to store the copies of the nodes and the recursion stack.
Solutions π‘
Cpp π»
class Solution {
public:
Node *cloneGraph(Node *node) {
if (node == nullptr) {
return nullptr;
}
unordered_map<Node *, Node *> nodeMap; // Map to store copies of nodes
return cloneNode(node, nodeMap);
}
private:
Node *cloneNode(Node *node, unordered_map<Node *, Node *> &nodeMap) {
// If a copy of the current node already exists, return it
if (nodeMap.find(node) != nodeMap.end()) {
return nodeMap[node];
}
Node *newNode = new Node(node->val); // Create a new copy of the current node
nodeMap[node] = newNode; // Add the current node to the map
// Recursively clone the neighbors of the current node
for (Node *neighbor : node->neighbors) {
newNode->neighbors.push_back(cloneNode(neighbor, nodeMap));
}
return newNode;
}
};
Python π
"""
# Definition for a Node.
class Node:
def __init__(self, val = 0, neighbors = None):
self.val = val
self.neighbors = neighbors if neighbors is not None else []
"""
class Solution:
def cloneGraph(self, node: "Node") -> "Node":
if not node:
return None
visited = {} # Dictionary to store the cloned nodes
def dfs(original_node):
if original_node in visited:
return visited[original_node]
new_node = Node(original_node.val)
visited[original_node] = new_node
for neighbor in original_node.neighbors:
new_neighbor = dfs(neighbor)
new_node.neighbors.append(new_neighbor)
return new_node
return dfs(node)
Max Area of Island π§
LeetCode Link: Max Area of Island
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 695 - Max Area of Island
Description: Given a grid of 0's and 1's, find the maximum area of an island in the grid. An island is a group of connected 1's (horizontally or vertically). You may assume all four edges of the grid are all surrounded by water.
Intuition: To find the maximum area of an island, we can use a depth-first search (DFS) approach. The idea is to traverse the grid and whenever we encounter a land (1), we explore its neighboring cells and count the number of connected lands.
Approach:
- Initialize a variable
maxArea
to store the maximum area of an island. - Iterate through each cell in the grid:
- If the current cell is a land (1), call the
dfs
function to explore its neighboring cells and update the maximum area.
- Define a helper function
dfs
:
- Check if the current cell is out of bounds or is not a land (1). If so, return 0.
- Mark the current cell as visited by changing its value to 0.
- Recursively call
dfs
for the neighboring cells (up, down, left, right) and sum the results.
- Return the maximum area stored in
maxArea
.
β Time Complexity: The time complexity is O(m * n), where m is the number of rows and n is the number of columns in the grid. We visit each cell once.
πΎ Space Complexity: The space complexity is O(m * n), where m is the number of rows and n is the number of columns in the grid. This is the space used for the recursion stack during the DFS traversal.
Solutions π‘
Cpp π»
class Solution {
public:
int maxAreaOfIsland(vector<vector<int>> &grid) {
if (grid.empty()) {
return 0;
}
int m = grid.size();
int n = grid[0].size();
int maxArea = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 1) {
maxArea = max(maxArea, dfs(grid, i, j));
}
}
}
return maxArea;
}
private:
int dfs(vector<vector<int>> &grid, int row, int col) {
if (row < 0 || row >= grid.size() || col < 0 || col >= grid[0].size() || grid[row][col] == 0) {
return 0;
}
grid[row][col] = 0; // Mark the current cell as visited
int area = 1; // Count the current cell as part of the island
// Recursively explore the neighboring cells (up, down, left, right)
area += dfs(grid, row - 1, col); // Up
area += dfs(grid, row + 1, col); // Down
area += dfs(grid, row, col - 1); // Left
area += dfs(grid, row, col + 1); // Right
return area;
}
};
Python π
class Solution:
def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
def dfs(row, col):
if (
row < 0
or row >= len(grid)
or col < 0
or col >= len(grid[0])
or grid[row][col] == 0
):
return 0
grid[row][col] = 0 # Mark as visited
area = 1
area += dfs(row + 1, col) # Check down
area += dfs(row - 1, col) # Check up
area += dfs(row, col + 1) # Check right
area += dfs(row, col - 1) # Check left
return area
max_area = 0
for row in range(len(grid)):
for col in range(len(grid[0])):
if grid[row][col] == 1:
max_area = max(max_area, dfs(row, col))
return max_area
Pacific Atlantic Water Flow π§
LeetCode Link: Pacific Atlantic Water Flow
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 417 - Pacific Atlantic Water Flow
Description: Given an m x n matrix of non-negative integers representing the height of each unit cell in a continent, the "Pacific ocean" touches the left and top edges of the matrix, and the "Atlantic ocean" touches the right and bottom edges. Water can only flow in four directions (up, down, left, or right) from a cell to its neighboring cells with equal or lower height. Find the list of grid coordinates where water can flow to both the Pacific and Atlantic oceans.
Intuition: To find the cells where water can flow to both the Pacific and Atlantic oceans, we can use a depth-first search (DFS) or breadth-first search (BFS) approach. The idea is to start the traversal from the ocean borders (Pacific and Atlantic) and mark the cells that can be reached by water. Finally, we find the cells that are marked by both traversals.
Approach:
- Create two boolean matrices
canReachPacific
andcanReachAtlantic
, initialized with false, to track the cells that can be reached by water from the respective oceans. - Perform a DFS or BFS traversal from the ocean borders to mark the cells that can be reached by water:
- For the Pacific ocean:
- Start from the leftmost column and the topmost row. Traverse all neighboring cells with equal or lower heights and mark them as reachable by water from the Pacific ocean.
- For the Atlantic ocean:
- Start from the rightmost column and the bottommost row. Traverse all neighboring cells with equal or lower heights and mark them as reachable by water from the Atlantic ocean.
- Iterate through all the cells and find the cells that are marked as reachable by both oceans.
- Return the list of grid coordinates representing the cells that can flow to both oceans.
β Time Complexity: The time complexity is O(m * n), where m is the number of rows and n is the number of columns in the matrix. We perform a DFS or BFS traversal on each cell once.
πΎ Space Complexity: The space complexity is O(m * n), where m is the number of rows and n is the number of columns in the matrix. This is the space used to store the boolean matrices and the recursion stack or the queue during the DFS or BFS traversal.
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<int>> pacificAtlantic(vector<vector<int>> &matrix) {
vector<vector<int>> result;
if (matrix.empty()) {
return result;
}
int m = matrix.size();
int n = matrix[0].size();
vector<vector<bool>> canReachPacific(m, vector<bool>(n, false)); // Matrix to track cells reachable from the Pacific ocean
vector<vector<bool>> canReachAtlantic(m, vector<bool>(n, false)); // Matrix to track cells reachable from the Atlantic ocean
// Traverse the top and bottom borders to mark cells reachable from the Pacific and Atlantic oceans
for (int col = 0; col < n; ++col) {
dfs(matrix, 0, col, INT_MIN, canReachPacific);
dfs(matrix, m - 1, col, INT_MIN, canReachAtlantic);
}
// Traverse the left and right borders to mark cells reachable from the Pacific and Atlantic oceans
for (int row = 0; row < m; ++row) {
dfs(matrix, row, 0, INT_MIN, canReachPacific);
dfs(matrix, row, n - 1, INT_MIN, canReachAtlantic);
}
// Find the cells that are reachable from both the Pacific and Atlantic oceans
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (canReachPacific[i][j] && canReachAtlantic[i][j]) {
result.push_back({i, j});
}
}
}
return result;
}
private:
void dfs(const vector<vector<int>> &matrix, int row, int col, int prevHeight, vector<vector<bool>> &canReachOcean) {
int m = matrix.size();
int n = matrix[0].size();
// Check if the current cell is out of bounds or has been visited already
if (row < 0 || row >= m || col < 0 || col >= n || matrix[row][col] < prevHeight || canReachOcean[row][col]) {
return;
}
canReachOcean[row][col] = true; // Mark the current cell as reachable
// Recursively traverse the neighboring cells
dfs(matrix, row - 1, col, matrix[row][col], canReachOcean); // Up
dfs(matrix, row + 1, col, matrix[row][col], canReachOcean); // Down
dfs(matrix, row, col - 1, matrix[row][col], canReachOcean); // Left
dfs(matrix, row, col + 1, matrix[row][col], canReachOcean); // Right
}
};
Python π
class Solution:
def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]:
if not heights:
return []
rows, cols = len(heights), len(heights[0])
pacific_reachable = set()
atlantic_reachable = set()
def dfs(r, c, reachable):
reachable.add((r, c))
for dr, dc in [(1, 0), (-1, 0), (0, 1), (0, -1)]:
nr, nc = r + dr, c + dc
if (
0 <= nr < rows
and 0 <= nc < cols
and (nr, nc) not in reachable
and heights[nr][nc] >= heights[r][c]
):
dfs(nr, nc, reachable)
for r in range(rows):
dfs(r, 0, pacific_reachable)
dfs(r, cols - 1, atlantic_reachable)
for c in range(cols):
dfs(0, c, pacific_reachable)
dfs(rows - 1, c, atlantic_reachable)
return list(pacific_reachable & atlantic_reachable)
Surrounded Regions π§
LeetCode Link: Surrounded Regions
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 130 - Surrounded Regions
Description: Given an m x n matrix board containing 'X' and 'O', capture all regions that are surrounded by 'X'. A region is captured by flipping all 'O's into 'X's in that surrounded region.
Intuition: To capture the surrounded regions, we need to identify the regions that are connected to the borders of the matrix. The regions that are not connected to the borders are the ones that need to be captured. We can use a depth-first search (DFS) approach to identify and mark the regions that are connected to the borders, and then iterate through the matrix to capture the remaining regions.
Approach:
- Traverse the borders of the matrix and perform a DFS to mark the regions that are connected to the borders:
- If a cell is 'O', perform a DFS to mark all its neighboring 'O' cells as connected to the border.
- Iterate through the entire matrix:
- If a cell is 'O' and not marked as connected to the border, capture it by changing it to 'X'.
- If a cell is marked as connected to the border, restore it to 'O'.
- The regions that are not marked as connected to the border are the captured regions.
β Time Complexity: The time complexity is O(m * n), where m is the number of rows and n is the number of columns in the matrix. We visit each cell once.
πΎ Space Complexity: The space complexity is O(m * n), where m is the number of rows and n is the number of columns in the matrix. This is the space used for the recursion stack during the DFS traversal.
Solutions π‘
Cpp π»
class Solution {
public:
void solve(vector<vector<char>> &board) {
if (board.empty()) {
return;
}
int m = board.size();
int n = board[0].size();
// Traverse the top and bottom borders
for (int col = 0; col < n; ++col) {
dfs(board, 0, col);
dfs(board, m - 1, col);
}
// Traverse the left and right borders
for (int row = 0; row < m; ++row) {
dfs(board, row, 0);
dfs(board, row, n - 1);
}
// Capture the remaining regions
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (board[i][j] == 'O') {
board[i][j] = 'X'; // Capture the region
} else if (board[i][j] == '#') {
board[i][j] = 'O'; // Restore the region
}
}
}
}
private:
void dfs(vector<vector<char>> &board, int row, int col) {
int m = board.size();
int n = board[0].size();
if (row < 0 || row >= m || col < 0 || col >= n || board[row][col] != 'O') {
return;
}
board[row][col] = '#'; // Mark the cell as connected to the border
dfs(board, row - 1, col); // Up
dfs(board, row + 1, col); // Down
dfs(board, row, col - 1); // Left
dfs(board, row, col + 1); // Right
}
};
Python π
class Solution:
def solve(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
def dfs(row, col):
if (
row < 0
or row >= len(board)
or col < 0
or col >= len(board[0])
or board[row][col] != "O"
):
return
board[row][col] = "E" # Mark as visited but not surrounded
# Check adjacent cells
dfs(row + 1, col) # Check down
dfs(row - 1, col) # Check up
dfs(row, col + 1) # Check right
dfs(row, col - 1) # Check left
# Traverse the boundary and mark connected 'O' cells as 'E'
for row in range(len(board)):
if board[row][0] == "O":
dfs(row, 0)
if board[row][len(board[0]) - 1] == "O":
dfs(row, len(board[0]) - 1)
for col in range(len(board[0])):
if board[0][col] == "O":
dfs(0, col)
if board[len(board) - 1][col] == "O":
dfs(len(board) - 1, col)
# Mark internal 'O' cells as 'X' and restore 'E' cells to 'O'
for row in range(len(board)):
for col in range(len(board[0])):
if board[row][col] == "O":
board[row][col] = "X"
elif board[row][col] == "E":
board[row][col] = "O"
Rotting Oranges π§
LeetCode Link: Rotting Oranges
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 994 - Rotting Oranges
Description: You are given an m x n grid where each cell can have one of three values:
- 0 representing an empty cell.
- 1 representing a fresh orange.
- 2 representing a rotten orange. Every minute, any fresh orange that is 4-directionally adjacent to a rotten orange becomes rotten. Return the minimum number of minutes that must elapse until no cell has a fresh orange. If this is impossible, return -1.
Intuition: To find the minimum number of minutes needed to rot all the oranges, we can use a breadth-first search (BFS) approach. We start with the initial rotten oranges and spread the rot to their adjacent fresh oranges. We continue this process in rounds until no more fresh oranges can be infected. The number of rounds needed corresponds to the minimum minutes required.
Approach:
- Initialize a queue to store the coordinates of the rotten oranges.
- Initialize variables to keep track of the number of fresh oranges and the number of minutes passed.
- Iterate through the grid to find the initial rotten oranges and count the number of fresh oranges.
- Enqueue the coordinates of the rotten oranges into the queue.
- Perform a BFS traversal:
- For each round, process all the rotten oranges in the queue.
- For each rotten orange, check its adjacent cells (up, down, left, right):
- If an adjacent cell is a fresh orange, mark it as rotten, decrease the count of fresh oranges, and enqueue its coordinates.
- Increment the number of minutes.
- After the BFS traversal, check if there are any remaining fresh oranges. If so, return -1.
- Return the number of minutes minus one since the last round is not counted as a minute needed to rot the oranges.
β Time Complexity: The time complexity is O(m * n), where m is the number of rows and n is the number of columns in the grid. In the worst case, we may need to visit all the cells.
πΎ Space Complexity: The space complexity is O(m * n), where m is the number of rows and n is the number of columns in the grid. This is the space used for the queue to store the coordinates of the rotten oranges.
Solutions π‘
Cpp π»
class Solution {
public:
int orangesRotting(vector<vector<int>> &grid) {
if (grid.empty()) {
return 0;
}
int m = grid.size();
int n = grid[0].size();
int freshOranges = 0;
int minutes = 0;
queue<pair<int, int>> rottenOranges; // Queue to store the coordinates of the rotten oranges
// Find the initial rotten oranges and count the number of fresh oranges
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == 2) {
rottenOranges.push({i, j});
} else if (grid[i][j] == 1) {
++freshOranges;
}
}
}
// Perform a BFS traversal
while (!rottenOranges.empty() && freshOranges > 0) {
int size = rottenOranges.size();
for (int i = 0; i < size; ++i) {
int row = rottenOranges.front().first;
int col = rottenOranges.front().second;
rottenOranges.pop();
// Check adjacent cells (up, down, left, right)
vector<pair<int, int>> directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (const auto &dir : directions) {
int newRow = row + dir.first;
int newCol = col + dir.second;
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && grid[newRow][newCol] == 1) {
// Mark the adjacent fresh orange as rotten
grid[newRow][newCol] = 2;
rottenOranges.push({newRow, newCol});
--freshOranges;
}
}
}
if (!rottenOranges.empty()) {
++minutes; // Increment the number of minutes
}
}
if (freshOranges > 0) {
return -1; // There are remaining fresh oranges
}
return minutes;
}
};
Python π
class Solution:
def orangesRotting(self, grid: List[List[int]]) -> int:
if not grid:
return -1
rows, cols = len(grid), len(grid[0])
fresh_count = 0 # Count of fresh oranges
rotten = deque() # Queue to store coordinates of rotten oranges
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] # Possible adjacent cells
# Initialize the queue with coordinates of rotten oranges
for row in range(rows):
for col in range(cols):
if grid[row][col] == 2:
rotten.append((row, col))
elif grid[row][col] == 1:
fresh_count += 1
minutes = 0 # Timer
while rotten:
level_size = len(rotten)
for _ in range(level_size):
row, col = rotten.popleft()
for dr, dc in directions:
new_row, new_col = row + dr, col + dc
# Check if the new cell is within bounds and has a fresh orange
if (
0 <= new_row < rows
and 0 <= new_col < cols
and grid[new_row][new_col] == 1
):
grid[new_row][new_col] = 2 # Infect the fresh orange
fresh_count -= 1
rotten.append((new_row, new_col))
if rotten:
minutes += 1
# If there are fresh oranges left, return -1; otherwise, return the elapsed minutes
return minutes if fresh_count == 0 else -1
Walls and Gates π§
LeetCode Link: Walls and Gates
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 286 - Walls and Gates
Description: You are given an m x n grid rooms initialized with these three possible values:
- -1: A wall or an obstacle.
- 0: A gate.
- INF: Infinity means an empty room. We use the value 231 - 1 = 2147483647 to represent INF, as you may assume that the distance to a gate is less than 2147483647. Fill each empty room with the distance to its nearest gate. If it is impossible to reach a gate, leave it as INF.
Intuition: To fill each empty room with the distance to its nearest gate, we can use a breadth-first search (BFS) approach. We start the BFS from each gate and propagate the distances to the neighboring empty rooms. We repeat this process for each gate to ensure that all empty rooms are filled with their nearest gate distances.
Approach:
- Initialize a queue to store the coordinates of the gates.
- Iterate through the grid and enqueue the coordinates of the gates into the queue.
- Perform a BFS traversal:
- For each gate, start the BFS and propagate the distances to the neighboring empty rooms.
- Initialize a distance variable as 0.
- While the queue is not empty, process the rooms in the queue:
- For each room, check its neighboring rooms (up, down, left, right):
- If a neighboring room is an empty room (INF), mark it with the current distance + 1, enqueue its coordinates, and update the grid.
- Increment the distance by 1.
- After the BFS traversal, the grid will be filled with the distances to the nearest gates.
β Time Complexity: The time complexity is O(m * n), where m is the number of rows and n is the number of columns in the grid. In the worst case, we may need to visit all the rooms.
πΎ Space Complexity: The space complexity is O(m * n), where m is the number of rows and n is the number of columns in the grid. This is the space used for the queue to store the coordinates of the gates.
Solutions π‘
Cpp π»
class Solution {
public:
void wallsAndGates(vector<vector<int>> &rooms) {
if (rooms.empty()) {
return;
}
int m = rooms.size();
int n = rooms[0].size();
queue<pair<int, int>> gates; // Queue to store the coordinates of the gates
// Enqueue the coordinates of the gates
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (rooms[i][j] == 0) {
gates.push({i, j});
}
}
}
// Perform a BFS traversal
while (!gates.empty()) {
int size = gates.size();
for (int i = 0; i < size; ++i) {
int row = gates.front().first;
int col = gates.front().second;
gates.pop();
// Check neighboring rooms (up, down, left, right)
vector<pair<int, int>> directions = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}};
for (const auto &dir : directions) {
int newRow = row + dir.first;
int newCol = col + dir.second;
if (newRow >= 0 && newRow < m && newCol >= 0 && newCol < n && rooms[newRow][newCol] == INT32_MAX) {
// Mark the neighboring room with the distance to the gate and enqueue its coordinates
rooms[newRow][newCol] = rooms[row][col] + 1;
gates.push({newRow, newCol});
}
}
}
}
}
};
Python π
class Solution:
def wallsAndGates(self, rooms: List[List[int]]) -> None:
if not rooms:
return
rows, cols = len(rooms), len(rooms[0])
gates = [(i, j) for i in range(rows) for j in range(cols) if rooms[i][j] == 0]
directions = [(1, 0), (-1, 0), (0, 1), (0, -1)]
for gate_row, gate_col in gates:
visited = set() # To track visited cells in BFS
queue = deque([(gate_row, gate_col, 0)])
while queue:
row, col, distance = queue.popleft()
rooms[row][col] = min(rooms[row][col], distance)
visited.add((row, col))
for dr, dc in directions:
new_row, new_col = row + dr, col + dc
if (
0 <= new_row < rows
and 0 <= new_col < cols
and rooms[new_row][new_col] != -1
and (new_row, new_col) not in visited
):
queue.append((new_row, new_col, distance + 1))
visited.add((new_row, new_col))
Course Schedule π§
LeetCode Link: Course Schedule
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 207 - Course Schedule
Description: There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai. For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1. Return true if you can finish all courses. Otherwise, return false.
Intuition: This problem can be approached as a graph problem where the courses represent nodes and the prerequisites represent directed edges. To determine if it is possible to finish all courses, we need to check if there is a cycle in the graph. If there is a cycle, it means there is a dependency loop, and we cannot finish all the courses.
Approach:
- Build an adjacency list representation of the graph using the prerequisites.
- Initialize a visited array to track the visited nodes during the DFS traversal.
- Iterate through each node in the graph and perform a DFS traversal to detect cycles:
- If the current node is being visited, it means there is a cycle, so return false.
- If the current node is not visited, perform a DFS traversal on its neighbors.
- If no cycles are detected after the DFS traversal, return true.
β Time Complexity: The time complexity is O(V + E), where V is the number of courses (nodes) and E is the number of prerequisites (edges). We visit each course and prerequisite once.
πΎ Space Complexity: The space complexity is O(V + E), where V is the number of courses (nodes) and E is the number of prerequisites (edges). This is the space used for the adjacency list and the visited array.
Solutions π‘
Cpp π»
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>> &prerequisites) {
vector<vector<int>> graph(numCourses); // Adjacency list representation of the graph
vector<int> visited(numCourses, 0); // Visited array to track the visited nodes
// Build the graph
for (const auto &prerequisite : prerequisites) {
int course = prerequisite[0];
int prerequisiteCourse = prerequisite[1];
graph[course].push_back(prerequisiteCourse);
}
// Perform a DFS traversal to detect cycles
for (int course = 0; course < numCourses; ++course) {
if (!dfs(course, graph, visited)) {
return false;
}
}
return true;
}
private:
bool dfs(int course, vector<vector<int>> &graph, vector<int> &visited) {
// If the current course is being visited, it means there is a cycle
if (visited[course] == 1) {
return false;
}
// If the current course is already visited, return true
if (visited[course] == -1) {
return true;
}
visited[course] = 1; // Mark the current course as being visited
// Perform a DFS traversal on the neighbors
for (const auto &neighbor : graph[course]) {
if (!dfs(neighbor, graph, visited)) {
return false;
}
}
visited[course] = -1; // Mark the current course as visited
return true;
}
};
// Topological sort
// Same time and space complexity but this can be more efficient in terms of practical performance
/*
class Solution {
public:
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> graph(numCourses); // Adjacency list representation of the graph
vector<int> inDegree(numCourses, 0); // In-degree of each course
// Build the graph and calculate the in-degree of each course
for (const auto& prerequisite : prerequisites) {
int course = prerequisite[0];
int prerequisiteCourse = prerequisite[1];
graph[prerequisiteCourse].push_back(course);
++inDegree[course];
}
queue<int> q; // Queue to store courses with in-degree 0
// Enqueue courses with in-degree 0
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] == 0) {
q.push(i);
}
}
// Perform topological sorting
while (!q.empty()) {
int course = q.front();
q.pop();
--numCourses; // Decrement the number of remaining courses
// Decrement the in-degree of neighbors and enqueue if their in-degree becomes 0
for (const auto& neighbor : graph[course]) {
if (--inDegree[neighbor] == 0) {
q.push(neighbor);
}
}
}
return numCourses == 0;
}
};
*/
Python π
class Solution:
def canFinish(self, numCourses: int, prerequisites: List[List[int]]) -> bool:
graph = {i: [] for i in range(numCourses)}
in_degree = [0] * numCourses
# Construct the graph and count in-degrees
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
# Initialize a queue with nodes having in-degree zero
queue = collections.deque(
[course for course, degree in enumerate(in_degree) if degree == 0]
)
# Perform topological sorting and update in-degrees
while queue:
node = queue.popleft()
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
# If any course has in-degree greater than zero, there's a cycle
return all(degree == 0 for degree in in_degree)
Course Schedule II π§
LeetCode Link: Course Schedule II
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 210 - Course Schedule II
Description: There are a total of numCourses courses you have to take, labeled from 0 to numCourses - 1. You are given an array prerequisites where prerequisites[i] = [ai, bi] indicates that you must take course bi first if you want to take course ai. For example, the pair [0, 1], indicates that to take course 0 you have to first take course 1. Return the ordering of courses you should take to finish all courses. If there are many valid answers, return any of them. If it is impossible to finish all courses, return an empty array.
Intuition: This problem can be approached as a graph problem where the courses represent nodes and the prerequisites represent directed edges. To determine the ordering of courses, we can use the topological sorting algorithm. If there is a cycle in the graph, it means there is a dependency loop, and we cannot finish all the courses.
Approach:
- Build an adjacency list representation of the graph using the prerequisites.
- Initialize an array to store the in-degree of each course. In-degree represents the number of prerequisites for each course.
- Create a queue and enqueue all the courses with an in-degree of 0.
- Perform a topological sorting:
- While the queue is not empty, dequeue a course:
- Decrement the in-degree of its neighbors by 1.
- If any neighbor has an in-degree of 0, enqueue it.
- Add the dequeued course to the result list.
- If all the courses have been visited, return the result list. Otherwise, return an empty array.
β Time Complexity: The time complexity is O(V + E), where V is the number of courses (nodes) and E is the number of prerequisites (edges). We visit each course and prerequisite once.
πΎ Space Complexity: The space complexity is O(V + E), where V is the number of courses (nodes) and E is the number of prerequisites (edges). This is the space used for the adjacency list, the in-degree array, the queue, and the result list.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> findOrder(int numCourses, vector<vector<int>> &prerequisites) {
vector<vector<int>> graph(numCourses); // Adjacency list representation of the graph
vector<int> inDegree(numCourses, 0); // In-degree of each course
vector<int> result; // Result list of the course order
// Build the graph and calculate the in-degree of each course
for (const auto &prerequisite : prerequisites) {
int course = prerequisite[0];
int prerequisiteCourse = prerequisite[1];
graph[prerequisiteCourse].push_back(course);
++inDegree[course];
}
queue<int> q; // Queue to store courses with in-degree 0
// Enqueue courses with in-degree 0
for (int i = 0; i < numCourses; ++i) {
if (inDegree[i] == 0) {
q.push(i);
}
}
// Perform topological sorting
while (!q.empty()) {
int course = q.front();
q.pop();
result.push_back(course); // Add the course to the result list
// Decrement the in-degree of neighbors and enqueue if their in-degree becomes 0
for (const auto &neighbor : graph[course]) {
if (--inDegree[neighbor] == 0) {
q.push(neighbor);
}
}
}
// If all the courses have been visited, return the result list
if (result.size() == numCourses) {
return result;
}
return {}; // Return an empty array if it is impossible to finish all courses
}
};
Python π
class Solution:
def findOrder(self, numCourses: int, prerequisites: List[List[int]]) -> List[int]:
graph = {i: [] for i in range(numCourses)}
in_degree = [0] * numCourses
order = []
# Construct the graph and count in-degrees
for course, prereq in prerequisites:
graph[prereq].append(course)
in_degree[course] += 1
# Initialize a queue with nodes having in-degree zero
queue = collections.deque(
[course for course, degree in enumerate(in_degree) if degree == 0]
)
# Perform topological sorting and update in-degrees
while queue:
node = queue.popleft()
order.append(node)
for neighbor in graph[node]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
# If the order doesn't contain all courses, there's a cycle
return order if len(order) == numCourses else []
Redundant Connection π§
LeetCode Link: Redundant Connection
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 684 - Redundant Connection
Description: In this problem, a tree is an undirected graph that is connected and has no cycles. You are given a graph that started as a tree with n nodes labeled from 1 to n, with one additional edge added. The added edge has two different vertices chosen from 1 to n, and was not an edge that already existed. The graph is represented as an array edges of length n where edges[i] = [ai, bi] indicates that there is an edge between nodes ai and bi in the graph. Return an edge that can be removed so that the resulting graph is a tree of n nodes. If there are multiple answers, return the answer that occurs last in the input.
Intuition: The problem is asking to find the redundant connection, which is the edge that creates a cycle in the given graph. We can solve this problem using a Union-Find algorithm.
Approach:
- Create a parent array of size n+1 to represent each node's parent.
- Iterate through the edges:
- Initialize variables to store the parent of each node in the current edge.
- If the parent of both nodes is the same, it means adding this edge will create a cycle, so return the current edge.
- Otherwise, union the nodes by setting the parent of one node to be the parent of the other node.
- If no redundant edge is found, return an empty vector.
β Time Complexity: The time complexity is O(n * Ξ±(n)), where n is the number of nodes and Ξ±(n) is the inverse Ackermann function. In practice, Ξ±(n) is a very slow-growing function and can be considered constant. Thus, the time complexity is effectively O(n).
πΎ Space Complexity: The space complexity is O(n), where n is the number of nodes. This is the space used for the parent array.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> findRedundantConnection(vector<vector<int>> &edges) {
int n = edges.size();
vector<int> parent(n + 1);
// Initialize the parent array
for (int i = 1; i <= n; ++i) {
parent[i] = i;
}
// Iterate through the edges
for (const auto &edge : edges) {
int node1 = edge[0];
int node2 = edge[1];
// Find the parent of each node in the current edge
int parent1 = findParent(parent, node1);
int parent2 = findParent(parent, node2);
// If both nodes have the same parent, return the current edge
if (parent1 == parent2) {
return edge;
}
// Union the nodes
parent[parent1] = parent2;
}
return {};
}
private:
int findParent(vector<int> &parent, int node) {
if (parent[node] != node) {
parent[node] = findParent(parent, parent[node]);
}
return parent[node];
}
};
Python π
class Solution:
def findRedundantConnection(self, edges: List[List[int]]) -> List[int]:
n = len(edges)
parent = list(range(n + 1)) # Initialize each node as its own parent
def find(x):
if parent[x] != x:
parent[x] = find(parent[x]) # Path compression
return parent[x]
def union(x, y):
parent[find(x)] = find(y)
for edge in edges:
u, v = edge
if find(u) == find(v):
return edge
union(u, v)
return []
Number of Connected Components in an Undirected Graph π§
LeetCode Link: Number of Connected Components in an Undirected Graph
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 323 - Number of Connected Components in an Undirected Graph
Description: Given n nodes labeled from 0 to n - 1 and a list of undirected edges (each edge is a pair of nodes), write a function to find the number of connected components in an undirected graph.
Intuition: This problem can be approached as a graph problem where the nodes represent the vertices and the edges represent the connections between the vertices. We can use depth-first search (DFS) or breadth-first search (BFS) to explore the graph and count the number of connected components.
Approach:
- Build an adjacency list representation of the graph using the given edges.
- Initialize a visited array to track the visited nodes during the graph traversal.
- Initialize a count variable to keep track of the number of connected components.
- Iterate through each node in the graph:
- If the node is not visited, perform a DFS or BFS traversal from that node:
- Increment the count by 1.
- Mark all the connected nodes as visited.
- Return the count, which represents the number of connected components.
β Time Complexity: The time complexity depends on the graph traversal algorithm used. Using DFS or BFS, the time complexity is O(V + E), where V is the number of nodes (vertices) and E is the number of edges. We visit each node and edge once.
πΎ Space Complexity: The space complexity is O(V + E), where V is the number of nodes (vertices) and E is the number of edges. This is the space used for the adjacency list and the visited array.
Solutions π‘
Cpp π»
class Solution {
public:
int countComponents(int n, vector<vector<int>> &edges) {
vector<vector<int>> graph(n); // Adjacency list representation of the graph
vector<int> visited(n, 0); // Visited array to track the visited nodes
int count = 0; // Number of connected components
// Build the graph
for (const auto &edge : edges) {
int node1 = edge[0];
int node2 = edge[1];
graph[node1].push_back(node2);
graph[node2].push_back(node1);
}
// Perform graph traversal to count the connected components
for (int i = 0; i < n; ++i) {
if (visited[i] == 0) {
++count;
dfs(i, graph, visited);
// Or use bfs(i, graph, visited) for BFS traversal
}
}
return count;
}
private:
void dfs(int node, vector<vector<int>> &graph, vector<int> &visited) {
visited[node] = 1; // Mark the current node as visited
// Perform DFS traversal on the neighbors
for (int neighbor : graph[node]) {
if (visited[neighbor] == 0) {
dfs(neighbor, graph, visited);
}
}
}
};
Python π
from collections import defaultdict, deque
class Solution:
def countComponents(self, n: int, edges: List[List[int]]) -> int:
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
def dfs(node):
visited.add(node)
for neighbor in graph[node]:
if neighbor not in visited:
dfs(neighbor)
visited = set()
components = 0
for node in range(n):
if node not in visited:
components += 1
dfs(node)
return components
Graph Valid Tree π§
LeetCode Link: Graph Valid Tree
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 261 - Graph Valid Tree
Description: You have a graph of n nodes labeled from 0 to n - 1. You are given an integer n and a list of edges where edges[i] = [ai, bi] indicates that there is an undirected edge between nodes ai and bi in the graph. Return true if the edges of the given graph make up a valid tree, and false otherwise.
Intuition: A valid tree is a connected graph with no cycles. To determine if the given graph is a valid tree, we can perform a depth-first search (DFS) or breadth-first search (BFS) traversal and check if there are any cycles and if all the nodes are reachable from a single source node.
Approach:
- Build an adjacency list representation of the graph using the given edges.
- Initialize a visited array to track the visited nodes during the graph traversal.
- Perform a DFS or BFS traversal from any node in the graph:
- Mark the current node as visited.
- Visit all the neighbors of the current node:
- If a neighbor is already visited and not the parent of the current node, it means there is a cycle, so return false.
- If a neighbor is not visited, perform a recursive DFS or enqueue it in the BFS queue.
- After the traversal, check if all the nodes are visited. If not, return false.
- Return true if no cycle is found and all nodes are visited.
β Time Complexity: The time complexity depends on the graph traversal algorithm used. Using DFS or BFS, the time complexity is O(V + E), where V is the number of nodes (vertices) and E is the number of edges. We visit each node and edge once.
πΎ Space Complexity: The space complexity is O(V + E), where V is the number of nodes (vertices) and E is the number of edges. This is the space used for the adjacency list and the visited array.
Solutions π‘
Cpp π»
class Solution {
public:
bool validTree(int n, vector<vector<int>> &edges) {
vector<vector<int>> graph(n); // Adjacency list representation of the graph
vector<int> visited(n, 0); // Visited array to track the visited nodes
// Build the graph
for (const auto &edge : edges) {
int node1 = edge[0];
int node2 = edge[1];
graph[node1].push_back(node2);
graph[node2].push_back(node1);
}
// Perform graph traversal to check for cycles and reachability
if (!dfs(0, -1, graph, visited)) {
return false;
}
// Check if all nodes are visited
for (int i = 0; i < n; ++i) {
if (visited[i] == 0) {
return false;
}
}
return true;
}
private:
bool dfs(int node, int parent, vector<vector<int>> &graph, vector<int> &visited) {
visited[node] = 1; // Mark the current node as visited
// Perform DFS traversal on the neighbors
for (int neighbor : graph[node]) {
if (visited[neighbor] == 0) {
if (!dfs(neighbor, node, graph, visited)) {
return false;
}
} else if (neighbor != parent) {
return false;
}
}
return true;
}
};
Python π
class Solution:
def validTree(self, n: int, edges: List[List[int]]) -> bool:
if len(edges) != n - 1:
return False
graph = defaultdict(list)
for u, v in edges:
graph[u].append(v)
graph[v].append(u)
visited = set()
def dfs(node, parent):
visited.add(node)
for neighbor in graph[node]:
if neighbor != parent:
if neighbor in visited or not dfs(neighbor, node):
return False
return True
# Check if the graph is connected
if not dfs(0, -1):
return False
return len(visited) == n
Word Ladder π§
LeetCode Link: Word Ladder
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 127 - Word Ladder
Description: Given two words, beginWord and endWord, and a dictionary wordList, return the length of the shortest transformation sequence from beginWord to endWord, such that:
- Only one letter can be changed at a time.
- Each transformed word must exist in the wordList.
If there is no such transformation sequence, return 0.
Intuition: This problem can be solved using a graph traversal algorithm such as breadth-first search (BFS). We can treat each word as a node in the graph and connect the words that differ by a single character. The problem then reduces to finding the shortest path from the beginWord to the endWord.
Approach:
- Build a set from the wordList for efficient lookup.
- Create a queue for BFS traversal and initialize it with the beginWord.
- Initialize a distance variable to track the length of the transformation sequence.
- Perform BFS:
- Pop the front word from the queue.
- For each character position in the word, replace it with each of the 26 alphabets and check if the resulting word is in the wordList.
- If the resulting word is the endWord, return the current distance + 1.
- If the resulting word is in the wordList, add it to the queue and remove it from the wordList to avoid revisiting.
- If no transformation sequence is found, return 0.
β Time Complexity: The time complexity is O(N * M^2), where N is the number of words in the wordList and M is the length of the words. In the worst case, we may need to explore all possible transformations of each word.
πΎ Space Complexity: The space complexity is O(N * M^2), where N is the number of words in the wordList and M is the length of the words. This is the space used for the wordList set, queue, and visited set.
Solutions π‘
Cpp π»
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string> &wordList) {
unordered_set<string> wordSet(wordList.begin(), wordList.end()); // Convert wordList to a set for efficient lookup
if (wordSet.find(endWord) == wordSet.end()) {
return 0; // endWord is not in the wordList, no transformation sequence possible
}
queue<string> q;
q.push(beginWord);
int distance = 1;
while (!q.empty()) {
int levelSize = q.size();
for (int i = 0; i < levelSize; ++i) {
string currentWord = q.front();
q.pop();
// Check each character position of the word and replace it with all possible alphabets
for (int j = 0; j < currentWord.length(); ++j) {
char originalChar = currentWord[j];
for (char c = 'a'; c <= 'z'; ++c) {
currentWord[j] = c;
if (currentWord == endWord) {
return distance + 1; // Transformation sequence found, return the distance
}
if (wordSet.find(currentWord) != wordSet.end()) {
q.push(currentWord); // Add the transformed word to the queue
wordSet.erase(currentWord); // Remove the transformed word from the wordSet
}
}
currentWord[j] = originalChar; // Revert back the character
}
}
++distance; // Increment the distance for each level of BFS traversal
}
return 0; // No transformation sequence found
}
};
// Bidirectional BFS
/*
class Solution {
public:
int ladderLength(string beginWord, string endWord, vector<string>& wordList) {
unordered_set<string> wordSet(wordList.begin(), wordList.end()); // Convert wordList to a set for efficient lookup
if (wordSet.find(endWord) == wordSet.end()) {
return 0; // endWord is not in the wordList, no transformation sequence possible
}
unordered_set<string> beginSet, endSet; // Sets for the current level of traversal from the beginWord and endWord
beginSet.insert(beginWord);
endSet.insert(endWord);
int distance = 1; // Distance between beginWord and endWord
while (!beginSet.empty() && !endSet.empty()) {
// Always expand the smaller set to reduce the search space
if (beginSet.size() > endSet.size()) {
swap(beginSet, endSet);
}
unordered_set<string> temp; // Temporary set to store the next level of words
for (const string& word : beginSet) {
string currentWord = word;
// Check each character position of the word and replace it with all possible alphabets
for (int i = 0; i < currentWord.length(); ++i) {
char originalChar = currentWord[i];
for (char c = 'a'; c <= 'z'; ++c) {
currentWord[i] = c;
if (endSet.find(currentWord) != endSet.end()) {
return distance + 1; // Transformation sequence found, return the distance
}
if (wordSet.find(currentWord) != wordSet.end()) {
temp.insert(currentWord); // Add the transformed word to the next level set
wordSet.erase(currentWord); // Remove the transformed word from the wordSet
}
}
currentWord[i] = originalChar; // Revert back the character
}
}
swap(beginSet, temp); // Update the current level set with the next level set
++distance; // Increment the distance for each level of BFS traversal
}
return 0; // No transformation sequence found
}
};
*/
Python π
from collections import deque
class Solution:
def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int:
wordSet = set(wordList)
if endWord not in wordSet:
return 0
queue = deque([(beginWord, 1)]) # Start from the beginWord with level 1
visited = set()
while queue:
word, level = queue.popleft()
if word == endWord:
return level
for i in range(len(word)):
for c in "abcdefghijklmnopqrstuvwxyz":
new_word = word[:i] + c + word[i + 1 :]
if new_word in wordSet and new_word not in visited:
visited.add(new_word)
queue.append((new_word, level + 1))
return 0
Advanced Graphs π
This section contains problems belonging to the Advanced Graphs category.
Problems
- π΄ Hard: Reconstruct Itinerary
- π‘ Medium: Min Cost to Connect All Points
- π‘ Medium: Network Delay Time
- π΄ Hard: Swim in Rising Water
- π΄ Hard: Alien Dictionary
- π‘ Medium: Cheapest Flights Within K Stops
Reconstruct Itinerary π§
LeetCode Link: Reconstruct Itinerary
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 332 - Reconstruct Itinerary
Description: Given a list of airline tickets represented by pairs of departure and arrival airports [from, to], reconstruct the itinerary in order. All of the tickets belong to a man who departs from "JFK", thus the itinerary must begin with "JFK". If there are multiple valid itineraries, you should return the itinerary that has the smallest lexical order when read as a single string.
Intuition: To reconstruct the itinerary, we need to find a valid sequence of flights that starts from "JFK" and covers all the tickets. We can approach this problem as a graph traversal, where each airport is a node, and each ticket is an edge from one airport to another.
Approach:
- Create a graph representation using a hashmap, where the key is the departure airport, and the value is a list of arrival airports. The list should be sorted in lexical order to find the smallest lexical order itinerary.
- Perform a Depth-First Search (DFS) traversal on the graph, starting from the "JFK" airport.
- During the DFS, for each airport visited, remove the used ticket (edge) from the graph to avoid using it again.
- Continue the DFS until all the tickets are used up, and we have a valid itinerary.
β Time Complexity: The time complexity of the DFS approach is O(E * log E), where E is the total number of tickets. Sorting the list of arrival airports for each departure airport takes O(log E) time.
πΎ Space Complexity: The space complexity is O(E), where E is the total number of tickets. We use a hashmap to represent the graph, which can contain E entries.
Solutions π‘
Cpp π»
class Solution {
public:
vector<string> findItinerary(vector<vector<string>> &tickets) {
// Create a graph representation using hashmap
unordered_map<string, multiset<string>> graph;
for (const auto &ticket : tickets) {
graph[ticket[0]].insert(ticket[1]);
}
vector<string> itinerary;
dfs("JFK", graph, itinerary);
// Reverse the itinerary to get the correct order
reverse(itinerary.begin(), itinerary.end());
return itinerary;
}
void dfs(string airport, unordered_map<string, multiset<string>> &graph, vector<string> &itinerary) {
while (!graph[airport].empty()) {
string nextAirport = *graph[airport].begin();
graph[airport].erase(graph[airport].begin());
dfs(nextAirport, graph, itinerary);
}
itinerary.push_back(airport);
}
};
/*
// Beats 100% Runtime
class Solution {
public:
// Using a map to represent the graph where key is the departure airport
// and value is a priority queue to store the arrival airports in sorted order
unordered_map<string, priority_queue<string, vector<string>, greater<string>>> airportGraph;
vector<string> itinerary;
void dfs(string airport) {
auto& destinations = airportGraph[airport];
while (!destinations.empty()) {
string nextAirport = destinations.top();
destinations.pop();
dfs(nextAirport);
}
itinerary.push_back(airport);
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
// Build the graph
for (const auto& ticket : tickets) {
airportGraph[ticket[0]].push(ticket[1]);
}
// Start DFS from "JFK" airport
dfs("JFK");
// Reverse the itinerary to get the correct order
reverse(itinerary.begin(), itinerary.end());
return itinerary;
}
};
*/
Python π
class Solution:
def findItinerary(self, tickets: List[List[str]]) -> List[str]:
graph = collections.defaultdict(list)
for start, end in sorted(tickets, reverse=True):
graph[start].append(end)
route = []
def dfs(node):
while graph[node]:
dfs(graph[node].pop())
route.append(node)
dfs("JFK")
return route[::-1]
Min Cost to Connect All Points π§
LeetCode Link: Min Cost to Connect All Points
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 1584 - Min Cost to Connect All Points
Description: You are given an array points representing integer coordinates of some points on a 2D-plane, where points[i] = [xi, yi]. The cost of connecting two points [xi, yi] and [xj, yj] is the manhattan distance between them: |xi - xj| + |yi - yj|, where |val| denotes the absolute value of val. Return the minimum cost to make all points connected. All points are connected if there is exactly one simple path between any two points.
Intuition: To connect all the points, we need to find the minimum spanning tree (MST) of the graph. The MST is a subgraph that connects all vertices together with the minimum possible total edge weight.
Approach:
- Create a graph representation using an adjacency matrix, where graph[i][j] represents the manhattan distance between point i and point j.
- Use Prim's algorithm to find the MST of the graph.
- Start the MST with an arbitrary point (let's say the first point), and keep adding the nearest non-visited point until all points are visited.
- Calculate the total cost of the MST, which will be the minimum cost to connect all points.
β Time Complexity: The time complexity of Prim's algorithm is O(V^2), where V is the number of vertices (points).
πΎ Space Complexity: The space complexity is O(V^2), where V is the number of vertices (points). We use an adjacency matrix to represent the graph.
Solutions π‘
Cpp π»
class Solution {
public:
int minCostConnectPoints(vector<vector<int>> &points) {
int n = points.size();
vector<bool> visited(n, false);
vector<int> minCost(n, INT_MAX);
// A lambda function to calculate the Manhattan distance between two points
auto getManhattanDistance = [](const vector<int> &p1, const vector<int> &p2) {
return abs(p1[0] - p2[0]) + abs(p1[1] - p2[1]);
};
int result = 0;
minCost[0] = 0; // Start with the first point
for (int i = 0; i < n; ++i) {
int u = -1;
// Find the nearest non-visited point
for (int j = 0; j < n; ++j) {
if (!visited[j] && (u == -1 || minCost[j] < minCost[u])) {
u = j;
}
}
visited[u] = true;
result += minCost[u];
// Update the minimum cost for the remaining points
for (int j = 0; j < n; ++j) {
if (!visited[j]) {
minCost[j] = min(minCost[j], getManhattanDistance(points[u], points[j]));
}
}
}
return result;
}
};
/*
Lambda Function in C++:
A lambda function, also known as an anonymous function or a lambda expression, is a compact and inline way
to define small functions in C++. It allows you to create function objects (functors) on the fly without
explicitly defining a named function. Lambda functions are particularly useful when you need a simple function
that you don't want to define separately.
The syntax for a lambda function is as follows:
[ captures ] ( parameters ) -> return_type {
// function body
}
- `captures`: This is an optional part that allows the lambda function to capture and use variables from the
surrounding scope. It can be used to access local variables, class members, or global variables within the lambda.
- `parameters`: These are the input parameters of the lambda function, similar to regular function parameters.
- `return_type`: This specifies the return type of the lambda function. If the return type is not specified, it
will be deduced automatically by the compiler.
- `function body`: This is the code that defines the behavior of the lambda function. It is similar to the body
of a regular function.
Example of a simple lambda function that adds two integers:
auto add = [](int a, int b) -> int {
return a + b;
};
int result = add(3, 5); // result will be 8
Lambda functions provide a concise and efficient way to define short, local functions, improving the readability
of your code and reducing the need for creating separate named functions.
*/
Python π
class Solution:
def minCostConnectPoints(self, points: List[List[int]]) -> int:
def distance(p1, p2):
return abs(p1[0] - p2[0]) + abs(p1[1] - p2[1])
n = len(points)
distances = []
for i in range(n):
for j in range(i + 1, n):
distances.append((distance(points[i], points[j]), i, j))
distances.sort()
parent = list(range(n))
rank = [0] * n
mst_cost = 0
def find(node):
if parent[node] != node:
parent[node] = find(parent[node])
return parent[node]
def union(node1, node2):
root1 = find(node1)
root2 = find(node2)
if root1 != root2:
if rank[root1] > rank[root2]:
parent[root2] = root1
else:
parent[root1] = root2
if rank[root1] == rank[root2]:
rank[root2] += 1
for distance, u, v in distances:
if find(u) != find(v):
union(u, v)
mst_cost += distance
return mst_cost
Network Delay Time π§
LeetCode Link: Network Delay Time
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 743 - Network Delay Time
Description: You are given a network of n nodes, labeled from 1 to n. You are also given times, a list of travel times as directed edges times[i] = (ui, vi, wi), where ui is the source node, vi is the target node, and wi is the time it takes for a signal to travel from source to target. We will send a signal from a given node k. Return the time it takes for all the n nodes to receive the signal. If it is impossible for all the n nodes to receive the signal, return -1.
Intuition: This problem can be solved using Dijkstra's algorithm, which finds the shortest paths from a source node to all other nodes in a weighted graph.
Approach:
- Create a graph representation using an adjacency list, where graph[u] contains a list of pairs (v, w), representing an edge from node u to node v with weight w.
- Use Dijkstra's algorithm to find the shortest path from the source node k to all other nodes in the graph.
- Initialize a min heap (priority queue) to keep track of the nodes to visit, and set the distance of the source node k to 0 and distances of all other nodes to infinity.
- Push the source node k into the min heap.
- While the min heap is not empty, pop the node with the minimum distance from the heap.
- For each neighbor of the current node, update the distance if a shorter path is found and push the neighbor into the min heap if it hasn't been visited yet.
- After processing all nodes, the distance array will contain the shortest time to reach each node from the source node k. Return the maximum value in the distance array, which represents the time it takes for all nodes to receive the signal.
- If there are nodes that cannot be reached from the source node k, the distance array will have some nodes with infinite distance. In this case, return -1.
β Time Complexity: The time complexity of Dijkstra's algorithm is O(E*logV), where E is the number of edges and V is the number of vertices.
πΎ Space Complexity: The space complexity is O(V+E), where V is the number of vertices and E is the number of edges, due to the adjacency list representation of the graph.
Solutions π‘
Cpp π»
class Solution {
public:
int networkDelayTime(vector<vector<int>> ×, int n, int k) {
// Create a graph representation using an adjacency list
unordered_map<int, vector<pair<int, int>>> graph;
for (const auto &time : times) {
graph[time[0]].push_back({time[1], time[2]});
}
// Initialize a min heap (priority queue) to track nodes to visit
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
vector<int> distance(n + 1, INT_MAX);
// Set distance of source node k to 0 and push it into the min heap
distance[k] = 0;
pq.push({0, k});
while (!pq.empty()) {
auto [dist, node] = pq.top();
pq.pop();
// Check if the node has been visited already
if (dist > distance[node]) {
continue;
}
// Update the distances for neighbors
for (const auto &neighbor : graph[node]) {
int nextNode = neighbor.first;
int newDist = dist + neighbor.second;
if (newDist < distance[nextNode]) {
distance[nextNode] = newDist;
pq.push({newDist, nextNode});
}
}
}
// Find the maximum distance, which represents the time for all nodes to receive the signal
int maxDist = 0;
for (int i = 1; i <= n; ++i) {
maxDist = max(maxDist, distance[i]);
}
// If some nodes cannot be reached, return -1
return maxDist == INT_MAX ? -1 : maxDist;
}
};
Python π
import heapq
from collections import defaultdict
class Solution:
def networkDelayTime(self, times: List[List[int]], n: int, k: int) -> int:
# Create an adjacency list representation of the graph
graph = defaultdict(list)
for u, v, w in times:
graph[u].append((v, w))
# Initialize distances to all nodes as infinity except for the source node
distances = [float("inf")] * (n + 1)
distances[k] = 0
# Priority queue to select the next node to visit based on the minimum distance
pq = [(0, k)]
while pq:
distance, node = heapq.heappop(pq)
if distance > distances[node]:
continue
for neighbor, weight in graph[node]:
if distance + weight < distances[neighbor]:
distances[neighbor] = distance + weight
heapq.heappush(pq, (distances[neighbor], neighbor))
# Find the maximum distance among all nodes
max_distance = max(distances[1:])
return max_distance if max_distance < float("inf") else -1
Swim in Rising Water π§
LeetCode Link: Swim in Rising Water
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 778 - Swim in Rising Water
Description: On an N x N grid, each cell is either empty (0) or blocked (1). A move consists of walking from one empty cell to another empty cell adjacent to it in one of the 4 cardinal directions (up, down, left, right). Time starts at 0, and each time you visit an empty cell, you walk to an adjacent empty cell and increase time by 1. The grid is said to be unreachable if we cannot walk from the top-left corner of the grid (0, 0) to the bottom-right corner of the grid (N-1, N-1) without walking through any blocked cells. Return the minimum time required to reach the bottom-right corner of the grid, or -1 if the grid is unreachable.
Intuition: This problem can be solved using a binary search approach. We can search for the minimum time required to reach the bottom-right corner, and then verify if it is possible to reach the destination using that time.
Approach:
- Implement a Depth-First Search (DFS) function that explores the grid to check if it is possible to reach the destination within a given time t. The DFS function takes the current position (x, y), the time t, the grid, and a visited set to track visited cells.
- In the DFS function, check if the current position is out of bounds or blocked, and return false if so.
- Check if the current position is the destination (bottom-right corner) and return true if so.
- Mark the current position as visited and recursively call the DFS function for all adjacent empty cells (up, down, left, right) with time t as the parameter.
- If any of the recursive calls return true, it means we can reach the destination within time t, so return true.
- If none of the recursive calls return true, return false, indicating that it is not possible to reach the destination within time t.
- Now, use a binary search to find the minimum time required to reach the destination. The search range is from 0 to the maximum height in the grid.
- While the low is less than or equal to the high, calculate the mid as (low + high) / 2 and call the DFS function with time mid to check if it is possible to reach the destination within mid time.
- If the DFS function returns true, it means it is possible to reach the destination within mid time, so set high to mid - 1 to search for smaller time.
- Otherwise, set low to mid + 1 to search for larger time.
- After the binary search, return low as the minimum time required to reach the destination if reachable, or -1 if unreachable.
β Time Complexity: The time complexity of the binary search is O(log N * N), where N is the side length of the grid. The DFS function visits all empty cells in the grid, so the overall time complexity is O(N^2).
πΎ Space Complexity: The space complexity is O(N^2) due to the visited set used in the DFS function.
Solutions π‘
Cpp π»
class Solution {
public:
bool dfs(int x, int y, int t, vector<vector<int>> &grid) {
int n = grid.size();
// Check if current position is out of bounds or blocked
if (x < 0 || x >= n || y < 0 || y >= n || grid[x][y] > t) {
return false;
}
// Check if current position is destination
if (x == n - 1 && y == n - 1) {
return true;
}
int prev = grid[x][y];
grid[x][y] = -1; // Mark the cell as visited
// Recursively call DFS for all adjacent empty cells
bool canReach = dfs(x + 1, y, t, grid) ||
dfs(x - 1, y, t, grid) ||
dfs(x, y + 1, t, grid) ||
dfs(x, y - 1, t, grid);
grid[x][y] = prev; // Reset the cell to its original value
return canReach;
}
int swimInWater(vector<vector<int>> &grid) {
int n = grid.size();
int low = 0, high = n * n - 1;
while (low <= high) {
int mid = low + (high - low) / 2;
// Check if it is possible to reach destination within time mid
if (dfs(0, 0, mid, grid)) {
high = mid - 1;
} else {
low = mid + 1;
}
}
return low;
}
};
/*
// Beats 100% Runtime
class Solution {
public:
int swimInWater(vector<vector<int>>& grid) {
int n = grid.size();
int left = max(grid[0][0], grid[n - 1][n - 1]); // Smallest possible value to start the binary search.
int right = n * n - 1; // Largest possible value to end the binary search.
while (left <= right) {
int mid = (left + right) / 2;
vector<vector<bool>> visited(n, vector<bool>(n, false));
if (dfs(grid, visited, 0, 0, mid)) {
// If it's possible to reach the destination with the threshold "mid",
// try to find a better (smaller) threshold in the left half of the binary search space.
right = mid - 1;
} else {
// If it's not possible to reach the destination with the threshold "mid",
// increase the threshold by searching in the right half of the binary search space.
left = mid + 1;
}
}
// At the end of the binary search, "right" will be the smallest threshold that
// makes it possible to reach the destination (0-indexed), so return "right + 1".
return right + 1;
}
private:
const vector<int> DIR{0, 1, 0, -1, 0};
// Depth-First Search function to check if it's possible to reach the destination (bottom-right cell)
// from the current cell (i, j) with a maximum threshold of k.
bool dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int i, int j, int k) {
int n = grid.size();
if (i == n - 1 && j == n - 1) {
// We reached the destination, return true.
return true;
}
if (visited[i][j]) {
// We already visited this cell, return false.
return false;
}
visited[i][j] = true;
// Explore all possible directions from the current cell.
for (int l = 0; l < DIR.size() - 1; ++l) {
int x = i + DIR[l];
int y = j + DIR[l + 1];
if (x >= 0 && x < n && y >= 0 && y < n && !visited[x][y] && grid[x][y] <= k) {
if (dfs(grid, visited, x, y, k)) {
return true;
}
}
}
return false;
}
};
*/
Python π
class Solution:
def swimInWater(self, grid: List[List[int]]) -> int:
def dfs(i, j, visited, time):
if i < 0 or i >= N or j < 0 or j >= N or visited[i][j] or grid[i][j] > time:
return False
if i == N - 1 and j == N - 1:
return True
visited[i][j] = True
return (
dfs(i + 1, j, visited, time)
or dfs(i - 1, j, visited, time)
or dfs(i, j + 1, visited, time)
or dfs(i, j - 1, visited, time)
)
N = len(grid)
left, right = 0, N * N
while left < right:
mid = (left + right) // 2
visited = [[False] * N for _ in range(N)]
if dfs(0, 0, visited, mid):
right = mid
else:
left = mid + 1
return left
Alien Dictionary π§
LeetCode Link: Alien Dictionary
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 269 - Alien Dictionary
Description: Given a list of words in the alien language, find the order of the characters in this language.
The alien language is represented by a given dictionary, which will have words in an arbitrary order. Each word consists of lowercase letters ('a' to 'z') and will not have duplicate characters.
It is guaranteed that no two words in the dictionary have the same ordering of letters.
If the order is invalid, return an empty string. There may be multiple valid orderings, you should return the smallest one in lexicographical order.
Intuition: This problem can be solved using topological sorting. The given dictionary represents the relationship between characters in the alien language. The order of characters in the alien language can be determined by their relative positions in the words of the dictionary.
Approach:
- Create a graph to represent the relationship between characters in the alien language.
- Traverse the dictionary and add edges to the graph based on the order of characters in adjacent words.
- Perform topological sorting on the graph to get the order of characters in the alien language.
- Return the result as a string.
β Time Complexity: The time complexity of the solution is O(N), where N is the total number of characters in all the words in the dictionary. The reason is that we iterate through each character once to build the graph and perform topological sorting.
πΎ Space Complexity: The space complexity of the solution is O(1) since we are using a fixed-size array of size 26 to represent the graph.
Topological Sorting:
- Intuition: Topological sorting is used to find the linear order of vertices in a directed acyclic graph (DAG) such that for every directed edge (u, v), vertex u comes before v in the ordering.
- Approach: We use a depth-first search (DFS) based approach to perform topological sorting. We start from a source vertex (a vertex with no incoming edges) and visit its neighbors recursively. After visiting all the neighbors, we add the current vertex to the result stack. The result stack will contain the vertices in the correct order.
Solutions π‘
Cpp π»
class Solution {
public:
string alienOrder(vector<string> &words) {
// Initialize the graph and indegree array
vector<vector<bool>> graph(26, vector<bool>(26, false));
vector<int> indegree(26, -1);
// Step 1: Build the graph and calculate indegrees
buildGraph(words, graph, indegree);
// Step 2: Perform topological sorting using DFS
string result = topologicalSort(graph, indegree);
return result;
}
private:
// Helper function to build the graph and calculate indegrees
void buildGraph(vector<string> &words, vector<vector<bool>> &graph, vector<int> &indegree) {
for (string &word : words) {
for (char c : word) {
indegree[c - 'a'] = 0;
}
}
for (int i = 1; i < words.size(); i++) {
string prevWord = words[i - 1];
string currWord = words[i];
int len = min(prevWord.length(), currWord.length());
for (int j = 0; j < len; j++) {
char prevChar = prevWord[j];
char currChar = currWord[j];
if (prevChar != currChar) {
if (!graph[prevChar - 'a'][currChar - 'a']) {
graph[prevChar - 'a'][currChar - 'a'] = true;
indegree[currChar - 'a']++;
}
break;
}
}
}
}
// Helper function for topological sorting using DFS
string topologicalSort(vector<vector<bool>> &graph, vector<int> &indegree) {
string result = "";
stack<char> st;
for (int i = 0; i < 26; i++) {
if (indegree[i] == 0) {
st.push('a' + i);
}
}
while (!st.empty()) {
char curr = st.top();
st.pop();
result += curr;
for (int i = 0; i < 26; i++) {
if (graph[curr - 'a'][i]) {
indegree[i]--;
if (indegree[i] == 0) {
st.push('a' + i);
}
}
}
}
// If there are still edges in the graph, it means there is a cycle
for (int i = 0; i < 26; i++) {
if (indegree[i] > 0) {
return "";
}
}
return result;
}
};
/*
// Another DFS solution
class Solution {
public:
string alienOrder(vector<string>& words) {
unordered_map<char, vector<char>> graph;
unordered_map<char, int> indegree;
// Step 1: Build the graph and calculate indegrees
for (string& word : words) {
for (char c : word) {
indegree[c] = 0;
}
}
for (int i = 1; i < words.size(); i++) {
string prevWord = words[i - 1];
string currWord = words[i];
int len = min(prevWord.length(), currWord.length());
for (int j = 0; j < len; j++) {
char prevChar = prevWord[j];
char currChar = currWord[j];
if (prevChar != currChar) {
graph[prevChar].push_back(currChar);
indegree[currChar]++;
break;
}
}
}
// Step 2: Perform topological sorting using DFS
string result;
stack<char> st;
for (const auto& entry : indegree) {
if (entry.second == 0) {
st.push(entry.first);
}
}
while (!st.empty()) {
char curr = st.top();
st.pop();
result += curr;
for (char next : graph[curr]) {
if (--indegree[next] == 0) {
st.push(next);
}
}
}
return result.size() == indegree.size() ? result : "";
}
};
*/
/*
// BFS Solution
class Solution {
public:
string alienOrder(vector<string>& words) {
vector<vector<bool>> adjList(26, vector<bool>(26, false));
vector<int> indegree(26, -1);
// Step 1: Build the adjacency list and calculate indegrees
buildGraph(words, adjList, indegree);
// Step 2: Perform topological sorting using BFS
string result = topologicalSort(adjList, indegree);
return result;
}
private:
// Helper function to build the adjacency list and calculate indegrees
void buildGraph(vector<string>& words, vector<vector<bool>>& adjList, vector<int>& indegree) {
// Initialize the indegree for all characters
for (string& word : words) {
for (char c : word) {
indegree[c - 'a'] = 0;
}
}
// Compare adjacent words to build the adjacency list and update indegrees
for (int i = 1; i < words.size(); i++) {
string prevWord = words[i - 1];
string currWord = words[i];
int len = min(prevWord.length(), currWord.length());
for (int j = 0; j < len; j++) {
char prevChar = prevWord[j];
char currChar = currWord[j];
if (prevChar != currChar) {
if (!adjList[prevChar - 'a'][currChar - 'a']) {
adjList[prevChar - 'a'][currChar - 'a'] = true;
indegree[currChar - 'a']++;
}
break;
}
}
}
}
// Helper function for topological sorting using BFS
string topologicalSort(vector<vector<bool>>& adjList, vector<int>& indegree) {
string result = "";
queue<char> q;
// Add characters with indegree 0 to the queue
for (int i = 0; i < 26; i++) {
if (indegree[i] == 0) {
q.push('a' + i);
}
}
// Perform BFS to get the topological ordering
while (!q.empty()) {
char curr = q.front();
q.pop();
result += curr;
for (int i = 0; i < 26; i++) {
if (adjList[curr - 'a'][i]) {
indegree[i]--;
if (indegree[i] == 0) {
q.push('a' + i);
}
}
}
}
// If there are still edges in the graph, it means there is a cycle
if (result.length() != 26) {
return "";
}
return result;
}
};
*/
Python π
from collections import defaultdict, deque
class Solution:
def alienOrder(self, words: List[str]) -> str:
graph = defaultdict(list)
in_degree = defaultdict(int)
for i in range(len(words) - 1):
word1, word2 = words[i], words[i + 1]
for j in range(min(len(word1), len(word2))):
if word1[j] != word2[j]:
graph[word1[j]].append(word2[j])
in_degree[word2[j]] += 1
break
queue = deque(char for char, indeg in in_degree.items() if indeg == 0)
result = []
while queue:
char = queue.popleft()
result.append(char)
for neighbor in graph[char]:
in_degree[neighbor] -= 1
if in_degree[neighbor] == 0:
queue.append(neighbor)
if len(result) < len(in_degree):
return ""
return "".join(result)
Cheapest Flights Within K Stops π§
LeetCode Link: Cheapest Flights Within K Stops
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 787 - Cheapest Flights Within K Stops
Description: There are n cities connected by m flights. Each flight starts from city u and arrives at city v with a price w. Now given all the cities and flights, together with starting city src and the destination dst, your task is to find the cheapest price from src to dst with at most k stops. If there is no such route, return -1.
Intuition: This problem can be solved using Dijkstra's algorithm with some modifications. Instead of stopping at the destination city, we need to find the cheapest price to reach the destination city within k stops. Therefore, we will modify Dijkstra's algorithm to allow k stops.
Approach:
- Create an adjacency list to represent the graph where the key is the source city, and the value is a list of pairs containing the destination city and the price of the flight.
- Use priority queue to implement Dijkstra's algorithm.
- Push the starting city "src" into the priority queue with cost 0 and stops 0.
- While the priority queue is not empty, do the following:
- Pop the top element from the priority queue.
- If the popped element is the destination city "dst", return the cost as the answer.
- If the number of stops is less than or equal to "k", then iterate through the neighbors of the current city, and push them into the priority queue with updated cost and stops.
- If we reach this point, it means there is no valid route, so return -1.
β Time Complexity: The time complexity of the Dijkstra's algorithm is O((E + V) * log(V)), where E is the number of flights and V is the number of cities. In the worst case, we can have E = V^2, so the time complexity is O((V^2) * log(V)).
πΎ Space Complexity: The space complexity is O(E + V), where E is the number of flights and V is the number of cities. We use an adjacency list to represent the graph.
Solutions π‘
Cpp π»
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>> &flights, int src, int dst, int K) {
vector<vector<pair<int, int>>> adjList(n);
for (const auto &flight : flights) {
adjList[flight[0]].emplace_back(flight[1], flight[2]);
}
// Initialize the cheapestCost vector with (n x K+1) dimensions and set all values to infinity
vector<vector<int>> cheapestCost(n, vector<int>(K + 2, INT_MAX));
priority_queue<vector<int>, vector<vector<int>>, greater<vector<int>>> pq;
pq.push({0, src, 0});
cheapestCost[src][0] = 0; // Cost to reach source with 0 stops is 0
while (!pq.empty()) {
vector<int> cur = pq.top();
pq.pop();
int cost = cur[0];
int city = cur[1];
int stops = cur[2];
if (city == dst) {
return cost;
}
if (stops <= K) {
for (const auto &neighbor : adjList[city]) {
int neighborCity = neighbor.first;
int neighborCost = neighbor.second;
// Prune if the cost exceeds the current minimum cost
if (cost + neighborCost >= cheapestCost[neighborCity][stops + 1]) {
continue;
}
pq.push({cost + neighborCost, neighborCity, stops + 1});
cheapestCost[neighborCity][stops + 1] = cost + neighborCost;
}
}
}
return -1;
}
};
/*
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int k) {
// Create an adjacency list to represent the graph
vector<pair<int, int>> adj[n];
for (int i = 0; i < flights.size(); i++) {
adj[flights[i][0]].push_back({flights[i][1], flights[i][2]});
}
// Create a cost array to store the minimum cost to reach each node
vector<int> cost(n, 1e9);
queue<vector<int>> q; // Each entry contains: {stops, node, cost}
cost[src] = 0; // Initialize the cost to reach the source node as 0
q.push({0, src, 0}); // Push the source node with 0 stops and cost 0 into the queue
while (!q.empty()) {
auto v = q.front();
q.pop();
// If the number of stops exceeds k, skip this node
if (v[0] > k) {
continue;
}
for (auto it : adj[v[1]]) {
int nbr = it.first; // Neighbor node
int cst = it.second; // Cost to reach the neighbor node
// If the new cost is less than the current recorded cost to reach the neighbor
if (v[2] + cst < cost[nbr]) {
cost[nbr] = v[2] + cst; // Update the minimum cost to reach the neighbor
q.push({v[0] + 1, nbr, cost[nbr]}); // Push the neighbor with the updated cost and one more stop into the queue
}
}
}
// If the cost to reach the destination is not infinity, return the cost
if (cost[dst] != 1e9) {
return cost[dst];
}
// Otherwise, return -1 (destination is not reachable)
return -1;
}
};
*/
Python π
import heapq
import math
from typing import List
class Solution:
def findCheapestPrice(
self, n: int, flights: List[List[int]], src: int, dst: int, max_stops: int
) -> int:
graph = [[] for _ in range(n)]
min_heap = [
(0, src, max_stops + 1)
] # (total_cost, current_city, remaining_stops)
distances = [[math.inf] * (max_stops + 2) for _ in range(n)]
for u, v, w in flights:
graph[u].append((v, w))
while min_heap:
total_cost, current_city, remaining_stops = heapq.heappop(min_heap)
if current_city == dst:
return total_cost
if remaining_stops > 0:
for neighbor, cost in graph[current_city]:
new_cost = total_cost + cost
if new_cost < distances[neighbor][remaining_stops - 1]:
distances[neighbor][remaining_stops - 1] = new_cost
heapq.heappush(
min_heap, (new_cost, neighbor, remaining_stops - 1)
)
return -1
1-D Dynamic Programming π
This section contains problems belonging to the 1-D Dynamic Programming category.
Problems
- π’ Easy: Climbing Stairs
- π’ Easy: Min Cost Climbing Stairs
- π‘ Medium: House Robber
- π‘ Medium: House Robber II
- π‘ Medium: Longest Palindromic Substring
- π‘ Medium: Palindromic Substrings
- π‘ Medium: Decode Ways
- π‘ Medium: Coin Change
- π‘ Medium: Maximum Product Subarray
- π‘ Medium: Word Break
- π‘ Medium: Longest Increasing Subsequence
- π‘ Medium: Partition Equal Subset Sum
Climbing Stairs π§
LeetCode Link: Climbing Stairs
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 70 - Climbing Stairs
Description: You are climbing a staircase that has n steps. You can either climb 1 or 2 steps at a time. Return the number of distinct ways to climb to the top.
Intuition: To reach the nth step, we can either take a single step from the (n-1)th step or take two steps from the (n-2)th step. This forms the basis of our dynamic programming approach, as we can break down the problem into subproblems and build the solution from there.
Approach:
- Initialize an array dp of size (n+1) to store the number of distinct ways to reach each step.
- Set the base cases: dp[0] = 1 (no steps needed) and dp[1] = 1 (one step to reach the first step).
- Iterate from 2 to n:
- Compute dp[i] by summing up the number of ways to reach the previous two steps: dp[i] = dp[i-1] + dp[i-2].
- Return dp[n], which represents the number of distinct ways to reach the top step.
β Time Complexity: The time complexity is O(n) since we iterate through the steps once.
πΎ Space Complexity: The space complexity is O(n) since we use an array of size (n+1) to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is finding the number of distinct ways to reach the current step.
- Recurrence Relation: dp[i] = dp[i-1] + dp[i-2], where dp[i] represents the number of distinct ways to reach the ith step.
- Base Case: dp[0] = 1 and dp[1] = 1, as there is only one way to reach the first two steps.
Solutions π‘
Cpp π»
class Solution {
public:
int climbStairs(int n) {
if (n <= 2) {
return n; // Base cases
}
vector<int> dp(n + 1);
dp[0] = 1;
dp[1] = 1;
for (int i = 2; i <= n; ++i) {
dp[i] = dp[i - 1] + dp[i - 2]; // Recurrence relation
}
return dp[n];
}
};
// O(1) Space
// class Solution {
// public:
// int climbStairs(int n) {
// if(n <= 2)
// return n;
// int first = 1, second = 2;
// for(int i = 2; i < n; i++) {
// int temp = second;
// second = first + second;
// first = temp;
// }
// return second;
// }
// };
Python π
class Solution:
def climbStairs(self, n: int) -> int:
if n <= 2:
return n
prev1 = 1 # Number of ways to reach the 1st stair
prev2 = 2 # Number of ways to reach the 2nd stair
for i in range(3, n + 1):
current = prev1 + prev2
prev1, prev2 = prev2, current # Update for the next iteration
return prev2 # Number of ways to reach the n-th stair
Min Cost Climbing Stairs π§
LeetCode Link: Min Cost Climbing Stairs
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 746 - Min Cost Climbing Stairs
Description: You are given an integer array cost where cost[i] is the cost of ith step on a staircase. Once you pay the cost, you can either climb one or two steps. You can either start from the 0th step or the 1st step. Return the minimum cost to reach the top of the floor.
Intuition: To reach the top of the floor with minimum cost, we can consider dynamic programming. At each step, we have two options: either take one step from the current step or take two steps from the previous step. We want to minimize the total cost, so we choose the minimum cost between these two options.
Approach:
- Create an array dp of size (n+1), where n is the size of the cost array.
- dp[i] represents the minimum cost to reach the ith step.
- Set the base cases: dp[0] = cost[0] and dp[1] = cost[1].
- Iterate from 2 to n:
- Compute dp[i] by taking the minimum cost between the (i-1)th step and the (i-2)th step, plus the cost of the current step: dp[i] = min(dp[i-1], dp[i-2]) + cost[i].
- Return the minimum cost between reaching the (n-1)th step and the nth step: min(dp[n-1], dp[n]).
β Time Complexity: The time complexity is O(n), where n is the size of the cost array. We iterate through the cost array once.
πΎ Space Complexity: The space complexity is O(n). We use an array of size (n+1) to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is finding the minimum cost to reach the current step.
- Recurrence Relation: dp[i] = min(dp[i-1], dp[i-2]) + cost[i], where dp[i] represents the minimum cost to reach the ith step.
- Base Case: dp[0] = cost[0] and dp[1] = cost[1], as the minimum cost to reach the first two steps is the cost of those steps.
Solutions π‘
Cpp π»
class Solution {
public:
int minCostClimbingStairs(vector<int> &cost) {
int n = cost.size();
vector<int> dp(n + 1);
dp[0] = cost[0];
dp[1] = cost[1];
for (int i = 2; i <= n; ++i) {
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i];
}
return min(dp[n - 1], dp[n]);
}
};
// O(1) space
// class Solution {
// public:
// int minCostClimbingStairs(vector<int>& cost) {
// int prev1 = cost[0];
// int prev2 = cost[1];
// for (int i = 2; i < cost.size(); i++) {
// int current = cost[i] + min(prev1, prev2);
// prev1 = prev2;
// prev2 = current;
// }
// return min(prev1, prev2);
// }
// };
Python π
class Solution:
def minCostClimbingStairs(self, cost: List[int]) -> int:
n = len(cost)
if n <= 1:
return 0 # No cost if there are 0 or 1 stairs
dp = [0] * n # Initialize a list to store minimum costs
# Base cases
dp[0] = cost[0]
dp[1] = cost[1]
for i in range(2, n):
dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]
return min(dp[n - 1], dp[n - 2])
House Robber π§
LeetCode Link: House Robber
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 198 - House Robber
Description: You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, and the only constraint stopping you from robbing each of them is that adjacent houses have a security system connected, and it will automatically contact the police if two adjacent houses are broken into on the same night. Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police.
Intuition: To maximize the amount of money robbed, we can consider dynamic programming. At each house, we have two options: either rob the current house or skip it. If we rob the current house, we cannot rob the previous house, so we take the maximum amount between the current house's value and the amount robbed from the previous house. If we skip the current house, we take the maximum amount robbed from the previous house. We want to maximize the total amount of money robbed, so we choose the maximum between these two options.
Approach:
- Create an array dp of size (n+1), where n is the size of the nums array.
- dp[i] represents the maximum amount of money that can be robbed up to the ith house.
- Set the base cases: dp[0] = 0 and dp[1] = nums[0].
- Iterate from 2 to n:
- Compute dp[i] by taking the maximum amount between the current house's value plus the amount robbed from the house two steps back and the amount robbed from the previous house: dp[i] = max(dp[i-2] + nums[i-1], dp[i-1]).
- Return dp[n], which represents the maximum amount of money that can be robbed.
β Time Complexity: The time complexity is O(n), where n is the size of the nums array. We iterate through the nums array once.
πΎ Space Complexity: The space complexity is O(n). We use an array of size (n+1) to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is finding the maximum amount of money that can be robbed up to the current house.
- Recurrence Relation: dp[i] = max(dp[i-2] + nums[i-1], dp[i-1]), where dp[i] represents the maximum amount of money that can be robbed up to the ith house.
- Base Case: dp[0] = 0 and dp[1] = nums[0], as there is no house to rob initially, and robbing the first house is the only option.
Solutions π‘
Cpp π»
class Solution {
public:
int rob(vector<int> &nums) {
int n = nums.size();
vector<int> dp(n + 1);
dp[0] = 0;
dp[1] = nums[0];
for (int i = 2; i <= n; ++i) {
dp[i] = max(dp[i - 2] + nums[i - 1], dp[i - 1]);
}
return dp[n];
}
};
// Space: O(1)
// class Solution {
// public:
// int rob(vector<int>& nums) {
// int prev = 0;
// int curr = 0;
// int next = 0;
// for (int i = 0; i < nums.size(); i++) {
// next = max(prev + nums[i], curr);
// prev = curr;
// curr = next;
// }
// return curr;
// }
// };
Python π
class Solution:
def rob(self, nums: List[int]) -> int:
n = len(nums)
if n == 0:
return 0
if n == 1:
return nums[0]
dp = [0] * n
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, n):
dp[i] = max(dp[i - 2] + nums[i], dp[i - 1])
return dp[-1]
House Robber II π§
LeetCode Link: House Robber II
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 213 - House Robber II
Description: You are a professional robber planning to rob houses along a street. Each house has a certain amount of money stashed, and the only constraint stopping you from robbing each of them is that adjacent houses have a security system connected, and it will automatically contact the police if two adjacent houses are broken into on the same night. Given an integer array nums representing the amount of money of each house, return the maximum amount of money you can rob tonight without alerting the police. Note: This problem is a variation of LeetCode 198 - House Robber with a circular street, where the first and last houses are adjacent.
Intuition: To maximize the amount of money robbed, we can consider dynamic programming. The problem becomes more complex due to the circular nature of the street. Since the first and last houses are adjacent, we have two possibilities: either rob the first house and skip the last house or skip the first house and rob the last house. We can solve this problem by splitting it into two separate subproblems:
- Rob houses from the first to the second-to-last house.
- Rob houses from the second to the last house. The maximum amount of money robbed will be the maximum between the two subproblems.
Approach:
- If the size of the nums array is 1, return nums[0] as it is the only house.
- Compute the maximum amount of money robbed by considering the two subproblems: a. Rob houses from the first to the second-to-last house using the same approach as in the House Robber problem (LeetCode 198). b. Rob houses from the second to the last house using the same approach as in the House Robber problem (LeetCode 198).
- Return the maximum amount between the two subproblems.
β Time Complexity: The time complexity is O(n), where n is the size of the nums array. We iterate through the nums array twice: once for each subproblem.
πΎ Space Complexity: The space complexity is O(1) since we use constant extra space to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is finding the maximum amount of money that can be robbed from a range of houses.
- Recurrence Relation: dp[i] = max(dp[i-2] + nums[i], dp[i-1]), where dp[i] represents the maximum amount of money that can be robbed up to the ith house.
- Base Case: dp[0] = nums[0] and dp[1] = max(nums[0], nums[1]), as the maximum amount to rob the first two houses depends on their values.
Solutions π‘
Cpp π»
class Solution {
public:
int rob(vector<int> &nums) {
int n = nums.size();
if (n == 1) {
return nums[0];
}
int max1 = robRange(nums, 0, n - 2);
int max2 = robRange(nums, 1, n - 1);
return max(max1, max2);
}
int robRange(vector<int> &nums, int start, int end) {
int prev1 = 0;
int prev2 = 0;
for (int i = start; i <= end; ++i) {
int current = max(prev1 + nums[i], prev2);
prev1 = prev2;
prev2 = current;
}
return prev2;
}
};
Python π
class Solution:
def rob(self, nums: List[int]) -> int:
def houseRobber(nums: List[int]) -> int:
n = len(nums)
if n == 0:
return 0
if n == 1:
return nums[0]
prev = 0
curr = 0
for num in nums:
temp = curr
curr = max(prev + num, curr)
prev = temp
return curr
if len(nums) == 1:
return nums[0]
# Rob first house and exclude the last house, or exclude the first house and rob the last house.
return max(houseRobber(nums[1:]), houseRobber(nums[:-1]))
Longest Palindromic Substring π§
LeetCode Link: Longest Palindromic Substring
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 5 - Longest Palindromic Substring
Description: Given a string s, return the longest palindromic substring in s.
Intuition: To find the longest palindromic substring, we can consider dynamic programming. A palindrome reads the same backward as forward, so we can use this property to solve the problem. We define a 2D array dp, where dp[i][j] represents whether the substring from index i to j is a palindrome. Using this definition, we can build the solution by considering smaller subproblems and expanding from there.
Approach:
- Create a 2D boolean array dp of size (n x n), where n is the length of the input string s.
- dp[i][j] will be true if the substring from index i to j is a palindrome, and false otherwise.
- Initialize the base cases:
- Set dp[i][i] to true, as single characters are palindromes.
- Set dp[i][i+1] to true if s[i] is equal to s[i+1], as two identical characters are palindromes.
- Iterate over the substring lengths from 3 to n:
- Iterate over the starting index i from 0 to n - len:
- Calculate the ending index j as i + len - 1.
- Update dp[i][j] to true if s[i] is equal to s[j] and dp[i+1][j-1] is true.
- Keep track of the longest palindromic substring by updating the start and end indices whenever a longer palindrome is found.
- Return the substring from the start index to the end index.
β Time Complexity: The time complexity is O(n^2), where n is the length of the input string s. We iterate over the dp array, which has a size of n x n.
πΎ Space Complexity: The space complexity is O(n^2) as well. We use a 2D array dp of size n x n to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is determining whether a substring from index i to j is a palindrome.
- Recurrence Relation: dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1], where dp[i][j] represents whether the substring from index i to j is a palindrome.
- Base Case: dp[i][i] = true (single character is a palindrome) and dp[i][i+1] = (s[i] == s[i+1]) (two identical characters are a palindrome).
Solutions π‘
Cpp π»
class Solution {
public:
string longestPalindrome(string s) {
int n = s.length();
vector<vector<bool>> dp(n, vector<bool>(n, false));
int start = 0;
int maxLen = 1;
// Base case: single characters are palindromes
for (int i = 0; i < n; i++) {
dp[i][i] = true;
}
// Base case: two identical characters are palindromes
for (int i = 0; i < n - 1; i++) {
if (s[i] == s[i + 1]) {
dp[i][i + 1] = true;
start = i;
maxLen = 2;
}
}
// Check for palindromes of length greater than 2
for (int len = 3; len <= n; len++) {
for (int i = 0; i < n - len + 1; i++) {
int j = i + len - 1;
if (s[i] == s[j] && dp[i + 1][j - 1]) {
dp[i][j] = true;
start = i;
maxLen = len;
}
}
}
return s.substr(start, maxLen);
}
};
// Expanding around the center approach
// This approach does not utilize dynamic programming
// Consider each element as middle of a palindrome and keep expanding
// class Solution {
// public:
// string longestPalindrome(string s) {
// // Two possibilities, palindrome can be even or odd length
// for(int i = 0; i < s.size() - 1; i++) {
// // To check odd palindromes
// helper(s, i, i);
// // To check even palindromes
// helper(s, i, i+1);
// }
// return s.substr(start, maxLength);
// }
// private:
// // need the index and length to cut the string using substr
// int start = 0, maxLength = 1;
// void helper(string S, int L, int R) {
// while(L >= 0 && R < S.size() && S[L] == S[R])
// L--, R++;
// int len = R - L - 1;
// if(len > maxLength) {
// start = L + 1;
// maxLength = len;
// }
// }
// };
Python π
class Solution:
def longestPalindrome(self, s: str) -> str:
def expandAroundCenter(left: int, right: int) -> str:
# Expand around the center while the characters at both ends are equal.
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
# Return the palindrome substring.
return s[left + 1 : right]
if len(s) < 2:
return s
longest = ""
for i in range(len(s)):
# Check for odd-length palindromes.
palindrome1 = expandAroundCenter(i, i)
if len(palindrome1) > len(longest):
longest = palindrome1
# Check for even-length palindromes.
palindrome2 = expandAroundCenter(i, i + 1)
if len(palindrome2) > len(longest):
longest = palindrome2
return longest
Palindromic Substrings π§
LeetCode Link: Palindromic Substrings
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 647 - Palindromic Substrings
Description: Given a string s, return the number of palindromic substrings in s. A substring is a contiguous sequence of characters within the string.
Intuition: To count the number of palindromic substrings, we can use a two-pointer approach. We iterate through each character in the string and treat it as a potential center of a palindrome. By expanding from the center, we check if the substring formed is a palindrome and count it as a valid palindrome.
Approach:
- Initialize a variable count to keep track of the number of palindromic substrings.
- Iterate through each character in the string:
- Consider each character as the center of a potential palindrome.
- Expand around the center using two pointers, one on each side.
- Count all valid palindromes found during expansion.
- Return the count of palindromic substrings.
β Time Complexity: The time complexity is O(n^2), where n is the length of the input string s. We iterate through the string and perform expansion for each character.
πΎ Space Complexity: The space complexity is O(1) since we only use a few variables to store indices and counts.
Solutions π‘
Cpp π»
// Better solution but not dynamic programming
class Solution {
public:
int countSubstrings(string s) {
int count = 0;
int n = s.length();
for (int i = 0; i < n; i++) {
count += countPalindromes(s, i, i); // Odd-length palindromes
count += countPalindromes(s, i, i + 1); // Even-length palindromes
}
return count;
}
int countPalindromes(string s, int left, int right) {
int count = 0;
while (left >= 0 && right < s.length() && s[left] == s[right]) {
count++;
left--;
right++;
}
return count;
}
};
/*
Dynamic Programming Approach
Intuition:
To count the number of palindromic substrings, we can consider dynamic programming.
A palindrome reads the same backward as forward, so we can use this property to solve the problem.
We define a 2D array dp, where dp[i][j] represents whether the substring from index i to j is a palindrome.
Using this definition, we can build the solution by considering smaller subproblems and expanding from there.
Approach:
1. Create a 2D boolean array dp of size (n x n), where n is the length of the input string s.
- dp[i][j] will be true if the substring from index i to j is a palindrome, and false otherwise.
2. Initialize the base cases:
- Set dp[i][i] to true, as single characters are palindromes.
- Set dp[i][i+1] to true if s[i] is equal to s[i+1], as two identical characters are palindromes.
3. Iterate over the substring lengths from 3 to n:
- Iterate over the starting index i from 0 to n - len:
- Calculate the ending index j as i + len - 1.
- Update dp[i][j] to true if s[i] is equal to s[j] and dp[i+1][j-1] is true.
4. Count the number of true values in the dp array, which represents the number of palindromic substrings.
Time Complexity:
The time complexity is O(n^2), where n is the length of the input string s. We iterate over the dp array, which has a size of n x n.
Space Complexity:
The space complexity is O(n^2) as well. We use a 2D array dp of size n x n to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is determining whether a substring from index i to j is a palindrome.
- Recurrence Relation: dp[i][j] = (s[i] == s[j]) && dp[i+1][j-1], where dp[i][j] represents whether the substring from index i to j is a palindrome.
- Base Case: dp[i][i] = true (single character is a palindrome) and dp[i][i+1] = (s[i] == s[i+1]) (two identical characters are a palindrome).
*/
// class Solution {
// public:
// int countSubstrings(string s) {
// int n = s.length();
// vector<vector<bool>> dp(n, vector<bool>(n, false));
// int count = 0;
// // Base case: single characters are palindromes
// for (int i = 0; i < n; i++) {
// dp[i][i] = true;
// count++;
// }
// // Base case: two identical characters are palindromes
// for (int i = 0; i < n - 1; i++) {
// if (s[i] == s[i + 1]) {
// dp[i][i + 1] = true;
// count++;
// }
// }
// // Check for palindromes of length greater than 2
// for (int len = 3; len <= n; len++) {
// for (int i = 0; i < n - len + 1; i++) {
// int j = i + len - 1;
// if (s[i] == s[j] && dp[i + 1][j - 1]) {
// dp[i][j] = true;
// count++;
// }
// }
// }
// return count;
// }
// };
Python π
class Solution:
def countSubstrings(self, s: str) -> int:
def expandAroundCenter(left: int, right: int) -> int:
count = 0
# Expand around the center while the characters at both ends are equal.
while left >= 0 and right < len(s) and s[left] == s[right]:
left -= 1
right += 1
count += 1
return count
if not s:
return 0
total_palindromes = 0
for i in range(len(s)):
# Check for odd-length palindromes.
total_palindromes += expandAroundCenter(i, i)
# Check for even-length palindromes.
total_palindromes += expandAroundCenter(i, i + 1)
return total_palindromes
Decode Ways π§
LeetCode Link: Decode Ways
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 91 - Decode Ways
Description: A message containing letters from A-Z can be encoded into numbers using the following mapping: 'A' -> "1", 'B' -> "2", ..., 'Z' -> "26". Given a string s containing only digits, return the number of ways to decode it. Note: The grouping (1 11 06) is invalid because "06" cannot be mapped into 'F' since "6" is not mapped to any letter.
Intuition: To count the number of ways to decode a given string, we can use dynamic programming. The problem can be broken down into smaller subproblems where we consider different lengths of substrings. We can build the solution by considering the number of ways to decode smaller substrings and combine them to get the final answer.
Approach:
- Initialize an array dp of size n+1, where n is the length of the input string s.
- dp[i] will represent the number of ways to decode the substring s[0:i].
- Initialize dp[0] to 1 as there is one way to decode an empty string.
- Iterate through the characters of the string:
- If the current character is not '0', update dp[i] by adding dp[i-1] to account for single-digit decoding.
- If the current character, along with the previous character, forms a valid two-digit number, update dp[i] by adding dp[i-2].
- Return dp[n], which represents the number of ways to decode the entire string.
β Time Complexity: The time complexity is O(n), where n is the length of the input string s. We iterate through each character of the string once.
πΎ Space Complexity: The space complexity is O(n) as well. We use an array dp of size n+1 to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is calculating the number of ways to decode a substring of length i.
- Recurrence Relation: dp[i] = dp[i-1] + dp[i-2], if the current character and the previous character form a valid two-digit number.
- Base Case: dp[0] = 1 (empty string has one way to decode).
Solutions π‘
Cpp π»
class Solution {
public:
int numDecodings(string s) {
int n = s.length();
vector<int> dp(n + 1, 0);
dp[0] = 1; // Base case: empty string has one way to decode
for (int i = 1; i <= n; i++) {
// Check if current character is not '0'
if (s[i - 1] != '0') {
dp[i] += dp[i - 1]; // Add the number of ways for single-digit decoding
}
// Check if current character and the previous character form a valid two-digit number
if (i > 1 && isValidTwoDigit(s[i - 2], s[i - 1])) {
dp[i] += dp[i - 2]; // Add the number of ways for two-digit decoding
}
}
return dp[n];
}
bool isValidTwoDigit(char c1, char c2) {
int num = (c1 - '0') * 10 + (c2 - '0');
return num >= 10 && num <= 26;
}
};
// class Solution {
// public:
// int numDecodings(string s) {
// int n = s.size();
// if (n == 0 || s[0] == '0') {
// return 0;
// }
// vector<int> dp(n + 1);
// dp[0] = 1;
// dp[1] = 1;
// for (int i = 2; i <= n; i++) {
// // Single digit
// int ones = stoi(s.substr(i - 1, 1));
// if (0 < ones && ones < 10) {
// dp[i] += dp[i - 1];
// }
// // Double digit
// int tens = stoi(s.substr(i - 2, 2));
// if (tens >= 10 && tens <= 26) {
// dp[i] += dp[i - 2];
// }
// }
// return dp[n];
// }
// };
Python π
class Solution:
def numDecodings(self, s: str) -> int:
n = len(s)
# Initialize a DP array to store the number of ways to decode substrings.
dp = [0] * (n + 1)
# Base cases:
dp[0] = 1 # An empty string can be decoded in one way.
dp[1] = (
1 if s[0] != "0" else 0
) # The first character can be decoded in one way if it's not '0'.
# Fill in the DP array.
for i in range(2, n + 1):
# Check the one-digit and two-digit possibilities.
one_digit = int(s[i - 1])
two_digits = int(s[i - 2 : i])
# If the one-digit is not '0', it can be decoded in the same way as dp[i-1].
if one_digit != 0:
dp[i] += dp[i - 1]
# If the two-digit is between 10 and 26, it can be decoded in the same way as dp[i-2].
if 10 <= two_digits <= 26:
dp[i] += dp[i - 2]
return dp[n]
Coin Change π§
LeetCode Link: Coin Change
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 322 - Coin Change
Description: You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money. Return the fewest number of coins that you need to make up that amount. If that amount of money cannot be made up by any combination of the coins, return -1.
Intuition: To find the fewest number of coins needed to make up a given amount, we can use dynamic programming. The problem can be broken down into smaller subproblems by considering different coin denominations. We can build the solution by considering the minimum number of coins needed for smaller amounts and combining them to find the answer.
Approach:
- Initialize an array dp of size amount+1, where dp[i] represents the fewest number of coins needed to make up the amount i.
- Initialize all elements of dp to infinity, except dp[0] which is set to 0.
- Iterate through the coin denominations:
- For each coin, iterate through the amounts from the coin value to the target amount.
- Update dp[j] by taking the minimum between dp[j] and dp[j-coin] + 1.
- Return dp[amount], which represents the fewest number of coins needed to make up the target amount.
β Time Complexity: The time complexity is O(amount * n), where amount is the target amount and n is the number of coins. We iterate through all possible amounts and all coin denominations.
πΎ Space Complexity: The space complexity is O(amount) as we use an array dp to store the fewest number of coins needed for each amount.
Dynamic Programming:
- Subproblem: The subproblem is calculating the fewest number of coins needed to make up an amount i.
- Recurrence Relation: dp[i] = min(dp[i], dp[i-coin] + 1) for each coin in the coin denominations.
- Base Case: dp[0] = 0 (zero coins are needed to make up an amount of zero).
Solutions π‘
Cpp π»
class Solution {
public:
int coinChange(vector<int> &coins, int amount) {
vector<int> dp(amount + 1, INT_MAX); // DP array to store the minimum number of coins needed for each amount
dp[0] = 0; // Base case: zero coins are needed to make up an amount of zero
for (int coin : coins) { // Iterate through each coin
for (int j = coin; j <= amount; j++) { // Iterate through each amount from coin to the target amount
if (dp[j - coin] != INT_MAX) { // Check if there is a valid previous amount (j - coin) that can be made up with coins
dp[j] = min(dp[j], dp[j - coin] + 1); // Update dp[j] with the minimum number of coins needed
}
}
}
return dp[amount] != INT_MAX ? dp[amount] : -1; // If dp[amount] is still INT_MAX, return -1 as the amount cannot be made with the given coins
}
};
/**
* ! Statement 1 -
* * Since the coins array is sorted in ascending order, once we encounter a coin that is larger than i, we can safely break out of the loop.
*
* ! Statement 2 -
* * We are taking mimimum because there could be multiple ways of getting the current amount but we want the lowest amt of coins
* * Ex: coins = [1, 3, 4, 7], so while iterating through first denomination ie 1
* * DP[3] will be 3, min(dp[3], dp[2] + 1) => min(MAX_INT, 2 + 1) => 3
* * But next iteration we find that 3 denomination exists
* * So, min(DP[i], DP[i-coin] + 1) => min(3, 0 + 1) => DP[i] = 1
*/
// class Solution {
// public:
// int coinChange(vector<int>& coins, int amount) {
// // Create a DP array to store the minimum number of coins needed for each amount
// vector<int> DP(amount + 1, INT_MAX);
// // Set the base case: 0 coins are needed to make amount 0
// DP[0] = 0;
// sort(coins.begin(), coins.end());
// for (int i = 1; i <= amount; i++) {
// for (auto coin : coins) {
// // ! Statement 1
// if (coin > i)
// break;
// // ! Statement 2
// if (DP[i - coin] != INT_MAX)
// DP[i] = min(DP[i], DP[i - coin] + 1);
// }
// }
// // If the final amount is still INT_MAX, it means amount cannot be made with the given coins
// return (DP[amount] == INT_MAX) ? -1 : DP[amount];
// }
// };
// int coinChange(vector<int>& coins, int amount) {
// vector<int> dp(amount + 1, amount + 1);
// // Base case
// dp[0] = 0;
// for (int i = 1; i <= amount; ++i) {
// for (int j = 0; j < coins.size(); ++j) {
// if (coins[j] <= i) {
// dp[i] = min(dp[i], dp[i - coins[j]] + 1);
// }
// }
// }
// return dp[amount] > amount ? -1 : dp[amount];
// }
Python π
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
# Initialize the DP array with a maximum value to represent impossible cases.
dp = [float("inf")] * (amount + 1)
# Base case: It takes 0 coins to make up the amount of 0.
dp[0] = 0
# Iterate through the DP array and update the minimum number of coins needed.
for coin in coins:
for i in range(coin, amount + 1):
dp[i] = min(dp[i], dp[i - coin] + 1)
# If dp[amount] is still float('inf'), it means it's impossible to make up the amount.
# Otherwise, dp[amount] contains the minimum number of coins needed.
return dp[amount] if dp[amount] != float("inf") else -1
Maximum Product Subarray π§
LeetCode Link: Maximum Product Subarray
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 152 - Maximum Product Subarray
Description: Given an integer array nums, find the contiguous subarray within an array (containing at least one number) that has the largest product.
Intuition: To find the maximum product subarray, we can use dynamic programming. The problem can be broken down into smaller subproblems by considering different subarrays. We can build the solution by considering the maximum product of subarrays ending at each position.
Approach:
- Initialize variables maxProduct, minProduct, and result to store the maximum product, minimum product, and final result, respectively.
- Iterate through the array nums:
- For each number, update maxProduct and minProduct:
- maxProduct is the maximum of the current number, maxProduct * number, and minProduct * number.
- minProduct is the minimum of the current number, maxProduct * number, and minProduct * number.
- Update the result with the maximum of result and maxProduct.
- Return the result, which represents the maximum product subarray.
β Time Complexity: The time complexity is O(n), where n is the size of the input array nums. We iterate through each element of the array once.
πΎ Space Complexity: The space complexity is O(1) as we only need a few variables to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is finding the maximum product of subarrays ending at each position.
- Recurrence Relation:
- maxProduct = max(nums[i], maxProduct * nums[i], minProduct * nums[i])
- minProduct = min(nums[i], maxProduct * nums[i], minProduct * nums[i])
- Base Case: Initialize maxProduct and minProduct with the first element of the array.
Solutions π‘
Cpp π»
class Solution {
public:
int maxProduct(vector<int> &nums) {
int n = nums.size();
int maxProduct = nums[0]; // Maximum product of subarrays ending at each position
int minProduct = nums[0]; // Minimum product of subarrays ending at each position
int result = nums[0]; // Final result
for (int i = 1; i < n; i++) {
// Update maxProduct and minProduct
int tempMax = maxProduct;
maxProduct = max({nums[i], maxProduct *nums[i], minProduct *nums[i]});
minProduct = min({nums[i], tempMax *nums[i], minProduct *nums[i]});
// Update the result
result = max(result, maxProduct);
}
return result;
}
};
Python π
class Solution:
def maxProduct(self, nums: List[int]) -> int:
# Initialize variables to keep track of the maximum and minimum product ending at the current position.
max_product = min_product = result = nums[0]
# Iterate through the array starting from the second element.
for i in range(1, len(nums)):
# If the current element is negative, swap max_product and min_product
# because multiplying a negative number can turn the maximum into the minimum.
if nums[i] < 0:
max_product, min_product = min_product, max_product
# Update max_product and min_product based on the current element.
max_product = max(nums[i], max_product * nums[i])
min_product = min(nums[i], min_product * nums[i])
# Update the overall result with the maximum product found so far.
result = max(result, max_product)
return result
Word Break π§
LeetCode Link: Word Break
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 139 - Word Break
Description: Given a string s and a dictionary of strings wordDict, determine if s can be segmented into a space-separated sequence of one or more dictionary words.
Intuition: To determine if a string can be segmented into words from a dictionary, we can use dynamic programming. The problem can be broken down into smaller subproblems by considering different prefixes of the input string. We can build the solution by checking if each prefix of the string can be segmented using words from the dictionary.
Approach:
- Initialize a vector dp of size n+1, where dp[i] represents whether the prefix s[0...i-1] can be segmented using words from the dictionary.
- Initialize dp[0] as true, indicating that an empty string can be segmented.
- Iterate through the string from left to right:
- For each index i, iterate from 0 to i:
- Check if dp[j] is true and the substring s[j...i-1] is in the dictionary.
- If both conditions are satisfied, set dp[i] as true.
- Return dp[n], which represents whether the entire string can be segmented.
β Time Complexity: The time complexity is O(n^2), where n is the length of the input string. We have nested loops to iterate through the string and its prefixes.
πΎ Space Complexity: The space complexity is O(n) as we use an extra array dp of size n+1 to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is determining whether a prefix s[0...i-1] can be segmented using words from the dictionary.
- Recurrence Relation: dp[i] = dp[j] && (s[j...i-1] is in the dictionary) for 0 <= j < i.
- Base Case: dp[0] = true, indicating that an empty string can be segmented.
Solutions π‘
Cpp π»
class Solution {
public:
bool wordBreak(string s, vector<string> &wordDict) {
int n = s.length();
vector<bool> dp(n + 1, false); // Dynamic programming array to store if substring from 0 to i-1 can be segmented
dp[0] = true; // Base case: empty string can be segmented
for (int i = 1; i <= n; i++) { // Iterate through each position in the string
for (int j = 0; j < i; j++) { // Iterate over all possible substrings ending at index i
// Check if substring from j to i-1 is in the word dictionary and if substring from 0 to j-1 can be segmented
if (dp[j] && find(wordDict.begin(), wordDict.end(), s.substr(j, i - j)) != wordDict.end()) {
dp[i] = true; // Set dp[i] to true if substring from 0 to i-1 can be segmented
break; // No need to check further substrings ending at index i
}
}
}
return dp[n]; // Return whether the entire string can be segmented
}
};
// class Solution {
// public:
// bool wordBreak(string s, vector<string>& wordDict) {
// int n = s.length();
// vector<bool> dp(n, false);
// dp[0] = true;
// for (int i = 0; i <= n; i++) {
// for (auto word: wordDict) {
// if (dp[i] && s.substr(i, word.size()).compare(word) == 0) {
// dp[i + word.size()] = true;
// }
// }
// }
// return dp[n];
// }
// };
Python π
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# Create a set for faster word lookup.
wordSet = set(wordDict)
# Initialize a boolean array dp where dp[i] is True if s[0:i] can be broken into words.
dp = [False] * (len(s) + 1)
dp[0] = True # An empty string can always be broken.
# Iterate through the string.
for i in range(1, len(s) + 1):
for j in range(i):
# Check if the substring s[j:i] is in the wordDict and if s[0:j] can be broken.
if dp[j] and s[j:i] in wordSet:
dp[i] = True
break # No need to continue checking.
return dp[len(s)]
Longest Increasing Subsequence π§
LeetCode Link: Longest Increasing Subsequence
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 300 - Longest Increasing Subsequence
Description: Given an integer array nums, return the length of the longest strictly increasing subsequence.
Intuition: To find the length of the longest increasing subsequence, we can use dynamic programming. The problem can be broken down into smaller subproblems by considering different subarrays. We can build the solution by considering the length of the longest increasing subsequence ending at each position.
Approach:
- Initialize an array dp of size n, where dp[i] represents the length of the longest increasing subsequence ending at index i.
- Initialize dp[i] as 1 for all indices, as the minimum length of an increasing subsequence is 1 (the element itself).
- Iterate through the array nums:
- For each index i, iterate from 0 to i-1:
- If nums[i] is greater than nums[j], update dp[i] as the maximum of dp[i] and dp[j] + 1. This means that we consider extending the increasing subsequence ending at index j with the current element at index i.
- Return the maximum value in the dp array, which represents the length of the longest increasing subsequence.
β Time Complexity: The time complexity is O(n^2), where n is the size of the input array nums. We have nested loops to iterate through the array.
πΎ Space Complexity: The space complexity is O(n) as we use an extra array dp of size n to store the lengths of the increasing subsequences.
Dynamic Programming:
- Subproblem: The subproblem is finding the length of the longest increasing subsequence ending at each position.
- Recurrence Relation: dp[i] = max(dp[i], dp[j] + 1) for 0 <= j < i, if nums[i] > nums[j].
- Base Case: Initialize dp[i] as 1 for all indices, as the minimum length of an increasing subsequence is 1.
Solutions π‘
Cpp π»
class Solution {
public:
int lengthOfLIS(vector<int> &nums) {
int n = nums.size();
vector<int> dp(n, 1); // Length of longest increasing subsequence ending at each index
for (int i = 1; i < n; i++) {
for (int j = 0; j < i; j++) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
return *max_element(dp.begin(), dp.end());
}
};
/**
* ! Using Binary Search
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
int n = nums.size(); // Length of the input vector
vector<int> dp; // Stores the current increasing subsequence
// Add the first element of nums to the subsequence vector
dp.push_back(nums[0]);
// Iterate through the remaining elements of nums
for(int i = 1; i < n; i++) {
// If the current element is greater than the last element in dp,
// it can extend the current increasing subsequence, so add it to dp
if(nums[i] > dp.back()) {
dp.push_back(nums[i]);
} else {
// If the current element is not greater, find its correct position
// in dp using binary search (lower_bound)
auto it = lower_bound(dp.begin(), dp.end(), nums[i]);
// Update the value at the found position to nums[i]
*it = nums[i];
}
}
// Return the length of the longest increasing subsequence
return dp.size();
}
};
*/
Python π
class Solution:
def lengthOfLIS(self, nums: List[int]) -> int:
if not nums:
return 0
# Initialize a dynamic programming array dp with all values set to 1.
dp = [1] * len(nums)
# Iterate through the array to find the longest increasing subsequence.
for i in range(len(nums)):
for j in range(i):
if nums[i] > nums[j]:
dp[i] = max(dp[i], dp[j] + 1)
# Return the maximum value in dp, which represents the length of the longest increasing subsequence.
return max(dp)
Partition Equal Subset Sum π§
LeetCode Link: Partition Equal Subset Sum
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 416 - Partition Equal Subset Sum
Description: Given a non-empty array nums containing only positive integers, determine if it can be partitioned into two subsets such that the sum of elements in both subsets is equal.
Intuition: To determine if the array can be partitioned into two equal subsets, we can use dynamic programming. The problem can be broken down into smaller subproblems by considering different subsets of the input array. We can build the solution by checking if there exists a subset with a target sum equal to half of the total sum of the array.
Approach:
- Calculate the total sum of the array.
- If the total sum is odd, return false. It's not possible to partition the array into equal subsets.
- Initialize a vector dp of size (sum/2) + 1, where dp[i] represents whether a subset with sum i can be formed.
- Initialize dp[0] as true, indicating that an empty subset with sum 0 can be formed.
- Iterate through the array nums:
- For each number num, iterate from (sum/2) down to num:
- If dp[i - num] is true, set dp[i] as true, indicating that a subset with sum i can be formed.
- Return dp[sum/2], which represents whether a subset with sum (sum/2) can be formed.
β Time Complexity: The time complexity is O(n * sum), where n is the size of the input array and sum is the total sum of the array.
πΎ Space Complexity: The space complexity is O(sum), as we use an extra array dp of size (sum/2) + 1 to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is determining whether a subset with a target sum can be formed using a subset of the array.
- Recurrence Relation: dp[i] = dp[i - num] for each num in the array, if dp[i - num] is true.
- Base Case: dp[0] = true, indicating that an empty subset with sum 0 can be formed.
Solutions π‘
Cpp π»
class Solution {
public:
bool canPartition(vector<int> &nums) {
int n = nums.size();
int sum = accumulate(nums.begin(), nums.end(), 0); // Calculate the total sum
if (sum % 2 != 0) {
return false; // If the total sum is odd, it's not possible to partition into equal subsets
}
int target = sum / 2;
vector<bool> dp(target + 1, false); // Subset with sum i can be formed
dp[0] = true; // Base case: Empty subset with sum 0 can be formed
for (int num : nums) {
for (int i = target; i >= num; i--) {
if (dp[i - num]) {
dp[i] = true;
}
}
}
return dp[target];
}
};
Python π
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total_sum = sum(nums)
# If the total sum is odd, it's impossible to partition into two equal subsets.
if total_sum % 2 != 0:
return False
target_sum = total_sum // 2
# Initialize a dynamic programming array dp with all values set to False.
dp = [False] * (target_sum + 1)
# It's always possible to achieve a sum of 0 using an empty subset.
dp[0] = True
for num in nums:
for i in range(target_sum, num - 1, -1):
# If it's possible to achieve a sum of 'i - num' using a subset,
# then it's also possible to achieve a sum of 'i' using a subset.
dp[i] = dp[i] or dp[i - num]
return dp[target_sum]
2-D Dynamic Programming π
This section contains problems belonging to the 2-D Dynamic Programming category.
Problems
- π‘ Medium: Unique Paths
- π‘ Medium: Longest Common Subsequence
- π‘ Medium: Best Time to Buy And Sell Stock With Cooldown
- π‘ Medium: Coin Change II
- π‘ Medium: Target Sum
- π‘ Medium: Interleaving String
- π΄ Hard: Longest Increasing Path In a Matrix
- π΄ Hard: Distinct Subsequences
- π΄ Hard: Edit Distance
- π΄ Hard: Burst Balloons
- π΄ Hard: Regular Expression Matching
Unique Paths π§
LeetCode Link: Unique Paths
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 62 - Unique Paths
Description: A robot is located at the top-left corner of a m x n grid. The robot can only move either down or right at any point in time. The robot is trying to reach the bottom-right corner of the grid. How many possible unique paths are there?
Intuition: To find the number of unique paths, we can use dynamic programming. The problem can be broken down into smaller subproblems by considering different positions in the grid. We can build the solution by counting the number of unique paths to reach each position.
Approach:
- Initialize a 2D array dp of size m x n, where dp[i][j] represents the number of unique paths to reach position (i, j).
- Initialize the first row and first column of dp to 1, as there is only one way to reach each position in the first row and first column.
- Iterate through the grid starting from position (1, 1):
- For each position (i, j), set dp[i][j] as the sum of dp[i-1][j] and dp[i][j-1]. This means that the number of unique paths to reach (i, j) is the sum of the paths from the above position (i-1, j) and the left position (i, j-1).
- Return dp[m-1][n-1], which represents the number of unique paths to reach the bottom-right corner of the grid.
β Time Complexity: The time complexity is O(m * n), where m and n are the dimensions of the grid.
πΎ Space Complexity: The space complexity is O(m * n), as we use a 2D array to store the number of unique paths for each position.
Dynamic Programming:
- Subproblem: The subproblem is finding the number of unique paths to reach each position in the grid.
- Recurrence Relation: dp[i][j] = dp[i-1][j] + dp[i][j-1].
- Base Case: Initialize the first row and first column of dp to 1, as there is only one way to reach each position in the first row and first column.
Solutions π‘
Cpp π»
class Solution {
public:
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 1)); // Number of unique paths to reach each position
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; // Number of unique paths = paths from above + paths from left
}
}
return dp[m - 1][n - 1]; // Number of unique paths to reach the bottom-right corner
}
};
Python π
class Solution:
def uniquePaths(self, m: int, n: int) -> int:
# Initialize a 2D dp grid of size m x n with all values set to 1.
dp = [[1] * n for _ in range(m)]
# Fill in the dp grid using dynamic programming.
for i in range(1, m):
for j in range(1, n):
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
# The value in dp[m-1][n-1] represents the number of unique paths.
return dp[m - 1][n - 1]
Longest Common Subsequence π§
LeetCode Link: Longest Common Subsequence
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 1143 - Longest Common Subsequence
Description: Given two strings text1 and text2, return the length of their longest common subsequence. A subsequence of a string is a new string generated from the original string with some characters (can be none) deleted without changing the relative order of the remaining characters.
Intuition: To find the longest common subsequence between two strings, we can use dynamic programming. The problem can be broken down into smaller subproblems by considering different suffixes of the strings. We can build the solution by finding the longest common subsequence for each pair of suffixes.
Approach:
- Initialize a 2D array dp of size (m+1) x (n+1), where dp[i][j] represents the length of the longest common subsequence between the first i characters of text1 and the first j characters of text2.
- Initialize the first row and first column of dp to 0.
- Iterate through the characters of text1 and text2:
- If the current characters are equal, dp[i][j] = dp[i-1][j-1] + 1. This means that we include the current character in the longest common subsequence.
- Otherwise, dp[i][j] = max(dp[i-1][j], dp[i][j-1]). This means that we exclude one character either from text1 or text2 to find the longest common subsequence.
- Return dp[m][n], which represents the length of the longest common subsequence.
β Time Complexity: The time complexity is O(m * n), where m and n are the lengths of text1 and text2, respectively.
πΎ Space Complexity: The space complexity is O(m * n), as we use a 2D array to store the length of the longest common subsequence.
Dynamic Programming:
- Subproblem: The subproblem is finding the longest common subsequence for each pair of suffixes of the strings.
- Recurrence Relation: If text1[i] == text2[j], dp[i][j] = dp[i-1][j-1] + 1. Otherwise, dp[i][j] = max(dp[i-1][j], dp[i][j-1]).
- Base Case: Initialize the first row and first column of dp to 0.
Solutions π‘
Cpp π»
class Solution {
public:
int longestCommonSubsequence(string text1, string text2) {
int m = text1.length();
int n = text2.length();
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0)); // Length of longest common subsequence
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (text1[i - 1] == text2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + 1; // Include the current character
} else {
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); // Exclude one character
}
}
}
return dp[m][n]; // Length of longest common subsequence
}
};
Python π
class Solution:
def longestCommonSubsequence(self, text1: str, text2: str) -> int:
m, n = len(text1), len(text2)
# Create a 2D dp array of size (m+1) x (n+1) and initialize it with zeros.
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Fill in the dp array using dynamic programming.
for i in range(1, m + 1):
for j in range(1, n + 1):
if text1[i - 1] == text2[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + 1
else:
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1])
# The value in dp[m][n] represents the length of the LCS.
return dp[m][n]
Best Time to Buy And Sell Stock With Cooldown π§
LeetCode Link: Best Time to Buy And Sell Stock With Cooldown
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 309 - Best Time to Buy and Sell Stock with Cooldown
Description: You are given an array prices where prices[i] is the price of a given stock on the ith day. You want to maximize your profit by choosing a single day to buy one stock and choosing a different day in the future to sell that stock. You cannot buy and sell the stock on the same day (i.e., you must sell the stock before buying again). Additionally, you must not participate in multiple transactions simultaneously (i.e., you must sell the stock before you buy again).
Intuition: To maximize the profit while taking into account the cooldown period, we can use dynamic programming. At each day, we have three possible states: buy, sell, or rest. The key idea is to track the maximum profit for each state and determine the optimal action to take on each day.
Approach:
- Initialize three variables: buy (maximum profit if the stock is bought), sell (maximum profit if the stock is sold), and rest (maximum profit if no action is taken). Set buy = -prices[0], sell = 0, and rest = 0.
- Iterate through the prices array starting from the second day:
- Update the buy variable as the maximum between the previous buy value and the profit from the previous rest state minus the current stock price.
- Update the sell variable as the maximum between the previous sell value and the profit from the previous buy state plus the current stock price.
- Update the rest variable as the maximum between the previous sell value and the previous rest value.
- Return the sell value, which represents the maximum profit at the end.
β Time Complexity: The time complexity is O(n), where n is the length of the prices array.
πΎ Space Complexity: The space complexity is O(1) as we only need three variables to store the maximum profit.
Dynamic Programming:
- Subproblem: The subproblem is determining the maximum profit at each day considering the buy, sell, and rest states.
- Recurrence Relation: buy = max(buy, rest - price), sell = max(sell, buy + price), rest = max(sell, rest).
- Base Case: Initialize buy = -prices[0], sell = 0, and rest = 0.
Solutions π‘
Cpp π»
class Solution {
public:
int maxProfit(vector<int> &prices) {
int n = prices.size();
if (n <= 1) {
return 0; // If there are no prices or only one price, no profit can be made
}
int buy = -prices[0]; // The maximum profit if the stock is bought at the current day
int sell = 0; // The maximum profit if the stock is sold at the current day
int rest = 0; // The maximum profit if no action is taken at the current day
for (int i = 1; i < n; i++) {
int prevBuy = buy; // Store the previous buy value for calculation
int prevSell = sell; // Store the previous sell value for calculation
int prevRest = rest; // Store the previous rest value for calculation
// Calculate the maximum profit if the stock is bought at the current day
// It is the maximum of the previous buy value (indicating that we didn't buy on the current day)
// and the profit of not buying on the previous day minus the current price
buy = max(prevBuy, prevRest - prices[i]);
// Calculate the maximum profit if the stock is sold at the current day
// It is the maximum of the previous sell value (indicating that we didn't sell on the current day)
// and the profit of buying on the previous day plus the current price
sell = max(prevSell, prevBuy + prices[i]);
// Calculate the maximum profit if no action is taken at the current day
// It is the maximum of the previous sell value (indicating that we didn't make any transactions on the current day)
// and the previous rest value (indicating that we carried over the same profit from the previous day)
rest = max(prevSell, prevRest);
}
return sell; // Return the maximum profit at the end of all days
}
};
/*
Each day presents three options or states that can be chosen:
1. Resting state (resting[i]): In this state, we choose not to take any action related to buying or selling stocks. The maximum profit in this state is determined by the maximum profit achieved in the previous day's resting state or selling state. Essentially, we carry over the maximum profit from the previous day's resting or selling state.
2. Buying state (buying[i]): In this state, we choose to buy stocks. The maximum profit in this state is determined by the maximum profit achieved in the previous day's buying state or the maximum profit achieved in the previous day's resting state minus the current day's stock price. We select the higher value between these two options to maximize our profit.
3. Selling state (selling[i]): In this state, we choose to sell stocks. The maximum profit in this state is determined by adding the stock price of the current day to the maximum profit achieved in the previous day's buying state. Selling stocks in this state allows us to realize the profit accumulated from the previous buying state.
*/
/*
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
// Create three auxiliary arrays to store the maximum profit at each state
vector<int> resting(n, 0); // Maximum profit when in a resting state
vector<int> buying(n, 0); // Maximum profit when in a buying state
vector<int> selling(n, 0); // Maximum profit when in a selling state
// Initialize the base cases for the first day
resting[0] = 0; // No profit in resting state
buying[0] = -prices[0]; // Maximum profit in buying state (buying on the first day)
selling[0] = INT_MIN; // No profit in selling state (impossible to sell on the first day)
// In buying state, we can either buy or rest
// In selling state, we either sell or rest but we can ignore rest to calculate what profit will be if we sold everyday
// In resting state, we carry over the maximum profit from the previous day's selling state.
// Iterate through the prices array to update the maximum profit at each state
for (int i = 1; i < n; i++) {
resting[i] = max(resting[i - 1], selling[i - 1]);
buying[i] = max(buying[i - 1], resting[i - 1] - prices[i]);
// Max profit if sold today
selling[i] = buying[i - 1] + prices[i];
}
// The maximum profit at the end will be the maximum between the resting state and the selling state
return max(resting[n - 1], selling[n - 1]);
}
};
*/
Python π
class Solution:
def maxProfit(self, prices: List[int]) -> int:
if not prices:
return 0
# Initialize variables to represent the maximum profit after each action.
buy = -prices[
0
] # Maximum profit after buying on day 0 (negative because we spend money).
sell = 0 # Maximum profit after selling on day 0 (no profit yet).
cooldown = 0 # Maximum profit after cooldown on day 0 (no profit yet).
for i in range(1, len(prices)):
# To maximize profit on day 'i', we can either:
# 1. Buy on day 'i'. We subtract the price of the stock from the maximum profit after cooldown on day 'i-2'.
new_buy = max(buy, cooldown - prices[i])
# 2. Sell on day 'i'. We add the price of the stock to the maximum profit after buying on day 'i-1'.
new_sell = buy + prices[i]
# 3. Do nothing (cooldown) on day 'i'. We take the maximum of the maximum profit after cooldown on day 'i-1' and after selling on day 'i-1'.
new_cooldown = max(cooldown, sell)
# Update the variables for the next iteration.
buy, sell, cooldown = new_buy, new_sell, new_cooldown
# The maximum profit will be the maximum of the profit after selling on the last day and the profit after cooldown on the last day.
return max(sell, cooldown)
Coin Change II π§
LeetCode Link: Coin Change II
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 518 - Coin Change 2
Description: You are given an integer array coins representing coins of different denominations and an integer amount representing a total amount of money. Return the number of combinations that make up that amount. You may assume that you have an infinite number of each kind of coin. The answer is guaranteed to fit into a signed 32-bit integer.
Intuition: To find the number of combinations that make up the amount, we can use dynamic programming. At each step, we can decide whether to include a coin or not. The key idea is to track the number of combinations for each amount up to the target amount.
Approach:
- Initialize a 1D array dp of size (amount + 1) and set dp[0] = 1, representing the base case where there is one way to make up an amount of 0.
- Iterate through the coins array:
- For each coin, iterate through the amounts from coin to the target amount:
- Update the dp[j] by adding the number of combinations for the amount j - coin.
- Return dp[amount], which represents the number of combinations that make up the target amount.
β Time Complexity: The time complexity is O(coins * amount), where coins is the number of coins and amount is the target amount.
πΎ Space Complexity: The space complexity is O(amount) as we only need a 1D array to store the number of combinations.
Dynamic Programming:
- Subproblem: The subproblem is finding the number of combinations for each amount up to the target amount.
- Recurrence Relation: dp[j] = dp[j] + dp[j - coin], where dp[j] represents the number of combinations for the amount j.
- Base Case: Initialize dp[0] = 1, representing one way to make up an amount of 0.
Solutions π‘
Cpp π»
class Solution {
public:
int change(int amount, vector<int> &coins) {
// Create a DP array to store the number of combinations
// dp[i] represents the number of combinations to make amount i
vector<int> dp(amount + 1, 0);
// Base case: There is one combination to make amount 0, using no coins
dp[0] = 1;
// Iterate over the coins
for (int coin : coins) {
// For each coin, iterate over the amounts starting from the coin value
for (int i = coin; i <= amount; ++i) {
// For each amount, add the number of combinations from the current amount minus the coin value
// This accounts for using the current coin to make the remaining amount
// We sum up these combinations for all the coins to get the total number of combinations for the current amount
dp[i] += dp[i - coin];
}
}
// Return the number of combinations to make the given amount using the coins
return dp[amount];
}
};
// class Solution {
// public:
// int change(int amount, vector<int>& coins) {
// // Create a 2D DP array to store the number of combinations
// // dp[i][j] represents the number of combinations to make amount j using coins up to the ith index
// vector<vector<int>> dp(coins.size() + 1, vector<int>(amount + 1, 0));
// // Base case: There is one combination to make amount 0, using no coins
// dp[0][0] = 1;
// // Iterate over the coins
// for (int i = 1; i <= coins.size(); ++i) {
// // For the first column (amount = 0), there is one combination for each coin, i.e., not selecting the coin
// dp[i][0] = 1;
// for (int j = 1; j <= amount; ++j) {
// // If the current coin value is less than or equal to the current amount, we have two options:
// // 1. Use the current coin by subtracting its value from the amount (j) and considering the number of combinations from the same row (coin)
// // 2. Skip the current coin and consider the number of combinations from the previous row (coin - 1) for the same amount (j)
// // We sum up these two options to get the total number of combinations
// if (coins[i - 1] <= j) {
// dp[i][j] = dp[i][j - coins[i - 1]] + dp[i - 1][j];
// } else {
// // If the current coin value is greater than the current amount, we cannot include it in the combination
// // So, we only consider the number of combinations from the previous row for the same amount
// dp[i][j] = dp[i - 1][j];
// }
// }
// }
// // Return the number of combinations to make the given amount using all the coins
// return dp[coins.size()][amount];
// }
// };
Python π
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
# Initialize a 1D array dp to store the number of combinations for each amount from 0 to amount.
dp = [0] * (amount + 1)
# There is one way to make amount 0 (by not using any coins).
dp[0] = 1
# Iterate through each coin denomination.
for coin in coins:
# Update the dp array for each amount from coin to amount.
for i in range(coin, amount + 1):
dp[i] += dp[i - coin]
# The dp[amount] contains the number of combinations to make the target amount.
return dp[amount]
Target Sum π§
LeetCode Link: Target Sum
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 494 - Target Sum
Description: You are given an integer array nums and an integer target. You want to build an expression out of nums by adding or subtracting each element in nums. Return the number of different expressions that you can build, which evaluates to the target.
Intuition: To find the number of different expressions that evaluate to the target, we can use dynamic programming. The problem can be converted to a subset sum problem, where we need to find a subset of elements from the array with a specific sum. To achieve this, we can transform the problem into a 0/1 knapsack problem. At each step, we can decide whether to include or exclude an element from the sum.
Approach:
- Calculate the sum of all elements in the nums array, as it helps to calculate the maximum possible positive sum.
- Initialize a 2D array dp of size (nums.size() + 1) x (2 * sum + 1), and set dp[0][sum] = 1 to represent the base case where no elements are included, and the sum is 0.
- Iterate through the nums array and update the dp array as follows:
- For each element nums[i], update dp[i+1][j] by adding dp[i][j + nums[i]] and dp[i][j - nums[i]], representing including and excluding the element from the sum.
- Return dp[nums.size()][sum + target], which represents the number of different expressions that evaluate to the target.
β Time Complexity: The time complexity is O(nums.size() * sum), where nums.size() is the number of elements in the array and sum is the sum of all elements.
πΎ Space Complexity: The space complexity is O(nums.size() * sum) as we use a 2D array to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is finding the number of different expressions that evaluate to a specific sum using a subset of elements from the array.
- Recurrence Relation: dp[i+1][j] = dp[i][j + nums[i]] + dp[i][j - nums[i]], where dp[i+1][j] represents the number of different expressions that evaluate to sum j using the first i elements from the array.
- Base Case: Initialize dp[0][sum] = 1 to represent the base case where no elements are included, and the sum is 0.
Solutions π‘
Cpp π»
class Solution {
public:
int findTargetSumWays(vector<int> &nums, int target) {
// Calculate the sum of all elements in the nums array
int sum = accumulate(nums.begin(), nums.end(), 0);
// If the target is greater than the sum or less than the negative sum, it's not possible to achieve the target
if (target > sum || target < -sum) {
return 0;
}
int n = nums.size();
// Create a 2D dp array to store the number of ways to achieve each possible sum
// dp[i][j] represents the number of ways to achieve sum j using the first i elements of the nums array
// We use 2 * sum + 1 as the column size to handle negative numbers
vector<vector<int>> dp(n + 1, vector<int>(2 * sum + 1, 0));
// Base case: There's one way to achieve a sum of 0 using an empty subset (no elements)
dp[0][sum] = 1;
// Loop through each element in the nums array
for (int i = 0; i < n; i++) {
// Loop through each possible sum in the dp array
for (int j = nums[i]; j <= 2 * sum - nums[i]; j++) {
// If there's a way to achieve the current sum (dp[i][j]), update the ways to achieve the sums j + nums[i] and j - nums[i]
if (dp[i][j]) {
dp[i + 1][j + nums[i]] += dp[i][j]; // Add nums[i] to the sum
dp[i + 1][j - nums[i]] += dp[i][j]; // Subtract nums[i] from the sum
}
}
}
// Return the number of ways to achieve the target sum
// We use dp[n][sum + target] because dp[n][sum] represents the number of ways to achieve a sum of 0, and we need to adjust it to the target
return dp[n][sum + target];
}
};
Python π
class Solution:
def findTargetSumWays(self, nums: List[int], S: int) -> int:
# Calculate the sum of all elements in the input array 'nums'.
total_sum = sum(nums)
# If the total sum is less than the target sum 'S', it's not possible to reach 'S'.
if (total_sum + S) % 2 != 0 or total_sum < abs(S):
return 0
# Calculate the target sum for positive signs. (total_sum + S) / 2
target = (total_sum + S) // 2
# Initialize a 1D array 'dp' to store the number of ways to reach each sum from 0 to 'target'.
dp = [0] * (target + 1)
# There is one way to reach a sum of 0 (by not selecting any element).
dp[0] = 1
# Iterate through each element in the input array 'nums'.
for num in nums:
# Update the 'dp' array for each sum from 'target' to 'num'.
for i in range(target, num - 1, -1):
dp[i] += dp[i - num]
# The 'dp[target]' contains the number of ways to reach the target sum 'S'.
return dp[target]
Interleaving String π§
LeetCode Link: Interleaving String
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 97 - Interleaving String
Description: Given strings s1, s2, and s3, find whether s3 is formed by the interleaving of s1 and s2.
Intuition: To determine if s3 can be formed by interleaving s1 and s2, we can use dynamic programming. The problem can be broken down into smaller subproblems, where we check if a prefix of s3 can be formed by interleaving prefixes of s1 and s2.
Approach:
- Create a 2D dp array of size (s1.length()+1) x (s2.length()+1), where dp[i][j] represents whether s3[0...i+j-1] can be formed by interleaving s1[0...i-1] and s2[0...j-1].
- Initialize dp[0][0] as true, representing the base case where both s1 and s2 are empty, and s3 is also empty.
- Fill in the dp array using the following recurrence relation:
- dp[i][j] is true if one of the following conditions is met:
- dp[i-1][j] is true, and s1[i-1] is equal to s3[i+j-1].
- dp[i][j-1] is true, and s2[j-1] is equal to s3[i+j-1].
- Return dp[s1.length()][s2.length()], which represents whether s3 can be formed by interleaving s1 and s2.
β Time Complexity: The time complexity is O(s1.length() * s2.length()), as we fill in the entire dp array.
πΎ Space Complexity: The space complexity is O(s1.length() * s2.length()), as we use a 2D array to store the intermediate results.
Dynamic Programming:
- Subproblem: The subproblem is checking if a prefix of s3 can be formed by interleaving prefixes of s1 and s2.
- Recurrence Relation: dp[i][j] is true if dp[i-1][j] is true and s1[i-1] is equal to s3[i+j-1], or if dp[i][j-1] is true and s2[j-1] is equal to s3[i+j-1].
- Base Case: Initialize dp[0][0] as true, representing the base case where both s1 and s2 are empty, and s3 is also empty.
Solutions π‘
Cpp π»
class Solution {
public:
bool isInterleave(string s1, string s2, string s3) {
int m = s1.length(), n = s2.length();
// If the lengths of s1 and s2 do not add up to the length of s3, it's not possible to form s3 by interleaving s1 and s2
if (m + n != s3.length()) {
return false;
}
// Create a 2D dp array to store the results of subproblems
// dp[i][j] represents whether the first i characters of s1 and the first j characters of s2 can form the first i + j characters of s3
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
// Base case: Both s1 and s2 are empty, and the result is true
dp[0][0] = true;
// Loop through each possible combination of i and j
for (int i = 0; i <= m; i++) {
for (int j = 0; j <= n; j++) {
// Check if the previous characters of s1 and s3 match
if (i > 0 && s1[i - 1] == s3[i + j - 1]) {
dp[i][j] = dp[i][j] || dp[i - 1][j];
}
// Check if the previous characters of s2 and s3 match
if (j > 0 && s2[j - 1] == s3[i + j - 1]) {
dp[i][j] = dp[i][j] || dp[i][j - 1];
}
}
}
// Return the result, whether the last characters of s1 and s2 can form the last characters of s3
return dp[m][n];
}
};
Python π
class Solution:
def isInterleave(self, s1: str, s2: str, s3: str) -> bool:
# Get the lengths of s1, s2, and s3.
m, n, p = len(s1), len(s2), len(s3)
# If the sum of lengths of s1 and s2 is not equal to the length of s3, return False.
if m + n != p:
return False
# Initialize a 2D DP array dp of size (m + 1) x (n + 1) to store intermediate results.
dp = [[False] * (n + 1) for _ in range(m + 1)]
# Base case: dp[0][0] is True since two empty strings can form an empty string.
dp[0][0] = True
# Fill in the first row of dp.
for j in range(1, n + 1):
dp[0][j] = dp[0][j - 1] and s2[j - 1] == s3[j - 1]
# Fill in the first column of dp.
for i in range(1, m + 1):
dp[i][0] = dp[i - 1][0] and s1[i - 1] == s3[i - 1]
# Fill in the rest of dp.
for i in range(1, m + 1):
for j in range(1, n + 1):
# For dp[i][j] to be True, one of the following conditions must be met:
# 1. dp[i-1][j] is True and s1[i-1] matches s3[i+j-1].
# 2. dp[i][j-1] is True and s2[j-1] matches s3[i+j-1].
dp[i][j] = (dp[i - 1][j] and s1[i - 1] == s3[i + j - 1]) or (
dp[i][j - 1] and s2[j - 1] == s3[i + j - 1]
)
# The result is stored in dp[m][n].
return dp[m][n]
Longest Increasing Path In a Matrix π§
LeetCode Link: Longest Increasing Path In a Matrix
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 329 - Longest Increasing Path in a Matrix
Description: Given an m x n integers matrix, return the length of the longest increasing path in the matrix. From each cell, you can either move in four directions: left, right, up, or down. You may not move diagonally or move outside the boundary (i.e., wrap-around is not allowed).
Intuition: To find the longest increasing path in the matrix, we can use dynamic programming. The problem can be broken down into smaller subproblems, where we find the longest increasing path starting from each cell in the matrix.
Approach:
- Create a 2D dp array of the same size as the matrix to store the length of the longest increasing path starting from each cell.
- Initialize the dp array to all zeros, as the minimum path length from any cell is 1 (the cell itself).
- For each cell (i, j) in the matrix, perform a depth-first search (DFS) to find the longest increasing path starting from that cell.
- During the DFS, for each neighboring cell (x, y) of the current cell (i, j) that is within bounds and has a greater value than the current cell, calculate the length of the longest increasing path starting from (x, y) using the DFS, and update the dp array accordingly.
- Return the maximum value in the dp array as the result, which represents the length of the longest increasing path in the matrix.
β Time Complexity: The time complexity is O(m * n), where m and n are the dimensions of the matrix, as we perform a DFS starting from each cell once.
πΎ Space Complexity: The space complexity is also O(m * n) as we use a 2D dp array to store the length of the longest increasing path for each cell.
Dynamic Programming:
- Subproblem: The subproblem is finding the longest increasing path starting from each cell in the matrix.
- Recurrence Relation: For each neighboring cell (x, y) of the current cell (i, j) that is within bounds and has a greater value than the current cell, calculate the length of the longest increasing path starting from (x, y) using the DFS, and update the dp array accordingly.
- Base Case: Initialize the dp array to all zeros, as the minimum path length from any cell is 1 (the cell itself).
Solutions π‘
Cpp π»
class Solution {
public:
int longestIncreasingPath(vector<vector<int>> &matrix) {
int m = matrix.size();
int n = matrix[0].size();
vector<vector<int>> dp(m, vector<int>(n, 0)); // dp[i][j] stores the longest increasing path starting at position (i, j)
int longestPath = 0;
// Iterate through each element in the matrix
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// Find the longest increasing path starting at position (i, j) and update the longestPath
longestPath = max(longestPath, dfs(matrix, dp, i, j));
}
}
return longestPath; // Return the overall longest increasing path
}
// Helper function to find the longest increasing path starting at position (i, j)
int dfs(vector<vector<int>> &matrix, vector<vector<int>> &dp, int i, int j) {
if (dp[i][j] > 0) {
return dp[i][j]; // If the result is already calculated, return it from the dp array
}
int m = matrix.size();
int n = matrix[0].size();
int longest = 1; // The minimum length is 1, considering the current element
// Check the four neighbors
vector<pair<int, int>> directions = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
for (const auto &dir : directions) {
int x = i + dir.first;
int y = j + dir.second;
if (x >= 0 && x < m && y >= 0 && y < n && matrix[x][y] > matrix[i][j]) {
// If the neighbor is within the matrix bounds and has a greater value, calculate the longest path recursively
longest = max(longest, 1 + dfs(matrix, dp, x, y));
}
}
dp[i][j] = longest; // Save the result in the dp array to avoid redundant computations
return longest;
}
};
Python π
class Solution:
def longestIncreasingPath(self, matrix: List[List[int]]) -> int:
if not matrix:
return 0
# Define directions for moving to neighboring cells: up, down, left, right.
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
# Function to perform DFS from a given cell (i, j).
def dfs(i, j):
# If the result for this cell is already calculated, return it.
if dp[i][j] != -1:
return dp[i][j]
# Initialize the result for this cell to 1 (counting itself).
dp[i][j] = 1
# Explore the four neighboring cells.
for dx, dy in directions:
x, y = i + dx, j + dy
if 0 <= x < m and 0 <= y < n and matrix[x][y] > matrix[i][j]:
# If the neighboring cell has a larger value, perform DFS.
dp[i][j] = max(dp[i][j], 1 + dfs(x, y))
return dp[i][j]
m, n = len(matrix), len(matrix[0])
dp = [[-1] * n for _ in range(m)] # Memoization table to store results.
max_path = 0
# Start DFS from each cell in the matrix.
for i in range(m):
for j in range(n):
max_path = max(max_path, dfs(i, j))
return max_path
Distinct Subsequences π§
LeetCode Link: Distinct Subsequences
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 115 - Distinct Subsequences
Description: Given two strings s and t, return the number of distinct subsequences of s which equals t. A string's subsequence is a new string formed from the original string by deleting some (can be none) of the characters without disturbing the relative positions of the remaining characters. It is guaranteed the answer fits on a 32-bit signed integer.
Intuition: To find the number of distinct subsequences of s which equals t, we can use dynamic programming. The problem can be broken down into smaller subproblems, where we find the number of distinct subsequences for each prefix of s and t.
Approach:
- Create a 2D dp array of size (m+1) x (n+1), where m is the length of string s and n is the length of string t. The dp[i][j] represents the number of distinct subsequences of the first i characters of string s and the first j characters of string t.
- Initialize the first column dp[i][0] to 1, as there is one way to delete all characters from string s to get an empty string, which matches the empty string t.
- For each (i, j) in the dp array, calculate the value using the recurrence relation:
- If the current characters in s and t match (s[i-1] == t[j-1]), dp[i][j] = dp[i-1][j-1] + dp[i-1][j].
- Otherwise, dp[i][j] = dp[i-1][j].
- Return dp[m][n], which represents the number of distinct subsequences of s which equals t.
β Time Complexity: The time complexity is O(m * n), where m is the length of string s and n is the length of string t, as we fill the dp array of size (m+1) x (n+1).
πΎ Space Complexity: The space complexity is O(m * n), where m is the length of string s and n is the length of string t, as we use a 2D dp array to store the number of distinct subsequences for each prefix of s and t.
Dynamic Programming:
- Subproblem: The subproblem is finding the number of distinct subsequences of the first i characters of string s and the first j characters of string t.
- Recurrence Relation: For each (i, j) in the dp array, calculate the value using the recurrence relation:
- If the current characters in s and t match (s[i-1] == t[j-1]), dp[i][j] = dp[i-1][j-1] + dp[i-1][j].
- Otherwise, dp[i][j] = dp[i-1][j].
- Base Case: Initialize the first column dp[i][0] to 1, as there is one way to delete all characters from string s to get an empty string, which matches the empty string t.
Solutions π‘
Cpp π»
class Solution {
public:
int numDistinct(string s, string t) {
int m = s.length();
int n = t.length();
// Create a 2D DP array to store the number of distinct subsequences
vector<vector<unsigned long long>> dp(m + 1, vector<unsigned long long>(n + 1, 0));
// Base case: there is one way to delete all characters from s to get an empty string (t = "")
for (int i = 0; i <= m; i++) {
dp[i][0] = 1;
}
// Calculate the number of distinct subsequences
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
// If the current characters match, we can either include or exclude s[i-1] in the subsequence
if (s[i - 1] == t[j - 1]) {
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j];
} else {
// If the characters don't match, we can only exclude s[i-1] from the subsequence
dp[i][j] = dp[i - 1][j];
}
}
}
// The value at dp[m][n] represents the number of distinct subsequences of t in s
return dp[m][n];
}
};
Python π
# This solution only passes 65/66 testcases for some reason. Tried other solutions and they don't work either, so it's probably a faulty testcase.
# If you have a solution that passes all testcases, please open a pr.
class Solution:
def numDistinct(self, s: str, t: str) -> int:
m, n = len(s), len(t)
# Create a 2D table dp to store the number of distinct subsequences.
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Initialize the first row of dp. There is one way to form an empty subsequence.
for i in range(m + 1):
dp[i][0] = 1
# Fill the dp table using dynamic programming.
for i in range(1, m + 1):
for j in range(1, n + 1):
# If the characters match, we have two options:
# 1. Include the current character in the subsequence (dp[i-1][j-1] ways).
# 2. Exclude the current character (dp[i-1][j] ways).
if s[i - 1] == t[j - 1]:
dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]
else:
# If the characters don't match, we can only exclude the current character.
dp[i][j] = dp[i - 1][j]
return dp[m][n]
Edit Distance π§
LeetCode Link: Edit Distance
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 72 - Edit Distance
Description: Given two strings word1 and word2, return the minimum number of operations required to convert word1 to word2. You have the following three operations permitted on a word:
- Insert a character
- Delete a character
- Replace a character
Intuition: The problem can be solved using dynamic programming. We can break down the problem into smaller subproblems and find the minimum number of operations required to transform prefixes of the two strings into each other.
Approach:
- Create a 2D DP table of size (m+1) x (n+1), where m and n are the lengths of word1 and word2, respectively.
- dp[i][j] represents the minimum number of operations required to convert the first i characters of word1 to the first j characters of word2.
- Initialize the first row and first column of the DP table as follows:
- dp[i][0] represents the minimum number of operations required to convert the first i characters of word1 to an empty string (i deletions).
- dp[0][j] represents the minimum number of operations required to convert an empty string to the first j characters of word2 (j insertions).
- Fill in the DP table by considering the following cases:
- If word1[i-1] is equal to word2[j-1], dp[i][j] will be the same as dp[i-1][j-1] since no operation is required.
- If word1[i-1] is not equal to word2[j-1], dp[i][j] will be the minimum of the following three cases: a. dp[i-1][j] + 1 (deletion in word1) b. dp[i][j-1] + 1 (insertion in word1) c. dp[i-1][j-1] + 1 (replacement in word1)
- The final answer will be stored in dp[m][n], representing the minimum number of operations required to convert word1 to word2.
β Time Complexity: The time complexity of the DP solution is O(m*n), where m and n are the lengths of word1 and word2, respectively. We need to fill in the entire DP table.
πΎ Space Complexity: The space complexity of the DP solution is O(m*n), where m and n are the lengths of word1 and word2, respectively. We use a 2D DP table to store the minimum number of operations for each subproblem.
Dynamic Programming:
- Subproblem: The subproblem is finding the minimum number of operations required to convert prefixes of word1 to prefixes of word2.
- Recurrence Relation: The minimum number of operations is determined by three cases: insertion, deletion, or replacement.
- Base Case: The base cases are the first row and the first column of the DP table, representing conversions to and from empty strings.
Solutions π‘
Cpp π»
class Solution {
public:
int minDistance(string word1, string word2) {
if (word1.empty() && word2.empty()) {
return 0;
}
if (word1.empty() || word2.empty()) {
return 1;
}
int m = word1.size();
int n = word2.size();
// Create a 2D DP table to store the minimum edit distance for subproblems
vector<vector<int>> dp(m + 1, vector<int>(n + 1, 0));
// Initialize the first row and first column of the DP table
// dp[i][0] represents the minimum edit distance between word1[0:i] and an empty string
// dp[0][j] represents the minimum edit distance between an empty string and word2[0:j]
for (int i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (int j = 0; j <= n; j++) {
dp[0][j] = j;
}
// Fill in the DP table
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (word1[i - 1] == word2[j - 1]) {
// If the characters at the current positions are equal, no operation is needed
// So, the minimum edit distance is the same as the previous subproblem
dp[i][j] = dp[i - 1][j - 1];
} else {
// If the characters at the current positions are different, we have three options:
// 1. Insert: dp[i][j - 1] represents the minimum edit distance if we insert a character from word2
// 2. Delete: dp[i - 1][j] represents the minimum edit distance if we delete a character from word1
// 3. Replace: dp[i - 1][j - 1] represents the minimum edit distance if we replace a character in word1
// Take the minimum of these three options and add 1 to account for the current operation
dp[i][j] = 1 + min({ dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1] });
}
}
}
// The final result is stored in dp[m][n], which represents the minimum edit distance between the two words
return dp[m][n];
}
};
// class Solution {
// public:
// int minDistance(string word1, string word2) {
// int n = word1.size();
// int m = word2.size();
// // Create a 2D DP table to store the minimum edit distance for subproblems
// int dp[n + 1][m + 1];
// // Initialize the DP table
// for (int i = 0; i <= n; i++) {
// for (int j = 0; j <= m; j++) {
// if (i == 0 || j == 0) {
// // Base case: If one of the strings is empty, the minimum edit distance is the length of the non-empty string
// dp[i][j] = max(i, j);
// } else if (word1[i - 1] == word2[j - 1]) {
// // If the characters at the current positions are equal, no operation is needed
// dp[i][j] = dp[i - 1][j - 1];
// } else {
// // If the characters at the current positions are different
// dp[i][j] = 1 + min(dp[i - 1][j - 1], min(dp[i - 1][j], dp[i][j - 1]));
// }
// }
// }
// // The final result is stored in dp[n][m], which represents the minimum edit distance between the two words
// return dp[n][m];
// }
// };
Python π
class Solution:
def minDistance(self, word1: str, word2: str) -> int:
m, n = len(word1), len(word2)
# Create a 2D table dp to store the minimum edit distance.
dp = [[0] * (n + 1) for _ in range(m + 1)]
# Initialize the first row and first column of dp.
for i in range(m + 1):
dp[i][0] = i
for j in range(n + 1):
dp[0][j] = j
for i in range(1, m + 1):
for j in range(1, n + 1):
# If the characters match, no additional operation is needed.
if word1[i - 1] == word2[j - 1]:
dp[i][j] = dp[i - 1][j - 1]
else:
# Choose the minimum of the three possible operations:
# 1. Insert a character (dp[i][j - 1] + 1)
# 2. Delete a character (dp[i - 1][j] + 1)
# 3. Replace a character (dp[i - 1][j - 1] + 1)
dp[i][j] = min(dp[i][j - 1], dp[i - 1][j], dp[i - 1][j - 1]) + 1
return dp[m][n]
Burst Balloons π§
LeetCode Link: Burst Balloons
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 312 - Burst Balloons
Description: You are given n balloons, indexed from 0 to n-1. Each balloon is painted with a number on it represented by an array nums. You are asked to burst all the balloons. If you burst the ith balloon, you will get nums[i - 1] * nums[i] * nums[i + 1] coins. If i - 1 or i + 1 goes out of bounds of the array, then treat it as if there is a balloon with a 1 painted on it. Return the maximum coins you can collect by bursting the balloons wisely.
Intuition: The problem can be solved using dynamic programming. We can divide the problem into smaller subproblems and find the maximum coins we can get by bursting balloons in different ranges.
Approach:
- Create a new array "numsWithBorders" by adding 1 at the beginning and end of the "nums" array. This helps in handling edge cases where i - 1 or i + 1 goes out of bounds.
- Create a 2D DP table of size (n+2) x (n+2), where n is the length of "numsWithBorders".
- dp[i][j] represents the maximum coins we can get by bursting balloons in the range [i, j].
- Initialize the DP table diagonally (for single balloons):
- dp[i][i] represents the maximum coins we can get from bursting the balloon at index i. Since there are no adjacent balloons, dp[i][i] will be numsWithBorders[i].
- Fill in the DP table by considering the following cases:
- For each subarray length "len" (from 2 to n), calculate the maximum coins for each possible range [i, j] of length "len".
- For each range [i, j], try bursting each balloon "k" in the range and calculate the coins obtained. Update dp[i][j] to the maximum coins obtained from all "k" in the range.
- The final answer will be stored in dp[0][n+1], representing the maximum coins we can get from bursting all balloons.
β Time Complexity: The time complexity of the DP solution is O(n^3), where n is the length of the "nums" array. We need to fill in the entire DP table, and for each subarray length "len", we consider all possible ranges of length "len".
πΎ Space Complexity: The space complexity of the DP solution is O(n^2), where n is the length of the "nums" array. We use a 2D DP table to store the maximum coins for each subproblem.
Dynamic Programming:
- Subproblem: The subproblem is finding the maximum coins we can get by bursting balloons in different ranges.
- Recurrence Relation: The maximum coins for each range [i, j] can be calculated by trying to burst each balloon "k" in the range and updating dp[i][j] with the maximum coins obtained from all "k" in the range.
- Base Case: The base cases are the diagonals of the DP table, representing single balloons without adjacent balloons.
Solutions π‘
Cpp π»
// https://leetcode.com/problems/burst-balloons/solutions/892552/for-those-who-are-not-able-to-understand-any-solution-with-diagram/
class Solution {
public:
int maxCoins(vector<int> &nums) {
int n = nums.size();
// Create a new array with borders containing 1 to simplify the logic
vector<int> numsWithBorders(n + 2, 1);
for (int i = 0; i < n; i++) {
numsWithBorders[i + 1] = nums[i];
}
// Create a 2D DP table to store the maximum coins for each subarray
vector<vector<int>> dp(n + 2, vector<int>(n + 2, 0));
// Initialize DP table diagonally for single balloons
for (int i = 1; i <= n; i++) {
dp[i][i] = numsWithBorders[i];
}
// Fill in DP table for subarrays of length 2 and more
for (int len = 2; len <= n + 1; len++) {
for (int i = 0; i <= n + 1 - len; i++) {
int j = i + len;
for (int k = i + 1; k < j; k++) {
// Calculate the maximum coins for the subarray [i, j] by considering
// each possible balloon to be the last one to be bursted in the subarray
dp[i][j] = max(dp[i][j], dp[i][k] + dp[k][j] + numsWithBorders[i] * numsWithBorders[k] * numsWithBorders[j]);
}
}
}
// The maximum coins that can be obtained from bursting all balloons is stored in dp[0][n + 1]
return dp[0][n + 1];
}
};
/*
// Beats 99% in Runtime and Memory
class Solution {
public:
int maxCoins(vector<int>& nums) {
int n = nums.size();
int dp[305][305]; // DP table to store the maximum coins obtained for each range of balloons
for (int len = 1; len <= n; ++len) { // Iterate over all possible lengths of subarrays
for (int start = 0; start + len <= n; ++start) { // Iterate over all possible starting positions of subarrays
int end = start + len - 1; // Calculate the end position of the current subarray
int leftNeighbor = (start == 0) ? 1 : nums[start - 1]; // Get the value of the left neighbor (if it exists)
int rightNeighbor = (end + 1 == n) ? 1 : nums[end + 1]; // Get the value of the right neighbor (if it exists)
if (len == 1) {
// If the subarray contains only one balloon, the maximum coins obtained will be just the value of that balloon
dp[start][end] = nums[start] * leftNeighbor * rightNeighbor;
continue;
}
// Calculate the maximum coins obtained by bursting each balloon in the current subarray
int maxCoinsStart = dp[start + 1][end] + nums[start] * leftNeighbor * rightNeighbor;
int maxCoinsEnd = dp[start][end - 1] + nums[end] * leftNeighbor * rightNeighbor;
int maxCoinsMiddle = 0;
for (int k = start + 1; k < end; ++k) {
// Calculate the maximum coins obtained by first bursting balloons in the left subarray,
// then bursting balloons in the right subarray, and finally bursting the balloon in the middle
int coinsLeft = dp[start][k - 1];
int coinsRight = dp[k + 1][end];
int coinsBurst = nums[k] * leftNeighbor * rightNeighbor;
maxCoinsMiddle = max(maxCoinsMiddle, coinsLeft + coinsRight + coinsBurst);
}
// Store the maximum coins obtained for the current subarray in the DP table
dp[start][end] = max(maxCoinsStart, max(maxCoinsEnd, maxCoinsMiddle));
}
}
return dp[0][n - 1]; // The result is stored in the top-right corner of the DP table for the whole array
}
};
*/
Python π
class Solution:
def maxCoins(self, nums: List[int]) -> int:
n = len(nums)
# Add virtual balloons at the beginning and end with a value of 1.
nums = [1] + nums + [1]
# Create a 2D table dp to store the maximum coins.
dp = [[0] * (n + 2) for _ in range(n + 2)]
# Iterate through different balloon ranges.
for length in range(2, n + 2):
for left in range(0, n + 2 - length):
right = left + length
for k in range(left + 1, right):
# Choose the best order to burst balloons in the range [left, right].
dp[left][right] = max(
dp[left][right],
nums[left] * nums[k] * nums[right] + dp[left][k] + dp[k][right],
)
return dp[0][n + 1]
Regular Expression Matching π§
LeetCode Link: Regular Expression Matching
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 10 - Regular Expression Matching
Description: Given an input string (s) and a pattern (p), implement regular expression matching with support for '.' and '*' where:
- '.' Matches any single character.
- '*' Matches zero or more of the preceding element.
The matching should cover the entire input string (not partial).
Intuition: This problem can be solved using dynamic programming. We can break down the problem into smaller subproblems and use a 2D DP table to store the results of these subproblems.
Approach:
- Create a 2D DP table of size (s.length() + 1) x (p.length() + 1), where s.length() and p.length() are the lengths of the input string "s" and pattern "p", respectively.
- dp[i][j] represents whether the substring s[0...i-1] matches the pattern p[0...j-1].
- Initialize the DP table:
- dp[0][0] represents whether an empty string matches an empty pattern, which is true. So, dp[0][0] = true.
- dp[i][0] represents whether an empty string matches the pattern p[0...i-1]. For all i > 0, dp[i][0] is false because a non-empty pattern cannot match an empty string.
- For dp[0][j], we need to handle patterns with '', which means we should check if p[j-1] is '' and if there's a valid pattern before it. If yes, then dp[0][j] will have the same result as dp[0][j-2].
- Fill in the DP table by considering the following cases:
- If p[j-1] is a regular character or '.', we need to check if s[i-1] matches p[j-1]. If yes, then dp[i][j] will be true if dp[i-1][j-1] is true.
- If p[j-1] is '', we have two sub-cases: a) If the character before '' in the pattern (p[j-2]) matches the current character in the string (s[i-1]) or it is '.', then we have two options:
- Ignore the '*' and the preceding character in the pattern: dp[i][j] will be true if dp[i][j-2] is true.
- Consider the '*' and the preceding character in the pattern: dp[i][j] will be true if dp[i-1][j] is true and s[i-1] matches the preceding character in the pattern (p[j-2]).
- The final answer will be stored in dp[s.length()][p.length()], representing whether the entire string "s" matches the entire pattern "p".
β Time Complexity: The time complexity of the DP solution is O(s.length() * p.length()), where s.length() and p.length() are the lengths of the input string "s" and pattern "p", respectively. We fill in the entire DP table of size (s.length() + 1) x (p.length() + 1).
πΎ Space Complexity: The space complexity of the DP solution is O(s.length() * p.length()), where s.length() and p.length() are the lengths of the input string "s" and pattern "p", respectively. We use a 2D DP table to store the results of subproblems.
Dynamic Programming:
- Subproblem: The subproblem is whether a substring s[0...i-1] matches a pattern p[0...j-1].
- Recurrence Relation: The result for dp[i][j] depends on whether s[i-1] matches p[j-1] and the results of previous subproblems (dp[i-1][j-1], dp[i-1][j], dp[i][j-2]).
- Base Case: The base cases are dp[0][0], dp[i][0] (for all i > 0), and dp[0][j] (for patterns with '*').
Solutions π‘
Cpp π»
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.length();
int n = p.length();
// Create a 2D DP table and initialize base cases
vector<vector<bool>> dp(m + 1, vector<bool>(n + 1, false));
// Base case: an empty pattern matches an empty string
dp[0][0] = true;
// Initialize the first row for pattern p
for (int j = 1; j <= n; j++) {
if (p[j - 1] == '*') {
// If the current character in pattern is '*', it can match zero or more of the preceding element
// So, we check if it matches two characters back in the pattern (j - 2) for an empty string
dp[0][j] = dp[0][j - 2];
}
}
// Fill in the DP table row by row
for (int i = 1; i <= m; i++) {
for (int j = 1; j <= n; j++) {
if (p[j - 1] == s[i - 1] || p[j - 1] == '.') {
// If the current pattern character matches the current string character or it's a '.',
// we take the value from the diagonal (dp[i - 1][j - 1]) as the result for the current cell
dp[i][j] = dp[i - 1][j - 1];
} else if (p[j - 1] == '*') {
// If the current pattern character is '*', it can match zero or more of the preceding element
// So, we check if it matches two characters back in the pattern (j - 2) for an empty string
// or if the current string character matches the preceding pattern character
// (dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'))
dp[i][j] = dp[i][j - 2] || (dp[i - 1][j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'));
}
}
}
return dp[m][n]; // The last cell (dp[m][n]) contains the result for the entire matching process
}
};
/*
// Beats 100% Runtime and O(n) space complexity
class Solution {
public:
bool isMatch(string s, string p) {
int m = s.length();
int n = p.length();
// Create a 1D DP array to store the matching results
vector<bool> dp(n + 1, false);
// Base case: an empty pattern matches an empty string
dp[0] = true;
// Initialize the DP array for the first row (when i = 0)
for (int j = 1; j <= n; j++) {
if (p[j - 1] == '*') {
// If the current character is '*', it can match zero or more of the preceding element
// So, we check if it matches two characters back in the pattern (j - 2)
dp[j] = dp[j - 2];
}
}
// Fill in the DP array row by row
for (int i = 1; i <= m; i++) {
bool prevDiagonal = dp[0]; // Store the value of the diagonal element (dp[i - 1][j - 1]) before updating it
dp[0] = false; // Update the base case for each row, as a non-empty pattern cannot match an empty string
// Iterate through the pattern characters
for (int j = 1; j <= n; j++) {
bool temp = dp[j]; // Store the current value of dp[i][j] before updating it
if (p[j - 1] == s[i - 1] || p[j - 1] == '.') {
// If the current pattern character matches the current string character or it's a '.',
// we take the value from the diagonal (dp[i - 1][j - 1]) as the result for the current cell
dp[j] = prevDiagonal;
} else if (p[j - 1] == '*') {
// If the current pattern character is '*', it can match zero or more of the preceding element
// So, we check if it matches two characters back in the pattern (j - 2) or if the current string
// character matches the preceding pattern character (dp[j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'))
dp[j] = dp[j - 2] || (dp[j] && (s[i - 1] == p[j - 2] || p[j - 2] == '.'));
} else {
// If the characters don't match and there's no wildcard, update the cell to false
dp[j] = false;
}
prevDiagonal = temp; // Update the diagonal element for the next iteration
}
}
return dp[n]; // The last cell (dp[m][n]) contains the result for the entire matching process
}
};
*/
Python π
class Solution:
def isMatch(self, s: str, p: str) -> bool:
m, n = len(s), len(p)
# Create a 2D table dp to store whether s[:i] matches p[:j].
dp = [[False] * (n + 1) for _ in range(m + 1)]
# Base case: empty string matches empty pattern.
dp[0][0] = True
# Fill the first row of dp based on '*' in the pattern.
for j in range(2, n + 1):
if p[j - 1] == "*":
dp[0][j] = dp[0][j - 2]
# Fill the dp table based on characters in s and p.
for i in range(1, m + 1):
for j in range(1, n + 1):
if p[j - 1] == s[i - 1] or p[j - 1] == ".":
dp[i][j] = dp[i - 1][j - 1]
elif p[j - 1] == "*":
dp[i][j] = dp[i][j - 2] or (
dp[i - 1][j] and (s[i - 1] == p[j - 2] or p[j - 2] == ".")
)
return dp[m][n]
Greedy π
This section contains problems belonging to the Greedy category.
Problems
- π‘ Medium: Maximum Subarray
- π‘ Medium: Jump Game
- π‘ Medium: Jump Game II
- π‘ Medium: Gas Station
- π‘ Medium: Hand of Straights
- π‘ Medium: Merge Triplets to Form Target Triplet
- π‘ Medium: Partition Labels
- π‘ Medium: Valid Parenthesis String
Maximum Subarray π§
LeetCode Link: Maximum Subarray
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 53 - Maximum Subarray
Description: Given an integer array nums, find the contiguous subarray (containing at least one number) which has the largest sum and return its sum.
Intuition: To find the maximum subarray sum, we can use the Kadane's algorithm. The idea is to keep track of the maximum sum ending at each position in the array. At each index i, we calculate the maximum subarray sum that ends at i by considering two cases:
- Include the current element in the subarray (by adding it to the sum ending at i-1).
- Start a new subarray at the current element (by taking the current element itself).
Approach:
- Initialize two variables, max_sum and current_sum, to track the maximum sum overall and the maximum sum ending at the current index, respectively.
- Iterate through the array.
- At each index i, update the current_sum by taking the maximum of nums[i] and nums[i] + current_sum. This step implements the Kadane's algorithm.
- Update the max_sum by taking the maximum of max_sum and current_sum.
- After iterating through the array, max_sum will represent the maximum subarray sum.
β Time Complexity: The time complexity is O(n), where n is the size of the input array. We only iterate through the array once.
πΎ Space Complexity: The space complexity is O(1), as we use only a constant amount of extra space.
Solutions π‘
Cpp π»
class Solution {
public:
int maxSubArray(vector<int> &nums) {
int max_sum = nums[0]; // Initialize max_sum with the first element
int current_sum = nums[0]; // Initialize current_sum with the first element
for (int i = 1; i < nums.size(); i++) {
// Calculate the maximum subarray sum ending at index i
current_sum = max(nums[i], nums[i] + current_sum);
// Update the overall maximum subarray sum
max_sum = max(max_sum, current_sum);
}
return max_sum;
}
};
Python π
class Solution:
def maxSubArray(self, nums: List[int]) -> int:
max_sum = float("-inf")
current_sum = 0
for num in nums:
current_sum = max(num, current_sum + num)
max_sum = max(max_sum, current_sum)
return max_sum
Jump Game π§
LeetCode Link: Jump Game
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 55 - Jump Game
Description: Given an array of non-negative integers nums, you are initially positioned at the first index of the array. Each element in the array represents your maximum jump length at that position. Determine if you can reach the last index.
Intuition: To check if it's possible to reach the last index, we can use a greedy approach. The idea is to keep track of the furthest position we can reach from the current position. If at any point, the furthest position we can reach is less than the current position, we know that we cannot reach the end of the array.
Approach:
- Initialize a variable,
max_reachable
, to store the furthest position we can reach from the current position. - Iterate through the array from the beginning.
- At each index, update
max_reachable
to be the maximum ofmax_reachable
andi + nums[i]
. This represents the furthest position we can reach from the current index. - If at any point,
max_reachable
is less than or equal to the current indexi
, return false as we cannot reach the end of the array. - If we successfully reach the end of the loop, return true as we can reach the last index.
β Time Complexity: The time complexity is O(n), where n is the size of the input array. We only iterate through the array once.
πΎ Space Complexity:
The space complexity is O(1), as we use only a constant amount of extra space for the max_reachable
variable.
Solutions π‘
Cpp π»
class Solution {
public:
bool canJump(vector<int> &nums) {
int max_reachable = 0; // Initialize the furthest position we can reach
for (int i = 0; i < nums.size(); i++) {
// Update max_reachable to represent the furthest position we can reach from the current index
max_reachable = max(max_reachable, i + nums[i]);
// If max_reachable is less than or equal to the current index, we cannot reach the end
if (max_reachable <= i) {
return false;
}
}
// If we reach the end of the loop, we can reach the last index
return true;
}
};
Python π
class Solution:
def canJump(self, nums: List[int]) -> bool:
max_reachable = 0
for i, jump_length in enumerate(nums):
if i > max_reachable:
return False
max_reachable = max(max_reachable, i + jump_length)
return True
Jump Game II π§
LeetCode Link: Jump Game II
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 45 - Jump Game II
Description: Given an array of non-negative integers nums, you are initially positioned at the first index of the array. Each element in the array represents your maximum jump length at that position. Your goal is to reach the last index in the minimum number of jumps. If it is not possible to reach the last index, return -1.
Intuition: To find the minimum number of jumps to reach the last index, we can use a greedy approach. The idea is to keep track of the farthest position we can reach from the current position, and the number of jumps needed to reach that position. We update the current position to be the farthest position and increment the jumps count.
Approach:
- Initialize
end
to represent the farthest position we can reach in the current jump. - Initialize
farthest
to represent the farthest position we can reach from any index in the current jump. - Initialize
jumps
to keep track of the number of jumps. - Iterate through the array from the beginning.
- For each index, update
farthest
to be the maximum offarthest
andi + nums[i]
. This represents the farthest position we can reach from any index in the current jump. - If
i
reachesend
, it means we have reached the end of the current jump. Updateend
to befarthest
, and increment thejumps
count. - If at any point,
end
is greater than or equal to the last index, return thejumps
count. - If we successfully reach the end of the loop and have not yet reached the last index, it means we cannot reach the last index. Return -1.
β Time Complexity: The time complexity is O(n), where n is the size of the input array. We only iterate through the array once.
πΎ Space Complexity: The space complexity is O(1), as we use only a constant amount of extra space for the variables.
Solutions π‘
Cpp π»
class Solution {
public:
int jump(vector<int> &nums) {
int end = 0;
int farthest = 0;
int jumps = 0;
for (int i = 0; i < nums.size() - 1; i++) {
// Update farthest to represent the farthest position we can reach from any index in the current jump
farthest = max(farthest, i + nums[i]);
// If i reaches end, it means we have reached the end of the current jump
if (i == end) {
// Update end to be the farthest position for the next jump
end = farthest;
// Increment the jumps count
jumps++;
// If end is greater than or equal to the last index, we have reached the end
if (end >= nums.size() - 1) {
return jumps;
}
}
}
// If we reach the end of the loop and have not yet reached the last index, return -1
return -1;
}
};
Python π
class Solution:
def jump(self, nums: List[int]) -> int:
jumps = 0
cur_end = 0
farthest_reachable = 0
for i in range(len(nums) - 1):
farthest_reachable = max(farthest_reachable, i + nums[i])
if i == cur_end:
jumps += 1
cur_end = farthest_reachable
return jumps
Gas Station π§
LeetCode Link: Gas Station
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 134 - Gas Station
Description: There are N gas stations along a circular route, where the amount of gas at station i is gas[i]. You have a car with an unlimited gas tank and it costs cost[i] of gas to travel from station i to its next station (i+1). You begin the journey with an empty tank at one of the gas stations. Return the starting gas station's index if you can travel around the circuit once in the clockwise direction, otherwise return -1.
Intuition: To determine if there exists a valid starting gas station, we can follow a greedy approach. If the total gas available is greater than or equal to the total cost of traveling the circuit, then there must be a starting gas station that allows us to complete the circuit without running out of gas.
Approach:
- Initialize variables
totalGas
andtotalCost
to store the total gas available and the total cost of traveling the circuit, respectively. - Initialize variables
currentGas
andstart
to store the current gas in the tank and the starting gas station index, respectively. - Iterate through the gas stations in a circular manner using a for loop.
- For each gas station
i
, calculate the differencediff
between the gas availablegas[i]
and the cost to travel to the next stationcost[i]
. Adddiff
tocurrentGas
to simulate traveling to the next station. - If
currentGas
becomes negative at any station, it means we cannot reach the next station. In this case, resetcurrentGas
to 0 and updatestart
to be the next station index. - Keep adding
diff
tototalGas
andtotalCost
as we traverse through the circular route. - At the end of the loop, check if
totalGas
is greater than or equal tototalCost
. If true, returnstart
as the starting gas station index. - If
totalGas
is less thantotalCost
, it means there is no valid starting gas station. Return -1.
β Time Complexity: The time complexity is O(n), where n is the number of gas stations. We only traverse through the gas stations once.
πΎ Space Complexity: The space complexity is O(1), as we use only a constant amount of extra space for the variables.
Solutions π‘
Cpp π»
class Solution {
public:
int canCompleteCircuit(vector<int> &gas, vector<int> &cost) {
int totalGas = 0; // Total gas available
int totalCost = 0; // Total cost of traveling the circuit
int currentGas = 0; // Current gas in the tank
int start = 0; // Starting gas station index
for (int i = 0; i < gas.size(); i++) {
int diff = gas[i] - cost[i]; // Difference between gas available and cost to travel to the next station
totalGas += gas[i];
totalCost += cost[i];
currentGas += diff;
// If currentGas becomes negative, reset it to 0 and update start to be the next station index
if (currentGas < 0) {
currentGas = 0;
start = i + 1;
}
}
// If totalGas is greater than or equal to totalCost, there exists a valid starting gas station
if (totalGas >= totalCost) {
return start;
}
// If totalGas is less than totalCost, there is no valid starting gas station
return -1;
}
};
Python π
class Solution:
def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int:
total_gas = 0
current_gas = 0
start_station = 0
for i in range(len(gas)):
total_gas += gas[i] - cost[i]
current_gas += gas[i] - cost[i]
if current_gas < 0:
start_station = i + 1
current_gas = 0
return start_station if total_gas >= 0 else -1
Hand of Straights π§
LeetCode Link: Hand of Straights
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 846 - Hand of Straights
Description:
Alice has a hand of cards, given as an array of integers hand
, where hand[i]
is the value of the ith card.
A valid hand is a hand where every group of W cards is made up of cards of the same value.
Return true if and only if she can reorder the cards in her hand to form a valid hand.
Intuition: To check if Alice can form valid groups of cards, we can use a greedy approach. We can sort the cards in ascending order and then try to form groups of size W.
Approach:
- Create a map to store the frequency of each card in the hand.
- Sort the hand in ascending order.
- Iterate through the sorted hand and for each card, try to form a group of size W.
- If the current card frequency in the map is greater than 0, decrement its frequency by 1. Then, check if there are W-1 consecutive cards with frequencies greater than 0 after this card. If true, decrement the frequencies of these consecutive cards by 1 to form a group.
- If we can't form a group of size W, return false.
- If all groups are successfully formed, return true.
β Time Complexity: The time complexity is O(n log n) due to sorting the hand, where n is the number of cards in the hand.
πΎ Space Complexity: The space complexity is O(n) to store the card frequencies in the map.
Solutions π‘
Cpp π»
class Solution {
public:
bool isNStraightHand(vector<int> &hand, int W) {
if (hand.size() % W != 0) {
return false; // If the hand size is not divisible by W, can't form valid groups
}
map<int, int> cardFreq; // Map to store the frequency of each card
for (int card : hand) {
cardFreq[card]++;
}
sort(hand.begin(), hand.end()); // Sort the hand in ascending order
for (int card : hand) {
if (cardFreq[card] > 0) {
for (int i = 0; i < W; i++) {
if (cardFreq[card + i] == 0) {
return false; // Can't form a group of size W
}
cardFreq[card + i]--;
}
}
}
return true; // All groups of size W are formed successfully
}
};
/*
// Beats 99% Runtime and Memory
class Solution {
public:
bool isNStraightHand(vector<int>& hand, int groupSize) {
int n = hand.size();
// Check if the hand size is divisible by the groupSize
if (n % groupSize != 0)
return false;
// Sort the hand array in ascending order
sort(hand.begin(), hand.end());
// Iterate through the hand array
for (int i = 0; i < n; i++) {
// Skip elements that have been marked as used (-1)
if (hand[i] == -1)
continue;
int k = i;
// Try to find the next groupSize consecutive elements
for (int j = 1; j < groupSize; j++) {
// Search for the next consecutive element by incrementing k
while (k < n && hand[i] + j != hand[k])
k++;
// If k reaches the end or the next element is not found, return false
if (k == n)
return false;
// Mark the found element as used by setting it to -1
hand[k] = -1;
}
}
// If all groups are found successfully, return true
return true;
}
};
*/
Python π
from collections import Counter
class Solution:
def isNStraightHand(self, hand: List[int], W: int) -> bool:
if len(hand) % W != 0:
return False
counter = Counter(hand)
hand.sort()
for card in hand:
if counter[card] > 0:
for i in range(W):
if counter[card + i] <= 0:
return False
counter[card + i] -= 1
return True
Merge Triplets to Form Target Triplet π§
LeetCode Link: Merge Triplets to Form Target Triplet
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 1899 - Merge Triplets to Form Target Triplet
Description:
A triplet is an array of three integers. You are given a 2D integer array triplets
, where triplets[i] = [ai, bi, ci] describes the ith triplet.
You are also given an integer array target
, where target = [x, y, z] represents the target triplet.
You want to form the target triplet by choosing three triplets from the triplets array (not necessarily distinct) and bitwise-ORing the elements of each chosen triplet.
Return true if it is possible to form the target triplet, otherwise, return false.
Intuition: To form the target triplet [x, y, z], we must be able to select three triplets from the given triplets array such that bitwise-OR of their elements results in [x, y, z].
Approach:
- Initialize three variables
x
,y
, andz
as 0. - Iterate through each triplet in the
triplets
array. - For each triplet, check if it can contribute to
x
,y
, orz
.
- If the triplet's elements are greater than or equal to
x
,y
, andz
respectively, then the triplet can contribute tox
,y
, orz
. - If it can contribute, update the corresponding
x
,y
, orz
value to the triplet's elements.
- After iterating through all triplets, check if
x
,y
, andz
are equal to the target triplet elements. - If they are equal, return true, else return false.
β Time Complexity: The time complexity of this approach is O(n), where n is the number of triplets.
πΎ Space Complexity: The space complexity is O(1) as we are using only a constant amount of extra space.
Solutions π‘
Cpp π»
class Solution {
public:
bool mergeTriplets(vector<vector<int>> &triplets, vector<int> &target) {
int x = 0, y = 0, z = 0;
for (auto &t : triplets) {
if (t[0] <= target[0] && t[1] <= target[1] && t[2] <= target[2]) {
x = max(x, t[0]);
y = max(y, t[1]);
z = max(z, t[2]);
}
}
return (x == target[0] && y == target[1] && z == target[2]);
}
};
Python π
class Solution:
def mergeTriplets(self, triplets: List[List[int]], target: List[int]) -> bool:
max_values = [0, 0, 0]
for triplet in triplets:
if (
triplet[0] <= target[0]
and triplet[1] <= target[1]
and triplet[2] <= target[2]
):
max_values = [max(max_values[i], triplet[i]) for i in range(3)]
return max_values == target
Partition Labels π§
LeetCode Link: Partition Labels
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 763 - Partition Labels
Description: Given a string s, partition s such that every substring in the partition is a palindrome. Return a list of integers representing the length of each partition.
Intuition: To partition the string into palindromic substrings, we need to find the last occurrence of each character in the string. If we know the last index of each character in the string, we can use that information to determine the boundaries of each partition. We can do this by iterating through the string and keeping track of the maximum index of each character encountered so far.
Approach:
- Create a hash map to store the last index of each character in the string.
- Initialize two variables
start
andend
to 0, which will represent the current partition's start and end. - Initialize an empty vector
result
to store the lengths of each partition. - Iterate through the string.
- For each character encountered, update its last index in the hash map.
- If the current index equals the maximum index of the character found so far, it means we have reached the end of the current partition.
- Add the length of the current partition to the
result
vector. - Update the
start
variable to the next index (i.e., the index immediately after the end of the current partition).
- After iterating through the string, return the
result
vector.
β Time Complexity: The time complexity of this approach is O(n), where n is the length of the string.
πΎ Space Complexity: The space complexity is O(1) as the size of the hash map is constant (26 characters).
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> partitionLabels(string s) {
unordered_map<char, int> lastOccurrence;
vector<int> result;
int start = 0, end = 0;
// Store the last occurrence of each character in the hash map
for (int i = 0; i < s.length(); ++i) {
lastOccurrence[s[i]] = i;
}
// Iterate through the string and find the boundaries of each partition
for (int i = 0; i < s.length(); ++i) {
end = max(end, lastOccurrence[s[i]]);
// If we have reached the end of the current partition
if (i == end) {
result.push_back(end - start + 1);
start = i + 1;
}
}
return result;
}
};
Python π
class Solution:
def partitionLabels(self, s: str) -> List[int]:
last_index = {}
for i, char in enumerate(s):
last_index[char] = i
result = []
start, end = 0, 0
for i, char in enumerate(s):
end = max(end, last_index[char])
if i == end:
result.append(end - start + 1)
start = end + 1
return result
Valid Parenthesis String π§
LeetCode Link: Valid Parenthesis String
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 678 - Valid Parenthesis String
Description: Given a string s containing only three types of characters: '(', ')', and '*', return true if s is valid.
The string is valid if the following conditions are met:
- Any left parenthesis '(' must have a corresponding right parenthesis ')'.
- Any right parenthesis ')' must have a corresponding left parenthesis '('.
- Left parenthesis '(' must go before the corresponding right parenthesis ')'.
- '*' could be treated as a single right parenthesis ')' or a single left parenthesis '(' or an empty string.
Intuition: To check the validity of the given string, we can use a greedy approach along with two stacks. We will keep two stacks, one to store the indices of the left parenthesis '(' and another to store the indices of the '' encountered so far. At any point, if we encounter a right parenthesis ')', we will first try to match it with the topmost left parenthesis in the left stack. If the left stack is empty, we will try to match it with the topmost '' in the '' stack. If both stacks are empty, we can't find a matching parenthesis, and the string is invalid. Otherwise, we continue to pop the matched parenthesis and '' from their respective stacks until we find a match or both stacks become empty. If after processing the entire string, both stacks are empty, the string is valid.
Approach:
- Initialize two stacks to store the indices of left parenthesis '(' and '*' encountered so far.
- Iterate through the string.
- If we encounter '(', push its index into the left stack.
- If we encounter '', push its index into the '' stack.
- If we encounter ')':
- If the left stack is not empty, pop the topmost index from the left stack as it matches the current ')'.
- Otherwise, if the '' stack is not empty, pop the topmost index from the '' stack as it can act as a right parenthesis.
- If both stacks are empty, return false as we can't find a matching parenthesis.
- After processing the entire string, we have some left parenthesis '(' and '' without matches. Now, we need to match each remaining '(' with a '' (if possible). To do this, we can pop pairs of '(' and '' from their respective stacks until either of them becomes empty. If the left stack becomes empty first, it means we have matched all '(' with ''. If the '' stack becomes empty first, it means we have some '' left without matches. We can ignore the '*' after matching all possible '(' as they can act as an empty string.
- If both stacks become empty during this process, return true, indicating that the string is valid.
- If any of the stacks still has elements, it means we can't find a match for some left parenthesis, and the string is invalid.
β Time Complexity: The time complexity is O(n), where n is the length of the string.
πΎ Space Complexity: The space complexity is O(n), as in the worst case, both stacks can have all elements of the string.
Solutions π‘
Cpp π»
class Solution {
public:
bool checkValidString(string s) {
stack<int> leftStack, starStack;
for (int i = 0; i < s.length(); ++i) {
if (s[i] == '(') {
leftStack.push(i);
} else if (s[i] == '*') {
starStack.push(i);
} else {
if (!leftStack.empty()) {
leftStack.pop();
} else if (!starStack.empty()) {
starStack.pop();
} else {
return false;
}
}
}
while (!leftStack.empty() && !starStack.empty()) {
if (leftStack.top() > starStack.top()) {
return false;
}
leftStack.pop();
starStack.pop();
}
return leftStack.empty();
}
};
/*
class Solution {
public:
bool checkValidString(string s) {
int leftOpen = 0; // Keep track of possible open parentheses
int leftMin = 0; // Minimum possible open parentheses
for (char c : s) {
if (c == '(') {
leftOpen++;
leftMin++;
} else if (c == ')') {
leftOpen--;
leftMin = max(leftMin - 1, 0); // Ensure leftMin doesn't go negative
} else { // c == '*'
leftOpen++;
leftMin = max(leftMin - 1, 0); // Ensure leftMin doesn't go negative
}
if (leftOpen < 0) {
return false; // If there are too many closing parentheses, return false
}
}
return leftMin == 0; // Check if all open parentheses can be matched
}
};
*/
Python π
class Solution:
def checkValidString(self, s: str) -> bool:
lower = upper = 0
for char in s:
if char == "(":
lower += 1
upper += 1
elif char == ")":
lower = max(lower - 1, 0)
upper -= 1
else: # char == '*'
lower = max(lower - 1, 0)
upper += 1
if upper < 0:
return False
return lower == 0
Intervals π
This section contains problems belonging to the Intervals category.
Problems
- π‘ Medium: Insert Interval
- π‘ Medium: Merge Intervals
- π‘ Medium: Non Overlapping Intervals
- π’ Easy: Meeting Rooms
- π‘ Medium: Meeting Rooms II
- π΄ Hard: Minimum Interval to Include Each Query
Insert Interval π§
LeetCode Link: Insert Interval
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 57 - Insert Interval
Description: Given a set of non-overlapping intervals sorted by their start times, insert a new interval into the intervals (merge if necessary).
You may assume that the intervals were initially sorted according to their start times.
Intuition: The key idea is to find the correct position to insert the new interval and merge any overlapping intervals if necessary.
Approach:
- Create an empty result vector to store the merged intervals.
- Iterate through the given intervals: a. If the current interval's end is less than the new interval's start, add the current interval to the result vector. b. If the current interval's start is greater than the new interval's end, it means we have found the correct position to insert the new interval. Add the new interval to the result vector and merge any remaining intervals if needed. c. If there is an overlap between the current interval and the new interval, update the new interval's start and end to cover the overlapping region.
- Add any remaining intervals to the result vector if needed.
- Return the result vector.
β Time Complexity: The time complexity is O(n), where n is the number of intervals.
πΎ Space Complexity: The space complexity is O(1) for the result vector, and no additional data structures are used, so the overall space complexity is also O(1).
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<int>> insert(vector<vector<int>> &intervals, vector<int> &newInterval) {
vector<vector<int>> result;
int i = 0;
int n = intervals.size();
// Add intervals that end before the new interval starts
while (i < n && intervals[i][1] < newInterval[0]) {
result.push_back(intervals[i]);
i++;
}
// Merge overlapping intervals
while (i < n && intervals[i][0] <= newInterval[1]) {
newInterval[0] = min(newInterval[0], intervals[i][0]);
newInterval[1] = max(newInterval[1], intervals[i][1]);
i++;
}
// Add the merged interval
result.push_back(newInterval);
// Add remaining intervals
while (i < n) {
result.push_back(intervals[i]);
i++;
}
return result;
}
};
Python π
class Solution:
def insert(
self, intervals: List[List[int]], newInterval: List[int]
) -> List[List[int]]:
result = []
new_start, new_end = newInterval
for interval in intervals:
if interval[1] < new_start:
result.append(interval)
elif interval[0] > new_end:
result.append([new_start, new_end])
new_start, new_end = interval
else:
new_start = min(new_start, interval[0])
new_end = max(new_end, interval[1])
result.append([new_start, new_end])
return result
Merge Intervals π§
LeetCode Link: Merge Intervals
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 56 - Merge Intervals
Description: Given a collection of intervals, merge all overlapping intervals.
Intuition: To merge overlapping intervals, we can sort the intervals based on their start times. Then, we can iterate through the sorted intervals and merge any overlapping intervals as we encounter them.
Approach:
- Sort the given intervals based on their start times.
- Create an empty result vector to store the merged intervals.
- Iterate through the sorted intervals: a. If the result vector is empty or the current interval's start is greater than the end of the last merged interval in the result vector, add the current interval to the result vector. b. If there is an overlap between the current interval and the last merged interval in the result vector, update the end of the last merged interval to the maximum of both intervals' ends.
- Return the result vector.
β Time Complexity: The time complexity is O(n log n), where n is the number of intervals. Sorting the intervals takes O(n log n) time, and merging them takes O(n) time.
πΎ Space Complexity: The space complexity is O(1) for the result vector, and no additional data structures are used, so the overall space complexity is also O(1).
Solutions π‘
Cpp π»
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>> &intervals) {
if (intervals.empty()) return {};
// Sort the intervals based on their start times
// Uses a lambda function as the third argument of the sort function to define the custom sorting criteria.
sort(intervals.begin(), intervals.end(), [](const vector<int> &a, const vector<int> &b) {
return a[0] < b[0];
});
vector<vector<int>> result;
result.push_back(intervals[0]);
for (int i = 1; i < intervals.size(); i++) {
vector<int> &lastInterval = result.back();
if (intervals[i][0] > lastInterval[1]) {
// No overlap, add the current interval to the result
result.push_back(intervals[i]);
} else {
// Overlap, update the end of the last merged interval
lastInterval[1] = max(lastInterval[1], intervals[i][1]);
}
}
return result;
}
};
Python π
class Solution:
def merge(self, intervals: List[List[int]]) -> List[List[int]]:
if not intervals:
return []
intervals.sort(key=lambda x: x[0])
result = [intervals[0]]
for interval in intervals[1:]:
if interval[0] <= result[-1][1]:
result[-1][1] = max(result[-1][1], interval[1])
else:
result.append(interval)
return result
Non Overlapping Intervals π§
LeetCode Link: Non Overlapping Intervals
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 435 - Non-overlapping Intervals
Description: Given an array of intervals, find the minimum number of intervals you need to remove to make the rest of the intervals non-overlapping.
Intuition: To find the minimum number of intervals to remove, we can use a greedy approach. If two intervals overlap, we should keep the one with the smaller end time, as this will leave more space for other intervals. If we keep intervals with smaller end times, we have a better chance of fitting more intervals in the remaining space.
Approach:
- Sort the given intervals based on their end times in ascending order.
- Initialize a variable "end" to store the end time of the last non-overlapping interval.
- Initialize a variable "count" to keep track of the number of intervals to remove (initialize to 0).
- Iterate through the sorted intervals: a. If the current interval's start time is greater than or equal to "end", it means it doesn't overlap with the last non-overlapping interval, so we update "end" to be the end time of the current interval. b. If the current interval's start time is less than "end", it means there is an overlap, and we need to remove one of the intervals. We increment the "count" variable and continue to the next interval.
- Return the value of "count", which represents the minimum number of intervals to remove.
β Time Complexity: The time complexity is O(n log n), where n is the number of intervals. Sorting the intervals based on their end times takes O(n log n) time.
πΎ Space Complexity: The space complexity is O(1) as we only use a constant amount of additional space.
Solutions π‘
Cpp π»
class Solution {
public:
int eraseOverlapIntervals(vector<vector<int>> &intervals) {
if (intervals.empty()) {
return 0;
}
// Sort the intervals based on their end times in ascending order
sort(intervals.begin(), intervals.end(), [](const vector<int> &a, const vector<int> &b) {
return a[1] < b[1];
});
int end = intervals[0][1]; // End time of the last non-overlapping interval
int count = 0; // Number of intervals to remove
for (int i = 1; i < intervals.size(); i++) {
// If the current interval's start time is less than "end", there is an overlap
if (intervals[i][0] < end) {
count++;
} else {
// No overlap, update "end" to be the end time of the current interval
end = intervals[i][1];
}
}
return count;
}
};
Python π
class Solution:
def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int:
if not intervals:
return 0
intervals.sort(key=lambda x: x[1])
non_overlapping = 1 # Count of non-overlapping intervals
prev_end = intervals[0][1]
for i in range(1, len(intervals)):
if intervals[i][0] >= prev_end:
non_overlapping += 1
prev_end = intervals[i][1]
return len(intervals) - non_overlapping
Meeting Rooms π§
LeetCode Link: Meeting Rooms
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 252 - Meeting Rooms
Description: Given an array of meeting time intervals where intervals[i] = [starti, endi], determine if a person could attend all meetings.
Intuition: To determine if a person could attend all meetings, we need to check if there are any overlapping intervals. If any two intervals overlap, the person cannot attend all meetings.
Approach:
- Sort the given intervals based on their start times in ascending order.
- Iterate through the sorted intervals: a. For each interval (except the first one), check if its start time is less than the end time of the previous interval. If so, it means there is an overlap, and the person cannot attend all meetings. Return false.
- If there are no overlaps, return true.
β Time Complexity: The time complexity is O(n log n), where n is the number of intervals. Sorting the intervals based on their start times takes O(n log n) time.
πΎ Space Complexity: The space complexity is O(1) as we only use a constant amount of additional space.
Solutions π‘
Cpp π»
class Solution {
public:
bool canAttendMeetings(vector<vector<int>> &intervals) {
if (intervals.empty()) {
return true;
}
// Sort the intervals based on their start times in ascending order
sort(intervals.begin(), intervals.end(), [](const vector<int> &a, const vector<int> &b) {
return a[0] < b[0];
});
for (int i = 1; i < intervals.size(); i++) {
// Check if there is an overlap
if (intervals[i][0] < intervals[i - 1][1]) {
return false;
}
}
return true;
}
};
Python π
class Solution:
def canAttendMeetings(self, intervals: List[List[int]]) -> bool:
if not intervals:
return True
intervals.sort(key=lambda x: x[0])
for i in range(1, len(intervals)):
if intervals[i][0] < intervals[i - 1][1]:
return False
return True
Meeting Rooms II π§
LeetCode Link: Meeting Rooms II
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 253 - Meeting Rooms II
Description: Given an array of meeting time intervals where intervals[i] = [starti, endi], find the minimum number of conference rooms required.
Intuition: To find the minimum number of conference rooms required, we can simulate the process of scheduling the meetings in a room.
Approach:
- Sort the given intervals based on their start times in ascending order.
- Use a priority queue to keep track of the end times of the meetings currently scheduled in different rooms.
- Iterate through the sorted intervals: a. If the priority queue is empty or the start time of the current interval is greater than the end time of the earliest meeting in the queue, it means we can schedule this meeting in one of the existing rooms. Pop the earliest meeting from the queue and push the end time of the current interval. b. If the start time of the current interval is less than or equal to the end time of the earliest meeting in the queue, it means we need to allocate a new room for this meeting. Push the end time of the current interval into the queue.
- The size of the priority queue at any time represents the number of conference rooms needed to accommodate the meetings.
β Time Complexity: The time complexity is O(n log n), where n is the number of intervals. Sorting the intervals based on their start times takes O(n log n) time, and pushing and popping elements from the priority queue takes O(log n) time for each meeting.
πΎ Space Complexity: The space complexity is O(n) as we use a priority queue to store the end times of the meetings.
Solutions π‘
Cpp π»
class Solution {
public:
int minMeetingRooms(vector<vector<int>> &intervals) {
if (intervals.empty()) {
return 0;
}
// Sort the intervals based on their start times in ascending order
sort(intervals.begin(), intervals.end(), [](const vector<int> &a, const vector<int> &b) {
return a[0] < b[0];
});
// Use a priority queue to keep track of end times of the meetings in different rooms
priority_queue<int, vector<int>, greater<int>> pq;
pq.push(intervals[0][1]); // Push the end time of the first meeting
for (int i = 1; i < intervals.size(); i++) {
int start = intervals[i][0];
int end = intervals[i][1];
// Check if the current meeting can be accommodated in an existing room
if (start >= pq.top()) {
pq.pop();
}
// Push the end time of the current meeting into the queue
pq.push(end);
}
// The size of the priority queue represents the minimum number of conference rooms needed
return pq.size();
}
};
Python π
import heapq
class Solution:
def minMeetingRooms(self, intervals: List[List[int]]) -> int:
if not intervals:
return 0
intervals.sort(key=lambda x: x[0])
min_heap = []
heapq.heappush(min_heap, intervals[0][1])
for i in range(1, len(intervals)):
if intervals[i][0] >= min_heap[0]:
heapq.heappop(min_heap)
heapq.heappush(min_heap, intervals[i][1])
return len(min_heap)
Minimum Interval to Include Each Query π§
LeetCode Link: Minimum Interval to Include Each Query
Difficulty: Hard
Problem Explanation π
Problem: LeetCode 1851 - Minimum Interval to Include Each Query
Description: You are given a 2D integer array intervals, where intervals[i] = [lefti, righti] describes the ith interval starting from lefti and ending at righti (inclusive). The size of the array is n and 1-indexed.
You are also given an integer array queries, where queries[j] = [leftj, rightj] describes the jth query starting from leftj and ending at rightj (inclusive). The size of the array is m and 1-indexed.
Find the minimum interval for each query such that it is completely covered by an interval in intervals and lci <= leftj <= rightj <= rci.
Return an integer array answer, where answer[j] is the minimum interval length for the jth query.
Intuition: To solve this problem efficiently, we can use a two-step approach:
- Sort both intervals and queries in ascending order based on their lengths.
- Use a priority queue (min heap) to keep track of the right boundaries of the current intervals.
Approach:
- Create two separate vectors of pairs: one for intervals and one for queries, where the first element of each pair represents the length of the interval/query, and the second element is the index of the interval/query in the original arrays.
- Sort both vectors in ascending order based on their lengths.
- Initialize a priority queue (min heap) to keep track of the right boundaries of the current intervals. We'll insert the intervals in the queue based on their right boundary.
- Iterate through the sorted intervals: a. While the current query length is smaller or equal to the current interval length, update the result for the query and remove it from the priority queue.
- Continue to process the next interval.
- Return the answer vector containing the minimum interval lengths for each query.
β Time Complexity: The time complexity is O(n log n + m log m), where n is the number of intervals and m is the number of queries. Sorting both vectors of pairs takes O(n log n + m log m) time, and processing intervals and queries takes O(n + m log n) time.
πΎ Space Complexity: The space complexity is O(n + m) to store the vectors of pairs and the priority queue.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> minInterval(vector<vector<int>> &intervals, vector<int> &queries) {
vector<int> sortedQueries = queries;
// [size of interval, end of interval]
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<pair<int, int>>> pq;
// {query -> size of interval}
unordered_map<int, int> m;
// Also need only valid intervals, so sort by start time & sort queries
sort(intervals.begin(), intervals.end());
sort(sortedQueries.begin(), sortedQueries.end());
vector<int> result;
int i = 0;
for (int j = 0; j < sortedQueries.size(); j++) {
int query = sortedQueries[j];
// Push intervals into the min heap whose start time is less than or equal to the current query value
while (i < intervals.size() && intervals[i][0] <= query) {
int left = intervals[i][0];
int right = intervals[i][1];
pq.push({right - left + 1, right});
i++;
}
// Pop the invalid intervals from the min heap (intervals whose end time is less than the current query value)
while (!pq.empty() && pq.top().second < query) {
pq.pop();
}
// Store the minimum interval size for the current query in the unordered_map
if (!pq.empty()) {
m[query] = pq.top().first;
} else {
m[query] = -1;
}
}
// Build the result vector using the unordered_map for each query
for (int j = 0; j < queries.size(); j++) {
result.push_back(m[queries[j]]);
}
return result;
}
};
Python π
import heapq
class Solution:
def minInterval(self, intervals: List[List[int]], queries: List[int]) -> List[int]:
intervals.sort(key=lambda x: x[0])
queries_sorted = sorted(enumerate(queries), key=lambda x: x[1])
min_heap = []
ans = [-1] * len(queries)
i = 0
for query_index, query in queries_sorted:
while i < len(intervals) and intervals[i][0] <= query:
start, end = intervals[i]
heapq.heappush(min_heap, (end - start + 1, end))
i += 1
while min_heap and min_heap[0][1] < query:
heapq.heappop(min_heap)
if min_heap:
ans[query_index] = min_heap[0][0]
return ans
Math and Geometry π
This section contains problems belonging to the Math and Geometry category.
Problems
- π‘ Medium: Rotate Image
- π‘ Medium: Spiral Matrix
- π‘ Medium: Set Matrix Zeroes
- π’ Easy: Happy Number
- π’ Easy: Plus One
- π‘ Medium: Pow(x,n)
- π‘ Medium: Multiply Strings
- π‘ Medium: Detect Squares
Rotate Image π§
LeetCode Link: Rotate Image
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 48 - Rotate Image
Description: You are given an n x n 2D matrix representing an image, rotate the image by 90 degrees (clockwise).
Intuition: To rotate the image by 90 degrees clockwise, we can perform two steps:
- Transpose the matrix (rows become columns, and vice versa).
- Reverse each row of the transposed matrix.
Approach:
- Transpose the matrix in-place:
- Iterate over the upper triangle of the matrix (i.e., elements above the main diagonal).
- Swap the element at matrix[i][j] with matrix[j][i].
- Reverse each row of the transposed matrix:
- For each row, use two pointers (start and end) and swap the elements until they meet in the middle.
β Time Complexity: The time complexity of this approach is O(n^2), where n is the number of rows (or columns) in the matrix.
πΎ Space Complexity: The space complexity is O(1) as we are performing the rotation in-place without using any additional space.
Solutions π‘
Cpp π»
class Solution {
public:
void rotate(vector<vector<int>> &matrix) {
int n = matrix.size();
// Step 1: Transpose the matrix
for (int i = 0; i < n; i++) {
for (int j = i; j < n; j++) {
swap(matrix[i][j], matrix[j][i]);
}
}
// Step 2: Reverse each row of the transposed matrix
for (int i = 0; i < n; i++) {
int start = 0;
int end = n - 1;
while (start < end) {
swap(matrix[i][start], matrix[i][end]);
start++;
end--;
}
}
}
};
Python π
class Solution:
def rotate(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
n = len(matrix)
# Transpose the matrix
for i in range(n):
for j in range(i, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]
# Reverse each row
for i in range(n):
matrix[i].reverse()
Spiral Matrix π§
LeetCode Link: Spiral Matrix
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 54 - Spiral Matrix
Description: Given an m x n matrix, return all elements of the matrix in spiral order.
Intuition: We can traverse the matrix in a spiral order by simulating the movement in four directions: right, down, left, and up. To keep track of the visited elements, we can maintain four boundaries: top, bottom, left, and right.
Approach:
- Initialize four variables: top = 0, bottom = m - 1, left = 0, right = n - 1.
- Loop until top <= bottom and left <= right:
- Traverse from left to right along the top boundary and increment top.
- Traverse from top to bottom along the right boundary and decrement right.
- Traverse from right to left along the bottom boundary and decrement bottom.
- Traverse from bottom to top along the left boundary and increment left.
- Keep adding the elements encountered during the traversal to the result vector.
β Time Complexity: The time complexity of this approach is O(m * n), where m is the number of rows and n is the number of columns in the matrix.
πΎ Space Complexity: The space complexity is O(1) as we are not using any additional space.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>> &matrix) {
vector<int> result;
if (matrix.empty()) {
return result;
}
int m = matrix.size();
int n = matrix[0].size();
int top = 0, bottom = m - 1, left = 0, right = n - 1;
while (top <= bottom && left <= right) {
// Traverse from left to right along the top boundary
for (int i = left; i <= right; i++) {
result.push_back(matrix[top][i]);
}
top++;
// Traverse from top to bottom along the right boundary
for (int i = top; i <= bottom; i++) {
result.push_back(matrix[i][right]);
}
right--;
// Traverse from right to left along the bottom boundary
if (top <= bottom) {
for (int i = right; i >= left; i--) {
result.push_back(matrix[bottom][i]);
}
bottom--;
}
// Traverse from bottom to top along the left boundary
if (left <= right) {
for (int i = bottom; i >= top; i--) {
result.push_back(matrix[i][left]);
}
left++;
}
}
return result;
}
};
Python π
class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
if not matrix:
return []
rows, cols = len(matrix), len(matrix[0])
result = []
# Define the boundaries of the current layer
top, bottom, left, right = 0, rows - 1, 0, cols - 1
while top <= bottom and left <= right:
# Traverse the top row
for j in range(left, right + 1):
result.append(matrix[top][j])
top += 1
# Traverse the right column
for i in range(top, bottom + 1):
result.append(matrix[i][right])
right -= 1
# Traverse the bottom row
if top <= bottom: # Avoid duplicate traversal
for j in range(right, left - 1, -1):
result.append(matrix[bottom][j])
bottom -= 1
# Traverse the left column
if left <= right: # Avoid duplicate traversal
for i in range(bottom, top - 1, -1):
result.append(matrix[i][left])
left += 1
return result
Set Matrix Zeroes π§
LeetCode Link: Set Matrix Zeroes
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 73 - Set Matrix Zeroes
Description: Given an m x n matrix, if an element is 0, set its entire row and column to 0. Do it in-place.
Intuition: To solve this problem in-place, we can use the first row and the first column of the matrix to keep track of which rows and columns need to be set to 0. We use two additional boolean variables to track whether the first row and first column themselves need to be set to 0.
Approach:
- First, we check if the first row and the first column need to be set to 0 by iterating through the first row and first column of the matrix.
- Then, we traverse the matrix starting from the second row and second column.
- If the current element matrix[i][j] is 0, we set the corresponding elements in the first row and first column to 0.
- Next, we traverse the matrix again from the second row and second column and set the elements to 0 if the corresponding element in the first row or first column is 0.
- Finally, we handle the first row and first column based on the boolean variables we used to track them.
β Time Complexity: The time complexity of this approach is O(m * n), where m is the number of rows and n is the number of columns in the matrix. We traverse the matrix twice.
πΎ Space Complexity: The space complexity is O(1) as we are using the first row and first column of the matrix to keep track of which rows and columns need to be set to 0.
Solutions π‘
Cpp π»
class Solution {
public:
void setZeroes(vector<vector<int>> &matrix) {
int m = matrix.size();
int n = matrix[0].size();
bool firstRowZero = false;
bool firstColZero = false;
// Check if first row needs to be set to 0
for (int j = 0; j < n; j++) {
if (matrix[0][j] == 0) {
firstRowZero = true;
break;
}
}
// Check if first column needs to be set to 0
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
firstColZero = true;
break;
}
}
// Mark rows and columns that need to be set to 0 in the first row and first column
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = 0;
matrix[0][j] = 0;
}
}
}
// Set elements to 0 based on the first row and first column
for (int i = 1; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][0] == 0 || matrix[0][j] == 0) {
matrix[i][j] = 0;
}
}
}
// Set first row and first column to 0 if needed
if (firstRowZero) {
for (int j = 0; j < n; j++) {
matrix[0][j] = 0;
}
}
if (firstColZero) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
}
};
Python π
class Solution:
def setZeroes(self, matrix: List[List[int]]) -> None:
"""
Do not return anything, modify matrix in-place instead.
"""
rows, cols = len(matrix), len(matrix[0])
zero_rows, zero_cols = set(), set()
# Mark rows and columns that need to be set to zero
for i in range(rows):
for j in range(cols):
if matrix[i][j] == 0:
zero_rows.add(i)
zero_cols.add(j)
# Set elements to zero based on marked rows and columns
for i in range(rows):
for j in range(cols):
if i in zero_rows or j in zero_cols:
matrix[i][j] = 0
Happy Number π§
LeetCode Link: Happy Number
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 202 - Happy Number
Description: A happy number is a number defined by the following process:
- Starting with any positive integer, replace the number by the sum of the squares of its digits.
- Repeat the process until the number equals 1 (where it will stay), or it loops endlessly in a cycle which does not include 1.
- Those numbers for which this process ends in 1 are happy.
Intuition: To determine if a number is a happy number, we can use a set to keep track of all the numbers we have encountered during the process. If we encounter a number that we have seen before, it means there is a cycle, and the number is not a happy number.
Approach:
- We start with the given number and calculate the sum of the squares of its digits.
- We continue this process until the sum becomes 1 or until we encounter a number that we have seen before.
- If the sum becomes 1, we return true, indicating that the number is a happy number.
- If we encounter a number we have seen before, we return false, indicating that the number is not a happy number.
β Time Complexity: The time complexity of this approach is difficult to determine as it depends on the number of iterations required to reach 1 or find a cycle. In practice, the process usually converges quickly for happy numbers, so the time complexity is considered to be approximately O(log n).
πΎ Space Complexity: The space complexity is O(log n) as we use a set to keep track of the numbers encountered during the process, and the number of unique numbers encountered is limited.
Solutions π‘
Cpp π»
class Solution {
public:
bool isHappy(int n) {
unordered_set<int> seen;
while (n != 1 && seen.find(n) == seen.end()) {
seen.insert(n);
n = getNextNumber(n);
}
return n == 1;
}
private:
int getNextNumber(int n) {
int sum = 0;
while (n > 0) {
int digit = n % 10;
sum += digit * digit;
n /= 10;
}
return sum;
}
};
Python π
class Solution:
def isHappy(self, n: int) -> bool:
def get_next(num):
next_num = 0
while num > 0:
num, digit = divmod(num, 10)
next_num += digit**2
return next_num
slow, fast = n, get_next(n)
while fast != 1 and slow != fast:
slow = get_next(slow)
fast = get_next(get_next(fast))
return fast == 1
Plus One π§
LeetCode Link: Plus One
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 66 - Plus One
Description: Given a non-empty array of decimal digits representing a non-negative integer, increment one to the integer. The digits are stored such that the most significant digit is at the head of the list, and each element in the array contains a single digit. You may assume the integer does not contain any leading zero, except for the number 0 itself.
Intuition: To increment a number represented as an array of digits, we need to add one to the last digit and handle any carry that may occur.
Approach:
- Start from the end of the digits array.
- Add one to the last digit.
- If the digit becomes 10 (carry occurs), set it to 0 and move to the next digit.
- Continue this process until there is no more carry or we reach the beginning of the digits array.
- If there is still a carry after processing all digits, it means the original number was all nines, and we need to add a new digit at the beginning of the array with a value of 1.
β Time Complexity: The time complexity is O(n), where n is the number of digits in the array. In the worst case, we may need to carry the addition all the way to the beginning of the array.
πΎ Space Complexity: The space complexity is O(1) as we are modifying the input array in place and not using any additional data structures.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> plusOne(vector<int> &digits) {
int n = digits.size();
// Start from the end and add one to the last digit
digits[n - 1] += 1;
// Handle any carry
for (int i = n - 1; i > 0; i--) {
if (digits[i] == 10) {
digits[i] = 0;
digits[i - 1] += 1;
} else {
break; // No more carry, exit the loop
}
}
// If there is still a carry, add a new digit at the beginning of the array
if (digits[0] == 10) {
digits[0] = 0;
digits.insert(digits.begin(), 1);
}
return digits;
}
};
/*
class Solution {
public:
vector<int> plusOne(vector<int>& digits) {
for(int i=digits.size()-1; i>=0; i--){
digits[i]++;
if(digits[i]==10){
digits[i]=0;
}
else{
break;
}
}
if(digits[0]==0){
digits.insert(digits.begin(), 1);
}
return digits;
}
};
*/
Python π
class Solution:
def plusOne(self, digits: List[int]) -> List[int]:
n = len(digits)
for i in range(n - 1, -1, -1):
if digits[i] < 9:
digits[i] += 1
return digits
digits[i] = 0
return [1] + digits
Pow(x,n) π§
LeetCode Link: Pow(x,n)
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 50 - Pow(x, n)
Description: Implement pow(x, n), which calculates x raised to the power n (i.e., x^n).
Intuition: To calculate the power x^n, we can use the concept of recursion and divide-and-conquer. The idea is to break down the problem into smaller subproblems and solve them recursively.
Approach:
- First, we handle the base case where n is 0 or 1. If n is 0, the result is 1. If n is 1, the result is x itself.
- If n is negative, we convert the problem to calculate 1/x raised to the power -n.
- To calculate x^n, we can divide the problem into two parts:
- Calculate x^(n/2) using recursion.
- If n is even, the result is (x^(n/2)) * (x^(n/2)).
- If n is odd, the result is (x^(n/2)) * (x^(n/2)) * x.
- We use a helper function to perform the recursive calculations.
β Time Complexity: The time complexity of this approach is O(log n) because we are dividing the problem size by 2 in each recursive call.
πΎ Space Complexity: The space complexity is O(log n) as well due to the recursive function calls.
Solutions π‘
Cpp π»
class Solution {
public:
double myPow(double x, long long n) {
// Base case: x^0 = 1
if (n == 0) {
return 1.0;
}
// Base case: x^1 = x
if (n == 1) {
return x;
}
// If n is negative, calculate 1/x^-n
if (n < 0) {
return 1.0 / myPow(x, -n);
}
// Calculate x^n using recursion and divide-and-conquer
double halfPow = myPow(x, n / 2);
double result = halfPow * halfPow;
// If n is odd, multiply with x
if (n % 2 != 0) {
result *= x;
}
return result;
}
};
/*
// Beats 100% runtime
class Solution {
public:
double myPow(double x, int n) {
// Handle negative exponent by inverting the base
if (n < 0) {
x = 1 / x;
}
// Take the absolute value of the exponent to avoid issues with INT_MIN
long num = labs(n);
// Initialize the result to 1
double pow = 1;
// Exponentiation by squaring algorithm
while (num) { // While the exponent is not zero
if (num & 1) { // If the least significant bit of the exponent is 1
pow *= x; // Multiply the result by the current base value
}
x *= x; // Square the base value for the next iteration
num >>= 1; // Right-shift the exponent to remove the least significant bit
}
return pow;
}
};
*/
Python π
class Solution:
def myPow(self, x: float, n: int) -> float:
def helper(base, exp):
if exp == 0:
return 1.0
temp = helper(base, exp // 2)
if exp % 2 == 0:
return temp * temp
else:
return base * temp * temp
if n < 0:
x = 1 / x
n = -n
return helper(x, n)
Multiply Strings π§
LeetCode Link: Multiply Strings
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 43 - Multiply Strings
Description: Given two non-negative integers num1 and num2 represented as strings, return the product of num1 and num2, also represented as a string.
Intuition: The basic idea behind multiplying two numbers represented as strings is to simulate the process of multiplication as we do on paper. We start with the least significant digits of both numbers and multiply them. We keep track of the carry and add the product to the corresponding position in the result string. We continue this process for all digits of both numbers, considering the correct position of the result digits based on the multiplication.
Approach:
- Create a result string to store the final product.
- Initialize an array to store the intermediate products (i.e., the products of each digit in num1 with each digit in num2).
- Traverse num1 and num2 from right to left, and calculate all the intermediate products, storing them in the array.
- Calculate the carry and the sum at each position of the result string, taking into account the intermediate products and any previous carry.
- After completing the traversal and calculations, the result string will hold the final product.
β Time Complexity: The time complexity of this approach is O(m * n), where m and n are the lengths of num1 and num2, respectively.
πΎ Space Complexity: The space complexity is O(m + n) to store the intermediate products.
Solutions π‘
Cpp π»
class Solution {
public:
string multiply(string num1, string num2) {
int m = num1.size();
int n = num2.size();
vector<int> products(m + n, 0); // To store intermediate products
string result = "";
// Calculate intermediate products
for (int i = m - 1; i >= 0; i--) {
for (int j = n - 1; j >= 0; j--) {
int mul = (num1[i] - '0') * (num2[j] - '0');
int sum = mul + products[i + j + 1]; // Add to the existing intermediate product
products[i + j] += sum / 10; // Carry
products[i + j + 1] = sum % 10; // Store the digit at correct position
}
}
// Build the result string
for (int p : products) {
if (!(result.empty() && p == 0)) {
result += to_string(p);
}
}
return result.empty() ? "0" : result;
}
};
Python π
class Solution:
def multiply(self, num1: str, num2: str) -> str:
if num1 == "0" or num2 == "0":
return "0"
m, n = len(num1), len(num2)
result = [0] * (m + n)
for i in range(m - 1, -1, -1):
for j in range(n - 1, -1, -1):
product = int(num1[i]) * int(num2[j])
sum_val = product + result[i + j + 1]
result[i + j + 1] = sum_val % 10
result[i + j] += sum_val // 10
return "".join(map(str, result)).lstrip("0")
Detect Squares π§
LeetCode Link: Detect Squares
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 2013 - Detect Squares
Intuition: Precompute the count of points in each row and column and then use this information to find the other three points of the square.
Approach:
- We use two unordered maps "rowMap" and "colMap" to keep track of the count of points in each row and column, respectively.
- When we add a new point (x, y), we increment its count in both "rowMap" and "colMap" accordingly.
- To count the number of squares with a given point (x, y) as the bottom-right corner, we look for other points in the same row "x."
- For each point found at column "col" in row "x," we calculate the edge length of the potential square. Let's call it "edgeLen," which is the absolute difference between "col" and "y."
- Then, we check if the points (x - edgeLen, col), (x + edgeLen, col), and (x + edgeLen, y) exist in the maps. If they do, we have found a square with (x, y) as the bottom-right corner.
- We repeat this process for all points in row "x" to find all squares with (x, y) as the bottom-right corner.
- Finally, we sum up the counts of all these squares to get the total number of squares with (x, y) as the bottom-right corner.
- The process is efficient as we use unordered maps to store the counts, making the lookups quick and allowing us to count squares in constant time.
β Time Complexity:
The time complexity of this approach is O(1) for add
and O(n) for count
(where "n" is the average number of points in the same row as the given point).
πΎ Space Complexity: The space complexity is O(N) (where "N" is the total number of points added).
Solutions π‘
Cpp π»
class DetectSquares {
public:
// We maintain two maps for row and column coordinates to store the count of points in each row and column.
vector<unordered_map<int, int>> rowMap;
vector<unordered_map<int, int>> colMap;
DetectSquares() : rowMap(1001), colMap(1001) {
// Constructor to initialize the rowMap and colMap with size 1001.
}
// Function to add a point to the grid.
void add(vector<int> point) {
// Increment the count of points in the corresponding row and column maps.
rowMap[point[0]][point[1]]++;
colMap[point[1]][point[0]]++;
}
// Function to count the number of squares that can be formed with a given point as the bottom-right corner.
int count(vector<int> point) {
int result = 0;
// Iterate through the row map for the given point's row.
for (auto [col, cCount] : rowMap[point[0]]) {
int edgeLen = abs(col - point[1]);
if (edgeLen == 0) {
continue; // Skip points that are at the same position as the given point.
}
// Calculate the row coordinates for the other two points of the square based on the edge length.
int row = point[0] - edgeLen;
if (colMap[col].find(row) != colMap[col].end()) {
auto rCount = colMap[col][row];
// Check if the other two points exist in the colMap.
if (colMap[point[1]].find(row) != colMap[point[1]].end()) {
// If so, update the result by multiplying the counts of the four points.
result += (rCount * cCount * colMap[point[1]][row]);
}
}
row = point[0] + edgeLen;
if (colMap[col].find(row) != colMap[col].end()) {
auto rCount = colMap[col][row];
// Check if the other two points exist in the colMap.
if (colMap[point[1]].find(row) != colMap[point[1]].end()) {
// If so, update the result by multiplying the counts of the four points.
result += (rCount * cCount * colMap[point[1]][row]);
}
}
}
return result;
}
};
Python π
from collections import defaultdict
from typing import List
class DetectSquares:
def __init__(self):
self.points = defaultdict(lambda: defaultdict(int))
def add(self, point: List[int]) -> None:
x, y = point
self.points[x][y] += 1
def count(self, point: List[int]) -> int:
x, y = point
count = 0
for y2 in self.points[x]:
if y2 != y:
side_length = abs(y2 - y)
for x2 in (x + side_length, x - side_length):
if x2 in self.points and y in self.points[x2]:
count += (
self.points[x2][y]
* self.points[x2][y2]
* self.points[x][y2]
)
return count
# Your DetectSquares object will be instantiated and called as such:
# obj = DetectSquares()
# obj.add(point)
# param_2 = obj.count(point)
Bit Manipulation π
This section contains problems belonging to the Bit Manipulation category.
Problems
- π’ Easy: Single Number
- π’ Easy: Number of 1 Bits
- π’ Easy: Counting Bits
- π’ Easy: Reverse Bits
- π’ Easy: Missing Number
- π‘ Medium: Sum of Two Integers
- π‘ Medium: Reverse Integer
Single Number π§
LeetCode Link: Single Number
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 136 - Single Number
Description: Given a non-empty array of integers nums, every element appears twice except for one. Find that single one.
Intuition: Since all elements appear twice in the array except for one, we can use the XOR operation to find the single element. The XOR operation on two equal numbers results in 0, so XORing all the numbers in the array will give us the single number that appears only once.
Approach:
- Initialize a variable 'result' to 0.
- Iterate through the elements in the array 'nums'.
- For each element, XOR it with the 'result'.
- The final value of 'result' will be the single number that appears only once.
β Time Complexity: The time complexity is O(n), where n is the number of elements in the array 'nums'. We iterate through the array once to perform the XOR operation.
πΎ Space Complexity: The space complexity is O(1) since we only use a constant amount of extra space for the 'result' variable.
Solutions π‘
Cpp π»
class Solution {
public:
int singleNumber(vector<int> &nums) {
int result = 0;
// Perform XOR operation on all elements in the array
for (int num : nums) {
result ^= num;
}
return result;
}
};
Python π
class Solution:
def singleNumber(self, nums: List[int]) -> int:
result = 0
for num in nums:
result ^= num
return result
Number of 1 Bits π§
LeetCode Link: Number of 1 Bits
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 191 - Number of 1 Bits
Description: Write a function that takes an unsigned integer and returns the number of '1' bits it has (also known as the Hamming weight).
Intuition: To count the number of '1' bits in the binary representation of a number, we can use the bit manipulation technique. The idea is to repeatedly shift the number to the right and check if the least significant bit is '1'. If it is, then we increment a count variable.
Approach:
- Initialize a variable 'count' to 0.
- Iterate while the input number 'n' is not equal to 0.
- Inside the loop, check if the least significant bit of 'n' is '1'.
- If it is, increment the 'count' variable.
- Right-shift 'n' by 1 to move on to the next bit.
- Continue the loop until all bits are processed.
- Return the 'count' variable, which contains the number of '1' bits in the input number.
β Time Complexity: The time complexity is O(log n), where n is the value of the input number. The number of iterations in the loop is equal to the number of bits in the binary representation of 'n', which is log(n) base 2.
πΎ Space Complexity: The space complexity is O(1) since we only use a constant amount of extra space for the 'count' variable.
Solutions π‘
Cpp π»
class Solution {
public:
int hammingWeight(uint32_t n) {
int count = 0;
// Iterate while n is not 0
while (n != 0) {
// Check if the least significant bit is 1
if (n & 1) {
count++;
}
// Right-shift n to move to the next bit
n >>= 1;
}
return count;
}
};
// class Solution {
// public:
// int hammingWeight(uint32_t n) {
// int count = 0;
// while (n != 0) {
// count++;
// n &= (n - 1); // Clear the least significant 1 bit
// }
// return count;
// }
// };
Python π
class Solution:
def hammingWeight(self, n: int) -> int:
count = 0
while n:
count += n & 1
n = n >> 1
return count
Counting Bits π§
LeetCode Link: Counting Bits
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 338 - Counting Bits
Description: Given a non-negative integer num, for every number i in the range 0 β€ i β€ num, calculate the number of 1's in their binary representation and return them as an array.
Intuition: To count the number of '1' bits in the binary representation of each number, we can use dynamic programming. The idea is to utilize the previously calculated results to build the result for the current number.
Approach:
- Create a vector 'result' of size num+1 to store the counts for all numbers from 0 to num.
- Initialize the first element of 'result' to 0 since the number 0 has 0 '1' bits.
- Use a for loop to iterate from 1 to num.
- For each number i, the number of '1' bits can be calculated using the expression result[i] = result[i >> 1] + (i & 1).
- The expression (i >> 1) calculates the number obtained by right-shifting i by one bit, which is equivalent to dividing i by 2.
- The expression (i & 1) checks if the least significant bit of i is '1'.
- The count of '1' bits for i is the count of '1' bits for (i >> 1) plus the count of the least significant bit (i & 1).
- Continue the loop until all numbers from 0 to num are processed.
- Return the 'result' vector containing the counts for all numbers.
β Time Complexity: The time complexity is O(n), where n is the value of the input number 'num'. The loop iterates 'num' times to calculate the count of '1' bits for each number.
πΎ Space Complexity: The space complexity is O(n) as we use a vector of size 'num+1' to store the counts for all numbers from 0 to num.
Solutions π‘
Cpp π»
class Solution {
public:
vector<int> countBits(int num) {
vector<int> result(num + 1, 0);
for (int i = 1; i <= num; ++i) {
result[i] = result[i >> 1] + (i & 1);
}
return result;
}
};
// class Solution {
// public:
// vector<int> countBits(int num) {
// vector<int> result(num + 1, 0);
// for (int i = 1; i <= num; ++i) {
// result[i] = result[i & (i - 1)] + 1;
// }
// return result;
// }
// };
Python π
class Solution:
def countBits(self, num: int) -> List[int]:
bits_count = [0] * (num + 1)
for i in range(1, num + 1):
bits_count[i] = bits_count[i // 2] + (i % 2)
return bits_count
Reverse Bits π§
LeetCode Link: Reverse Bits
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 190 - Reverse Bits
Description: Reverse bits of a given 32 bits unsigned integer.
Intuition: To reverse the bits of the given integer, we can use bit manipulation techniques.
Approach:
- Initialize a variable 'result' to 0, which will store the reversed bits of the input number.
- Use a for loop to iterate 32 times (since it is a 32-bit integer).
- For each iteration, shift the 'result' to the left by 1 bit to make space for the next bit.
- Extract the least significant bit from the input number using the expression (n & 1).
- Add the extracted bit to the 'result' using the bitwise OR operation (result |= (n & 1)).
- Right shift the input number by 1 bit to get rid of the least significant bit (n >>= 1).
- Continue the loop until all 32 bits are processed.
- After the loop, the 'result' will contain the reversed bits of the input number.
- Return the 'result'.
β Time Complexity: The time complexity is O(1) since the number of iterations is fixed (32 iterations for a 32-bit integer).
πΎ Space Complexity: The space complexity is O(1) as we only use a constant amount of space to store the 'result' and other variables.
Solutions π‘
Cpp π»
#include <cstdint>
class Solution {
public:
uint32_t reverseBits(uint32_t n) {
uint32_t result = 0;
for (int i = 0; i < 32; ++i) {
result <<= 1;
result |= (n & 1);
n >>= 1;
}
return result;
}
};
Python π
class Solution:
def reverseBits(self, n: int) -> int:
reversed_num = 0
for _ in range(32):
reversed_num = (reversed_num << 1) | (n & 1)
n = n >> 1
return reversed_num
Missing Number π§
LeetCode Link: Missing Number
Difficulty: Easy
Problem Explanation π
Problem: LeetCode 268 - Missing Number
Description: Given an array nums containing n distinct numbers in the range [0, n], return the only number in the range that is missing from the array.
Intuition: We can use the mathematical concept of finding the sum of the first n natural numbers and subtract the sum of the elements in the given array to find the missing number.
Approach:
- Initialize a variable 'expectedSum' to store the sum of the first n natural numbers.
- Iterate through the array 'nums' and calculate the sum of its elements, storing it in the variable 'actualSum'.
- Calculate the missing number as 'expectedSum - actualSum' and return it.
β Time Complexity: The time complexity is O(n) as we need to iterate through the entire array once to calculate the sum of its elements.
πΎ Space Complexity: The space complexity is O(1) as we use only a constant amount of extra space to store variables.
Solutions π‘
Cpp π»
class Solution {
public:
int missingNumber(std::vector<int> &nums) {
int n = nums.size();
int expectedSum = n * (n + 1) / 2;
int actualSum = 0;
for (int num : nums) {
actualSum += num;
}
return expectedSum - actualSum;
}
};
Python π
class Solution:
def missingNumber(self, nums: List[int]) -> int:
n = len(nums)
missing_num = n
for i in range(n):
missing_num ^= i ^ nums[i]
return missing_num
Sum of Two Integers π§
LeetCode Link: Sum of Two Integers
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 371 - Sum of Two Integers
Description: Given two integers a and b, return the sum of the two integers without using the '+' and '-' operators.
Intuition: We can use bitwise operations to perform addition without using the '+' operator. The bitwise XOR operation (^) will give us the sum of two integers without considering the carry. To handle the carry, we can use the bitwise AND operation (&) and left shift (<<) to calculate the carry.
Approach:
-
While 'b' is not equal to 0 (there is still a carry): a. Calculate the carry as 'carry = a & b'. b. Update 'a' as 'a = a ^ b' to get the sum without carry. c. Update 'b' as 'b = carry << 1' to prepare for the next iteration.
-
Return 'a' as the final sum.
β Time Complexity: The time complexity is O(1) because we perform a constant number of bitwise operations.
πΎ Space Complexity: The space complexity is O(1) as we use only a constant amount of extra space.
Solutions π‘
Cpp π»
class Solution {
public:
int getSum(int a, int b) {
while (b != 0) {
int carry = (unsigned int)(a & b) << 1; // Use unsigned int to handle negative numbers
a = a ^ b;
b = carry;
}
return a;
}
};
Python π
class Solution:
def getSum(self, a: int, b: int) -> int:
MASK = 0xFFFFFFFF
MAX_INT = 0x7FFFFFFF
while b != 0:
a, b = (a ^ b) & MASK, ((a & b) << 1) & MASK
return a if a <= MAX_INT else ~(a ^ MASK)
Reverse Integer π§
LeetCode Link: Reverse Integer
Difficulty: Medium
Problem Explanation π
Problem: LeetCode 7 - Reverse Integer
Description: Given a signed 32-bit integer x, return x with its digits reversed. If reversing x causes the value to go outside the signed 32-bit integer range [-2^31, 2^31 - 1], then return 0.
Intuition: To reverse an integer, we can repeatedly extract the last digit of the number and add it to the result after multiplying the result by 10.
Approach:
- Initialize a variable 'result' to store the reversed number and set it to 0.
- While the input 'x' is not equal to 0: a. Extract the last digit of 'x' using 'x % 10'. b. Check if 'result' is going to overflow:
- If 'result' is greater than INT_MAX/10 or 'result' is equal to INT_MAX/10 and the last digit is greater than 7, then return 0. c. Multiply 'result' by 10 and add the last digit to it. d. Update 'x' by removing the last digit using 'x /= 10'.
- Return the 'result' as the reversed number.
β Time Complexity: The time complexity of this approach is O(log(x)), where x is the given input number.
πΎ Space Complexity: The space complexity is O(1) as we are using only a constant amount of extra space.
Solutions π‘
Cpp π»
class Solution {
public:
int reverse(int x) {
int result = 0;
while (x != 0) {
int lastDigit = x % 10;
if (result > INT_MAX / 10 || (result == INT_MAX / 10 && lastDigit > 7)) {
return 0;
}
if (result < INT_MIN / 10 || (result == INT_MIN / 10 && lastDigit < -8)) {
return 0;
}
result = result * 10 + lastDigit;
x /= 10;
}
return result;
}
};
Python π
class Solution:
def reverse(self, x: int) -> int:
INT_MAX = 2**31 - 1
INT_MIN = -(2**31)
reversed_num = 0
sign = 1 if x > 0 else -1
x = abs(x)
while x != 0:
pop = x % 10
x //= 10
if reversed_num > (INT_MAX - pop) // 10:
return 0
reversed_num = reversed_num * 10 + pop
return reversed_num * sign