P1026 [NOIP 2001 提高组] 统计单词个数
题目描述
给出一个长度不超过 200 的由小写英文字母组成的字母串(该字串以每行 20 个字母的方式输入,且保证每行一定为 20 个)。要求将此字母串分成 k 份,且每份中包含的单词个数加起来总数最大。
每份中包含的单词可以部分重叠。当选用一个单词之后,其第一个字母不能再用。例如字符串 this
中可包含 this
和 is
,选用 this
之后就不能包含 th
。
单词在给出的一个不超过 6 个单词的字典中。
要求输出最大的个数。
输入格式
每组的第一行有两个正整数 p,k。 p 表示字串的行数,k 表示分为 k 个部分。
接下来的 p 行,每行均有 20 个字符。
再接下来有一个正整数 s,表示字典中单词个数。 接下来的 s 行,每行均有一个单词。
输出格式
1个整数,分别对应每组测试数据的相应结果。
输入输出样例
输入 #1
1 3 thisisabookyouareaoh 4 is a ok sab
输出 #1
7
说明/提示
【数据范围】
对于 100% 的数据,2≤k≤40,1≤s≤6。
【样例解释】 划分方案为 this / isabookyoua / reaoh
【题目来源】
NOIP 2001 提高组第三题
题目分析
本题要求我们在一个字符串中划分k个部分,使得每个部分包含的单词总数最大。这是一个典型的动态规划问题,结合字符串处理技巧,需要仔细设计状态转移方程。
问题重述
给定一个字符串s,将其分成k段,每段至少包含一个字符。已知若干单词,统计每段中包含的单词数(单词可以重叠,多次出现多次计数),求如何分割才能使所有段的单词数之和最大。
解题思路
基本思路
预处理:计算字符串中每个位置开始的单词出现情况
动态规划:
状态定义:dp[i][j]表示前i个字符分成j段的最大单词数
状态转移:dp[i][j] = max(dp[t][j-1] + count[t+1][i]),其中t从j-1到i-1
单词统计:预处理count数组,count[i][j]表示子串s[i..j]中的单词数
算法选择
动态规划是解决此类分段优化问题的标准方法,时间复杂度为O(n^2*k),对于题目给定的数据范围(n≤200,k≤40)完全足够。
完整代码
cpp
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;const int N = 210, K = 50;
int p, k, n, m;
string s, word[10];
int dp[N][K], cnt[N][N];
bool vis[N][N];// 检查s[l..r]是否以某个单词开头
bool check(int l, int r) {string sub = s.substr(l, r-l+1);for (int i = 1; i <= m; i++) {if (sub.find(word[i]) == 0) return true;}return false;
}// 预处理cnt数组
void preprocess() {for (int r = 1; r <= n; r++) {for (int l = r; l >= 1; l--) {cnt[l][r] = cnt[l+1][r];if (check(l, r)) cnt[l][r]++;}}
}int main() {cin >> p >> k;s = " ";for (int i = 1; i <= p; i++) {string tmp; cin >> tmp;s += tmp;}cin >> m;for (int i = 1; i <= m; i++) cin >> word[i];n = s.size() - 1;preprocess();// 初始化dp数组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++) {for (int t = j-1; t < i; t++) {dp[i][j] = max(dp[i][j], dp[t][j-1] + cnt[t+1][i]);}}}cout << dp[n][k] << endl;return 0;
}
代码详解
数据结构
字符串处理:
cpp
string s, word[10]; // s是主串,word存储单词
动态规划数组:
cpp
int dp[N][K]; // dp[i][j]表示前i个字符分成j段的最大单词数
计数数组:
cpp
int cnt[N][N]; // cnt[l][r]表示子串s[l..r]中的单词数
预处理函数
cpp
void preprocess() {for (int r = 1; r <= n; r++) {for (int l = r; l >= 1; l--) {cnt[l][r] = cnt[l+1][r];if (check(l, r)) cnt[l][r]++;}}
}
从右向左填充cnt数组
利用之前计算结果优化当前计算
检查函数
cpp
bool check(int l, int r) {string sub = s.substr(l, r-l+1);for (int i = 1; i <= m; i++) {if (sub.find(word[i]) == 0) return true;}return false;
}
检查子串s[l..r]是否以某个单词开头
使用string的find方法简化实现
动态规划核心
cpp
for (int j = 2; j <= k; j++) {for (int i = j; i <= n; i++) {for (int t = j-1; t < i; t++) {dp[i][j] = max(dp[i][j], dp[t][j-1] + cnt[t+1][i]);}}
}
外层循环枚举分段数j
中层循环枚举前i个字符
内层循环枚举分割点t
状态转移方程:dp[i][j] = max(dp[t][j-1] + cnt[t+1][i])
复杂度分析
时间复杂度:
预处理:O(n^2 * m * L),L是单词平均长度
动态规划:O(n^2 * k)
空间复杂度:O(n^2 + n*k)
优化思考
单词查找优化:
可以使用Trie树加速单词查找
或者预处理所有单词的哈希值
空间优化:
dp数组可以优化为一维,因为每次只用到j-1的状态
cnt数组可以只保留必要部分
并行计算:
预处理阶段可以并行处理不同区间的检查
测试样例
样例1
输入:
text
1 3 thisisabookyouareaoh 4 is a ok sab
输出:
text
7
解释:
最佳分割方式为"this isabookyouareaoh",各部分单词数为3+3+1=7
样例2
输入:
text
2 2 abcde fghij 2 abc fgh
输出:
text
2
解释:
分割为"abcde"和"fghij",每部分各含1个单词
边界情况
k=1:
整个字符串作为一段
k=n:
每个字符作为一段
单词长度=1:
需要正确处理单字符单词的匹配
总结
本题通过动态规划有效解决了字符串分割优化问题,关键点在于:
合理设计dp状态表示
预处理子串单词数加速计算
正确处理状态转移方程
代码实现清晰高效,展示了动态规划在字符串处理中的典型应用。该解法可以扩展到类似的分段优化问题中。
扩展思考
单词不可重叠:
需要修改cnt数组的计算方式
记录单词出现位置避免重复计数
最小化单词数:
修改状态转移为取最小值
多字符串处理:
可以扩展为在多文本中寻找最优分割
这个题目很好地训练了动态规划思维和字符串处理能力,是算法学习的优秀范例。