|
| 1 | +### 题目描述 |
| 2 | + |
| 3 | +这是 LeetCode 上的 **[2213. 由单个字符重复的最长子字符串](https://leetcode-cn.com/problems/longest-substring-of-one-repeating-character/solution/by-ac_oier-0lso/)** ,难度为 **困难**。 |
| 4 | + |
| 5 | +Tag : 「区间求和」、「线段树」 |
| 6 | + |
| 7 | + |
| 8 | + |
| 9 | +给你一个下标从 0ドル$ 开始的字符串 `s` 。另给你一个下标从 0ドル$ 开始、长度为 $k$ 的字符串 `queryCharacters`,一个下标从 0ドル$ 开始、长度也是 $k$ 的整数 下标 数组 `queryIndices`,这两个都用来描述 $k$ 个查询。 |
| 10 | + |
| 11 | +第 $i$ 个查询会将 `s` 中位于下标 $queryIndices[i]$ 的字符更新为 $queryCharacters[i]$。 |
| 12 | + |
| 13 | +返回一个长度为 $k$ 的数组 `lengths`,其中 $lengths[i]$ 是在执行第 $i$ 个查询之后 `s` 中仅由单个字符重复组成的 最长子字符串的长度 。 |
| 14 | + |
| 15 | +示例 1: |
| 16 | +``` |
| 17 | +输入:s = "babacc", queryCharacters = "bcb", queryIndices = [1,3,3] |
| 18 | + |
| 19 | +输出:[3,3,4] |
| 20 | + |
| 21 | +解释: |
| 22 | +- 第 1 次查询更新后 s = "bbbacc" 。由单个字符重复组成的最长子字符串是 "bbb" ,长度为 3 。 |
| 23 | +- 第 2 次查询更新后 s = "bbbccc" 。由单个字符重复组成的最长子字符串是 "bbb" 或 "ccc",长度为 3 。 |
| 24 | +- 第 3 次查询更新后 s = "bbbbcc" 。由单个字符重复组成的最长子字符串是 "bbbb" ,长度为 4 。 |
| 25 | +因此,返回 [3,3,4] 。 |
| 26 | +``` |
| 27 | +示例 2: |
| 28 | +``` |
| 29 | +输入:s = "abyzz", queryCharacters = "aa", queryIndices = [2,1] |
| 30 | + |
| 31 | +输出:[2,3] |
| 32 | + |
| 33 | +解释: |
| 34 | +- 第 1 次查询更新后 s = "abazz" 。由单个字符重复组成的最长子字符串是 "zz" ,长度为 2 。 |
| 35 | +- 第 2 次查询更新后 s = "aaazz" 。由单个字符重复组成的最长子字符串是 "aaa" ,长度为 3 。 |
| 36 | +因此,返回 [2,3] 。 |
| 37 | +``` |
| 38 | + |
| 39 | +提示: |
| 40 | +* 1ドル <= s.length <= 10^5$ |
| 41 | +* `s` 由小写英文字母组成 |
| 42 | +* $k == queryCharacters.length == queryIndices.length$ |
| 43 | +* 1ドル <= k <= 10^5$ |
| 44 | +* `queryCharacters` 由小写英文字母组成 |
| 45 | +* 0ドル <= queryIndices[i] < s.length$ |
| 46 | + |
| 47 | +--- |
| 48 | + |
| 49 | +### 线段树 |
| 50 | + |
| 51 | +这是一道经典的线段树应用题。 |
| 52 | + |
| 53 | +根据题意,涉及的操作 "似乎" 是「单点修改」和「区间查询」,那么根据 [(题解) 307. 区域和检索 - 数组可修改](https://leetcode-cn.com/problems/range-sum-query-mutable/solution/guan-yu-ge-lei-qu-jian-he-wen-ti-ru-he-x-41hv/) 的总结,我们应该使用的是「树状数组」吗? |
| 54 | + |
| 55 | +其实并不是(或者说不能直接是),原因在于我们查询的是「修改过后 `s` 中相同字符连续段的最大长度」,而当我们进行所谓的「单点修改」时,会导致原本的连续段被破坏,或者形成新的连续段。也就是此处的修改对于结果而言,并不是单点的。 |
| 56 | + |
| 57 | +使用线段树求解,我们唯一需要考虑的是:在 `Node` 中维护些什么信息? |
| 58 | + |
| 59 | +对于线段树的节点信息设计,通常会包含基本的左右端点 `l`、`r` 以及查询目标值 `val` ,然后再考虑维护 `val` 还需要一些什么辅助信息。 |
| 60 | + |
| 61 | +对于本题,我们还需要额外维护 `prefix` 和 `suffix`,分别代表「当前区间 $[l, r]$ 内前缀相同字符连续段的最大长度」和「当前区间 $[l, r]$ 内后缀相同字符连续段的最大长度」。 |
| 62 | + |
| 63 | +然后考虑每次修改,如何使用子节点信息更新父节点(`pushup` 操作): |
| 64 | + |
| 65 | +* 对于一般性的合并(当前节点的两个子节点的衔接点不是相同字符)而言: |
| 66 | + |
| 67 | + * `tr[u].prefix = tr[u << 1].prefix`:当前父节点(区间)的前缀最大长度等于左子节点(区间)的前缀最大长度; |
| 68 | + * `tr[u].suffix = tr[u << 1 | 1].suffix`:当前父节点(区间)的后缀最大长度等于右子节点(区间)的后缀最大长度; |
| 69 | + * `tr[u].val = max(left.val, right.val)`:当前父节点(区间)的最大长度为两子节点(区间)的最大长度。 |
| 70 | +* 对于非一般性的合并(当前节点的两个子节点的衔接点为相同字符): |
| 71 | + * `tr[u].val = max(tr[u].val, left.suffix + right.prefix)`:首先可以确定的是「左区间的后缀」和「右区间的前缀」可以拼接在一起,因此可以使用拼接长度来尝试更新当前节点的最大值; |
| 72 | + * `tr[u].suffix = bLen + left.suffix`:其中 `bLen` 为右节点(区间)的长度。当且仅当右节点(区间)整一段都是相同字符时(即满足 `right.prefix = right.suffix = bLen`),可以使用 `bLen + left.suffix` 来更新 `tr[u].suffix`; |
| 73 | + * `tr[u].prefix = aLen + right.prefix`:其中 `aLen` 为左节点(区间)的长度。当且仅当左节点(区间)整一段都是相同字符时(即满足 `left.prefix = left.prefix = aLen`),可以使用 `aLen + right.prefix` 来更新 `tr[u].prefix `。 |
| 74 | + |
| 75 | +代码: |
| 76 | +```Java |
| 77 | +class Solution { |
| 78 | + class Node { |
| 79 | + int l, r, prefix, suffix, val; |
| 80 | + Node(int _l, int _r) { |
| 81 | + l = _l; r = _r; |
| 82 | + prefix = suffix = val = 1; |
| 83 | + } |
| 84 | + } |
| 85 | + char[] cs; |
| 86 | + Node[] tr; |
| 87 | + void build(int u, int l, int r) { |
| 88 | + tr[u] = new Node(l, r); |
| 89 | + if (l == r) return ; |
| 90 | + int mid = l + r >> 1; |
| 91 | + build(u << 1, l, mid); |
| 92 | + build(u << 1 | 1, mid + 1, r); |
| 93 | + } |
| 94 | + void update(int u, int x, char c) { |
| 95 | + if (tr[u].l == x && tr[u].r == x) { |
| 96 | + cs[x - 1] = c; |
| 97 | + return ; |
| 98 | + } |
| 99 | + int mid = tr[u].l + tr[u].r >> 1; |
| 100 | + if (x <= mid) update(u << 1, x, c); |
| 101 | + else update(u << 1 | 1, x, c); |
| 102 | + pushup(u); |
| 103 | + } |
| 104 | + int query(int u, int l, int r) { |
| 105 | + if (l <= tr[u].l && tr[u].r <= r) return tr[u].val; |
| 106 | + int ans = 0; |
| 107 | + int mid = tr[u].l + tr[u].r >> 1; |
| 108 | + if (l <= mid) ans = query(u << 1, l, r); |
| 109 | + if (r > mid) ans = Math.max(ans, query(u << 1 | 1, l, r)); |
| 110 | + return ans; |
| 111 | + } |
| 112 | + void pushup(int u) { |
| 113 | + Node left = tr[u << 1], right = tr[u << 1 | 1]; |
| 114 | + int aLen = left.r - left.l + 1, bLen = right.r - right.l + 1; |
| 115 | + char ac = cs[left.r - 1], bc = cs[right.l - 1]; |
| 116 | + tr[u].prefix = left.prefix; tr[u].suffix = right.suffix; |
| 117 | + tr[u].val = Math.max(left.val, right.val); |
| 118 | + if (ac == bc) { |
| 119 | + if (left.prefix == aLen) tr[u].prefix = aLen + right.prefix; |
| 120 | + if (right.prefix == bLen) tr[u].suffix = bLen + left.suffix; |
| 121 | + tr[u].val = Math.max(tr[u].val, left.suffix + right.prefix); |
| 122 | + } |
| 123 | + } |
| 124 | + public int[] longestRepeating(String s, String queryCharacters, int[] queryIndices) { |
| 125 | + cs = s.toCharArray(); |
| 126 | + int n = cs.length, m = queryCharacters.length(); |
| 127 | + tr = new Node[n * 4]; |
| 128 | + build(1, 1, n); |
| 129 | + for (int i = 0; i < n; i++) update(1, i + 1, cs[i]); |
| 130 | + int[] ans = new int[m]; |
| 131 | + for (int i = 0; i < m; i++) { |
| 132 | + update(1, queryIndices[i] + 1, queryCharacters.charAt(i)); |
| 133 | + ans[i] = query(1, 1, n); |
| 134 | + } |
| 135 | + return ans; |
| 136 | + } |
| 137 | +} |
| 138 | +``` |
| 139 | +* 时间复杂度:$O(m * \log{n})$ |
| 140 | +* 空间复杂度:$O(n)$ |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +### 最后 |
| 145 | + |
| 146 | +这是我们「刷穿 LeetCode」系列文章的第 `No.2213` 篇,系列开始于 2021年01月01日,截止于起始日 LeetCode 上共有 1916 道题目,部分是有锁题,我们将先把所有不带锁的题目刷完。 |
| 147 | + |
| 148 | +在这个系列文章里面,除了讲解解题思路以外,还会尽可能给出最为简洁的代码。如果涉及通解还会相应的代码模板。 |
| 149 | + |
| 150 | +为了方便各位同学能够电脑上进行调试和提交代码,我建立了相关的仓库:https://github.com/SharingSource/LogicStack-LeetCode 。 |
| 151 | + |
| 152 | +在仓库地址里,你可以看到系列文章的题解链接、系列文章的相应代码、LeetCode 原题链接和其他优选题解。 |
| 153 | + |
0 commit comments