2483. Minimum Penalty for a Shop
Problem Description
You are given a string customers
that represents the customer visit log of a shop. The string consists only of characters 'N'
and 'Y'
:
'Y'
at positioni
means customers come at thei
th hour'N'
at positioni
means no customers come at thei
th hour
The shop can choose to close at any hour j
(where 0 <= j <= n
, and n
is the length of the string). When the shop closes at hour j
, it means the shop is closed starting from hour j
.
The penalty is calculated based on two scenarios:
- When the shop is open (hours before
j
) but no customers come (character is'N'
), the penalty increases by1
- When the shop is closed (hours from
j
onwards) but customers come (character is'Y'
), the penalty increases by1
Your task is to find the earliest hour at which the shop should close to minimize the total penalty.
For example, if customers = "YYNY"
:
- If closing at hour 0: shop is never open, penalty = 3 (missed customers at hours 0, 1, 3)
- If closing at hour 1: shop open at hour 0, penalty = 2 (missed customers at hours 1, 3)
- If closing at hour 2: shop open at hours 0-1, penalty = 1 (missed customer at hour 3)
- If closing at hour 3: shop open at hours 0-2, penalty = 1 (no customers at hour 2)
- If closing at hour 4: shop open at hours 0-3, penalty = 1 (no customers at hour 2)
The minimum penalty is 1, achieved at hours 2, 3, or 4, so return 2 (the earliest hour).
Intuition
The key insight is that for any closing time j
, we can calculate the total penalty by counting two types of "mismatches":
- Hours before
j
where the shop is open but no customers come ('N'
characters) - Hours from
j
onwards where the shop is closed but customers come ('Y'
characters)
To efficiently calculate these penalties for all possible closing times, we can use prefix sums. If we know how many 'Y'
characters appear in the first i
positions, we can quickly determine:
- How many
'N'
characters are before positionj
(which equalsj - count_of_Y_before_j
) - How many
'Y'
characters are from positionj
onwards (which equalstotal_Y - count_of_Y_before_j
)
By precomputing a prefix sum array s
where s[i]
represents the number of 'Y'
characters in the first i
positions, we can calculate the penalty for closing at hour j
as:
- Penalty from being open with no customers:
j - s[j]
- Penalty from being closed with customers:
s[n] - s[j]
- Total penalty:
j - s[j] + s[n] - s[j] = j - 2*s[j] + s[n]
Since we want the minimum penalty and the earliest hour that achieves it, we iterate through all possible closing times from 0
to n
, calculate each penalty, and keep track of the minimum penalty and its corresponding earliest hour.
Learn more about Prefix Sum patterns.
Solution Approach
The solution uses prefix sum combined with enumeration to efficiently find the optimal closing time.
Step 1: Build Prefix Sum Array
First, we create a prefix sum array s
of size n + 1
where s[i]
stores the count of 'Y'
characters in the first i
positions of the string:
s = [0] * (n + 1)
for i, c in enumerate(customers):
s[i + 1] = s[i] + int(c == 'Y')
For example, if customers = "YYNY"
, then s = [0, 1, 2, 2, 3]
.
Step 2: Enumerate All Possible Closing Times
We iterate through each possible closing hour j
from 0
to n
(inclusive) and calculate the penalty for each:
ans, cost = 0, inf
for j in range(n + 1):
t = j - s[j] + s[-1] - s[j]
if cost > t:
ans, cost = j, t
The penalty formula t = j - s[j] + s[-1] - s[j]
breaks down as:
j - s[j]
: Number of hours beforej
where shop is open but no customers come (count of'N'
before positionj
)s[-1] - s[j]
: Number of hours fromj
onwards where shop is closed but customers come (count of'Y'
from positionj
to end)s[-1]
is the total number of'Y'
characters in the entire string
Step 3: Track Minimum Penalty
We maintain two variables:
cost
: The minimum penalty found so far (initialized to infinity)ans
: The earliest closing hour that achieves this minimum penalty
Whenever we find a strictly smaller penalty (cost > t
), we update both the answer and the minimum cost. Since we iterate from hour 0 to n, we automatically get the earliest hour when there are ties.
Time Complexity: O(n)
- We make one pass to build the prefix sum and another to find the best closing time.
Space Complexity: O(n)
- For storing the prefix sum array.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with customers = "NYNY"
.
Step 1: Build the prefix sum array
We create array s
where s[i]
counts the number of 'Y' characters in the first i
positions:
s[0] = 0
(no characters counted yet)s[1] = 0
(first character is 'N', no 'Y' found)s[2] = 1
(second character is 'Y', total count = 1)s[3] = 1
(third character is 'N', count stays at 1)s[4] = 2
(fourth character is 'Y', total count = 2)
So s = [0, 0, 1, 1, 2]
Step 2: Calculate penalty for each closing hour
For each closing hour j
, we use the formula: penalty = j - 2*s[j] + s[4]
-
j = 0 (shop never opens):
- Penalty =
0 - 2*0 + 2 = 2
- This counts the 2 'Y' characters we miss (at positions 1 and 3)
- Penalty =
-
j = 1 (shop open at hour 0):
- Penalty =
1 - 2*0 + 2 = 3
- Open with no customers at hour 0 (penalty +1)
- Closed but customers come at hours 1 and 3 (penalty +2)
- Penalty =
-
j = 2 (shop open at hours 0-1):
- Penalty =
2 - 2*1 + 2 = 2
- Open with no customers at hour 0 (penalty +1)
- Closed but customers come at hour 3 (penalty +1)
- Penalty =
-
j = 3 (shop open at hours 0-2):
- Penalty =
3 - 2*1 + 2 = 4
- Open with no customers at hours 0 and 2 (penalty +2)
- Closed but customers come at hour 3 (penalty +1)
- Wait, let's recalculate: we have 'N' at position 0 and 'N' at position 2, so penalty from being open = 2. We have 'Y' at position 3, so penalty from being closed = 1. Total = 3.
- Using formula:
3 - 2*1 + 2 = 3
- Penalty =
-
j = 4 (shop always open):
- Penalty =
4 - 2*2 + 2 = 2
- Open with no customers at hours 0 and 2 (penalty +2)
- Never closed, so no penalty from closed hours
- Penalty =
Step 3: Find the minimum
The penalties are: [2, 3, 2, 3, 2]
The minimum penalty is 2, which occurs at hours 0, 2, and 4. Since we want the earliest hour, we return 0.
Solution Implementation
1class Solution:
2 def bestClosingTime(self, customers: str) -> int:
3 """
4 Find the best hour to close the shop to minimize penalty.
5 Penalty = number of 'N' hours before closing + number of 'Y' hours after closing
6
7 Args:
8 customers: String of 'Y' (customer arrives) or 'N' (no customer) for each hour
9
10 Returns:
11 The optimal closing hour (0 to n inclusive)
12 """
13 n = len(customers)
14
15 # Build prefix sum array for counting 'Y's
16 # prefix_sum[i] = number of 'Y's in customers[0:i]
17 prefix_sum = [0] * (n + 1)
18 for i, customer in enumerate(customers):
19 prefix_sum[i + 1] = prefix_sum[i] + (1 if customer == 'Y' else 0)
20
21 # Find the closing hour with minimum penalty
22 best_hour = 0
23 min_penalty = float('inf')
24
25 for closing_hour in range(n + 1):
26 # Calculate penalty for closing at this hour
27 # Penalty before closing: number of 'N's from hour 0 to closing_hour-1
28 # = closing_hour - number of 'Y's before closing_hour
29 penalty_before = closing_hour - prefix_sum[closing_hour]
30
31 # Penalty after closing: number of 'Y's from closing_hour to end
32 # = total 'Y's - 'Y's before closing_hour
33 penalty_after = prefix_sum[-1] - prefix_sum[closing_hour]
34
35 total_penalty = penalty_before + penalty_after
36
37 # Update best closing hour if we found a lower penalty
38 if total_penalty < min_penalty:
39 best_hour = closing_hour
40 min_penalty = total_penalty
41
42 return best_hour
43
1class Solution {
2 public int bestClosingTime(String customers) {
3 int totalHours = customers.length();
4
5 // Create prefix sum array to count 'Y' customers up to each position
6 // prefixYCount[i] represents the count of 'Y' customers from index 0 to i-1
7 int[] prefixYCount = new int[totalHours + 1];
8 for (int i = 0; i < totalHours; ++i) {
9 prefixYCount[i + 1] = prefixYCount[i] + (customers.charAt(i) == 'Y' ? 1 : 0);
10 }
11
12 // Initialize variables to track the best closing hour and minimum penalty
13 int bestClosingHour = 0;
14 int minimumPenalty = Integer.MAX_VALUE;
15
16 // Try each possible closing hour from 0 to totalHours
17 for (int closingHour = 0; closingHour <= totalHours; ++closingHour) {
18 // Calculate penalty for closing at this hour:
19 // - Penalty for 'N' hours when shop is open: closingHour - prefixYCount[closingHour]
20 // - Penalty for 'Y' customers after closing: prefixYCount[totalHours] - prefixYCount[closingHour]
21 int penaltyForNBeforeClosing = closingHour - prefixYCount[closingHour];
22 int penaltyForYAfterClosing = prefixYCount[totalHours] - prefixYCount[closingHour];
23 int totalPenalty = penaltyForNBeforeClosing + penaltyForYAfterClosing;
24
25 // Update best closing hour if we found a lower penalty
26 if (minimumPenalty > totalPenalty) {
27 bestClosingHour = closingHour;
28 minimumPenalty = totalPenalty;
29 }
30 }
31
32 return bestClosingHour;
33 }
34}
35
1class Solution {
2public:
3 int bestClosingTime(string customers) {
4 int n = customers.size();
5
6 // prefixSum[i] stores the count of 'Y' (customers) from index 0 to i-1
7 vector<int> prefixSum(n + 1);
8 for (int i = 0; i < n; ++i) {
9 prefixSum[i + 1] = prefixSum[i] + (customers[i] == 'Y');
10 }
11
12 int bestHour = 0;
13 int minCost = INT_MAX;
14
15 // Try each possible closing hour from 0 to n
16 for (int closingHour = 0; closingHour <= n; ++closingHour) {
17 // Calculate penalty for closing at this hour:
18 // - Hours before closing with 'N': closingHour - prefixSum[closingHour]
19 // - Hours after closing with 'Y': prefixSum[n] - prefixSum[closingHour]
20 int noCustomerPenalty = closingHour - prefixSum[closingHour];
21 int customerPenalty = prefixSum[n] - prefixSum[closingHour];
22 int totalCost = noCustomerPenalty + customerPenalty;
23
24 // Update the best closing hour if we found a lower cost
25 if (totalCost < minCost) {
26 bestHour = closingHour;
27 minCost = totalCost;
28 }
29 }
30
31 return bestHour;
32 }
33};
34
1function bestClosingTime(customers: string): number {
2 const n: number = customers.length;
3
4 // prefixSum[i] stores the count of 'Y' (customers) from index 0 to i-1
5 // This helps us calculate penalties efficiently
6 const prefixSum: number[] = new Array(n + 1).fill(0);
7 for (let i = 0; i < n; i++) {
8 prefixSum[i + 1] = prefixSum[i] + (customers[i] === 'Y' ? 1 : 0);
9 }
10
11 let bestHour: number = 0;
12 let minCost: number = Number.MAX_SAFE_INTEGER;
13
14 // Try each possible closing hour from 0 to n
15 // Hour 0 means closing before opening, hour n means never closing
16 for (let closingHour = 0; closingHour <= n; closingHour++) {
17 // Calculate penalty for closing at this hour:
18 // noCustomerPenalty: penalty for hours before closing that have 'N' (no customers)
19 // This is total hours before closing minus customers in those hours
20 const noCustomerPenalty: number = closingHour - prefixSum[closingHour];
21
22 // customerPenalty: penalty for hours after closing that have 'Y' (customers present)
23 // This is total customers minus customers served before closing
24 const customerPenalty: number = prefixSum[n] - prefixSum[closingHour];
25
26 // Total cost is sum of both penalties
27 const totalCost: number = noCustomerPenalty + customerPenalty;
28
29 // Update the best closing hour if we found a lower cost
30 // In case of tie, we keep the earlier hour (smallest index)
31 if (totalCost < minCost) {
32 bestHour = closingHour;
33 minCost = totalCost;
34 }
35 }
36
37 return bestHour;
38}
39
Time and Space Complexity
Time Complexity: O(n)
The algorithm consists of two main loops:
- The first loop iterates through the
customers
string once to build the prefix sum arrays
, takingO(n)
time. - The second loop iterates from
0
ton+1
to calculate the penalty for each possible closing time, takingO(n+1) = O(n)
time.
Each operation inside both loops (array access, arithmetic operations, and comparisons) takes O(1)
time. Therefore, the overall time complexity is O(n) + O(n) = O(n)
.
Space Complexity: O(n)
The algorithm uses additional space for:
- The prefix sum array
s
of sizen+1
, which requiresO(n+1) = O(n)
space. - A few constant variables (
ans
,cost
,t
,i
,j
), which requireO(1)
space.
Therefore, the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Off-by-One Error with Closing Hour Interpretation
A common mistake is misunderstanding what "closing at hour j" means. Many developers incorrectly assume that closing at hour j
means the shop is still open during hour j
and closes after it.
Incorrect Understanding:
# Wrong: Treating hour j as still open
for closing_hour in range(n + 1):
# Incorrectly counting j as an open hour
penalty_before = closing_hour + 1 - prefix_sum[closing_hour + 1] # WRONG!
penalty_after = prefix_sum[-1] - prefix_sum[closing_hour + 1] # WRONG!
Correct Understanding:
- Closing at hour
j
means the shop is closed starting from hourj
- Hours
0
toj-1
are open, hoursj
ton-1
are closed - This is why we use
prefix_sum[closing_hour]
notprefix_sum[closing_hour + 1]
2. Forgetting the Edge Case of Never Opening or Never Closing
Some implementations forget that the shop can close at hour 0
(never open) or hour n
(never close).
Incomplete Range:
# Wrong: Missing edge cases
for closing_hour in range(1, n): # Misses hour 0 and hour n!
# Calculate penalty...
Complete Solution:
# Correct: Include all possibilities from 0 to n
for closing_hour in range(n + 1): # Includes 0 (never open) and n (always open)
# Calculate penalty...
3. Using Wrong Comparison for Finding Minimum
When multiple hours have the same minimum penalty, we need the earliest one. Using >=
instead of >
would give us the latest hour with minimum penalty.
Wrong Comparison:
# Wrong: This would give the LATEST hour with minimum penalty
for closing_hour in range(n + 1):
total_penalty = calculate_penalty(closing_hour)
if total_penalty <= min_penalty: # WRONG! Uses <=
best_hour = closing_hour
min_penalty = total_penalty
Correct Comparison:
# Correct: This gives the EARLIEST hour with minimum penalty
for closing_hour in range(n + 1):
total_penalty = calculate_penalty(closing_hour)
if total_penalty < min_penalty: # Correct! Uses strict <
best_hour = closing_hour
min_penalty = total_penalty
4. Inefficient Penalty Calculation
Without using prefix sums, recalculating the penalty for each closing hour becomes O(n²) instead of O(n).
Inefficient Approach:
# Wrong: O(n²) time complexity
for closing_hour in range(n + 1):
penalty = 0
# Count 'N's before closing
for i in range(closing_hour):
if customers[i] == 'N':
penalty += 1
# Count 'Y's after closing
for i in range(closing_hour, n):
if customers[i] == 'Y':
penalty += 1
Efficient Solution with Prefix Sum:
# Correct: O(n) time complexity using prefix sum
prefix_sum = [0] * (n + 1)
for i, c in enumerate(customers):
prefix_sum[i + 1] = prefix_sum[i] + (1 if c == 'Y' else 0)
for closing_hour in range(n + 1):
penalty = (closing_hour - prefix_sum[closing_hour]) + (prefix_sum[-1] - prefix_sum[closing_hour])
What's the output of running the following function using the following tree as input?
1def serialize(root):
2 res = []
3 def dfs(root):
4 if not root:
5 res.append('x')
6 return
7 res.append(root.val)
8 dfs(root.left)
9 dfs(root.right)
10 dfs(root)
11 return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4 StringJoiner res = new StringJoiner(" ");
5 serializeDFS(root, res);
6 return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10 if (root == null) {
11 result.add("x");
12 return;
13 }
14 result.add(Integer.toString(root.val));
15 serializeDFS(root.left, result);
16 serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2 let res = [];
3 serialize_dfs(root, res);
4 return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8 if (!root) {
9 res.push("x");
10 return;
11 }
12 res.push(root.val);
13 serialize_dfs(root.left, res);
14 serialize_dfs(root.right, res);
15}
16
Recommended Readings
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!