- September Week 4
- 24.09.23 - 2707. Extra Characters in a String
- 24.09.24 - 3043. Find the Length of the Longest Common Prefix
- 24.09.25 - 2416. Sum of Prefix Scores of Strings
- 24.09.26 - 729. My Calendar I
- 24.09.27 - 731. My Calendar II
- 24.09.28 - 641. Design Circular Deque
- 24.09.29 - 432. All O`one Data Structure
- 24.09.30 - 1381. Design a Stack With Increment Operation
답지 참고..
- DP.
- Trie
The problem is about breaking a given string, s of length n, into non-overlapping substrings such that each substring is present in a given dictionary of words. The objective is to minimize the number of extra characters left over after the string is broken up optimally. The maximum number of characters that could possibly be left over after breaking up the string is n. This is the case where we find no match in the dictionary and all the characters must be removed. In the best-case scenario, no characters need to be removed (i.e. we can match every character to a non-overlapping substring).
We will consider breaking the given string into non-overlapping substrings that exist in the dictionary while minimizing the number of extra characters left over.
To solve this problem, we can utilize a recursive approach with memoization. We define a recursive function dp that takes an index start as a parameter. This index represents the current position in the string where we are considering adding characters to form a valid word. The function dp returns the minimum number of extra characters needed to form a valid concatenation of words starting from the start index.
The dp function represents the dynamic programming approach used to solve the problem. It takes a single argument, start, which represents the index in the string s that we are currently considering. We will try to find a word in dictionary that starts at this index.
The recurrence relation in the dp function is as follows:
- If the
startindex reaches the end of the string (start == n), indicating that we have considered all characters ins, the function returns 0, as no extra characters are needed. - If the
startindex is not at the end of the string, the function considers two possibilities:- Counting the current character at
startas an extra character by recursively callingdpwith the next index (start + 1). This corresponds to the case where the current character is not part of any valid word in the dictionary. The result is incremented by 1, as we are counting the current character as an extra. - Iterating over all possible
endindices fromstartto the end of the string. For eachend, the function checks if the substrings[start:end+1]exists indictionary. We can convertdictionaryto a set before starting the DP to make these checks more efficient. If it does, the function recursively callsdpwith the next index after the valid word's end index,end + 1. The result is updated to the minimum value between the current minimum and the value returned from the recursive call.
- Counting the current character at
For each recursive call, we keep track of the minimum number of extra characters needed to form a valid concatenation. We update this minimum by considering both possibilities and selecting the option with the minimum number of extra characters.
To optimize the solution, we use memoization, which allows us to avoid redundant calculations. By caching the results of previously computed recursive calls, we can retrieve them directly instead of recomputing them, which significantly improves the efficiency of the algorithm.
The initial call to the recursive function is made with start set to 0, indicating that we start from the beginning of the string. The result of the function is the minimum number of extra characters needed to form a valid concatenation of words from the dictionary.
Here's how this algorithm will work for the string s "LTSCD" and dictionary ["LT", "CD"]:
- To achieve
O(1)lookups, convert the list of strings in the dictionary to a set. - Define a recursive function called
dpthat takes the starting index of the substring as a parameter. - At each recursive call of
dpcheck if the starting indexstarthas reached the end of the strings. If so, return 0. - Set
ans, the answer for the current state, todp(start + 1) + 1. - If the starting index is not at the end of the string, explore all possible substrings starting from the current index
start. - For each possible substring, check if it exists in the
dictionary. If it does, recursively calculate the minimum number of extra characters starting from the next indexdp(end + 1). - Keep track of the minimum number of extra characters encountered so far (
ans) and update it whenever a lower value is found. - To optimize the solution and avoid redundant computations, utilize memoization. Store the results of previously computed subproblems in a separate data structure.
- Finally, call the
dpfunction with the starting index set to 0.
class Solution {
public:
int minExtraChar(string s, vector<string> dictionary) {
int n = s.length();
unordered_set<string> dictionarySet(dictionary.begin(), dictionary.end());
unordered_map<int, int> memo;
function<int(int)> dp = [&](int start) {
if (start == n) {
return 0;
}
if (memo.count(start)) {
return memo[start];
}
// To count this character as a left over character
// move to index 'start + 1'
int ans = dp(start + 1) + 1;
for (int end = start; end < n; end++) {
auto curr = s.substr(start, end - start + 1);
if (dictionarySet.count(curr)) {
ans = min(ans, dp(end + 1));
}
}
return memo[start] = ans;
};
return dp(0);
}
};Let
Let dictionary.
Let dictionary.
-
Time complexity:
$O(N^3)$
There can be$N + 1$ unique states of thedpmethod. In each state ofdp, we iterate overend, which is$O(N)$ iterations. In each of these iterations, we create a substring, which costs$O(N)$ . Hence, the overall cost of thedpmethod is$O(N^3)$ . -
Space complexity:
$O(N + M \cdot K)$
The HashSet used to store the strings in thedictionarywill incur a cost of$O(M \cdot K)$ . Additionally, thedpmethod will consume stack space and traverse to a depth of$N$ in the worst-case scenario, resulting in a cost of$O(N)$ .
As shown in the first approach to solve this problem, we can utilize a dynamic programming approach. But this time bottom up. This solution converts the top-down approach used above to a bottom-up approach. We start by initializing a dynamic programming table dp with values corresponding to the minimum number of extra characters at each position in the string. Notice that here, dp[start] is equal to dp(start) from the previous approach.
With bottom-up, we need to start from the base case. The base case we defined above is when start = n. Thus, we iterate through the string backward (starting from n - 1), considering each position as a potential starting point for a substring. For each position, we can apply the same recurrence from the previous approach - explore all possible substrings starting from that point and calculate the minimum number of extra characters associated with each substring. We update the dynamic programming table accordingly.
By the end of the iteration, the value at the first position of the dynamic programming table represents the minimum number of extra characters left over after breaking the string optimally. This value is our desired result, which we return as the output.
- To achieve
O(1)lookups, convert the list of strings in the dictionary to a set. - Create a dynamic programming array
dpof sizen + 1. - Iterate over the string
sfrom right to left, starting from last character (n - 1) down to the first character (0). - Initialize
dp[start]bydp[start + 1] + 1to consider the case where the character at indexstartis an extra character. - For each starting index
start, consider all possible substrings starting fromstartand ending at various indicesendfromstartton - 1. - If the substring from
starttoendis found in thedictionaryset, updatedp[start]by taking the minimum of its current value anddp[end + 1]. - Finally, return the value at
dp[0].
class Solution {
public:
int minExtraChar(string s, vector<string> dictionary) {
int n = s.length();
unordered_set<string> dictionarySet(dictionary.begin(), dictionary.end());
vector<int> dp(n + 1, 0);
for (int start = n - 1; start >= 0; start--) {
dp[start] = dp[start + 1] + 1;
for (int end = start; end < n; end++) {
auto curr = s.substr(start, end - start + 1);
if (dictionarySet.count(curr)) {
dp[start] = min(dp[start], dp[end + 1]);
}
}
}
return dp[0];
}
};Let
Let dictionary.
Let dictionary.
-
Time complexity:
$O(N^3)$
The two nested loops used to perform the dynamic programming operation cost$O(N^2)$ . The substring method inside the inner loop costs another$O(N)$ . Hence, the overall time complexity is$O(N^3)$ . -
Space complexity:
$O(N + M \cdot K)$
The HashSet used to store the strings in thedictionarywill incur a cost of$O(M \cdot K)$ . Thedparray will incur a cost of $O(N)`.
To optimize the top-down dynamic programming approach shared earlier we can try to get rid of the substring method. If we can get rid of the substring method we can reduce the time complexity to
First, we create a trie data structure by building a trie from the given dictionary of words. Each TrieNode represents a character, and we connect the nodes to form a hierarchical structure based on the characters in the words. We mark the nodes that correspond to the end of a word.
To find the minimum number of extra characters, we use the same recursive function dp from the first approach, with a few modifications. It takes an index representing the starting position in the string.
Like in the first approach, we initialize the answer for a given start index as dp(start + 1) + 1. Then we try all possible end positions by iteration over the string starting from end = start. As we iterate, we traverse the Trie data structure to check if the characters in the string exist in the trie.
If we encounter a TrieNode marked as the end of a word, we update the minimum count by recursively calling dp on the next index without adding any extra characters. If we find that no TrieNode exists at all for a character, we can immediately break since no words will exist beyond this point.
- Start by defining a
TrieNodeclass withchildrenandis_wordattributes. Each node represents a character in the trie. - The
buildTriefunction is used to construct the trie by iterating through each word in the dictionary and adding it to the trie character by character. - Define a recursive helper function called
dp. - At each recursive call of
dpcheck if the starting indexstarthas reached the end of the strings. If so, return 0. - The base case of the recursion is when the starting index reaches the end of the string, in which case it returns 0.
- Traverse the trie starting from the root and follow the characters of the substring, checking if each character exists in the trie.
- If a character is not found in the trie, break out of the loop.
- If a valid substring is found in the trie (
node.is_word == true), calldp(end + 1). - Track the minimum number of extra characters encountered so far(
ans) and update it whenever a lower value is found. - To optimize the solution, apply memoization. Store the results of previously computed subproblems in a separate data structure.
- Finally, call
dpwith the starting index set to 0.
class TrieNode {
public:
unordered_map<char, TrieNode*> children;
bool is_word;
};
class Solution {
public:
int minExtraChar(string s, vector<string>& dictionary) {
int n = s.length();
auto root = buildTrie(dictionary);
unordered_map<int, int> memo;
function<int(int)> dp = [&](int start) {
if (start == n) {
return 0;
}
if (memo.count(start)) {
return memo[start];
}
// To count this character as a left over character
// move to index 'start + 1'
int ans = dp(start + 1) + 1;
TrieNode* node = root;
for (int end = start; end < n; end++) {
char c = s[end];
if (node->children.find(c) == node->children.end()) {
break;
}
node = node->children[c];
if (node->is_word) {
ans = min(ans, dp(end + 1));
}
}
return memo[start] = ans;
};
return dp(0);
}
TrieNode* buildTrie(vector<string>& dictionary) {
auto root = new TrieNode();
for (auto& word : dictionary) {
auto node = root;
for (auto& c : word) {
if (node->children.find(c) == node->children.end()) {
node->children[c] = new TrieNode();
}
node = node->children[c];
}
node->is_word = true;
}
return root;
}
};Let
Let dictionary.
Let dictionary.
-
Time complexity:
$O(N^2 + M \cdot K)$
There can be$N + 1$ unique states of thedpmethod. Each state of thedpmethod costs$O(N)$ to compute. Hence, the overall cost of thedpmethod is$O(N^2)$ . Building the trie costs$O(M \cdot K)$ . -
Space complexity:
$O(N + M \cdot K)$
The Trie used to store the strings in thedictionarywill incur a cost of$O(M \cdot K)$ . Additionally, thedpmethod will consume stack space and traverse to a depth of$N$ , resulting in a cost of $O(N)`.
We can optimize the bottom-up approach the same way we optimized the top-down approach, by using a Trie to avoid needing to create substrings.
We initialize a dynamic programming table, dp, where each position represents the minimum number of extra characters starting from that index. This is the same table as the one from approach 2.
For each index start, we initialize dp[start] = dp[start + 1] + 1 as the base case. Then we iterate backward through the string, starting from the last index. For each index, we update the corresponding value in the dp table by considering all possible substrings starting from that position. We traverse the Trie data structure, checking if the characters in the string exist in the Trie. If a character doesn't exist in the Trie, we can immediately break.
If we encounter a TrieNode marked as the end of a word during traversal, we update the dp value at the start index by taking the minimum between the current value and the value at the end index without adding any extra characters.
The algorithm used in the solution can be explained in the following short points:
- Define a
TrieNodeclass withchildrenandis_wordattributes. Each node represents a character in the trie. - The
buildTriefunction is used to construct the trie by iterating through each word in the dictionary and adding it to the trie character by character. - Initialize the root of the trie, the length of the input string, and a dynamic programming array
dpof sizen + 1. - Iterate over the string
sfrom right to left, starting from the last character down to the first character. - For each starting index
start, calculate the minimum number of extra characters needed to break down the substring fromstartto the end of the string. - Initialize
dp[start]withdp[start + 1] + 1. - Traverse the trie starting from the root and follow the characters of the substring, checking if each character exists in the trie.
- If a character is not found in the trie, break out of the for loop.
- If a valid substring is found in the trie (
node.is_word == true), updatedp[start]by taking the minimum of its current value anddp[end + 1]. - Finally, return the value at
dp[0].
class TrieNode {
public:
unordered_map<char, TrieNode*> children;
bool is_word;
};
class Solution {
public:
int minExtraChar(string s, vector<string>& dictionary) {
int n = s.length();
auto root = buildTrie(dictionary);
vector<int> dp(n + 1, 0);
for (int start = n - 1; start >= 0; start--) {
dp[start] = dp[start + 1] + 1;
auto node = root;
for (int end = start; end < n; end++) {
if (node->children.find(s[end]) == node->children.end()) {
break;
}
node = node->children[s[end]];
if (node->is_word) {
dp[start] = min(dp[start], dp[end + 1]);
}
}
}
return dp[0];
}
TrieNode* buildTrie(vector<string>& dictionary) {
auto root = new TrieNode();
for (auto& word : dictionary) {
auto node = root;
for (auto& c : word) {
if (node->children.find(c) == node->children.end()) {
node->children[c] = new TrieNode();
}
node = node->children[c];
}
node->is_word = true;
}
return root;
}
};Let
Let dictionary.
Let dictionary.
-
Time complexity:
$O(N^2 + M \cdot K)$
The two nested for loops that are being used for the dynamic programming operation cost$O(N^2)$ . Building the trie costs $O(M \cdot K)`. -
Space complexity: $O(N + M \cdot K)
The Trie used to store the strings indictionarywill incur a cost of $O(M \cdot K). Thedparray will incur a cost of $O(N)`.
파멸적인 시간.
// 1502ms, 156.44MB
class Solution {
public:
int longestCommonPrefix(vector<int>& arr1, vector<int>& arr2) {
// Put all the possible prefixes of each element in arr1 into a HashSet.
set<string> S;
for(int n : arr1) {
string str = to_string(n);
for(int s{}, e = str.length();s<e;++s) {
string tmp = str.substr(0, s + 1);
if(S.count(tmp)) continue;
S.insert(tmp);
}
}
int answer{};
for(int n : arr2) {
string str = to_string(n);
for(int s{}, e = str.length();s<e;++s) {
string tmp = str.substr(0, s + 1);
if(S.count(tmp)) {
answer = max(answer, static_cast<int>(tmp.length()));
}
}
}
return answer;
}
};가져오기 귀찮음.
class Solution {
public:
int longestCommonPrefix(vector<int>& arr1, vector<int>& arr2) {
unordered_set<int> arr1Prefixes; // Set to store all prefixes from arr1
// Step 1: Build all possible prefixes from arr1
for (int val : arr1) {
while (!arr1Prefixes.count(val) && val > 0) {
// Insert current value as a prefix
arr1Prefixes.insert(val);
// Generate the next shorter prefix by removing the last digit
val /= 10;
}
}
int longestPrefix = 0;
// Step 2: Check each number in arr2 for the longest matching prefix
for (int val : arr2) {
while (!arr1Prefixes.count(val) && val > 0) {
// Reduce val by removing the last digit if not found in the
// prefix set
val /= 10;
}
if (val > 0) {
// Length of the matched prefix using log10 to determine the
// number of digits
longestPrefix =
max(longestPrefix, static_cast<int>(log10(val) + 1));
}
}
return longestPrefix;
}
};Let arr1, arr2, arr1, and arr2.
-
Time Complexity: $$ O(m \cdot \log_{10}(M) + n \cdot \log_{10}(N)) $$ For each number in
arr1, we repeatedly divide the number by 10 to generate its prefixes. Since dividing a number by 10 reduces the number of digits logarithmically, this process takes$O(\log_{10}(M))$ for each number inarr1. Hence, for$m$ numbers, the total time complexity is$O(m \cdot \log_{10}(M))$ . Similarly, for each number inarr2, we reduce it by repeatedly dividing it by 10 to check if it matches any prefix in the set. This also takes$O(\log_{10}(N))$ for each number in `arr2$. Hence, for$n$ numbers, the total time complexity is$O(n \cdot \log_{10}(N))$ . The overall time complexity is$O(m \cdot \log_{10}(M) + n \cdot \log_{10}(N))$ . -
Space Complexity: $$ O(m \cdot \log_{10}(M)) $$ Each number in
arr1contributes$O(\log_{10}(M))$ space to the set, as it generates prefixes proportional to the number of digits (logarithmic in the value of the number with base 10). With$m$ numbers inarr1, the total space complexity for the set is$O(m \cdot \log_{10}(M))$ . The algorithm uses constant space for variables likelongestPrefixand loop variables, so this doesn’t contribute significantly to the space complexity. Thus, the total space complexity is$O(m \cdot \log_{10}(M))$ .
class TrieNode {
public:
// Each node has up to 10 possible children (digits 0-9)
TrieNode* children[10];
TrieNode() {
for (int i = 0; i < 10; ++i) {
children[i] = nullptr;
}
}
};
class Trie {
public:
TrieNode* root;
Trie() { root = new TrieNode(); }
// Insert a number into the Trie by treating it as a string of digits
void insert(int num) {
TrieNode* node = root;
string numStr = to_string(num);
for (char digit : numStr) {
int idx = digit - '0';
if (!node->children[idx]) {
node->children[idx] = new TrieNode();
}
node = node->children[idx];
}
}
// Find the longest common prefix for a number in arr2 with the Trie
int findLongestPrefix(int num) {
TrieNode* node = root;
string numStr = to_string(num);
int len = 0;
for (char digit : numStr) {
int idx = digit - '0';
if (node->children[idx]) {
// Increase length if the current digit matches
len++;
node = node->children[idx];
} else {
// Stop if no match for the current digit
break;
}
}
return len;
}
};
class Solution {
public:
int longestCommonPrefix(vector<int>& arr1, vector<int>& arr2) {
Trie trie;
// Step 1: Insert all numbers from arr1 into the Trie
for (int num : arr1) {
trie.insert(num);
}
int longestPrefix = 0;
// Step 2: Find the longest prefix match for each number in arr2
for (int num : arr2) {
int len = trie.findLongestPrefix(num);
longestPrefix = max(longestPrefix, len);
}
return longestPrefix;
}
};Let arr1, arr2.
-
Time Complexity: $$ O(m \cdot d + n \cdot d) = O(m + n) $$ For each number in
arr1, we insert it into the Trie by processing each digit. Since each number has up to$d$ digits, inserting a single number takes$O(d)$ time. Therefore, inserting all$m$ numbers fromarr1into the Trie takes$O(m \cdot d)$ time. For each number inarr2, we check how long its prefix matches with any prefix in the Trie. This involves traversing up to$d$ digits of the number, which takes$O(d)$ time per number. For all$n$ numbers in `arr2$, the time complexity for this step is$O(n \cdot d)$ . Overall, the total time complexity is$O(m \cdot d + n \cdot d) = O(m + n)$ . -
Space Complexity: $$ O(m \cdot d) = O(m) $$ Each node in the Trie represents a digit (0-9), and each number from
arr1can contribute up to$d$ nodes. Thus, the total space used by the Trie for storing all prefixes is$O(m \cdot d)$ . The algorithm uses constant space for variables likelongestPrefixand loop variables, which is negligible compared to the space used by the Trie. Thus, the total space complexity is$O(m \cdot d) = O(m)$ .
Trie
// 1082ms, 703.54MB
struct Trie {
int cnt{};
unordered_map<char, Trie*> um;
Trie() = default;
};
class Solution {
public:
vector<int> sumPrefixScores(vector<string>& words) {
Trie * root = new Trie{};
for(string& word : words) {
Trie * now = root;
for(char c : word) {
if(now->um.count(c) == 0) { // 새로 만들기
Trie * newNode = new Trie{};
now->um.emplace(c, newNode);
}
now = now->um[c]; // 이동
now->cnt++;
}
}
vector<int> answer;
for(string& word : words) {
int sum{};
Trie * now = root;
for(char c : word) {
now = now->um[c];
sum += now->cnt;
}
answer.push_back(sum);
}
delete root;
return answer;
}
};vector말고 그냥 배열
// 541ms, 704.04MB
struct Trie {
int cnt{};
Trie* next[26]{};
Trie() = default;
};
class Solution {
public:
vector<int> sumPrefixScores(vector<string>& words) {
Trie* root = new Trie{};
for(string& word : words) {
Trie* now = root;
for(char c : word) {
if(!now->next[c - 'a']) {
Trie * newNode = new Trie{};
now->next[c-'a'] = newNode;
}
now = now->next[c - 'a']; // 이동
now->cnt++;
}
}
vector<int> answer;
for(string& word : words) {
int sum{};
Trie * now = root;
for(char c : word) {
now = now->next[c - 'a']; // 이동
sum += now->cnt;
}
answer.push_back(sum);
}
delete root;
return answer;
}
};Solution 참고 - Brute Force
// 88ms, 41.58MB
// Brute Force
class MyCalendar {
private:
vector<pair<int, int>> calender;
public:
MyCalendar() {
}
bool book(int start, int end) {
for(auto [s, e] : calender) {
if(start < e && s < end) {
return false;
}
}
calender.emplace_back(start, end);
return true;
}
};
/**
* Your MyCalendar object will be instantiated and called as such:
* MyCalendar* obj = new MyCalendar();
* bool param_1 = obj->book(start,end);
*/class MyCalendar {
private:
set<pair<int, int>> calendar;
public:
bool book(int start, int end) {
const pair<int, int> event{start, end};
const auto nextEvent = calendar.lower_bound(event);
if (nextEvent != calendar.end() && nextEvent->first < end) {
return false;
}
if (nextEvent != calendar.begin()) {
const auto prevEvent = prev(nextEvent);
if (prevEvent->second > start) {
return false;
}
}
calendar.insert(event);
return true;
}
};Like Approach 1, let
-
Time Complexity: $$ O(N \cdot \log N) $$ For each new event, we search to ensure that the event is legal in
$O(\log N)$ time, then insert it in$O(\log N)$ time. Thus, the overall time complexity is$O(N \cdot \log N)$ . -
Space Complexity: $$ O(N) $$ This is the space complexity due to the size of the data structures used, which scales linearly with the number of events,
$N$ .
다른 아이디어가 없다..
class MyCalendarTwo {
public:
vector<pair<int, int>> bookings;
vector<pair<int, int>> overlapBookings;
MyCalendarTwo() {}
bool book(int start, int end) {
// Returns false if the new booking overlaps with the existing
// double-booked bookings.
for (pair<int, int> booking : overlapBookings) {
if (doesOverlap(booking.first, booking.second, start, end)) {
return false;
}
}
// Add the double overlapping if any with the new booking.
for (pair<int, int> booking : bookings) {
if (doesOverlap(booking.first, booking.second, start, end)) {
overlapBookings.push_back(
getOverlapped(booking.first, booking.second, start, end));
}
}
// Add the booking to the list of bookings.
bookings.push_back({start, end});
return true;
}
private:
// Return true if the booking [start1, end1) & [start2, end2) overlaps.
bool doesOverlap(int start1, int end1, int start2, int end2) {
return max(start1, start2) < min(end1, end2);
}
// Return overlapping booking between [start1, end1) & [start2, end2).
pair<int, int> getOverlapped(int start1, int end1, int start2, int end2) {
return {max(start1, start2), min(end1, end2)};
}
};Here, bookings.
-
Time Complexity: $$ O(N) $$ The time complexity for the
book(start, end)function is$O(N)$ because we iterate through thebookingslist to check for overlaps and possibly add a new booking. Additionally, we check theoverlapBookingslist, which tracks overlaps. Since the size ofoverlapBookingsis always smaller than or equal to the size ofbookings, the overall time complexity remains$O(N)$ . -
Space Complexity: $$ O(N) $$ We maintain two lists:
bookingsfor all the bookings andoverlapBookingsfor the overlapping intervals. The size ofoverlapBookingscan never exceed the size ofbookings, so the total space complexity is$O(N)$ .
class MyCalendarTwo {
public:
// Store the number of bookings at each point.
map<int, int> bookingCount;
// The maximum number of overlapped bookings allowed.
int maxOverlappedBooking;
MyCalendarTwo() { maxOverlappedBooking = 2; }
bool book(int start, int end) {
// Increase and decrease the booking count at the start and end
// respectively.
bookingCount[start]++;
bookingCount[end]--;
int overlappedBooking = 0;
// Find the prefix sum.
for (pair<int, int> bookings : bookingCount) {
overlappedBooking += bookings.second;
// If the number of bookings is more than 2, return false.
// Also roll back the counts for this booking as we won't add it.
if (overlappedBooking > maxOverlappedBooking) {
bookingCount[start]--;
bookingCount[end]++;
// Remove the entries from the map to avoid unnecessary
// iteration.
if (bookingCount[start] == 0) {
bookingCount.erase(start);
}
if (bookingCount[end] == 0) {
bookingCount.erase(end);
}
return false;
}
}
return true;
}
};Here, bookings.
-
Time Complexity: $$ O(N) $$ The time complexity for the
book(start, end)function is$O(N)$ . This is because we iterate over the bookings entries in the map and find the prefix sum. The number of entries would be$O(N)$ , and for each of these, we can have constant time operations with$O(\log N)$ complexity. Once we find a triple booking, we return, and no more iterations are required. Hence, the time complexity for the functionbook(start, end)becomes$O(N)$ . -
Space Complexity: $$ O(N) $$ The space complexity is
$O(N)$ because we store the start and end points of each booking in the map. Each booking requires two entries in the map, so for$N$ bookings, we store$2N$ entries. Therefore, the space complexity is proportional to$N$ .
자료 구조 문제..
struct Node {
int val;
Node* next;
Node* prev;
Node(int val, Node* next = NULL, Node* prev = NULL)
: val(val), next(next), prev(prev) {}
};
class MyCircularDeque {
private:
Node* head;
Node* rear;
int size;
int capacity;
public:
MyCircularDeque(int k) {
head = NULL;
rear = NULL;
size = 0;
capacity = k;
}
bool insertFront(int value) {
if (isFull()) return false;
if (head == NULL) {
head = new Node(value);
rear = head;
} else {
Node* newHead = new Node(value);
newHead->next = head;
head->prev = newHead;
head = newHead;
}
size++;
return true;
}
bool insertLast(int value) {
if (isFull()) return false;
if (head == NULL) {
head = new Node(value);
rear = head;
} else {
Node* newNode = new Node(value, NULL, rear);
rear->next = newNode;
rear = newNode;
}
size++;
return true;
}
bool deleteFront() {
if (isEmpty()) return false;
if (size == 1) {
head = NULL;
rear = NULL;
} else {
Node* nextNode = head->next;
delete head;
head = nextNode;
}
size--;
return true;
}
bool deleteLast() {
if (isEmpty()) return false;
if (size == 1) {
head = NULL;
rear = NULL;
} else {
Node* prevNode = rear->prev;
delete rear;
rear = prevNode;
}
size--;
return true;
}
int getFront() { return (isEmpty()) ? -1 : head->val; }
int getRear() { return (isEmpty()) ? -1 : rear->val; }
bool isEmpty() { return size == 0; }
bool isFull() { return size == capacity; }
};-
Time Complexity: $$ O(1) $$ Because we maintain access to the front and rear elements at all times, all operations simply involve pointer manipulations that take
$O(1)$ time. -
Space Complexity: $$ O(k) $$ In the worst case, there will be maximum
$k$ nodes in our doubly linked list, which will involve instantiating$k$ node objects and thus take$O(k)$ space.
class MyCircularDeque {
private:
vector<int> queue;
int front;
int rear;
int size;
int capacity;
public:
MyCircularDeque(int k) {
queue = vector<int>(k);
size = 0;
capacity = k;
front = 0;
rear = k - 1;
}
bool insertFront(int value) {
if (isFull()) return false;
front = (front - 1 + capacity) % capacity;
queue[front] = value;
size++;
return true;
}
bool insertLast(int value) {
if (isFull()) return false;
rear = (rear + 1) % capacity;
queue[rear] = value;
size++;
return true;
}
bool deleteFront() {
if (isEmpty()) return false;
front = (front + 1) % capacity;
size--;
return true;
}
bool deleteLast() {
if (isEmpty()) return false;
rear = (rear - 1 + capacity) % capacity;
size--;
return true;
}
int getFront() {
if (isEmpty()) return -1;
return queue[front];
}
int getRear() {
if (isEmpty()) return -1;
return queue[rear];
}
bool isEmpty() { return size == 0; }
bool isFull() { return size == capacity; }
};-
Time Complexity: $$ O(1) $$ Similar to Approach 1, we maintain the references for the front and rear elements at all times, where all operations are simply arithmetic operations that take
$O(1)$ time. -
Space Complexity: $$ O(k) $$ Our fixed-sized array will always have
$k$ elements and thus will take$O(k)$ space.
답지 참고..
#### Approach: Using Doubly Linked List
```cpp
class Node {
public:
int freq;
Node* prev;
Node* next;
unordered_set<string> keys;
Node(int freq) : freq(freq), prev(nullptr), next(nullptr) {}
};
class AllOne {
private:
Node* head; // Dummy head
Node* tail; // Dummy tail
unordered_map<string, Node*> map; // Mapping from key to its node
public:
// Initialize your data structure here.
AllOne() {
head = new Node(0); // Create dummy head
tail = new Node(0); // Create dummy tail
head->next = tail; // Link dummy head to dummy tail
tail->prev = head; // Link dummy tail to dummy head
}
// Inserts a new key <Key> with value 1. Or increments an existing key by 1.
void inc(string key) {
if (map.find(key) != map.end()) {
Node* node = map[key];
int freq = node->freq;
node->keys.erase(key); // Remove key from current node
Node* nextNode = node->next;
if (nextNode == tail || nextNode->freq != freq + 1) {
// Create a new node if next node does not exist or freq is not
// freq + 1
Node* newNode = new Node(freq + 1);
newNode->keys.insert(key);
newNode->prev = node;
newNode->next = nextNode;
node->next = newNode;
nextNode->prev = newNode;
map[key] = newNode;
} else {
// Increment the existing next node
nextNode->keys.insert(key);
map[key] = nextNode;
}
// Remove the current node if it has no keys left
if (node->keys.empty()) {
removeNode(node);
}
} else { // Key does not exist
Node* firstNode = head->next;
if (firstNode == tail || firstNode->freq > 1) {
// Create a new node
Node* newNode = new Node(1);
newNode->keys.insert(key);
newNode->prev = head;
newNode->next = firstNode;
head->next = newNode;
firstNode->prev = newNode;
map[key] = newNode;
} else {
firstNode->keys.insert(key);
map[key] = firstNode;
}
}
}
// Decrements an existing key by 1. If Key's value is 1, remove it from the
// data structure.
void dec(string key) {
if (map.find(key) == map.end()) {
return; // Key does not exist
}
Node* node = map[key];
node->keys.erase(key);
int freq = node->freq;
if (freq == 1) {
// Remove the key from the map if freq is 1
map.erase(key);
} else {
Node* prevNode = node->prev;
if (prevNode == head || prevNode->freq != freq - 1) {
// Create a new node if the previous node does not exist or freq
// is not freq - 1
Node* newNode = new Node(freq - 1);
newNode->keys.insert(key);
newNode->prev = prevNode;
newNode->next = node;
prevNode->next = newNode;
node->prev = newNode;
map[key] = newNode;
} else {
// Decrement the existing previous node
prevNode->keys.insert(key);
map[key] = prevNode;
}
}
// Remove the node if it has no keys left
if (node->keys.empty()) {
removeNode(node);
}
}
// Returns one of the keys with maximal value.
string getMaxKey() {
if (tail->prev == head) {
return ""; // No keys exist
}
return *(tail->prev->keys.begin()); // Return one of the keys from the
// tail's previous node
}
// Returns one of the keys with minimal value.
string getMinKey() {
if (head->next == tail) {
return ""; // No keys exist
}
return *(
head->next->keys
.begin()); // Return one of the keys from the head's next node
}
private:
void removeNode(Node* node) {
Node* prevNode = node->prev;
Node* nextNode = node->next;
prevNode->next = nextNode; // Link previous node to next node
nextNode->prev = prevNode; // Link next node to previous node
delete node; // Free the memory of the removed node
}
};-
Time complexity: $$ O(1) $$ The
incanddecmethods both perform operations that are constant time. Ininc, whether inserting a new key or updating an existing one, the operations primarily involve updating pointers in the linked list and updating the hash map, which are$O(1)$ operations.Similarly, in
dec, removing a key, updating the hash map, and possibly creating a new node or modifying the previous node also take constant time. Therefore, both operations run in$O(1)$ .The
getMaxKeyandgetMinKeymethods return a key from the front or back of the linked list, which is also$O(1)$ since it involves accessing the first or last element of the list. -
Space complexity: $$ O(N) $$ The space used by the
AllOnedata structure is primarily due to the hash map and the linked list ofNodes.The hash map stores pointers to nodes for each unique key, requiring
$O(N)$ space where$N$ is the number of unique keys.Each
Nodecontains a set ofkeys, which can also grow with the number of unique keys in the worst case. Hence, the total space consumed by the linked list of nodes will also contribute to$O(N)$ .
// 28ms, 25.99MB
class CustomStack {
public:
CustomStack(int maxSize) {
st.assign(maxSize, 0);
now = 0;
siz = maxSize;
}
void push(int x) {
if(now >= siz) return; // overflow
st[now++] = x;
}
int pop() {
if(now == 0) return -1; // empty
return st[--now];
}
void increment(int k, int val) {
for(int i{}, e = min(now, k);i<e;++i) {
st[i] += val;
}
}
private:
vector<int> st;
int now{}, siz{};
};
/**
* Your CustomStack object will be instantiated and called as such:
* CustomStack* obj = new CustomStack(maxSize);
* obj->push(x);
* int param_2 = obj->pop();
* obj->increment(k,val);
*/The problem on LeetCode asks you to design a custom stack that supports three operations: push, pop, and increment. Here's a high-level approach to solve the problem efficiently:
push(x): Adds an elementxto the stack.pop(): Removes the top element from the stack and returns it. If empty, returns-1.increment(k, val): Increments the bottomkelements of the stack byval.
- Use a normal stack for
pushandpop. - For
increment, instead of incrementing directly, use an auxiliary array to track pending increments, applying them only duringpop()to avoid repeated updates.
Here's the C++ code implementation:
// 35ms, 25.92MB
class CustomStack {
public:
vector<int> stack;
vector<int> inc;
int maxSize;
CustomStack(int maxSize) {
this->maxSize = maxSize;
}
void push(int x) {
if (stack.size() < maxSize) {
stack.push_back(x);
inc.push_back(0);
}
}
int pop() {
if (stack.empty()) {
return -1;
}
int idx = stack.size() - 1;
int res = stack.back() + inc.back();
if (idx > 0) {
inc[idx - 1] += inc[idx];
}
stack.pop_back();
inc.pop_back();
return res;
}
void increment(int k, int val) {
int limit = min(k, (int)stack.size()) - 1;
if (limit >= 0) {
inc[limit] += val;
}
}
};push(x): Adds the value to the stack and initializes a0in theincarray for future increments.pop(): Pops the top element, applying any pending increment from theincarray.increment(k, val): Instead of modifying the bottomkelements directly, we increment theinc[k-1]to propagate the increment lazily when the elements are popped.
This approach ensures that all operations are efficient, avoiding repeated operations during increment.
