最多的不重叠子字符串

标签: 贪心 字符串

难度: Hard

给你一个只包含小写字母的字符串 s ,你需要找到 s 中最多数目的非空子字符串,满足如下条件:

  1. 这些字符串之间互不重叠,也就是说对于任意两个子字符串 s[i..j] 和 s[x..y] ,要么 j < x 要么 i > y 。
  2. 如果一个子字符串包含字符 char ,那么 s 中所有 char 字符都应该在这个子字符串中。

请你找到满足上述条件的最多子字符串数目。如果有多个解法有相同的子字符串数目,请返回这些子字符串总长度最小的一个解。可以证明最小总长度解是唯一的。

请注意,你可以以 任意 顺序返回最优解的子字符串。

示例 1:

输入:s = "adefaddaccc"
输出:["e","f","ccc"]
解释:下面为所有满足第二个条件的子字符串:
[
  "adefaddaccc"
  "adefadda",
  "ef",
  "e",
  "f",
  "ccc",
]
如果我们选择第一个字符串,那么我们无法再选择其他任何字符串,所以答案为 1 。如果我们选择 "adefadda" ,剩下子字符串中我们只可以选择 "ccc" ,它是唯一不重叠的子字符串,所以答案为 2 。同时我们可以发现,选择 "ef" 不是最优的,因为它可以被拆分成 2 个子字符串。所以最优解是选择 ["e","f","ccc"] ,答案为 3 。不存在别的相同数目子字符串解。

示例 2:

输入:s = "abbaccd"
输出:["d","bb","cc"]
解释:注意到解 ["d","abba","cc"] 答案也为 3 ,但它不是最优解,因为它的总长度更长。

提示:

  • 1 <= s.length <= 10^5
  • s 只包含小写英文字母。

Submission

运行时间: 156 ms

内存: 50.7 MB

class Solution:
    def maxNumOfSubstrings(self, s: str) -> List[str]:

        positions = defaultdict(list)
        for i, c in enumerate(s):
            positions[c].append(i)
        graph = defaultdict(list)  # graph[c]表示被c所包含的子符。

        for c1 in positions:
            for c2 in positions:
                if c1 != c2:
                    c1_left, c1_right = positions[c1][0], positions[c1][-1]
                    c2_position = positions[c2]
                    if bisect_right(c2_position, c1_right) != bisect_right(c2_position, c1_left):
                        graph[c1].append(c2)
        subs = []

        def dfs(c, visited: set) -> (int, int):
            l, r = positions[c][0], positions[c][-1]
            visited.add(c)
            for nxt in graph[c]:  # 扩展
                if nxt not in visited:
                    nl, nr = dfs(nxt, visited)
                    l, r = min(l, nl), max(r, nr)
            return l, r

        for c in positions:
            visited = set()
            c2_position, r = dfs(c, visited)  # 深度遍历搜索,包含字符c的最短子串
            subs.append(((c2_position, r), visited))
        subs.sort(key=lambda p: p[0][1] - p[0][0])  # 根据长度排序

        left = set(positions.keys())  # 未使用的字符
        ans = []

        for (c2_position, r), visited in subs:
            if not visited - left:  # 该子串中所有的字符都未被使用
                ans.append(s[c2_position:r + 1])
                left -= visited  # 移除子串中所有字符
        return ans


# 作者:gidbft
# 链接:https://leetcode.cn/problems/maximum-number-of-non-overlapping-substrings/solutions/2165380/jian-tu-shen-du-bian-li-sou-suo-by-gidbf-jx16/
# 来源:力扣(LeetCode)
# 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


Explain

这道题的解决方案基于图的深度优先搜索。首先,记录每个字符在字符串中的所有位置,然后构建一个图,图中的每个节点代表一个字符,边表示两个字符的位置有重叠,即一个字符的位置区间包含了另一个字符。对每个字符执行深度优先搜索,以确定可以合并为一个最小区间的所有字符,这个区间就代表了一个有效的子字符串。所有这些子字符串根据长度进行排序,最后通过遍历这些区间,选择那些不重叠的区间,即构成解的一部分。算法需要确保每个字符至多只在一个选择的子字符串中出现。

