题目:P1026 统计单词个数

$\text{蒟蒻在DP之路上求索着}$

作为一道NOIP的中档题目,它很好地把我虐趴下了展现出了NOIP的难度水平.以至于不看题解我根本没思路花了好久好久才A掉.

思路

这道题让我学习了如何使用$STL$中的$string$.它真是太好用了.
$string$中有一个$find(str)$函数,可以用于从字符串中找到$str$第一次出现的首字母下标.这为字符串的匹配提供了极大的便利.题目中还有一个要求就是分块.而$string$中恰好有一个叫做$substr(ind,length)$,可以用来从第$ind$位开始提取出长度为$length$的字符串.因此这道题简直是为$string$量身定制的.

接下来我们来看题目.在得到这个字符串之后,题目要求我们分块并分别统计单词个数.那么我们就要想了:分块的方法千千万,有没有什么方法使得我们能直接求出单词个数呢?如果没有,我们有没有办法储存区间的单词个数来节约时间呢?显然,直接求出单词个数是不现实的.那我们考虑怎么预处理.因为分块的不确定性很大,一块的长度可能很长,也可能很短.因此我们不得不统计$tot[l][r](1\le l\le r\le L)$.这是一个长度平方级别的预处理,完全可以承受的住.但是我们怎么处理呢?

题目中有一个要求,说我们在选择一个单词以后它的首字母就不能再次作为另一个单词的一部分了.换句话说,对于一个新字母来说,如果它是一个单词的首字母,那么这个区间中最多多加一个单词.就像是01背包,这个字母只能取一次.因此怎样让它只取一次呢?倒序!01背包就是这么做的.我们从后往前枚举位置,当往前扩展到一个新字母的时候,我们看看有没有区间中能不能形成一个新的单词.如果有,这个区间的$tot$就加上1,注意,只加1.通过这种方式,我们就可以预处理出任何一个区间内的单词个数了.

接下来就是DP的过程了.类似于在序列中插入一些东西,我们可以设$f[i][j]$表示把前i个字母分成j组的答案.这也是整个问题的子问题.接下来我们怎么转移呢?我们来”时光倒流”一下.之前我们可能已经形成了若干组,那么这一个字母到底属于那一组呢?有两种可能,一种是自成一组,要么是和之前归为一组,而和之前的归为一组又要考虑之前一组有多少字母.我们可以通过枚举之前最后一组和倒数第二组的分界线来确定.总而言之,我们将会得到以下方程
$$
f[i][j]=max(f[i][j], f[k][j]+tot[k+1][i])(j\le k<i)
$$
k就是我们的断点.

通过这道题我们可以注意到string的好用字符串问题和区间DP有时会有异曲同工之处(其实这道题就是区间DP).一定要按照DP的思路,一步一步地解题,而不是面对一团乱麻叹气.

#include <cmath>
#include <queue>
#include <deque>
#include <cctype>
#include <string>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;

template<class T> inline void read(T &x) {
    x = 0;
    char ch = getchar(), w = 0;
    while (!isdigit(ch))
        w = (ch =='-'), ch = getchar();
    while (isdigit(ch))
        x = (x << 1) + (x << 3) + (ch ^ 48), ch = getchar();
    x = w ? -x : x;
    return;
}

int n, m, s, tot[300][300], f[300][300];
string org, word[10];

int main() {
    read(n), read(m);
    org += "0";
    for (int i = 1; i <= n; ++i) {
        string a;
        cin >> a, org += a;
    }
    read(s);
    for (int i = 1; i <= s; ++i)
        cin >> word[i];
    int L = org.length() - 1;
    for (int i = L; i >= 1; --i) {
        for (int j = i; j >= 1; --j) {
            tot[j][i] = tot[j + 1][i];
            string a = org.substr(j, i - j + 1);
            for (int k = 1; k <= s; ++k)
                if (a.find(word[k]) == 0) {
                    tot[j][i]++;
                    break;
                }
        }
    }
    for (int i = 1; i <= L; ++i)
        f[i][i] = f[i - 1][i - 1] + tot[i][i];
    for (int i = 1; i <= L; ++i)
        f[i][1] = tot[1][i];
    for (int i = 1; i <= L; ++i) {
        for (int j = 1; j <= m && j <= i; ++j) {
            for (int k = j; k < i; ++k) {
                f[i][j] = max(f[i][j], f[k][j - 1] + tot[k + 1][i]);
            }
        }
    }
    cout << f[L][m] << endl;
    return 0;
}

——Gensokyo