📌  相关文章
📜  包含另一个字符串作为其子字符串的字符串的字典序最小和最大字谜(1)

📅  最后修改于: 2023-12-03 15:37:01.566000             🧑  作者: Mango

包含另一个字符串作为其子字符串的字符串的字典序最小和最大字谜

给定一个字符串数组,我们要找出一个字符串,使其包含另一个字符串作为其子字符串,且其字典序最小或最大。

解题思路

本题的解法可以借助于字典树和后缀数组两种数据结构。

字典树可以高效地查询一个字符串是否包含另一个字符串作为其子字符串。对于每个输入的字符串,我们在字典树上插入其所有可能的子串,在插入过程中标记该点是否是某个字符串的末尾。查询一个字符串是否包含另一个字符串作为其子字符串,则可以在字典树上查找该字符串的每个子串是否都可以从根节点到达,且其中某个子串标记为某个字符串的末尾。

具体实现时,我们可以使用线段树+字典树的方式来维护字典树上的节点信息,并使用深度优先遍历来查询每个字符串是否包含另一个字符串作为其子字符串。

另外一种解法是使用后缀数组。后缀数组可以高效地查询一个字符串的所有后缀是否包含另一个字符串作为其前缀。对于每个输入的字符串,我们将其所有后缀插入到后缀数组中,并使用二分查找来查询其中是否有子串包含另一个字符串。具体的查询过程中,我们可以先找到该另一个字符串在后缀数组中的位置区间,然后在该区间内二分查找是否存在以该另一个字符串为前缀的后缀。

代码实现
字典树实现
class SegmentTree:
    def __init__(self, n, init_val=0):
        self.n = n
        self.tree = [init_val] * (4 * n)

    def update(self, idx, val):
        def _update(node, l, r):
            if l == r:
                self.tree[node] = val
                return
            mid = (l + r) // 2
            if idx <= mid:
                _update(node * 2, l, mid)
            else:
                _update(node * 2 + 1, mid + 1, r)
            self.tree[node] = min(self.tree[node * 2], self.tree[node * 2 + 1])
        _update(1, 1, self.n)

    def query(self, qs, qe):
        def _query(node, l, r):
            if qs > r or qe < l:
                return float('inf')
            if qs <= l and qe >= r:
                return self.tree[node]
            mid = (l + r) // 2
            return min(_query(node * 2, l, mid), _query(node * 2 + 1, mid + 1, r))
        return _query(1, 1, self.n)


