P1026 统计单词个数 题解
P1026 统计单词个数 题解
一道比较好的线性dp题目。
dp中一类比较典型的模型就是将数据分成一定的段落,使某一特定的值最大。
设 \(f(i,j)\) 表示将前 \(i\) 个数据分成 \(j\) 段,所能获得的最大目标值。则有
其中v(l,r)是表示在段落 \([l,r]\) 上的一个值,具体意义根据题目而定。
初始状态:
目标状态:
其中 \(N\) 表示数据总量,\(K\) 表示要求分成的段落数。
此算法的时间复杂度为 \(O(n^3)\) 。
我们再来看这道题。很明显,我们可以用 \(v(l,r)\) 表示在字符串 \([l,r]\)中间的单词数量,然后上面的dp方程就是正解。这里注意,我用的是string
,下标是以 \(0\) 开始的。
那么怎么求 \(v(l,r)\) 呢?我们仔细观察题目:
每份中包含的单词可以部分重叠。当选用一个单词之后,其第一个字母不能再用。例如字符串 this 中可包含 this 和 is,选用 this 之后就不能包含 th。
我们可以再用一个dp。我们尝试用 \(v(l,r)\) 的值推出另外的 \(v\) 值。比如说上例,我们可以发现 this
字符串的 \([1,3]\) 部分是 his
。\([0,3]\) 是 this
。假设匹配串只有 is
, this
和 th
。那么 \(v(1,3) = 1\) (is
), \(v(0,3) = 2\) (this
,is
)。
我们可以发现,由于 this
以t
(也就是 str[0]
)开头,并且包含在 \([0,3]\) 之中,所以 \(v(0,3)=v(1,3)+1\) 。
所以得出通式:
同时,
当选用一个单词之后,其第一个字母不能再用。
所以即使有多个满足上述条件的匹配串,也只会 \(+1\)。
这里需要注意dp的顺序,是从大到小枚举\(l,r\),其中 \(r\) 在外层, \(l\) 在内层。
这里可以使用 string
的 substr(l,k)
函数,它表示截取以 \(l\) 开头,长度为 \(k\) 的子串。还有 find(str)
函数,它表示从源字符串里搜索子字符串 \(str\) ,若搜到了则返回 0,否则返回 1。
Code:
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 205;
string str, sub[MAXN];
int p, k, s;
int f[MAXN][MAXN];
int value[MAXN][MAXN];
void init()
{
cin >> p >> k;
for (int i = 1; i <= p; i++)
{
string tmp;
cin >> tmp;
str += tmp;
}
cin >> s;
for (int i = 1; i <= s; i++)
{
cin >> sub[i];
}
}
bool isthere(int l, int r)
{
string obj = str.substr(l, r - l + 1);
for (int i = 1; i <= s; i++)
{
if (!obj.find(sub[i]))
return true;
}
return false;
}
void get_value()
{
for (int j = str.length() - 1; j >= 0; j--)
{
for (int i = j - 1; i >= 0; i--)
{
value[i][j] = value[i + 1][j];
if (isthere(i, j))
value[i][j]++;
}
}
}
void dp()
{
for (unsigned i = 0; i < str.length(); i++)
{
f[i][1] = value[0][i];
}
for (unsigned i = 0; i < str.length(); i++)
{
for (unsigned j = 2; j <= k; j++)
{
for (unsigned br = j; br < i; br++)
{
f[i][j] = max(f[i][j], f[br][j - 1] + value[br + 1][i]);
}
}
}
cout << f[str.length() - 1][k] << endl;
}
int main()
{
init();
get_value();
dp();
return 0;
}