2241. Design an ATM Machine


  • There is an ATM machine that stores banknotes of 5 denominations: 20, 50, 100, 200, and 500 dollars. Initially the ATM is empty. The user can use the machine to deposit or withdraw any amount of money.
  • When withdrawing, the machine prioritizes using banknotes of larger values.
    • For example, if you want to withdraw $300 and there are 2 $50 banknotes, 1 $100 banknote, and 1 $200 banknote, then the machine will use the $100 and $200 banknotes.
    • However, if you try to withdraw $600 and there are 3 $200 banknotes and 1 $500 banknote, then the withdraw request will be rejected because the machine will first try to use the $500 banknote and then be unable to use banknotes to complete the remaining $100. Note that the machine is not allowed to use the $200 banknotes instead of the $500 banknote.
  • Implement the ATM class:
    • ATM() Initializes the ATM object.
    • void deposit(int[] banknotesCount) Deposits new banknotes in the order $20, $50, $100, $200, and $500.
    • int[] withdraw(int amount) Returns an array of length 5 of the number of banknotes that will be handed to the user in the order $20, $50, $100, $200, and $500, and update the number of banknotes in the ATM after withdrawing. Returns [-1] if it is not possible (do not withdraw any banknotes in this case).

Example 1

Input
["ATM", "deposit", "withdraw", "deposit", "withdraw", "withdraw"]
[[], [[0,0,1,2,1]], [600], [[0,1,0,1,1]], [600], [550]]
Output
[null, null, [0,0,1,0,1], null, [-1], [0,1,0,0,1]]

Explanation
ATM atm = new ATM();
atm.deposit([0,0,1,2,1]); // Deposits 1 $100 banknote, 2 $200 banknotes,
                          // and 1 $500 banknote.
atm.withdraw(600);        // Returns [0,0,1,0,1]. The machine uses 1 $100 banknote
                          // and 1 $500 banknote. The banknotes left over in the
                          // machine are [0,0,0,2,0].
atm.deposit([0,1,0,1,1]); // Deposits 1 $50, $200, and $500 banknote.
                          // The banknotes in the machine are now [0,1,0,3,1].
atm.withdraw(600);        // Returns [-1]. The machine will try to use a $500 banknote
                          // and then be unable to complete the remaining $100,
                          // so the withdraw request will be rejected.
                          // Since the request is rejected, the number of banknotes
                          // in the machine is not modified.
atm.withdraw(550);        // Returns [0,1,0,0,1]. The machine uses 1 $50 banknote
                          // and 1 $500 banknote.

Method 1

【O(1) time | O(1) space】
package Leetcode.SlideWindow;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @author zhengxingxing
 * @date 2025/01/04
 */
public class SubstringWithConcatenationOfAllWords {
    public static List<Integer> findSubstring(String s, String[] words) {
        // List to store the final result, records the starting indices of substrings that meet the condition
        List<Integer> result = new ArrayList<>();

        // Basic input checks, return empty result if input is null or empty
        if (s == null || s.length() == 0 || words == null || words.length == 0) {
            return result;
        }

        // Get basic information
        int wordLen = words[0].length();    // Length of each word
        int wordCount = words.length;       // Total number of words
        int totalLen = wordLen * wordCount; // Total length of all concatenated words

        // If the original string's length is smaller than the required total length, it can't match, return empty result
        if (s.length() < totalLen) {
            return result;
        }

        // Step 1: Create a HashMap to count how many times each word should appear in the words array
        // Key is the word, value is how many times the word should appear
        Map<String, Integer> wordMap = new HashMap<>();
        for (String word : words) {
            wordMap.put(word, wordMap.getOrDefault(word, 0) + 1);
        }

        // Step 2: Iterate over all possible starting positions
        // Since the length of each word is fixed, only need to iterate over wordLen offsets
        // For example, if the word length is 3, we only need to start from indices 0, 1, 2 to cover all possibilities
        for (int i = 0; i < wordLen; i++) {
            int left = i;                              // Left boundary of the sliding window
            int count = 0;                            // The number of matched words in the current window
            Map<String, Integer> curMap = new HashMap<>();  // Count of words in the current window

            // Step 3: Use sliding window, each time move by one word length
            for (int j = i; j <= s.length() - wordLen; j += wordLen) {
                // Get the word at the current position
                String word = s.substring(j, j + wordLen);

                // If the word is one of the target words (exists in the words array)
                if (wordMap.containsKey(word)) {
                    // Add the word to the count in the current window
                    curMap.put(word, curMap.getOrDefault(word, 0) + 1);
                    count++; // Increase the number of matched words

                    // If the current word appears more than required, shrink the left boundary of the window
                    // Remove the leftmost word until the count of the current word is within the required limit
                    while (curMap.get(word) > wordMap.get(word)) {
                        String leftWord = s.substring(left, left + wordLen);
                        curMap.put(leftWord, curMap.get(leftWord) - 1);
                        count--;
                        left += wordLen;
                    }

                    // If all words are found (matching the required count)
                    if (count == wordCount) {
                        result.add(left); // Add the current valid starting index to the result

                        // Remove the leftmost word and continue searching for the next match
                        // This allows finding all possible matching positions
                        String leftWord = s.substring(left, left + wordLen);
                        curMap.put(leftWord, curMap.get(leftWord) - 1);
                        count--;
                        left += wordLen;
                    }
                } else {
                    // If a word is encountered that is not in the target word list
                    // Reset all states and start matching from the next position
                    curMap.clear();
                    count = 0;
                    left = j + wordLen;
                }
            }
        }
        return result;
    }

    public static void main(String[] args) {
        // Test case 1: Basic case
        // "barfoo" and "foobar" are valid substrings
        String s1 = "barfoothefoobarman";
        String[] words1 = {"foo","bar"};
        System.out.println("Test Case 1: " + findSubstring(s1, words1)); // Expected output: [0, 9]

        // Test case 2: Repeated characters
        // Test handling of repeated words
        String s2 = "aaaaaaaa";
        String[] words2 = {"aa","aa"};
        System.out.println("Test Case 2: " + findSubstring(s2, words2));

        // Test case 3: No matches
        // Test case where no matches are found
        String s3 = "wordgoodgoodgoodbestword";
        String[] words3 = {"word","good","best","good"};
        System.out.println("Test Case 3: " + findSubstring(s3, words3)); // Expected output: []
    }
}




Enjoy Reading This Article?

Here are some more articles you might like to read next:

  • 2264. Largest 3-Same-Digit Number in String
  • 3042. Count Prefix and Suffix Pairs I
  • 1016. Binary String With Substrings Representing 1 To N
  • 1408. String Matching in an Array
  • 3019. Number of Changing Keys