From 6e62163ed5770f2183411ac3e6add6dd241da9ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 15 Sep 2026 05:09:52 +0000 Subject: [PATCH] feat: add solution for 2472. Maximum Number of Non-overlapping Palindrome Substrings --- .../analysis.md | 80 +++++++++++++++++++ .../problem.md | 59 ++++++++++++++ .../solution.go | 48 +++++++++++ .../solution_test.go | 41 ++++++++++ 4 files changed, 228 insertions(+) create mode 100644 problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/analysis.md create mode 100644 problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/problem.md create mode 100644 problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/solution.go create mode 100644 problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/solution_test.go diff --git a/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/analysis.md b/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/analysis.md new file mode 100644 index 0000000..a5280cd --- /dev/null +++ b/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/analysis.md @@ -0,0 +1,80 @@ +# 2472. Maximum Number of Non-overlapping Palindrome Substrings + +[LeetCode Link](https://leetcode.com/problems/maximum-number-of-non-overlapping-palindrome-substrings/) + +Difficulty: Hard +Topics: Two Pointers, String, Dynamic Programming, Greedy +Acceptance Rate: 51.2% + +## Hints + +### Hint 1 + +Strip the palindrome part away for a moment and ask what the problem really is: you are handed a collection of intervals `[i, j]` (every palindromic substring of length at least `k`) and asked to pick as many as possible without any two touching. That is the classic **interval scheduling** / **non-overlapping intervals** setup. Every solution to this problem is really "enumerate the candidate intervals efficiently, then schedule them." + +### Hint 2 + +Two sub-problems, solve them separately: + +1. **Which intervals exist?** Deciding "is `s[i..j]` a palindrome?" for every pair naively costs O(n) per pair, which is O(n³) overall — too slow for `n = 2000`. There is a standard O(n²) way to get *all* of them at once, and it comes from a tiny recurrence: a string is a palindrome exactly when its two ends match **and** the string inside them is a palindrome. Think about what order you must fill a 2-D table in for that recurrence to be usable. +2. **How do you schedule them?** Define a one-dimensional DP over suffixes: `dp[i]` = the best answer achievable using only `s[i:]`. At index `i` you either skip character `i`, or you commit to some palindrome starting at `i`. + +### Hint 3 + +The insight that makes both halves click: + +- For the palindrome table, iterate `i` **downward** (from `n-1` to `0`) and `j` upward from `i`. Then `isPal[i][j] = s[i] == s[j] && (j-i < 2 || isPal[i+1][j-1])` — the inner substring `[i+1, j-1]` has a larger left index, so it was already computed. Lengths 1 and 2 are the base cases folded into `j-i < 2`. +- For the scheduling, `dp[i] = max(dp[i+1], 1 + dp[j+1])` over every `j >= i+k-1` with `isPal[i][j]`. And here is the greedy kicker: among all palindromes starting at `i`, you never need more than the **shortest** valid one. If an optimal solution uses a longer palindrome starting at `i`, swapping in the shorter one keeps the same count and frees up strictly more room on the right — an exchange argument. So the inner loop can `break` at the first match. + +## Approach + +The solution has two phases. + +**Phase 1 — precompute every palindrome in O(n²).** + +Build a boolean table `isPal[i][j]` meaning "`s[i..j]` is a palindrome". The recurrence is: + +``` +isPal[i][j] = (s[i] == s[j]) && (j - i < 2 || isPal[i+1][j-1]) +``` + +`j - i < 2` covers the two base cases: a single character (`i == j`) is always a palindrome, and a two-character block is a palindrome iff its characters match. For anything longer, we peel off the outer pair and defer to the inside. + +The dependency is `(i, j) -> (i+1, j-1)`, so we fill with `i` descending and `j` ascending. In the code the table is one flat `[]bool` of size `n*n` indexed as `i*n + j` — for `n = 2000` that is 4 MB in one allocation instead of 2000 separate slices, which is both faster and friendlier to the garbage collector. + +**Phase 2 — DP over suffixes.** + +Let `dp[i]` be the maximum number of non-overlapping valid substrings selectable from `s[i:]`, with `dp[n] = 0`. Walk `i` from `n-1` down to `0`: + +- Option A: don't start a substring at `i`. That gives `dp[i+1]`. +- Option B: start one at `i`. Scan `j` from `i+k-1` rightward for the first `j` with `isPal[i][j]` true. Taking it gives `1 + dp[j+1]`. + +`dp[i]` is the max of the two, and the answer is `dp[0]`. + +The `break` after the first match in Option B is the greedy step justified in Hint 3: a shorter palindrome starting at `i` is never worse than a longer one, because `dp` is non-increasing in its index (`dp[j+1] >= dp[j'+1]` whenever `j <= j'`), so `1 + dp[j+1]` is already maximal at the smallest feasible `j`. + +**Worked example: `s = "abaccdbbd"`, `k = 3`.** + +Indices: `a(0) b(1) a(2) c(3) c(4) d(5) b(6) b(7) d(8)`. Palindromes of length ≥ 3 are `"aba"` at `[0,2]` and `"dbbd"` at `[5,8]`. + +Filling `dp` right-to-left: `dp[9] = 0`, and `dp[8] = dp[7] = dp[6] = 0` (no length-3 palindrome starts there). At `i = 5`, `j = 8` gives `isPal[5][8] = true` (`"dbbd"`), so `dp[5] = max(dp[6], 1 + dp[9]) = 1`. Indices 4 and 3 start nothing, so `dp[4] = dp[3] = 1`. At `i = 0`, `j = 2` matches (`"aba"`), so `dp[0] = max(dp[1], 1 + dp[3]) = max(1, 2) = 2`. Answer: **2**. + +Note how `dp[3] = 1` already carries the `"dbbd"` choice forward — that is exactly the "schedule the rest of the string optimally" subproblem, and it is why the greedy break is safe. + +**A note on the alternative.** There is a slicker O(n·k)-ish solution that expands around each of the `2n-1` centers left to right and greedily takes the first palindrome of length ≥ `k` that starts at or after the last chosen endpoint. It uses O(1) extra space and is worth studying once you have the DP down, but the center ordering argument is fiddlier to get right. The DP above is the version to reach for under interview pressure: it is easy to justify out loud and comfortably fast enough for `n = 2000`. + +## Complexity Analysis + +Time Complexity: O(n²) — the palindrome table is O(n²) to fill, and the DP phase does at most O(n) work per index in the worst case (for example `s = "aaaa...a"` with large `k`), so O(n²) overall. With n = 2000 that's ~4·10⁶ operations. + +Space Complexity: O(n²) for the palindrome table, plus O(n) for the `dp` array. The flat `[]bool` layout keeps this at ~4 MB for the maximum input. + +## Edge Cases + +- **`k == 1`** — every single character is a palindrome of sufficient length, so the answer is always `len(s)`. Make sure the inner loop starts at `j = i + k - 1 = i` and not `i + 1`; an off-by-one here silently halves the answer on strings like `"abc"`. +- **`k > len(s)`** — no substring can be long enough, so the answer is `0`. The loop bound `j <= n-1` makes the inner loop body never execute, and `dp` stays all zeros. Worth guarding explicitly (the code returns early) so the O(n²) table is never even built. +- **Empty string** — outside the stated constraints (`1 <= s.length`), but the early return keeps it from panicking on an index. Cheap insurance. +- **No palindrome of length ≥ k exists** (Example 2, `"adbcda"` with `k = 2`) — the answer is `0`, not `1`. It's easy to write a greedy that accidentally counts a single character. +- **Highly repetitive strings like `"aaaaaaaa"`** — this is the worst case for the DP and the case where the "take the shortest palindrome" greedy matters most. With `k = 2` the answer is `len(s) / 2`; a solution that greedily grabs the *longest* palindrome at each position would return `1`. +- **Even vs. odd length palindromes** — `"aa"` and `"aba"` must both be found. The `j - i < 2` base case handles both; a table built only from odd centers would miss `"cc"` in Example 1. +- **The palindrome that must be split** — e.g. `s = "abbaaa"`, `k = 2`. Taking `"abba"` yields 2, and so does taking `"bb"` then `"aa"`. Cases like this are where an untested greedy tends to go wrong, so keep one in your test table. diff --git a/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/problem.md b/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/problem.md new file mode 100644 index 0000000..4cde6d7 --- /dev/null +++ b/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/problem.md @@ -0,0 +1,59 @@ +--- +number: "2472" +frontend_id: "2472" +title: "Maximum Number of Non-overlapping Palindrome Substrings" +slug: "maximum-number-of-non-overlapping-palindrome-substrings" +difficulty: "Hard" +topics: + - "Two Pointers" + - "String" + - "Dynamic Programming" + - "Greedy" +acceptance_rate: 5123.1 +is_premium: false +created_at: "2026-09-15T05:07:02.711546+00:00" +fetched_at: "2026-09-15T05:07:02.711546+00:00" +link: "https://leetcode.com/problems/maximum-number-of-non-overlapping-palindrome-substrings/" +date: "2026-09-15" +--- + +# 2472. Maximum Number of Non-overlapping Palindrome Substrings + +You are given a string `s` and a **positive** integer `k`. + +Select a set of **non-overlapping** substrings from the string `s` that satisfy the following conditions: + + * The **length** of each substring is **at least** `k`. + * Each substring is a **palindrome**. + + + +Return _the**maximum** number of substrings in an optimal selection_. + +A **substring** is a contiguous sequence of characters within a string. + + + +**Example 1:** + + + **Input:** s = "abaccdbbd", k = 3 + **Output:** 2 + **Explanation:** We can select the substrings underlined in s = "_**aba**_ cc _**dbbd**_ ". Both "aba" and "dbbd" are palindromes and have a length of at least k = 3. + It can be shown that we cannot find a selection with more than two valid substrings. + + +**Example 2:** + + + **Input:** s = "adbcda", k = 2 + **Output:** 0 + **Explanation:** There is no palindrome substring of length at least 2 in the string. + + + + +**Constraints:** + + * `1 <= k <= s.length <= 2000` + * `s` consists of lowercase English letters. diff --git a/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/solution.go b/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/solution.go new file mode 100644 index 0000000..8516456 --- /dev/null +++ b/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/solution.go @@ -0,0 +1,48 @@ +package main + +// 2472. Maximum Number of Non-overlapping Palindrome Substrings +// +// Two phases: +// 1. Precompute isPal[i][j] ("s[i..j] is a palindrome") with the recurrence +// isPal[i][j] = s[i] == s[j] && (j-i < 2 || isPal[i+1][j-1]), filling i +// downward so the inner substring is always ready. Stored as one flat +// []bool indexed i*n+j to keep the 2000x2000 worst case in a single 4 MB +// allocation. +// 2. DP over suffixes: dp[i] = best answer for s[i:], with +// dp[i] = max(dp[i+1], 1+dp[j+1]) over palindromic s[i..j] of length >= k. +// Since dp is non-increasing, the shortest palindrome starting at i is +// always at least as good as a longer one, so the scan breaks on the first +// match. +func maxPalindromes(s string, k int) int { + n := len(s) + if n == 0 || k > n { + return 0 + } + + isPal := make([]bool, n*n) + for i := n - 1; i >= 0; i-- { + row := i * n + for j := i; j < n; j++ { + if s[i] == s[j] && (j-i < 2 || isPal[row+n+j-1]) { + isPal[row+j] = true + } + } + } + + dp := make([]int, n+1) + for i := n - 1; i >= 0; i-- { + dp[i] = dp[i+1] + row := i * n + for j := i + k - 1; j < n; j++ { + if isPal[row+j] { + // Shortest valid palindrome at i wins; no need to look further. + if best := 1 + dp[j+1]; best > dp[i] { + dp[i] = best + } + break + } + } + } + + return dp[0] +} diff --git a/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/solution_test.go b/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/solution_test.go new file mode 100644 index 0000000..ae44f91 --- /dev/null +++ b/problems/2472-maximum-number-of-non-overlapping-palindrome-substrings/solution_test.go @@ -0,0 +1,41 @@ +package main + +import ( + "strings" + "testing" +) + +func TestSolution(t *testing.T) { + tests := []struct { + name string + s string + k int + expected int + }{ + {"example 1: pick \"aba\" and \"dbbd\"", "abaccdbbd", 3, 2}, + {"example 2: no palindrome of length at least k", "adbcda", 2, 0}, + {"edge case: k is 1 so every character counts", "abc", 1, 3}, + {"edge case: single character with k of 1", "a", 1, 1}, + {"edge case: k larger than the string", "abc", 4, 0}, + {"edge case: empty input", "", 1, 0}, + {"edge case: k equals the string length and it is a palindrome", "aaaaa", 5, 1}, + {"edge case: repeated characters split into pairs", "aaaaa", 2, 2}, + {"edge case: repeated characters split into triples", "aaaaaaaaaa", 3, 3}, + {"edge case: shorter k on example 1 finds \"aba\", \"cc\", \"bb\"", "abaccdbbd", 2, 3}, + {"edge case: splitting \"abba\" into \"bb\" and \"aa\" is no worse", "abbaaa", 2, 2}, + {"edge case: all candidates overlap around one center", "racecar", 3, 1}, + {"edge case: nested palindromes favour the shorter pick", "abacaba", 3, 2}, + {"edge case: only an even-length palindrome qualifies", "leetcode", 2, 1}, + {"edge case: no palindrome of length 3 exists", "abc", 3, 0}, + {"edge case: k of 1 on a palindrome-free string", "adbcda", 1, 6}, + {"edge case: long uniform string at maximum constraint", strings.Repeat("a", 2000), 2, 1000}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := maxPalindromes(tt.s, tt.k); got != tt.expected { + t.Errorf("maxPalindromes(%q, %d) = %v, want %v", tt.s, tt.k, got, tt.expected) + } + }) + } +}