【1147. 段式回文】
来源:力扣(LeetCode)
描述:
你会得到一个字符串 text
。你应该把它分成 k
个子字符串 (subtext1, subtext2,…, subtextk)
,要求满足:
subtexti
是 非空 字符串- 所有子字符串的连接等于
text
( 即subtext1 + subtext2 + ... + subtextk == text
) - 对于所有
i
的有效值( 即1 <= i <= k
) ,subtexti == subtextk - i + 1
均成立
返回 k
可能最大值。
示例 1:
输入:text = "ghiabcdefhelloadamhelloabcdefghi"
输出:7
解释:我们可以把字符串拆分成 "(ghi)(abcdef)(hello)(adam)(hello)(abcdef)(ghi)"。
示例 2:
输入:text = "merchant"
输出:1
解释:我们可以把字符串拆分成 "(merchant)"。
示例 3:
输入:text = "antaprezatepzapreanta"
输出:11
解释:我们可以把字符串拆分成 "(a)(nt)(a)(pre)(za)(tpe)(za)(pre)(a)(nt)(a)"。
提示:
- 1 <= text.length <= 1000
- text 仅由小写英文字符组成
方法一:贪心 + 双指针
思路与算法
题目首先给出一个长度为 n 的字符串 text,现在我们需要将其分割为 k 个非空子字符串 (subtext1 , subtext2 ,…, subtextk ),且需要满足对于 1 ≤ i ≤ k 有 subtexti = subtextk−i+1 成立,并且全部字符串依次连接后等于 text。现在我们需要计算 k 可能的最大值。现在我们用 text[l, r] 来表示 text[l], text[l + 1], …, text[r] 这段字符串区间,给出贪心方案如下:
假设现在我们需要进行分割的非空字符串为 text[l, r],0 ≤ l ≤ r < n,则找到长度最短的能满足相同且无重叠的前后缀进行分割
- 若找不到,则 text[l, r] 整体作为一个段式回文直接返回 1。
- 否则此时该字符串可以被分割为前缀,中间部分与后缀字符串组成
- 若中间部分字符串非空,则返回中间部分字符串能分割成的段式回文的最大数目加 2。
- 否则字符串 text[l, r] 最多只能拆分为前后缀 2 个段式回文,直接返回 2 即可。
然后我们按照该方案,返回从字符串 text[0, n − 1] 开始分割能得到的最多段式回文个数即可。
现在我们给出该贪心方案的证明:
假设现在给定一个字符串 text[l, r],它有两个长度分别为 len1 和 len2 的相同且无重叠的前后缀,其中 len1 < len2 。现在我们需要证明,在对字符串 text[l, r] 进行分割时,选取长度为 len1 的前后缀一定比选取长度为 len2 的前后缀进行分割更优。
我们记字符串 text[l, r] 长度为 len1 的前后缀为 A,此时的中间部分字符串为 X,长度为 len2 的前后缀为 AB,此时的中间部分字符串为 Y,则我们有
text[l, r] = (A)X(A) = (AB)Y(AB)
- 若字符串 B 的长度大于等于 A,则 B 可以被分割为 CA 的形式,其中 C 可以为空字符串。那么有
text[l, r] = (ACA)Y(ACA)
此时我们可以在选取前缀为 A 的基础上得到
text[l, r] = (A)(C)(A)Y(A)(C)(A)
即此时我们可以比直接分割长度为 len2 的前后缀多得到 4 个段式回文,所以此时选取 len1 的前后缀进行分割更优。
- 否则当字符串 B 的长度小于 A 时,字符串 A 可以被分割为 CB 的形式。有
text[l, r] = (CBB)Y(CBB)
此时我们可以在选取前缀为 A 的基础上得到
text[l, r] = (A)(B)Y(B)(A)
即此时我们可以比直接分割长度为 len2 的前后缀多得到 2 个段式回文,所以此时选取 len1 的前后缀进行分割更优。
综上,优先选取长度较小的前后缀进行分割一定是比选取长度较大的前后缀进行分割更优。即该贪心方案的正确性得证。
代码:
class Solution {
public:bool judge(const string& text, int l1, int l2, int len) {while (len --) {if (text[l1] != text[l2]) {return false;}++l1;++l2;}return true;}int longestDecomposition(string text) {int n = text.size();int res = 0;int l = 0, r = n - 1;while (l <= r) {int len = 1;while (l + len - 1 < r - len + 1) {if (judge(text, l, r - len + 1, len)) {res += 2;break;}++len;}if (l + len - 1 >= r - len + 1) {++res;}l += len;r -= len;}return res;}
};
执行用时:0 ms, 在所有 C++ 提交中击败了100.00%的用户
内存消耗:6.1 MB, 在所有 C++ 提交中击败了88.76%的用户
复杂度分析
时间复杂度:O(n 2),其中 n 为字符串 text 的长度,用「双指针」判断两字符串是否相等的时间复杂度为 O(n),需要判断 O(n) 次,所以总的时间复杂度为 O(n 2)。
空间复杂度:O(1),仅使用常量空间。
方法二:滚动哈希
思路与算法
在「方法一」的实现过程中,我们可以对字符串 text 进行「滚动哈希」预处理,这样在对于字符串判断某一个长度的前后缀是否相同时,可以在 O(1) 时间得到前后缀的哈希值进行判断。
代码:
class Solution {
public:vector<long long> pre1, pre2;vector<long long> pow1, pow2;static constexpr int MOD1 = 1000000007;static constexpr int MOD2 = 1000000009;int Base1, Base2;void init(string& s) {mt19937 gen{random_device{}()};Base1 = uniform_int_distribution<int>(1e6, 1e7)(gen);Base2 = uniform_int_distribution<int>(1e6, 1e7)(gen);while (Base2 == Base1) {Base2 = uniform_int_distribution<int>(1e6, 1e7)(gen);}int n = s.size();pow1.resize(n);pow2.resize(n);pre1.resize(n + 1);pre2.resize(n + 1);pow1[0] = pow2[0] = 1;pre1[1] = pre2[1] = s[0];for (int i = 1; i < n; ++i) {pow1[i] = (pow1[i - 1] * Base1) % MOD1;pow2[i] = (pow2[i - 1] * Base2) % MOD2;pre1[i + 1] = (pre1[i] * Base1 + s[i]) % MOD1;pre2[i + 1] = (pre2[i] * Base2 + s[i]) % MOD2;}}pair<int, int> getHash(int l, int r) {return {(pre1[r + 1] - ((pre1[l] * pow1[r + 1 - l]) % MOD1) + MOD1) % MOD1, (pre2[r + 1] - ((pre2[l] * pow2[r + 1 - l]) % MOD2) + MOD2) % MOD2};}int longestDecomposition(string text) {init(text);int n = text.size();int res = 0;int l = 0, r = n - 1;while (l <= r) {int len = 1;while (l + len - 1 < r - len + 1) {if (getHash(l, l + len - 1) == getHash(r - len + 1, r)) {res += 2;break;}++len;}if (l + len - 1 >= r - len + 1) {++res;}l += len;r -= len;}return res;}
};
执行用时:4 ms, 在所有 C++ 提交中击败了72.47%的用户
内存消耗:7 MB, 在所有 C++ 提交中击败了68.54%的用户
复杂度分析
时间复杂度:O(n),其中 n 为字符串 text 的长度,预处理字符串哈希的时间复杂度为 O(n),双指针的时间复杂度为 O(n)。
空间复杂度:O(n),其中 n 为字符串 text 的长度,主要为预处理字符串哈希的空间开销。
author:LeetCode-Solution