1 条题解
-
0
H. 缺失的分隔符 详细题解
问题重述
给定一个字符串 (),它是由一个字典中的单词按字典序升序拼接而成,单词之间没有分隔符。需要将 分割成若干个互不相同的单词,且这些单词按字典序严格递增(因为原字典已排序且单词互异)。要求最大化单词个数,并输出任意一种合法分割方案。
动态规划思路
贪心法不可行,使用区间 DP。
状态定义
设 表示:已经构造了从 开头到位置 的单词,且最后一个单词是子串 (闭区间)时,从位置 开始最多还能再构造出多少个单词(包括当前已构造的?通常定义 为“从当前状态开始,后续能构造的最大单词数”)。
更常见的实现是: 表示以 作为最后一个单词,从 到末尾能形成的最大单词数(包括后续所有单词,但不包括 本身)。这样最终答案需要在起始处加上第一个单词。但为了方便转移,我们定义 表示已经完成了以 结尾的单词,从下一个位置开始还能形成的最多单词数。那么初始时,我们需要枚举第一个单词 ,然后答案就是 的最大值。
不过标程中的表述略有不同,我们按照官方解法来:
设 表示如果最后一个构造的单词是子串 ,那么之后还能再构造出多少个单词。
因此转移时,我们要从 开始选取下一个单词 ,并且要保证 字典序大于 。然后状态转移到 ,并加上 (当前这个新单词)。所以:
其中 必须满足 且 在 内。
边界条件:如果 (即已经处理完整个字符串),则 (没有后续单词)。
如何找到可行的
对于固定的 ,我们要找到所有 使得 。关键观察:
- 如果 的最长公共前缀(LCP)与 的长度等于 ,即 是 的前缀,那么任何比 更长的子串 ()都一定大于 ,因为长的那个串在相同前缀后还有额外字符。此时第一个可行的 是 。
- 否则,设 ,即从 和 开始的最长公共前缀长度,且 。那么 。如果 ,则子串 只要长度 就大于 ;否则,任何以 开头的子串都不可能大于 (因为第一个不同字符更小),因此不存在可行的 。
因此,只需找到第一个可行的 ,记为 。之后所有 也都是可行的(因为增加长度不会改变字典序比较的结果,只要之前已经大于)。所以,我们需要计算的是:
$$DP[x][y] = \max_{z \ge firstZ} \big( 1 + DP[y+1][z] \big) $$这等价于 。
快速计算 LCP
为了得到 ,我们需要计算 ,即后缀 和后缀 的最长公共前缀长度,但不能超过 。
定义辅助数组 表示后缀 和 的最长公共前缀长度。显然有递推:
$$LCPS[p][q] = \begin{cases} 0 & \text{if } S[p] \neq S[q] \\ 1 + LCPS[p+1][q+1] & \text{if } S[p] = S[q] \end{cases}$$边界:当 或 时 。
那么, 与 的 LCP 长度就是 。因为 可能超过 ,需要截断。
这样我们就能在 时间内得到第一个不同字符的位置,从而确定 :
- 设 。
- 如果 (即 是 的前缀),则第一个可行的 为 ,即 。
- 否则,比较 和 :
- 若 ,则第一个可行的 为 。
- 否则,不存在可行的 ( 为 )。
后缀最大值优化
对于每个固定的 ,我们需要快速查询 。我们可以预处理一个后缀最大值数组 ,其中 。这样在转移时,。
由于 共有 个状态,每个转移 ,总复杂度 。
回溯构造答案
计算完所有 后,我们从开头开始构造。首先枚举第一个单词 ,选择使 最大的 (若有多个任选)。然后递归地选择下一个单词:已知当前单词为 ,那么下一个单词的起始位置为 ,我们需要选择一个 使得 达到最大值,且 是可行的(即满足字典序条件)。由于我们记录了后缀最大值,可以知道最佳的 就是满足 的任意 (通常取最小 即可)。依次输出每个单词。
复杂度与实现细节
- 预计算 :。
- 预计算后缀最大值:。
- DP 本身也是 。
- 回溯:。
- 总时间复杂度 ,空间 ,对于 可接受(约 个状态,每个状态存储整数,内存约 MB,需要注意优化)。
参考代码框架
#include <bits/stdc++.h> using namespace std; int main() { string S; cin >> S; int n = S.size(); // LCPS[p][q] : 后缀 S[p..] 和 S[q..] 的 LCP 长度 vector<vector<int>> lcps(n+2, vector<int>(n+2, 0)); for (int p = n-1; p >= 0; --p) { for (int q = n-1; q >= 0; --q) { if (S[p] == S[q]) lcps[p][q] = 1 + lcps[p+1][q+1]; } } // dp[x][y] : 最后一个单词为 S[x..y] 时,之后最多还能再构造多少个单词 // 为了方便,我们定义 dp 值为 -1 表示不可能 vector<vector<int>> dp(n+2, vector<int>(n+2, -1)); vector<vector<int>> sufMax(n+2, vector<int>(n+2, -1)); // 边界:y == n 时,没有后续,dp[x][n] = 0 for (int x = 0; x <= n; ++x) dp[x][n] = 0; // 从后向前计算 for (int y = n-1; y >= 0; --y) { // 先计算 sufMax[y+1][*](需要后一行已经算好) for (int z = n; z >= 0; --z) { sufMax[y+1][z] = max(dp[y+1][z], (z+1 <= n ? sufMax[y+1][z+1] : -1)); } for (int x = y; x >= 0; --x) { int len = y - x + 1; int l = min(lcps[x][y+1], len); if (l == len) { // S[x..y] 是 S[y+1..] 的前缀 int firstZ = y + len; if (firstZ <= n) { int best = sufMax[y+1][firstZ]; if (best != -1) dp[x][y] = best + 1; } } else { if (S[x+l] < S[y+1+l]) { int firstZ = y + 1 + l; if (firstZ <= n) { int best = sufMax[y+1][firstZ]; if (best != -1) dp[x][y] = best + 1; } } } } } // 找出第一个单词的最佳起始 y int bestY = -1, bestCnt = -1; for (int y = 0; y < n; ++y) { if (dp[0][y] != -1) { int cnt = 1 + dp[0][y]; if (cnt > bestCnt) { bestCnt = cnt; bestY = y; } } } // 回溯输出 vector<string> words; int x = 0, y = bestY; while (y < n) { words.push_back(S.substr(x, y-x+1)); // 找下一个单词 int nx = y+1; int len = y-x+1; int l = min(lcps[x][y+1], len); int firstZ; if (l == len) firstZ = y + len; else firstZ = y + 1 + l; // 在 nx 行中,取最佳的 z 使得 dp[nx][z] 等于 sufMax[nx][firstZ] int target = sufMax[nx][firstZ]; int nz = -1; for (int z = firstZ; z <= n; ++z) { if (dp[nx][z] == target) { nz = z; break; } } x = nx; y = nz; } cout << bestCnt << '\n'; for (const string& w : words) cout << w << '\n'; return 0; }
总结
本题的核心是利用 预计算任意两个后缀的 LCP,从而在 内判断字典序关系,并用后缀最大值优化 DP 转移。最终得到 的解法,完美解决 的规模。
- 1
信息
- ID
- 7169
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 5
- 标签
- 递交数
- 0
- 已通过
- 0
- 上传者