Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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 ""
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
}