Skip to content

Commit e6306d8

Browse files
committed
Solved LeetCode 1520 using interval-closure expansion and right-endpoint greedy selection to maximize non-overlapping substrings containing all occurrences of their characters with runtime = 31ms.
1 parent 176381d commit e6306d8

1 file changed

Lines changed: 175 additions & 0 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
package LeetCode.Strings;
2+
3+
import java.util.*;
4+
5+
public class LeetCode_1520_MaxNumsOfNonOverlappingSubStrings {
6+
public static void main(String[] args) {
7+
8+
// Sample 1 (LeetCode): "adefaddaccc" -> ["e","f","c"] (3 substrings)
9+
System.out.println("Sample 1 -> " + maxNumOfSubstrings("adefaddaccc")
10+
+ " (expected 3 substrings, e.g. [e, f, c])");
11+
12+
// Sample 2 (LeetCode): "abbaccd" -> 3 substrings, e.g. ["bb","cc","d"]
13+
System.out.println("Sample 2 -> " + maxNumOfSubstrings("abbaccd")
14+
+ " (expected 3 substrings)");
15+
16+
// Edge case: all identical characters -> one substring covers all occurrences
17+
System.out.println("Edge (aaaa) -> " + maxNumOfSubstrings("aaaa")
18+
+ " (expected: [aaaa], size 1)");
19+
20+
// Edge case: each character appears exactly once -> every char its own substring
21+
System.out.println("Edge (abcd) -> " + maxNumOfSubstrings("abcd")
22+
+ " (expected: [a, b, c, d], size 4)");
23+
24+
// Edge case: fully interleaved -> one big substring is forced
25+
System.out.println("Edge (abab) -> " + maxNumOfSubstrings("abab")
26+
+ " (expected: [abab], size 1)");
27+
28+
// Edge case: single character
29+
System.out.println("Edge (a) -> " + maxNumOfSubstrings("a")
30+
+ " (expected: [a], size 1)");
31+
}
32+
33+
34+
/*
35+
Approach: Interval expansion + greedy selection
36+
37+
Problem restated:
38+
Split s into as many non-overlapping substrings as possible, such that each chosen substring contains EVERY occurrence of every character it contains.
39+
Return any maximum-size partition.
40+
41+
Idea:
42+
1. For each character, its minimal valid substring must span from its first occurrence to its last occurrence. Call this [left, right].
43+
44+
2. But that span may include OTHER characters whose own [left, right] extend beyond the current interval.
45+
So we iteratively expand the interval until it is "closed": every character inside has all its occurrences inside.
46+
Example: "adefaddaccc"
47+
a: [0, 7], d: [1, 6], e: [2, 2], f: [3, 3], c: [8, 10]
48+
d's span [1,6] contains 'a' at indices 0 and 7 -> expand d to [0,7].
49+
Now every character inside [0,7] (a,d,e,f) is fully contained.
50+
51+
3. Sort the closed intervals by right endpoint ascending, and for equal right endpoints by left endpoint DESCENDING (shortest first).
52+
This ordering is critical for the greedy to work.
53+
54+
4. Greedily scan: keep `end` = end of last chosen interval.
55+
If a segment's left > end, it doesn't overlap anything chosen so far, so take it and update end = its right.
56+
57+
Why greedy works:
58+
Sorting by right endpoint ascending ensures that when we pick a segment, it finishes as early as possible, leaving maximum room for subsequent picks.
59+
The left-descending tie-break ensures that among segments with the same end, the one that starts later (i.e. shorter, less likely to conflict with previously-chosen segments) is considered first — though in practice the count is the same.
60+
61+
Time: O(n * 26) for interval expansion (amortized) + O(26 log 26) sort
62+
Space: O(26) for segment bookkeeping
63+
*/
64+
static List<String> maxNumOfSubstrings(String s) {
65+
Seg[] seg = new Seg[26];
66+
for (int i = 0; i < 26; ++i) {
67+
seg[i] = new Seg(-1, -1);
68+
}
69+
70+
// Step 1: record leftmost and rightmost occurrence of each character
71+
for (int i = 0; i < s.length(); ++i) {
72+
int charIdx = s.charAt(i) - 'a';
73+
if (seg[charIdx].left == -1) {
74+
seg[charIdx].left = seg[charIdx].right = i;
75+
} else {
76+
seg[charIdx].right = i;
77+
}
78+
}
79+
80+
// Step 2: iteratively expand each character's interval until it is "closed" (i.e. contains every occurrence of every character within it)
81+
for (int i = 0; i < 26; ++i) {
82+
if (seg[i].left != -1) {
83+
for (int j = seg[i].left; j <= seg[i].right; ++j) {
84+
int charIdx = s.charAt(j) - 'a';
85+
86+
// If this inner character is already fully contained, keep going
87+
if (seg[i].left <= seg[charIdx].left &&
88+
seg[charIdx].right <= seg[i].right) {
89+
continue;
90+
}
91+
92+
// Otherwise, expand to include the inner character's full span
93+
seg[i].left = Math.min(seg[i].left, seg[charIdx].left);
94+
seg[i].right = Math.max(seg[i].right, seg[charIdx].right);
95+
96+
// Restart the inner scan from the new (possibly earlier) left edge
97+
j = seg[i].left;
98+
}
99+
}
100+
}
101+
102+
// Step 3: sort closed intervals (right asc, left desc)
103+
Arrays.sort(seg);
104+
105+
// Step 4: greedy pick of non-overlapping intervals
106+
List<String> ans = new ArrayList<>();
107+
int end = -1;
108+
for (Seg segment : seg) {
109+
int left = segment.left, right = segment.right;
110+
if (left == -1) {
111+
continue; // unused character slot
112+
}
113+
if (end == -1 || left > end) { // no overlap with last pick
114+
end = right;
115+
ans.add(s.substring(left, right + 1));
116+
}
117+
}
118+
return ans;
119+
}
120+
121+
/*
122+
Segment representing a character's minimal valid substring bounds.
123+
124+
Comparator:
125+
- primary: right ascending (finish early, leave room)
126+
- tie-break: left descending (among equal rights, prefer shorter)
127+
*/
128+
static class Seg implements Comparable<Seg> {
129+
int left, right;
130+
131+
Seg(int left, int right) {
132+
this.left = left;
133+
this.right = right;
134+
}
135+
136+
@Override
137+
public int compareTo(Seg rhs) {
138+
if (right == rhs.right) {
139+
return rhs.left - left; // left descending
140+
}
141+
return right - rhs.right; // right ascending
142+
}
143+
}
144+
}
145+
146+
/*
147+
---------------------------------------------------------
148+
Complexity Analysis
149+
---------------------------------------------------------
150+
151+
Approach: Interval expansion + greedy selection
152+
153+
Let n = s.length() and Σ = 26 (alphabet size).
154+
155+
Time Complexity: O(n + Σ^2)
156+
157+
- Recording first/last occurrence: O(n).
158+
- Interval expansion: each character's interval only ever grows, and each expansion resets the inner scan.
159+
In the worst case this is O(Σ * n) but in practice converges quickly; the tight bound commonly cited is O(n * Σ).
160+
- Sorting Σ intervals: O(Σ log Σ) = O(1) since Σ = 26 is constant.
161+
- Greedy scan: O(Σ).
162+
163+
Dominant term: O(n * Σ) worst case; O(n + Σ^2) amortized in typical inputs.
164+
165+
Space Complexity: O(Σ)
166+
167+
- seg[26] array of small Seg objects.
168+
- Output list holds at most 26 substrings (one per distinct character).
169+
170+
Key Observation:
171+
Each character's minimal valid substring is determined by its first and last occurrence, but this span may be forced to grow to contain other characters' full spans , hence the closure/expansion step.
172+
Once intervals are closed, sorting by right endpoint and greedily picking non-overlapping ones yields the maximum count, because finishing early always leaves more room.
173+
174+
---------------------------------------------------------
175+
*/

0 commit comments

Comments
 (0)