Mutating counts without cleanup
Wrong move: Zero-count keys stay in map and break distinct/count constraints.
Usually fails on: Window/map size checks are consistently off by one.
Fix: Delete keys when count reaches zero.
Break down a hard problem into reliable checkpoints, edge-case handling, and complexity trade-offs.
You are given a string s that consists of lowercase English letters.
You can perform the following operation any number of times (possibly zero times):
s and delete any one occurrence.Return the lexicographically smallest resulting string that can be formed this way.
Example 1:
Input: s = "aaccb"
Output: "aacb"
Explanation:
We can form the strings "acb", "aacb", "accb", and "aaccb". "aacb" is the lexicographically smallest one.
For example, we can obtain "aacb" by choosing 'c' and deleting its first occurrence.
Example 2:
Input: s = "z"
Output: "z"
Explanation:
We cannot perform any operations. The only string we can form is "z".
Constraints:
1 <= s.length <= 105s contains lowercase English letters only.Problem summary: You are given a string s that consists of lowercase English letters. You can perform the following operation any number of times (possibly zero times): Choose any letter that appears at least twice in the current string s and delete any one occurrence. Return the lexicographically smallest resulting string that can be formed this way.
Start with the most direct exhaustive search. That gives a correctness anchor before optimizing.
Pattern signal: Hash Map · Stack · Greedy
"aaccb"
"z"
remove-duplicate-letters)Source-backed implementations are provided below for direct study and interview prep.
// Accepted solution for LeetCode #3816: Lexicographically Smallest String After Deleting Duplicate Characters
class Solution {
public String lexSmallestAfterDeletion(String s) {
int[] cnt = new int[26];
int n = s.length();
for (int i = 0; i < n; ++i) {
++cnt[s.charAt(i) - 'a'];
}
StringBuilder stk = new StringBuilder();
for (int i = 0; i < n; ++i) {
char c = s.charAt(i);
while (stk.length() > 0 && stk.charAt(stk.length() - 1) > c
&& cnt[stk.charAt(stk.length() - 1) - 'a'] > 1) {
--cnt[stk.charAt(stk.length() - 1) - 'a'];
stk.setLength(stk.length() - 1);
}
stk.append(c);
}
while (cnt[stk.charAt(stk.length() - 1) - 'a'] > 1) {
--cnt[stk.charAt(stk.length() - 1) - 'a'];
stk.setLength(stk.length() - 1);
}
return stk.toString();
}
}
// Accepted solution for LeetCode #3816: Lexicographically Smallest String After Deleting Duplicate Characters
func lexSmallestAfterDeletion(s string) string {
cnt := [26]int{}
for _, c := range s {
cnt[c-'a']++
}
stk := []byte{}
for _, c := range s {
for len(stk) > 0 && stk[len(stk)-1] > byte(c) && cnt[stk[len(stk)-1]-'a'] > 1 {
cnt[stk[len(stk)-1]-'a']--
stk = stk[:len(stk)-1]
}
stk = append(stk, byte(c))
}
for cnt[stk[len(stk)-1]-'a'] > 1 {
cnt[stk[len(stk)-1]-'a']--
stk = stk[:len(stk)-1]
}
return string(stk)
}
# Accepted solution for LeetCode #3816: Lexicographically Smallest String After Deleting Duplicate Characters
class Solution:
def lexSmallestAfterDeletion(self, s: str) -> str:
cnt = Counter(s)
stk = []
for c in s:
while stk and stk[-1] > c and cnt[stk[-1]] > 1:
cnt[stk.pop()] -= 1
stk.append(c)
while stk and cnt[stk[-1]] > 1:
cnt[stk.pop()] -= 1
return "".join(stk)
// Accepted solution for LeetCode #3816: Lexicographically Smallest String After Deleting Duplicate Characters
// Rust example auto-generated from java reference.
// Replace the signature and local types with the exact LeetCode harness for this problem.
impl Solution {
pub fn rust_example() {
// Port the logic from the reference block below.
}
}
// Reference (java):
// // Accepted solution for LeetCode #3816: Lexicographically Smallest String After Deleting Duplicate Characters
// class Solution {
// public String lexSmallestAfterDeletion(String s) {
// int[] cnt = new int[26];
// int n = s.length();
// for (int i = 0; i < n; ++i) {
// ++cnt[s.charAt(i) - 'a'];
// }
// StringBuilder stk = new StringBuilder();
// for (int i = 0; i < n; ++i) {
// char c = s.charAt(i);
// while (stk.length() > 0 && stk.charAt(stk.length() - 1) > c
// && cnt[stk.charAt(stk.length() - 1) - 'a'] > 1) {
// --cnt[stk.charAt(stk.length() - 1) - 'a'];
// stk.setLength(stk.length() - 1);
// }
// stk.append(c);
// }
// while (cnt[stk.charAt(stk.length() - 1) - 'a'] > 1) {
// --cnt[stk.charAt(stk.length() - 1) - 'a'];
// stk.setLength(stk.length() - 1);
// }
// return stk.toString();
// }
// }
// Accepted solution for LeetCode #3816: Lexicographically Smallest String After Deleting Duplicate Characters
function lexSmallestAfterDeletion(s: string): string {
const cnt: number[] = new Array(26).fill(0);
const n = s.length;
const a = 'a'.charCodeAt(0);
for (let i = 0; i < n; ++i) {
++cnt[s.charCodeAt(i) - a];
}
const stk: string[] = [];
for (let i = 0; i < n; ++i) {
const c = s[i];
while (
stk.length > 0 &&
stk[stk.length - 1] > c &&
cnt[stk[stk.length - 1].charCodeAt(0) - a] > 1
) {
--cnt[stk.pop()!.charCodeAt(0) - a];
}
stk.push(c);
}
while (cnt[stk[stk.length - 1].charCodeAt(0) - a] > 1) {
--cnt[stk.pop()!.charCodeAt(0) - a];
}
return stk.join('');
}
Use this to step through a reusable interview workflow for this problem.
For each element, scan left (or right) to find the next greater/smaller element. The inner scan can visit up to n elements per outer iteration, giving O(n²) total comparisons. No extra space needed beyond loop variables.
Each element is pushed onto the stack at most once and popped at most once, giving 2n total operations = O(n). The stack itself holds at most n elements in the worst case. The key insight: amortized O(1) per element despite the inner while-loop.
Review these before coding to avoid predictable interview regressions.
Wrong move: Zero-count keys stay in map and break distinct/count constraints.
Usually fails on: Window/map size checks are consistently off by one.
Fix: Delete keys when count reaches zero.
Wrong move: Pushing without popping stale elements invalidates next-greater/next-smaller logic.
Usually fails on: Indices point to blocked elements and outputs shift.
Fix: Pop while invariant is violated before pushing current element.
Wrong move: Locally optimal choices may fail globally.
Usually fails on: Counterexamples appear on crafted input orderings.
Fix: Verify with exchange argument or monotonic objective before committing.