diff --git a/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/analysis.md b/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/analysis.md new file mode 100644 index 0000000..1a46ff8 --- /dev/null +++ b/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/analysis.md @@ -0,0 +1,70 @@ +# 3734. Lexicographically Smallest Palindromic Permutation Greater Than Target + +[LeetCode Link](https://leetcode.com/problems/lexicographically-smallest-palindromic-permutation-greater-than-target/) + +Difficulty: Hard +Topics: Two Pointers, String, Enumeration +Acceptance Rate: 48.5% + +## Hints + +### Hint 1 + +The search space looks enormous — `n` can be 300, so you can never enumerate permutations. But a palindrome carries far less information than its length suggests. Ask yourself: how many characters do you actually have to *decide* before the whole string is pinned down? Start by counting letters and figuring out when a palindromic permutation exists at all. + +### Hint 2 + +Once you realise the first half (plus a middle character when `n` is odd) determines everything, the problem becomes the classic "smallest string greater than `target`" construction: pick a position `i` where your answer stops matching `target` and becomes strictly larger, then fill the remainder as small as possible. The twist is that the letters you may use at each position come from a shrinking multiset, not from a free alphabet. + +### Hint 3 + +Among all valid answers, the best one is the one that agrees with `target` on the **longest** prefix — if candidate A matches `target` through index `i` and candidate B breaks earlier at `j < i`, then at position `j` candidate A still holds `target[j]` while B holds something bigger, so A is smaller. So enumerate the break position from late to early and take the first that works. The special case is "no break inside the first half at all": then the first half must equal `target[:h]` exactly, which produces exactly **one** candidate string — build it and compare it to `target` directly. That single check silently covers the middle character and the entire mirrored second half, which is where most buggy solutions get lost. + +## Approach + +Let `n = len(s)` and `h = n / 2`. + +**Step 0 — feasibility.** Count each letter of `s`. A palindromic permutation exists iff at most one letter has an odd count. (Parity does the rest of the work for you: if `n` is odd the number of odd counts is odd, so "at most one" means "exactly one"; if `n` is even it means "zero".) Otherwise return `""`. + +**Step 1 — collapse the problem to the first half.** Every palindromic permutation has the shape + +``` +P = H + [mid] + reverse(H) +``` + +where `H` is any arrangement of the *half multiset* `half[c] = cnt[c] / 2`, and `mid` (only when `n` is odd) is forced to be the unique odd-count letter. So we are no longer choosing among permutations of `s` — we are choosing `H`, and `H` determines `P` completely. + +**Step 2 — order candidates by prefix agreement.** We want the smallest `P > target`. Classify candidates by the first index `i` where `P[i] > target[i]` (with `P[:i] == target[:i]`). As Hint 3 argues, larger `i` gives a smaller `P`, so we want the largest feasible `i`. + +Note that any `i >= h` forces `H == target[:h]` — the break happens in the middle character or in the mirrored tail, but the first half was already locked to `target`'s first half. And `H == target[:h]` yields exactly one string. So all of those cases collapse into a single candidate: + +- Walk `target[0..h-1]` consuming from the half multiset. If it consumes cleanly, the multiset of `target[:h]` *is* the half multiset. Build `cand = target[:h] + mid + reverse(target[:h])` and, if `cand > target`, return it immediately — nothing can beat it. + +**Step 3 — break inside the first half.** Otherwise the break index `i` satisfies `i < h`. While walking in Step 2, record `states[i]`: the multiset remaining after consuming `target[:i]`. The walk stops at the first infeasible index, giving `maxPrefix`; any `i > maxPrefix` is impossible because the prefix itself cannot be built. + +For `i` from `min(maxPrefix, h-1)` down to `0`: + +- Look for the smallest letter `c > target[i]` still present in `states[i]`. +- If found: the first half is `target[:i] + c + (everything left, sorted ascending)`. The sorted tail is optimal because the string already exceeds `target` at index `i`, so the remaining first-half positions are compared only against each other, and they are the earliest undetermined positions in `P`. +- Mirror it (inserting `mid` if `n` is odd) and return. + +If no `i` works, return `""`. + +**Worked example** — `s = "baba"`, `target = "abba"`. Counts are `a:2, b:2`, no odd letter, `h = 2`, half multiset `{a:1, b:1}`. Step 2: `target[:2] = "ab"` consumes cleanly, so `cand = "ab" + "ba" = "abba"`, which is *not* strictly greater than `target`. Step 3: at `i = 1` the remaining multiset is `{b:1}` and `target[1] = 'b'` — nothing larger. At `i = 0` the remaining multiset is `{a:1, b:1}` and `target[0] = 'a'`, so pick `'b'`; the leftover `{a:1}` sorted gives half `"ba"`, and mirroring gives `"baab"`. ✅ + +## Complexity Analysis + +Time Complexity: O(n · Σ) where Σ = 26 — the prefix walk is O(n), and the backward scan tries at most `h` break positions each doing an O(Σ) alphabet lookup, with the O(n) string construction happening only once on the successful position. Effectively O(n · 26) ≈ O(n). + +Space Complexity: O(n · Σ) for the stored per-prefix multisets (`h + 1` arrays of 26 ints), plus O(n) for the output. This can be reduced to O(n + Σ) by walking the prefix backwards and un-consuming letters instead of caching states, but with `n <= 300` the cached version is clearer. + +## Edge Cases + +- **No palindromic permutation at all** (two or more odd-count letters, e.g. `s = "abc"`): must return `""` before any construction logic runs. +- **`n == 1`**: `h = 0`, the first half is empty and the answer is just the single letter of `s`. Step 2 must still fire (the empty prefix trivially "consumes cleanly") so that `s > target` is checked; Step 3's loop is empty. Getting this wrong returns `""` for `s = "b", target = "a"`. +- **The mirrored candidate equals `target`** (`s = "abba"`, `target = "abba"`): the comparison must be *strict*, so this candidate is rejected and the search falls through to Step 3, yielding `"baab"`. +- **The mirrored candidate is smaller than `target`** (odd `n` where `mid < target[h]`, or a mirrored tail that loses): also rejected, and Step 3 must still run rather than returning `""`. +- **Odd `n` with a forced middle character**: `mid` is never a free choice. If the first half equals `target[:h]`, the middle is whatever letter had the odd count — you cannot bump it upward to manufacture a win. +- **`target` above every palindromic permutation** (`s = "aabb"`, `target = "zzzz"`): every break position fails and the answer is `""`. +- **All letters identical** (`s = "aaaa"`): the half multiset has a single letter, so there is exactly one palindrome; the answer is it-or-nothing. +- **Prefix becomes infeasible early** (`s = "aabb"`, `target = "aaaa"`): `target[:h]` uses `'a'` twice but only one `'a'` lives in the half multiset, so `maxPrefix = 1` and break positions beyond it must not be attempted — indexing `states` past its end is the easy crash here. diff --git a/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/problem.md b/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/problem.md new file mode 100644 index 0000000..9dad6f2 --- /dev/null +++ b/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/problem.md @@ -0,0 +1,81 @@ +--- +number: "3734" +frontend_id: "3734" +title: "Lexicographically Smallest Palindromic Permutation Greater Than Target" +slug: "lexicographically-smallest-palindromic-permutation-greater-than-target" +difficulty: "Hard" +topics: + - "Two Pointers" + - "String" + - "Enumeration" +acceptance_rate: 4845.5 +is_premium: false +created_at: "2026-08-28T11:36:34.432564+00:00" +fetched_at: "2026-08-28T11:36:34.432564+00:00" +link: "https://leetcode.com/problems/lexicographically-smallest-palindromic-permutation-greater-than-target/" +date: "2026-08-28" +--- + +# 3734. Lexicographically Smallest Palindromic Permutation Greater Than Target + +You are given two strings `s` and `target`, each of length `n`, consisting of lowercase English letters. + +Return the **lexicographically smallest string** that is **both** a **palindromic permutation** of `s` and **strictly** greater than `target`. If no such permutation exists, return an empty string. + + + +**Example 1:** + +**Input:** s = "baba", target = "abba" + +**Output:** "baab" + +**Explanation:** + + * The palindromic permutations of `s` (in lexicographical order) are `"abba"` and `"baab"`. + * The lexicographically smallest permutation that is strictly greater than `target` is `"baab"`. + + + +**Example 2:** + +**Input:** s = "baba", target = "bbaa" + +**Output:** "" + +**Explanation:** + + * The palindromic permutations of `s` (in lexicographical order) are `"abba"` and `"baab"`. + * None of them is lexicographically strictly greater than `target`. Therefore, the answer is `""`. + + + +**Example 3:** + +**Input:** s = "abc", target = "abb" + +**Output:** "" + +**Explanation:** + +`s` has no palindromic permutations. Therefore, the answer is `""`. + +**Example 4:** + +**Input:** s = "aac", target = "abb" + +**Output:** "aca" + +**Explanation:** + + * The only palindromic permutation of `s` is `"aca"`. + * `"aca"` is strictly greater than `target`. Therefore, the answer is `"aca"`. + + + + + +**Constraints:** + + * `1 <= n == s.length == target.length <= 300` + * `s` and `target` consist of only lowercase English letters. diff --git a/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/solution.go b/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/solution.go new file mode 100644 index 0000000..ee3dacc --- /dev/null +++ b/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/solution.go @@ -0,0 +1,105 @@ +package main + +// 3734. Lexicographically Smallest Palindromic Permutation Greater Than Target +// +// A palindrome is fully determined by its first half (plus the middle character +// when n is odd), so instead of enumerating permutations of s we only choose the +// first half from the multiset cnt[c]/2. To get the smallest palindrome strictly +// greater than target we prefer the candidate that agrees with target on the +// longest prefix: +// +// 1. If target's own first half is exactly the available half multiset, the +// palindrome built from it is the unique candidate matching target on the +// first h characters. Return it when it is strictly greater than target. +// 2. Otherwise pick a break position i < h, as large as possible: keep +// target[:i] in the first half, place the smallest still-available character +// greater than target[i] at i, and dump everything left over in ascending +// order. The string already exceeds target at i, so the sorted tail is the +// smallest legal completion. +func smallestPalindrome(s string, target string) string { + n := len(s) + + var cnt [26]int + for i := 0; i < n; i++ { + cnt[s[i]-'a']++ + } + + // A palindromic permutation exists only when at most one letter is odd. + odd, oddCount := 0, 0 + for c := 0; c < 26; c++ { + if cnt[c]%2 == 1 { + odd, oddCount = c, oddCount+1 + } + } + if oddCount > 1 { + return "" + } + + h := n / 2 + var half [26]int + for c := 0; c < 26; c++ { + half[c] = cnt[c] / 2 + } + + build := func(firstHalf []byte) string { + b := make([]byte, n) + copy(b, firstHalf) + if n%2 == 1 { + b[h] = byte('a' + odd) + } + for i := 0; i < h; i++ { + b[n-1-i] = firstHalf[i] + } + return string(b) + } + + // Step 1: consume target[:h] greedily, recording the multiset left over + // after every prefix length. states[i] is what remains after target[:i]. + states := make([][26]int, 1, h+1) + states[0] = half + cur := half + maxPrefix := h + for i := 0; i < h; i++ { + c := int(target[i] - 'a') + if cur[c] == 0 { + maxPrefix = i + break + } + cur[c]-- + states = append(states, cur) + } + + // The whole first half can mirror target's first half: that is the single + // best-prefixed candidate, so it wins whenever it beats target. + if maxPrefix == h { + if cand := build([]byte(target[:h])); cand > target { + return cand + } + } + + // Step 2: break as late as possible inside the first half. + start := maxPrefix + if start > h-1 { + start = h - 1 + } + for i := start; i >= 0; i-- { + rem := states[i] + for c := int(target[i]-'a') + 1; c < 26; c++ { + if rem[c] == 0 { + continue + } + rem[c]-- + firstHalf := make([]byte, 0, h) + firstHalf = append(firstHalf, target[:i]...) + firstHalf = append(firstHalf, byte('a'+c)) + for d := 0; d < 26; d++ { + for k := 0; k < rem[d]; k++ { + firstHalf = append(firstHalf, byte('a'+d)) + } + } + return build(firstHalf) + } + } + + return "" +} diff --git a/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/solution_test.go b/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/solution_test.go new file mode 100644 index 0000000..51c25d0 --- /dev/null +++ b/problems/3734-lexicographically-smallest-palindromic-permutation-greater-than-target/solution_test.go @@ -0,0 +1,135 @@ +package main + +import ( + "math/rand" + "sort" + "testing" +) + +func TestSolution(t *testing.T) { + tests := []struct { + name string + s string + target string + expected string + }{ + {"example 1: next palindrome after a valid one", "baba", "abba", "baab"}, + {"example 2: every palindromic permutation is too small", "baba", "bbaa", ""}, + {"example 3: no palindromic permutation exists", "abc", "abb", ""}, + {"example 4: the only palindrome already beats target", "aac", "abb", "aca"}, + + {"edge case: n == 1, equal so not strictly greater", "a", "a", ""}, + {"edge case: n == 1, single letter beats target", "b", "a", "b"}, + {"edge case: target equals a palindromic permutation", "abba", "abba", "baab"}, + {"edge case: target prefix not buildable from half multiset", "aabb", "aaaa", "abba"}, + {"edge case: target above every palindromic permutation", "aabb", "zzzz", ""}, + {"edge case: odd length with forced middle character", "aabbc", "aaaaa", "abcba"}, + {"edge case: two odd counts on even length", "ab", "aa", ""}, + {"edge case: all identical letters, target smaller", "aaaa", "aaab", ""}, + {"edge case: all identical letters, target smaller everywhere", "bbbb", "aaaa", "bbbb"}, + {"edge case: break must happen at the very first position", "aabb", "abbb", "baab"}, + {"edge case: mirrored half is exactly target's half", "abab", "abab", "abba"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := smallestPalindrome(tt.s, tt.target) + if got != tt.expected { + t.Errorf("smallestPalindrome(%q, %q) = %q, want %q", tt.s, tt.target, got, tt.expected) + } + }) + } +} + +// bruteForce enumerates every distinct permutation of s and returns the +// lexicographically smallest palindrome strictly greater than target. +func bruteForce(s, target string) string { + var cnt [26]int + for i := 0; i < len(s); i++ { + cnt[s[i]-'a']++ + } + + n := len(s) + best := "" + buf := make([]byte, 0, n) + + var isPalindrome func(b []byte) bool + isPalindrome = func(b []byte) bool { + for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 { + if b[i] != b[j] { + return false + } + } + return true + } + + var rec func() + rec = func() { + if len(buf) == n { + cand := string(buf) + if isPalindrome(buf) && cand > target && (best == "" || cand < best) { + best = cand + } + return + } + for c := 0; c < 26; c++ { + if cnt[c] == 0 { + continue + } + cnt[c]-- + buf = append(buf, byte('a'+c)) + rec() + buf = buf[:len(buf)-1] + cnt[c]++ + } + } + rec() + + return best +} + +func TestSolutionAgainstBruteForce(t *testing.T) { + rng := rand.New(rand.NewSource(20260828)) + + for iter := 0; iter < 400; iter++ { + n := 1 + rng.Intn(6) + alphabet := 1 + rng.Intn(3) + + letters := make([]byte, n) + for i := range letters { + letters[i] = byte('a' + rng.Intn(alphabet)) + } + s := string(letters) + + // Half the time derive target from a shuffle of s so near-miss + // prefixes get exercised, otherwise pick it uniformly at random. + var target string + if rng.Intn(2) == 0 { + shuffled := append([]byte(nil), letters...) + rng.Shuffle(n, func(i, j int) { shuffled[i], shuffled[j] = shuffled[j], shuffled[i] }) + target = string(shuffled) + } else { + raw := make([]byte, n) + for i := range raw { + raw[i] = byte('a' + rng.Intn(alphabet)) + } + target = string(raw) + } + + want := bruteForce(s, target) + got := smallestPalindrome(s, target) + if got != want { + t.Fatalf("smallestPalindrome(%q, %q) = %q, want %q", s, target, got, want) + } + + // The answer must be a permutation of s when one exists. + if got != "" { + a, b := []byte(got), []byte(s) + sort.Slice(a, func(i, j int) bool { return a[i] < a[j] }) + sort.Slice(b, func(i, j int) bool { return b[i] < b[j] }) + if string(a) != string(b) { + t.Fatalf("smallestPalindrome(%q, %q) = %q is not a permutation of s", s, target, got) + } + } + } +}