题解:P1026 [NOIP 2001 提高组] 统计单词个数

· · 题解

前言

update on 2025.9.1:修复了 \LaTeX 显示错乱的问题。

题目要求

给出一个长度不超过 200 的由小写英文字母组成的字母串(该字串以每行 20 个字母的方式输入,且保证每行一定为 20 个)。要求将此字母串分成

# dp 推导 为方便下文描述,定义 $cnt_{i,j}$ 表示字符串中从第 $i$ 个字符到第 $j$ 个字符的子串($1 \leq i \leq j \leq n$,$n$ 为字符串总长度)能统计到的最大单词数。 ## 初始化 由于单词统计规则(选用后第一个字母不可再用),子串 $[i,j]$ 的最优解依赖于子串 $[i+1,j]$ 的最优解,因此采用 从后往前 的遍历顺序($i$ 从 $n$ 到 $1$,$j$ 从 $i$ 到 $n$): - 初始情况(子串长度为 $1$):若 $i = j$,则 $cnt_{i,j} = 1$,当且仅当该字符是字典中的单词;否则 $cnt_{i,j}=0$。 - 一般情况(子串长度 $>1$): + 不使用以 $i$ 开头的单词:此时 $cnt_{i,j}$ 直接继承 $cnt_{i+1,j}$(即从 $i+1$ 开始统计子串 $[i+1,j]$ 的单词数)。 + 使用以 $i$ 开头的单词:检查是否存在字典中的单词以 $i$ 开头且完全包含在 $[i,j]$ 内。若存在,则 $cnt_{i,j} = \max(cnt_{i,j}, 1 + cnt_{i+1,j})$(选此单词得 $1$ 分,后续从 $i+1$ 开始统计,因 $i$ 已被使用)。 ## dp 状态与转移推导 ### 状态定义 定义 $dp_{i,j}$ 表示将字符串的前 $i$ 个字符($1 \leq i \leq n$)分成 $j$ 份($1 \leq j \leq k$)时,能得到的最大单词总数。所以,求前 $i$ 个字符分 $j$ 份的最优解,可依赖前 $m$ 个字符分 $j-1$ 份的最优解($m < i$)。 ### 边界条件 当 $j=1$(仅分 $1$ 份)时,前 $i$ 个字符的最大单词数就是整个子串 $[1,i]$ 的最大单词数——因为没有分割,直接使用预处理好的 $cnt_{1,i}$。因此边界条件为: $$\large dp_{i,1} = cnt_{1,i} \quad (1 \leq i \leq n)$$ ### 状态转移方程 当 $j \geq 2$ 时,需考虑“如何将前 $i$ 个字符分成 $j$ 份”。我们可以先将前 $m$ 个字符分成 $j-1$ 份($m$ 为前 $j-1$ 份的结束位置),再将第 $m+1$ 到 $i$ 个字符作为第 $j$ 份。此时需满足“每份至少 $1$ 个字符”的约束,即前 $j-1$ 份至少需要 $j-1$ 个字符,故 $m \geq j-1$;第 $j$ 份至少需要 $1$ 个字符,故 $m < i$。 不难得出 $m \in \left[j-1,i-1\right]$。对于每个合法的 $m$,前 $j-1$ 份的最大单词数为 $dp_{m,j-1}$,第 $j$ 份的最大单词数为 $cnt_{m+1,i}$,总单词数为两者之和。取所有可能 $m$ 对应的最大值,即为 $dp_{i,j}$ 的结果。 最终状态转移方程为: $$\large dp_{i,j} = \max_{m \in [j-1, i-1]} \{dp_{m,j-1} + cnt_{m+1,i} \} \quad (2 \leq j \leq k, j \leq i \leq n)$$ # 时间复杂度 - 预处理 $cnt$ 数组:$i$ 遍历 $n$ 次,$j$ 遍历 $n$ 次,每次检查单词的最大长度为 $L$(字典中最长单词长度),复杂度为 $O(n^2 L)$。 - dp 计算:$j$ 遍历 $k$ 次,$i$ 遍历 $n$ 次,$m$ 遍历 $n$ 次,复杂度为 $O(kn^2)$。 总时间复杂度为 $O(n^2 (L + k))$。对于本题的数据范围,该时间复杂度可以通过,~~不需要吸氧。~~ # 细节决定成败 本题前三个数据的换行符是 `\r\n`,这就会导致 `getline` 挂掉。对于本题的数据,我们应该使用 `cin` 来读入字符串。 P.S.`cin.tie()->sync_with_stdio(0);` 是个好东西。 # AC Code 码风不好,不喜勿喷。 ```cpp #include <bits/stdc++.h> using namespace std; int p, k, m; char s[205], dic[10][25]; int sz[10], mlen, dp[205][45], cnt[205][205]; string readstr(int p) { string res = "", tmp = ""; for (int i = 1; i <= p; i++) { cin >> tmp; res += tmp; } return res; } bool check(int a, int b) { int l = b - a + 1; if (l > mlen) return false; for (int i = 0; i < m; i++) { if (sz[i] != l) continue; bool ok = true; for (int j = 0; j < l; j++) { if (s[a + j] != dic[i][j]) { ok = false; break; } } if (ok) return true; } return false; } void init() { int n = strlen(s + 1); for (int i = n; i >= 1; i--) { cnt[i][i] = 0; if (check(i, i)) cnt[i][i] = 1; for (int j = i + 1; j <= n; j++) { cnt[i][j] = cnt[i + 1][j]; for (int l = 1; l <= mlen && i + l - 1 <= j; l++) { if (check(i, i + l - 1)) { cnt[i][j] = max(cnt[i][j], 1 + cnt[i + 1][j]); break; } } } } } int main() { cin.tie()->sync_with_stdio(0); cin >> p >> k; string tmp = readstr(p); int len = tmp.size(); for (int i = 0; i < len; i++) { s[i + 1] = tmp[i]; } s[len + 1] = '\0'; cin >> m; mlen = 0; for (int i = 0; i < m; i++) { cin >> dic[i]; sz[i] = strlen(dic[i]); if (sz[i] > mlen) mlen = sz[i]; } init(); int n = strlen(s + 1); for (int i = 1; i <= n; i++) { dp[i][1] = cnt[1][i]; } for (int j = 2; j <= k; j++) { for (int i = j; i <= n; i++) { dp[i][j] = 0; for (int m = j - 1; m < i; m++) { dp[i][j] = max(dp[i][j], dp[m][j - 1] + cnt[m + 1][i]); } } } cout << dp[n][k] << endl; return 0; } ``` # 结语 码字不易,点个赞再走呗~