2483. Minimum Penalty for a Shop - Explanation

Problem Link

Description

You are given the customer visit log of a shop represented by a 0-indexed string customers consisting only of characters 'N' and 'Y':

  • if the i-th character is 'Y', it means that customers come at the i-th hour
  • whereas 'N' indicates that no customers come at the i-th hour.

If the shop closes at the j-th hour (0 <= j <= n), the penalty is calculated as follows:

  • For every hour when the shop is open and no customers come, the penalty increases by 1.
  • For every hour when the shop is closed and customers come, the penalty increases by 1.

Return the earliest hour at which the shop must be closed to incur a minimum penalty.

Note that if a shop closes at the j-th hour, it means the shop is closed at the hour j.

Example 1:

Input: customers = "YYNY"

Output: 2

Explanation:

  • Closing the shop at the 0th hour incurs in 1+1+0+1 = 3 penalty.
  • Closing the shop at the 1st hour incurs in 0+1+0+1 = 2 penalty.
  • Closing the shop at the 2nd hour incurs in 0+0+0+1 = 1 penalty.
  • Closing the shop at the 3rd hour incurs in 0+0+1+1 = 2 penalty.
  • Closing the shop at the 4th hour incurs in 0+0+1+0 = 1 penalty.
    Closing the shop at 2nd or 4th hour gives a minimum penalty. Since 2 is earlier, the optimal closing time is 2.

Example 2:

Input: customers = "NNNNN"

Output: 0

Explanation: It is best to close the shop at the 0th hour as no customers arrive.

Example 3:

Input: customers = "YYYY"

Output: 4

Explanation: It is best to close the shop at the 4th hour as customers arrive at each hour.

Constraints:

  • 1 <= customers.length <= 100,000
  • customers consists only of characters 'Y' and 'N'.


Topics

Company Tags

Please upgrade to NeetCode Pro to view company tags.



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Prefix Sum - Used to precompute counts of characters before each position
  • Suffix Sum - Used to precompute counts of characters from each position onward
  • String Iteration - Processing characters sequentially while maintaining running totals
  • Greedy Optimization - Tracking the optimal answer while iterating through possible choices

1. Brute Force

Intuition

The shop can close at any hour from 0 to n (inclusive). If we close at hour i, we incur a penalty of 1 for each 'N' before hour i (shop was open but no customer) and 1 for each 'Y' from hour i onward (shop was closed but customer came). We try every possible closing time and pick the one with minimum penalty.

Algorithm

  1. Initialize res and minPenalty to n (worst case).
  2. For each possible closing hour i from 0 to n:
    • Count 'N' characters in positions 0 to i-1 (penalty for being open when no one came).
    • Count 'Y' characters in positions i to n-1 (penalty for being closed when customers came).
    • If this total penalty is less than minPenalty, update minPenalty and res.
  3. Return res.
class Solution:
    def bestClosingTime(self, customers: str) -> int:
        n = len(customers)
        res = n
        minPenalty = n

        for i in range(n + 1):
            penalty = 0
            for j in range(i):
                if customers[j] == 'N':
                    penalty += 1
            for j in range(i, n):
                if customers[j] == 'Y':
                    penalty += 1

            if penalty < minPenalty:
                minPenalty = penalty
                res = i

        return res

Time & Space Complexity

  • Time complexity: O(n2)O(n ^ 2)
  • Space complexity: O(1)O(1)

2. Prefix & Suffix

Intuition

Instead of recounting 'N' and 'Y' characters for each closing hour, we precompute prefix counts of 'N' and suffix counts of 'Y'. The prefix array tells us how many 'N' characters appear before each position, and the suffix array tells us how many 'Y' characters appear from each position onward. The penalty at any closing hour is simply the sum of these two precomputed values.

Algorithm

  1. Build prefixN[i] = count of 'N' in positions 0 to i-1.
  2. Build suffixY[i] = count of 'Y' in positions i to n-1.
  3. For each closing hour i from 0 to n:
    • Calculate penalty as prefixN[i] + suffixY[i].
    • Track the minimum penalty and its corresponding hour.
  4. Return the hour with minimum penalty.
class Solution:
    def bestClosingTime(self, customers: str) -> int:
        n = len(customers)
        cnt = 0

        prefixN = []
        for c in customers:
            prefixN.append(cnt)
            if c == 'N':
                cnt += 1
        prefixN.append(cnt)

        suffixY = [0] * (n + 1)
        for i in range(n - 1, -1, -1):
            suffixY[i] = suffixY[i + 1]
            if customers[i] == 'Y':
                suffixY[i] += 1

        res = n
        minPenalty = n
        for i in range(n + 1):
            penalty = prefixN[i] + suffixY[i]
            if penalty < minPenalty:
                minPenalty = penalty
                res = i

        return res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

3. Iteration (Two Pass)

Intuition

We can avoid storing arrays by computing on the fly. First, count all 'Y' characters. This represents the penalty if we close at hour 0 (we miss all customers). Then iterate through the string: each 'Y' we pass reduces the penalty (we served them), and each 'N' we pass increases it (we were open for nothing). Track the minimum penalty as we go.

Algorithm

  1. Count total 'Y' characters as cntY. Initialize minPenalty = cntY, res = 0, and cntN = 0.
  2. Iterate through each character at index i:
    • If it is 'Y', decrement cntY (one fewer missed customer).
    • If it is 'N', increment cntN (one more wasted open hour).
    • Calculate penalty as cntN + cntY.
    • If penalty is less than minPenalty, update minPenalty and set res = i + 1.
  3. Return res.
class Solution:
    def bestClosingTime(self, customers: str) -> int:
        cntY = sum(c == "Y" for c in customers)

        minPenalty = cntY
        res = cntN = 0
        for i, c in enumerate(customers):
            if c == "Y":
                cntY -= 1
            else:
                cntN += 1

            penalty = cntN + cntY
            if penalty < minPenalty:
                res = i + 1
                minPenalty = penalty

        return res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(1)O(1)

4. Iteration (One Pass)

Intuition

We can solve this in a single pass using a clever observation. Instead of tracking absolute penalty, we track a relative score. Treat 'Y' as +1 (benefit of staying open) and 'N' as -1 (cost of staying open). As we iterate, we accumulate this score. The optimal closing time is right after the point where this cumulative score is maximized, meaning we captured the most value from being open.

Algorithm

  1. Initialize res = 0, minPenalty = 0, and penalty = 0.
  2. Iterate through each character at index i:
    • Add +1 if the character is 'Y', otherwise add -1.
    • If penalty > minPenalty, update minPenalty = penalty and res = i + 1.
  3. Return res.
class Solution:
    def bestClosingTime(self, customers: str) -> int:
        res = minPenalty = 0
        penalty = 0

        for i, c in enumerate(customers):
            penalty += 1 if c == 'Y' else -1

            if penalty > minPenalty:
                minPenalty = penalty
                res = i + 1

        return res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(1)O(1)

Common Pitfalls

Confusing Closing Time Semantics

The shop closes at hour i means it is open during hours 0 to i-1 and closed from hour i onward. Many solutions incorrectly interpret this as the shop being open during hour i, leading to off-by-one errors in penalty calculations.

Not Considering Closing at Hour 0 or Hour n

Valid closing times range from 0 (never open) to n (open all day). Forgetting to check i = 0 or i = n causes missing the optimal answer when the best strategy is to never open or stay open the entire day.

Returning Wrong Index on Tie

When multiple closing times have the same minimum penalty, the problem asks for the earliest one. Using <= instead of < when updating the result, or not iterating in the correct order, can return a later hour instead of the earliest optimal hour.