1 条题解
-
0
题目分析
给定一个字符串(),要求找出最长的强大子序列的长度。强大子序列定义为可以由某个字符串重复次()拼接而成的子序列。
核心观察
设最长强大子序列为,它可以表示为$T = \underbrace{T' + T' + \cdots + T'}_{k \text{ 次}}$,则。
由于很小,和不能同时很大。我们可以根据的大小分类讨论:
- 当时,需要考虑和的情况(可以被覆盖,因为)
- 当时,,长度很小
算法设计
情况1:
当时,我们需要在中找到一个子序列,它由两个相同的子序列拼接而成。
方法:将分成两部分(在某个位置分割),然后计算和的最长公共子序列(LCS)长度。那么就是一个候选答案(因为我们可以从中取,从中取另一个)。
复杂度:分割点有个,每次LCS计算,总复杂度。
情况2:
类似地,将分成三部分,计算三个字符串的最长公共子序列长度,则是候选答案。
复杂度:分割点有种,每次LCS3计算,总复杂度。由于,,需要优化。
优化分析:对于固定的分割,LCS3的计算复杂度为。由均值不等式,当时乘积最大,为。分割点数量为,因此总复杂度不超过,常数很小,实际可行。
情况3:
此时。关键观察:如果将分成5个长度尽可能相等的部分,那么必然是其中至少一个部分的子序列(鸽巢原理)。
算法步骤:
- 将分成5段,每段长度或
- 对每一段,枚举它的所有非空子序列(最多种)
- 对每个子序列,在中贪心地匹配,计算最多能重复多少次
- 如果重复次数,更新答案
复杂度:$5 \times 2^{|S|/5} \times |S| = O(5 \cdot 2^{16} \cdot 80) \approx 2.6 \times 10^7$,可行。
最终复杂度
总时间复杂度:,对于完全可行。
代码实现
#include <bits/stdc++.h> using namespace std; string s; int n; int ans = 0; // 计算两个字符串的最长公共子序列长度 int lcs2(const string& a, const string& b) { int la = a.size(), lb = b.size(); vector<vector<int>> dp(la + 1, vector<int>(lb + 1, 0)); for (int i = 1; i <= la; i++) { for (int j = 1; j <= lb; j++) { if (a[i - 1] == b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; else dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); } } return dp[la][lb]; } // 计算三个字符串的最长公共子序列长度 int lcs3(const string& a, const string& b, const string& c) { int la = a.size(), lb = b.size(), lc = c.size(); vector<vector<vector<int>>> dp(la + 1, vector<vector<int>>(lb + 1, vector<int>(lc + 1, 0))); for (int i = 1; i <= la; i++) { for (int j = 1; j <= lb; j++) { for (int k = 1; k <= lc; k++) { if (a[i - 1] == b[j - 1] && b[j - 1] == c[k - 1]) dp[i][j][k] = dp[i - 1][j - 1][k - 1] + 1; else dp[i][j][k] = max({dp[i - 1][j][k], dp[i][j - 1][k], dp[i][j][k - 1]}); } } } return dp[la][lb][lc]; } // 处理 k = 2 的情况 void solve_k2() { for (int split = 1; split < n; split++) { string s1 = s.substr(0, split); string s2 = s.substr(split); int len = lcs2(s1, s2); ans = max(ans, len * 2); } } // 处理 k = 3 的情况 void solve_k3() { for (int split1 = 1; split1 < n; split1++) { for (int split2 = split1 + 1; split2 < n; split2++) { string s1 = s.substr(0, split1); string s2 = s.substr(split1, split2 - split1); string s3 = s.substr(split2); int len = lcs3(s1, s2, s3); ans = max(ans, len * 3); } } } // 处理 k >= 5 的情况 void solve_kge5() { // 将 s 分成 5 段,长度尽量平均 vector<string> parts; int base = n / 5; int rem = n % 5; int start = 0; for (int i = 0; i < 5; i++) { int segLen = base + (i < rem ? 1 : 0); parts.push_back(s.substr(start, segLen)); start += segLen; } // 枚举所有可能的 T'(是某一段的子序列) for (int idx = 0; idx < 5; idx++) { string part = parts[idx]; int m = part.size(); // 枚举 part 的所有非空子序列 for (int mask = 1; mask < (1 << m); mask++) { string t; for (int i = 0; i < m; i++) { if (mask >> i & 1) t.push_back(part[i]); } // 贪心匹配,计算 t 在 s 中最多能重复多少次 int cnt = 0; int p = 0; for (char ch : s) { if (ch == t[p]) { p++; if (p == t.size()) { cnt++; p = 0; } } } if (cnt >= 5) { ans = max(ans, (int)t.size() * cnt); } } } } int main() { cin >> s; n = s.size(); ans = 0; solve_k2(); // k = 2 solve_k3(); // k = 3 solve_kge5(); // k >= 5 cout << ans << endl; return 0; }正确性证明
-
情况:通过枚举所有分割点,LCS保证了两个/三个子序列完全相同,且每个子序列来自的不同部分,因此拼接后是的子序列。
-
情况:由于长度,将分成5段后,由鸽巢原理,必然完整地出现在某一段的子序列中。枚举所有可能的后,贪心匹配能求出最大重复次数,保证了正确性。
-
覆盖所有情况:任何强大子序列对应某个,必然落入或的讨论范围(被覆盖)。
- 1
信息
- ID
- 6259
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者