diff --git a/problems/1520-maximum-number-of-non-overlapping-substrings/analysis.md b/problems/1520-maximum-number-of-non-overlapping-substrings/analysis.md new file mode 100644 index 0000000..2dc5058 --- /dev/null +++ b/problems/1520-maximum-number-of-non-overlapping-substrings/analysis.md @@ -0,0 +1,57 @@ +# 1520. Maximum Number of Non-Overlapping Substrings + +[LeetCode Link](https://leetcode.com/problems/maximum-number-of-non-overlapping-substrings/) + +Difficulty: Hard +Topics: Hash Table, String, Greedy, Sorting +Acceptance Rate: 50.0% + +## Hints + +### Hint 1 + +The string can be up to 10^5 characters, but the alphabet is only 26 letters. That gap is the whole problem: any answer is described by a small set of intervals, not by arbitrary substrings. Start by asking what the *smallest* valid substring containing a given letter looks like, and note that once you turn substrings into `[start, end]` intervals, this becomes a very familiar scheduling-style question. + +### Hint 2 + +For a letter `c`, any valid substring that contains `c` must span at least `[first[c], last[c]]`. But pulling in that range may drag in other letters whose own occurrences reach further out, so the range has to grow. Keep growing until it is closed under "contains every occurrence of every letter inside it". You now have at most 26 candidate intervals — one per letter — and the original problem reduces to picking as many non-overlapping ones as possible. + +### Hint 3 + +Two things make the whole thing click: + +1. While expanding `[first[c], last[c]]` to the right, if you ever meet a letter whose *first* occurrence lies to the **left** of your start, then no valid substring can start at `first[c]` — discard this candidate entirely. You don't need to expand leftwards, because that longer interval will be produced by some other letter's candidate anyway. +2. The surviving candidates are **laminar**: any two are either disjoint or one fully contains the other (they can never partially overlap — a partial overlap would violate the closure property). So the classic interval-scheduling greedy of "sort by right endpoint, take every interval that starts after the last taken end" simultaneously maximizes the count *and* minimizes total length: whenever a big interval and a nested small one compete, the small one wins on the right endpoint. + +## Approach + +**Step 1 — first/last tables.** Scan `s` once and record, for each of the 26 letters, the index of its first and last occurrence. Letters absent from `s` are skipped. + +**Step 2 — build one candidate interval per letter.** For a letter `c` present in `s`, set `start = first[c]` and `end = last[c]`, then walk `j` from `start` to `end` (with `end` growing as you go): + +- If `first[s[j]] < start`, the interval would have to extend left of `start`; that contradicts starting at `first[c]`, so mark this candidate invalid and stop. +- Otherwise set `end = max(end, last[s[j]])`. + +If the walk finishes, `[start, end]` is the **minimal** valid substring whose start is `first[c]`. Because the loop bound `end` only grows and `j` only moves forward, one candidate costs `O(n)` and all 26 cost `O(26n)`. + +**Step 3 — greedy selection.** Sort the valid candidates by `end` ascending and sweep: keep a variable `prevEnd` (initially `-1`) and take a candidate whenever `start > prevEnd`, updating `prevEnd = end`. Emit `s[start : end+1]` for each taken candidate. + +**Why the greedy is optimal on both criteria.** Candidates cannot partially overlap: if `[a1, b1]` and `[a2, b2]` overlapped with `a1 < a2 <= b1 < b2`, some letter occurring in `[a2, b1]` would have its last occurrence beyond `b1`, so the first interval would have been expanded past `b1` — contradiction. With only nesting or disjointness left, sorting by right endpoint means an inner interval is always considered before the outer one that contains it. Taking the inner one never costs you a substring (anything disjoint from the outer is also disjoint from the inner) and strictly reduces total length, which is exactly the tie-breaking rule the problem asks for. + +**Worked example — `s = "adefaddaccc"`.** Candidates: `a → [0,7]` (valid, pulls in `d`), `d → invalid` (hits `a` at index 4 whose first occurrence 0 is left of start 1), `e → [2,2]`, `f → [3,3]`, `c → [8,10]`. Sorted by end: `[2,2], [3,3], [0,7], [8,10]`. The sweep takes `[2,2]` → `"e"`, `[3,3]` → `"f"`, skips `[0,7]` (starts at 0, before `prevEnd = 3`), takes `[8,10]` → `"ccc"`. Answer: `["e", "f", "ccc"]`. + +## Complexity Analysis + +Time Complexity: O(26 * n) = O(n) — one pass for the first/last tables, at most 26 expansion passes each bounded by `n`, and sorting at most 26 intervals (constant). +Space Complexity: O(1) auxiliary (two 26-entry tables plus at most 26 candidate intervals), excluding the O(n) total size of the returned substrings. + +## Edge Cases + +- **Single character (`"a"`)** — the shortest legal input; must return `["a"]`, not an empty list. +- **All characters identical (`"aaaa"`)** — exactly one candidate spanning the whole string, so the answer is one substring equal to `s`. +- **All characters distinct (`"abcd"`)** — every letter is its own candidate; the answer is `n` single-character substrings, the maximum possible. +- **Fully nested letters (`"abcba"`)** — candidates are `[0,4]`, `[1,3]`, `[2,2]`, each containing the next; only one can be chosen and it must be the innermost, so the answer is `["c"]`, not `["abcba"]`. +- **Interleaved letters (`"abab"`)** — expanding `a` reaches `b`'s last occurrence and vice versa, so only one candidate survives and the answer is the whole string; a solution that forgets to keep growing `end` inside the loop will wrongly return two substrings. +- **Candidates rejected by the left check (`"adefadda..."`, letter `d`)** — forgetting the `first[s[j]] < start` test produces intervals that are not actually closed, breaking the laminar property the greedy relies on. +- **Ties between a nested and an enclosing interval (`"abbaccd"`)** — both `["abba","cc","d"]` and `["bb","cc","d"]` have three substrings; sorting by `end` is what forces the shorter `"bb"`, satisfying the minimum-total-length requirement. +- **Long input (10^5 characters)** — the expansion loop must reuse the growing `end` rather than restarting, otherwise the per-letter pass degrades and the solution times out. diff --git a/problems/1520-maximum-number-of-non-overlapping-substrings/problem.md b/problems/1520-maximum-number-of-non-overlapping-substrings/problem.md new file mode 100644 index 0000000..a1cfe03 --- /dev/null +++ b/problems/1520-maximum-number-of-non-overlapping-substrings/problem.md @@ -0,0 +1,65 @@ +--- +number: "1520" +frontend_id: "1520" +title: "Maximum Number of Non-Overlapping Substrings" +slug: "maximum-number-of-non-overlapping-substrings" +difficulty: "Hard" +topics: + - "Hash Table" + - "String" + - "Greedy" + - "Sorting" +acceptance_rate: 4999.3 +is_premium: false +created_at: "2026-09-18T04:57:45.731491+00:00" +fetched_at: "2026-09-18T04:57:45.731491+00:00" +link: "https://leetcode.com/problems/maximum-number-of-non-overlapping-substrings/" +date: "2026-09-18" +--- + +# 1520. Maximum Number of Non-Overlapping Substrings + +Given a string `s` of lowercase letters, you need to find the maximum number of **non-empty** substrings of `s` that meet the following conditions: + + 1. The substrings do not overlap, that is for any two substrings `s[i..j]` and `s[x..y]`, either `j < x` or `i > y` is true. + 2. A substring that contains a certain character `c` must also contain all occurrences of `c`. + + + +Find _the maximum number of substrings that meet the above conditions_. If there are multiple solutions with the same number of substrings, _return the one with minimum total length._ It can be shown that there exists a unique solution of minimum total length. + +Notice that you can return the substrings in **any** order. + + + +**Example 1:** + + + **Input:** s = "adefaddaccc" + **Output:** ["e","f","ccc"] + **Explanation:** The following are all the possible substrings that meet the conditions: + [ + "adefaddaccc" + "adefadda", + "ef", + "e", + "f", + "ccc", + ] + If we choose the first string, we cannot choose anything else and we'd get only 1. If we choose "adefadda", we are left with "ccc" which is the only one that doesn't overlap, thus obtaining 2 substrings. Notice also, that it's not optimal to choose "ef" since it can be split into two. Therefore, the optimal way is to choose ["e","f","ccc"] which gives us 3 substrings. No other solution of the same number of substrings exist. + + +**Example 2:** + + + **Input:** s = "abbaccd" + **Output:** ["d","bb","cc"] + **Explanation:** Notice that while the set of substrings ["d","abba","cc"] also has length 3, it's considered incorrect since it has larger total length. + + + + +**Constraints:** + + * `1 <= s.length <= 105` + * `s` contains only lowercase English letters. diff --git a/problems/1520-maximum-number-of-non-overlapping-substrings/solution_daily_20260918.go b/problems/1520-maximum-number-of-non-overlapping-substrings/solution_daily_20260918.go new file mode 100644 index 0000000..6d79306 --- /dev/null +++ b/problems/1520-maximum-number-of-non-overlapping-substrings/solution_daily_20260918.go @@ -0,0 +1,76 @@ +package main + +import "sort" + +// Greedy over at most 26 candidate intervals. +// +// For every letter present in s, the smallest valid substring that starts at +// that letter's first occurrence is found by expanding [first[c], last[c]] to +// the right until it contains every occurrence of every letter inside it. If the +// expansion meets a letter whose first occurrence lies left of the start, no +// valid substring starts there and the candidate is dropped (the wider interval +// is produced by that other letter instead). +// +// Surviving candidates are laminar (nested or disjoint, never partially +// overlapping), so sorting them by right endpoint and taking every interval that +// starts after the last taken end maximizes the count while minimizing total +// length: a nested interval is always considered before the one enclosing it. +// +// Time: O(26*n). Space: O(1) auxiliary. +func maxNumOfSubstrings(s string) []string { + const alphabet = 26 + + var first, last [alphabet]int + for i := range first { + first[i] = -1 + last[i] = -1 + } + for i := 0; i < len(s); i++ { + c := s[i] - 'a' + if first[c] == -1 { + first[c] = i + } + last[c] = i + } + + type interval struct{ start, end int } + candidates := make([]interval, 0, alphabet) + + for c := 0; c < alphabet; c++ { + start := first[c] + if start == -1 { + continue + } + + end := last[c] + valid := true + for j := start; j <= end; j++ { + cur := s[j] - 'a' + if first[cur] < start { + valid = false + break + } + if last[cur] > end { + end = last[cur] + } + } + if valid { + candidates = append(candidates, interval{start, end}) + } + } + + sort.Slice(candidates, func(i, j int) bool { + return candidates[i].end < candidates[j].end + }) + + result := make([]string, 0, len(candidates)) + prevEnd := -1 + for _, iv := range candidates { + if iv.start > prevEnd { + result = append(result, s[iv.start:iv.end+1]) + prevEnd = iv.end + } + } + + return result +} diff --git a/problems/1520-maximum-number-of-non-overlapping-substrings/solution_daily_20260918_test.go b/problems/1520-maximum-number-of-non-overlapping-substrings/solution_daily_20260918_test.go new file mode 100644 index 0000000..084881a --- /dev/null +++ b/problems/1520-maximum-number-of-non-overlapping-substrings/solution_daily_20260918_test.go @@ -0,0 +1,73 @@ +package main + +import ( + "reflect" + "sort" + "testing" +) + +// sorted returns a sorted copy so results can be compared regardless of order, +// as the problem allows returning the substrings in any order. +func sorted(in []string) []string { + out := append([]string(nil), in...) + sort.Strings(out) + return out +} + +func TestSolution(t *testing.T) { + tests := []struct { + name string + s string + expected []string + }{ + { + name: "example 1: nested candidate is skipped in favor of two short ones", + s: "adefaddaccc", + expected: []string{"e", "f", "ccc"}, + }, + { + name: "example 2: tie broken by minimum total length", + s: "abbaccd", + expected: []string{"bb", "cc", "d"}, + }, + { + name: "edge case: single character", + s: "a", + expected: []string{"a"}, + }, + { + name: "edge case: all characters identical", + s: "aaaa", + expected: []string{"aaaa"}, + }, + { + name: "edge case: all characters distinct", + s: "abcd", + expected: []string{"a", "b", "c", "d"}, + }, + { + name: "edge case: interleaved letters force a single substring", + s: "abab", + expected: []string{"abab"}, + }, + { + name: "edge case: fully nested letters, innermost wins", + s: "abcba", + expected: []string{"c"}, + }, + { + name: "edge case: enclosing candidate discarded for two inner ones", + s: "cbacdcbc", + expected: []string{"a", "d"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := maxNumOfSubstrings(tt.s) + if !reflect.DeepEqual(sorted(result), sorted(tt.expected)) { + t.Errorf("maxNumOfSubstrings(%q) = %v, want %v (any order)", tt.s, result, tt.expected) + } + }) + } +}