时间复杂度: O(n + 26^2 + V log V)

空间复杂度: O(n + V)

# Enhanced with comments for clarity
class Solution:
    def maxNumOfSubstrings(self, s: str) -> List[str]:

        # Track positions of each character
        positions = defaultdict(list)
        for i, c in enumerate(s):
            positions[c].append(i)
        # Build a graph to find overlaps
        graph = defaultdict(list)

        # Create edges between characters that overlap
        for c1 in positions:
            for c2 in positions:
                if c1 != c2:
                    c1_left, c1_right = positions[c1][0], positions[c1][-1]
                    c2_position = positions[c2]
                    if bisect_right(c2_position, c1_right) != bisect_right(c2_position, c1_left):
                        graph[c1].append(c2)
        subs = []

        # Find minimal covering substring using DFS
        def dfs(c, visited: set) -> (int, int):
            l, r = positions[c][0], positions[c][-1]
            visited.add(c)
            for nxt in graph[c]:
                if nxt not in visited:
                    nl, nr = dfs(nxt, visited)
                    l, r = min(l, nl), max(r, nr)
            return l, r

        # Search for all substrings
        for c in positions:
            visited = set()
            c2_position, r = dfs(c, visited)
            subs.append(((c2_position, r), visited))
        # Sort by length
        subs.sort(key=lambda p: p[0][1] - p[0][0])

        left = set(positions.keys())
        ans = []

        # Select non-overlapping substrings
        for (c2_position, r), visited in subs:
            if not visited - left:
                ans.append(s[c2_position:r + 1])
                left -= visited
        return ans

Explore

在构建图时,比较每个字符的位置区间是否有重叠是为了确定字符间的依赖关系,以便找到可以合并为一个连续区间的所有字符。具体条件是,如果一个字符c1的位置区间(从第一个出现到最后一个出现的位置)与另一个字符c2的某个位置有重叠,即c1的最右位置大于c2的第一个位置且c1的第一个位置小于c2的最后一个位置,这两个字符的区间就被视为有重叠。这种情况下,在图中创建从c1到c2的边,表示在选择子字符串时需要同时考虑这两个字符。

在DFS遍历过程中,更新和比较字符的最左和最右位置有助于确定能够包含该字符及其相关重叠字符的最小连续区间。这是因为如果一个字符与其他字符有重叠,那么在构成一个有效的子字符串时,这些字符应该被合并到一个更大的区间中。通过递归地更新这些值,我们能够找到包含所有相关字符的最小区间。这对于后续的选择最大数量的不重叠子字符串是非常关键的,因为它保证了每个选择的子字符串都是最小且必要的。

在实现中,首先将所有可能的子字符串按照长度进行排序,然后从最短的子字符串开始选择。在选择过程中,维护一个集合来记录已经被包含在选择的子字符串中的字符。对于每个待选择的子字符串,检查它包含的字符集合是否与已选择的子字符串中的字符有重叠。如果没有重叠,就将这个子字符串添加到答案中,并更新已包含字符的集合。这种方法通过贪心策略确保了每次选择的都是最小的且不与已选择的子字符串重叠的区间,从而达到了题目要求的不重叠条件。

这个条件意味着如果选择的子字符串中包含某个字符char,则这个子字符串必须包括字符串s中该字符char的所有出现。这确保了子字符串的完整性和独立性。在算法实现中,这通过记录每个字符在字符串s中的第一个位置和最后一个位置(即完整区间)来体现。在使用DFS搜索合适的子字符串时,对于每个字符,我们都会计算包含它及其所有相关(重叠)字符的最小区间,从而确保如果子字符串中包括了字符char,就一定包含了s中该字符的所有出现。