每份得分只依赖两个端点:预处理 cut[t][j] 计数表,用 f[j][m]=max(f[t][m-1]+cut[t][j]) 恰好分成 k 份求最大值。
OJ: roj
题目 ID: 3510
难度:普及+/提高-
标签:动态规划线性DP字符串python
创建: 2026-10-02 04:33
更新: 2026-10-10 11:30
形式化题目
给定小写字母串
- 单词可以在串中任意位置匹配,可以部分重叠;
- 选用一个单词后它的第一个字母不能再用——同一起点最多选一个单词,因此同一起点匹配到多个字典词也只计 1 次;
- 每份只数完整落在该份内的单词,跨过切点的单词不属于任何一份。
每份得分即该份内可数的单词个数,求所有划分方案的总得分最大值。
样例:{is, a, ok, sab},串 thisisabookyouareaoh,输出 7。先标出串里每个能匹配单词的起点:
| 起点 |
匹配单词 | 最短单词末尾 |
|---|---|---|
| 2 | is | 3 |
| 4 | is | 5 |
| 5 | sab | 7 |
| 6 | a | 6 |
| 9 | ok | 10 |
| 14 | a | 14 |
| 17 | a | 17 |
这张表展示样例全部 7 个可数起点(其余位置匹配不到任何单词)。题面给出的划分 this / isabookyoua / reaoh 恰好把它们分开数:第 1 份只有起点 2,第 2 份含起点 4、5、6、9、14,第 3 份只有起点 17,合计 is 与起点 5 的 sab 虽然重叠,但首字母下标不同,可以同时计数。
正解
思路
计数模型:起点之间互不冲突
规则 2 只限制"同一起点选一个单词",而不同起点的首字母是不同下标,单词区间重叠也不互相占用(规则 1 明确允许部分重叠)。所以一份的得分就是可数起点的个数,不需要在一份里再做"选哪些词"的贪心或区间调度。
对起点
即最短的匹配单词完整落在份内——最短的放得下,等价于"存在放得下的单词"。同一起点匹配多个词(字典还可能有重复单词)都被
朴素做法与瓶颈
朴素做法是枚举全部
预处理计数表
固定右端点
状态与转移
设
:前 份每份至少 1 个字符; :最后一份至少 1 个字符——枚举范围本身就保证了"恰好 份、每份非空", 的状态不可达(代码里留哨兵 NEG)。- 答案是
。遍历顺序 逐层递增, ( )总在使用前已算出。必须是“恰好”而非“至多”:多切一刀会把跨切点的起点切废,分得越细可能亏得越多(例如 200 个 a配词aaaaa,共 196 个起点,最优 193, 只剩 157)。若按"至多 份"算,两者都会得到 193,与真实数据不符。
样例 DP 表
下表是样例
| 0 | — | — | — |
| 4 | 1 | 1 | 1 |
| 6 | 2 | 2 | 2 |
| 8 | 4 | 4 | 4 |
| 11 | 5 | 5 | 5 |
| 15 | 6 | 6 | 6 |
| 18 | 7 | 7 | 7 |
| 20 | 7 | 7 | 7 |
最后一格来自 this / isabookyoua / reaoh 的逐份计数。表中"—"表示 a 串配词 aaaaa)会因跨界损失拉开列间差距。
代码
正文的 match_end() / ends、build_cut() / cut、max_words() / f。
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int NEG = -1000000000;
// end[p]:从 p 出发能匹配的最短单词的末尾下标;匹配不到为 -1
int end_[205];
int cut[205][205]; // cut[t][j]:把 [t, j) 单独作一份时能数到的单词起点数
int f[205][45]; // f[j][m]:前 j 个字符分成 m 份的最大单词数
// 多组输入,每组算一次
int max_words(const string& text, const vector<string>& words, int k) {
int n = (int)text.size();
for (int p = 0; p < n; p++) {
int best = -1;
for (size_t i = 0; i < words.size(); i++) {
if (text.compare(p, words[i].size(), words[i]) == 0) {
int e = p + (int)words[i].size() - 1;
if (best == -1 || e < best) best = e;
}
}
end_[p] = best;
}
// cut 打表
for (int j = 1; j <= n; j++) {
int acc = 0;
for (int t = j - 1; t >= 0; t--) {
if (end_[t] != -1 && end_[t] <= j - 1) acc++;
cut[t][j] = acc;
}
}
for (int j = 0; j <= n; j++)
for (int m = 0; m <= k; m++)
f[j][m] = NEG;
f[0][0] = 0;
for (int j = 1; j <= n; j++) {
for (int m = 1; m <= min(j, k); m++) {
int best = NEG;
for (int t = m - 1; t < j; t++) {
if (f[t][m - 1] > NEG) {
int v = f[t][m - 1] + cut[t][j];
if (v > best) best = v;
}
}
f[j][m] = best;
}
}
return f[n][k];
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
string line;
vector<string> lines;
while (getline(cin, line)) {
// 去首尾空白
string t;
int a = 0, b = (int)line.size();
while (a < b && isspace((unsigned char)line[a])) a++;
while (b > a && isspace((unsigned char)line[b - 1])) b--;
t = line.substr(a, b - a);
if (!t.empty()) lines.push_back(t);
}
size_t i = 0;
while (i < lines.size()) {
int p, k;
stringstream ss(lines[i]);
ss >> p >> k;
i++;
string text;
for (int j = 0; j < p && i < lines.size(); j++, i++)
text += lines[i];
int s = 0;
if (i < lines.size()) {
stringstream ss2(lines[i]);
ss2 >> s;
i++;
}
vector<string> words;
for (int j = 0; j < s && i < lines.size(); j++, i++)
words.push_back(lines[i]);
cout << max_words(text, words, k) << "\n";
}
return 0;
}#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-02 04:37
# update_at: 2026-10-02 04:47
import sys
NEG = -10**9 # 不可达状态的哨兵:份数超过字符数时凑不出合法划分
def match_end(text: str, words: list[str]) -> list[int | None]:
"""end[p]:从 p 出发能匹配的最短单词的末尾下标;该处匹配不到任何单词则为 None。
同一起点匹配多个单词时只留最短的:整段落在一份里的前提是末尾不越界,
最短的能放下,其余更长的也一定能放下。
"""
ends: list[int | None] = []
for p in range(len(text)):
lens = [len(w) for w in words if text.startswith(w, p)]
ends.append(p + min(lens) - 1 if lens else None)
return ends
def build_cut(ends: list[int | None]) -> list[list[int]]:
"""cut[t][j]:把 [t, j) 单独作一份时能数到的单词起点数。
起点 p 只有在“单词完整落在份内”(末尾 ≤ j-1)时才计 1 次;
多个单词共享同一起点只算一个(首字母只能用一次)。
"""
n = len(ends)
cut = [[0] * (n + 1) for _ in range(n + 1)]
for j in range(1, n + 1):
acc = 0
for t in range(j - 1, -1, -1): # 右端点固定,左端点从右往左累计
acc += ends[t] is not None and ends[t] <= j - 1
cut[t][j] = acc
return cut
def max_words(text: str, words: list[str], k: int) -> int:
"""把 text 切成恰好 k 份非空连续段,最大化各份内可数的单词起点总数。"""
n = len(text)
cut = build_cut(match_end(text, words))
# f[j][m]:前 j 个字符恰好分成 m 份的最大单词数;t 是最后一份的左端点
f = [[NEG] * (k + 1) for _ in range(n + 1)]
f[0][0] = 0
for j in range(1, n + 1):
for m in range(1, min(j, k) + 1): # 每份至少 1 个字符,m > j 不可达
f[j][m] = max(f[t][m - 1] + cut[t][j] for t in range(m - 1, j))
return f[n][k]
def solve() -> None:
lines = [ln.strip() for ln in sys.stdin.read().splitlines() if ln.strip()]
out: list[str] = []
i = 0
while i < len(lines): # 输入可能包含多组测试数据
p, k = map(int, lines[i].split()) # p 行字母串,切成 k 份
i += 1
text = "".join(lines[i:i + p]) # 每行恰好 20 个字母,合并成一个串
i += p
s = int(lines[i]) # 字典单词个数
i += 1
words = lines[i:i + s]
i += s
out.append(str(max_words(text, words, k)))
print("\n".join(out))
if __name__ == "__main__":
solve()复杂度
- 时间:预处理
为 ,建 为 ,DP 三重循环 ; 时约 次候选转移,与标准 C++ 解法同阶。 - 空间:
为 、 为 ,合计 ,约 0.5 MB。
总结
- 计数规则决定模型:同一起点只计 1 次、不同起点互不冲突,所以一份的得分是区间内可数起点的计数,不需要选词贪心。
- 只保留每个起点的最短匹配末尾
,把"单词是否完整落在份内"压成一次下标比较。 - 分段 DP 枚举最后一份左端点,
把区间得分封装成 查表;“恰好 份非空"由枚举范围 保证,不能放松成"至多”。 - 三步实现(合并串 → 预处理
ends/cut→ DP)与推导一一对应,8 组真实数据全量 PASS(0.015–0.042 s、峰值约 15.2 MB,限制 1000 ms / 32 MB),Python 无 TLE/MLE 风险。