class Trie:
    def __init__(self):
        self.root = {}
        self.idx = 0

    def insert(self, s):
        node = self.root
        for ch in s:
            if ch not in node:
                self.idx += 1
                node[ch] = self.idx
            node = self.get_node(node[ch])
        node['$'] = True

    def get_node(self, idx):
        return self.nodes[idx // leaf_size][idx % leaf_size]

    def set_node(self, idx, val):
        self.nodes[idx // leaf_size][idx % leaf_size] = val

    def build(self):
        leaves = ((self.idx - 1) // leaf_size + 1) * leaf_size
        self.nodes = [[0] * leaf_size + [float('inf')] * (leaves - self.idx) for _ in range((leaves - 1) // leaf_size + 1)]
        self.set_node(0, {})
        for ch, idx in self.root.items():
            self.set_node(idx, {})

        for node_idx, ch in enumerate(self.root.keys()):
            self.build_node(node_idx, self.get_node(self.root[ch]), ch)

    def build_node(self, node_idx, node, ch):
        self.set_node(node_idx, node)
        for i in range(len(self.nodes)):
            self.nodes[i][node_idx % leaf_size] = min(self.nodes[i][node_idx % leaf_size], node_idx)

        for ch, idx in node.items():
            self.build_node(idx, self.get_node(idx), ch)

    def query(self, s):
        node = self.get_node(0)
        for ch in s:
            if ch not in node:
                return None
            node = self.get_node(node[ch])
        return self.query_subtree(node)

    def query_subtree(self, node):
        if '$' in node:
            return node['$']
        min_idx = float('inf')
        for ch, idx in node.items():
            if ch == '$':
                continue
            if idx < min_idx:
                min_idx = idx
        if min_idx == float('inf'):
            return None
        return self.query_subtree(self.get_node(min_idx))

    def __str__(self):
        res = []
        queue = [(0, self.root)]
        while queue:
            level, node = queue.pop(0)
            res.append(f'{level}: {node}')
            for ch, idx in node.items():
                if ch == '$':
                    res.append(f'{level+1}: $')
                    continue
                queue.append((level+1, self.get_node(idx)))
        return '\n'.join(res)


def min_max_string(puzzle: List[str]) -> List[str]:
    n = len(puzzle)
    t = Trie()
    for s in puzzle:
        t.insert(s[::-1] + '$')
        t.insert(s)

    t.build()

    res_min = []
    res_max = []

    for s in puzzle:
        prefix = ''
        node = t.get_node(0)
        for ch in s:
            prefix += ch
            if t.query(prefix[::-1]) is not None:
                break
            if ch not in node:
                break
            node = t.get_node(node[ch])

        res_min.append(prefix[::-1])
        if None in [t.query(p[::-1]) for p in puzzle]:
            res_max.append(s)
        else:
            suffix = ''
            node = t.get_node(0)
            for ch in s[::-1]:
                suffix = ch + suffix
                if t.query(suffix) is not None:
                    break
                if ch not in node:
                    break
                node = t.get_node(node[ch])

            res_max.append(suffix)

    return [res_min, res_max]

后缀数组实现
def build_suffix_array(s):
    n = len(s)
    sa = list(range(n))
    rank = [ord(s[i]) for i in sa]
    k = 1
    while k < n:
        tmp = [(rank[sa[i]], rank[sa[i] + k] if sa[i] + k < n else -1, i) for i in range(n)]
        tmp.sort()
        rank[sa[0]] = 0
        for i in range(1, n):
            rank[sa[i]] = rank[sa[i - 1]] + (tmp[i - 1] != tmp[i])
        k *= 2
        if rank[sa[-1]] == n - 1:
            break
    return sa


def min_max_string(puzzle: List[str]) -> List[str]:
    n = len(puzzle)
    s = ''.join(chr(i + ord('a')) for i in range(26))
    pos = len(s)

    sa = build_suffix_array(s + '#' + '#'.join(puzzle) + '#' + s[::-1])
    lcp = [0] * (len(sa) - 1)
    for i in range(len(sa) - 1):
        j = sa[i]
        k = sa[i + 1]
        while j < len(s) + 2 * n + 1 and k < len(s) + 2 * n + 1 and s[j] == s[k]:
            j += 1
            k += 1
        lcp[i] = j - sa[i]

    stack = []
    res_min = []
    res_max = []

    for i in range(2, n + 2):
        while stack and lcp[stack[-1]] > lcp[i]:
            stack.pop()
        if not stack:
            res_min.append(puzzle[i - 2][:lcp[i]])
        stack.append(i)

    for i in range(n + 1, 2 * n + 1):
        while len(stack) > 1 and lcp[stack[-1]] > lcp[i]:
            stack.pop()
        if lcp[stack[-1]] == len(puzzle[stack[-2] - 1]) and not (i - n - 1 == stack[-2] - 1):
            continue
        if lcp[stack[-1]] == len(puzzle[stack[-2] - 1]) and (i - n - 1 == stack[-2] - 1):
            cur = puzzle[i - n - 2]
            for j in range(lcp[stack[-1]], len(cur)):
                if cur[j] < puzzle[stack[-2] - 1][j]:
                    break
                if cur[j] > puzzle[stack[-2] - 1][j]:
                    res_max.append(cur[:j])
                    break
            else:
                res_max.append(cur[:lcp[stack[-1]]])
            continue
        if not stack:
            continue
        res_max.append(puzzle[stack[-2] - 1][:lcp[stack[-1]]])

    return [res_min, res_max]
总结

本题采用了不同的数据结构来解决,分别是使用字典树和后缀数组。字典树思路较直观,但是细节较多;后缀数组思路经典,但是实现较为复杂。为了更好地理解本题的解法,在实现时需要细致地调试和分析,尤其是在实现后缀数组求解过程中。