题目描述
你得到一个包含 NNN 个单词的字典,每个单词有一个整数权重 WWW。同时给你一个字符串 SSS,初始得分为 000。游戏规则如下:
- 你必须依次选择 SSS 中若干连续的、不重叠的子串(即每个字符恰好被覆盖一次)。
- 对于每个选出的子串:
- 如果它在字典中,则将对应的权重 WWW 加到得分上。
- 如果它不在字典中,则扣除 P×LP \times LP×L 分,其中 LLL 是该子串的长度,PPP 是给定的惩罚值。
- 游戏的目标是最大化最终得分。
你需要输出能够获得的最大得分。
输入格式:
- 第一行是整数 TTT(T≤20T \leq 20T≤20),表示测试用例数量。
- 每个测试用例:
- 第一行是两个整数 NNN(N≤10000N \leq 10000N≤10000)和 PPP(0≤P≤100000 \leq P \leq 100000≤P≤10000)。
- 接下来 NNN 行,每行一个单词(只包含小写字母,长度不超过 100100100)和其权重 WWW(0≤W≤100000 \leq W \leq 100000≤W≤10000)。
- 最后一行是字符串 SSS(只包含小写字母,长度不超过 100001000010000)。
输出格式:
- 对于每个测试用例,输出一行
Case #: score,其中#是测试用例编号,score是最大得分。
题目分析
1. 问题本质
这是一个分段决策问题:我们需要将字符串 SSS 分割成若干连续子串,每个子串要么是字典中的单词(加分),要么不是(扣分)。每个字符必须被恰好一个子串覆盖,且子串之间不能重叠。目标是最大化总得分。
2. 关键点
- 字典中的单词长度不一,最长达 100100100,因此我们需要快速判断任意子串是否在字典中。
- SSS 的长度最大为 100001000010000,如果暴力枚举所有子串并查询字典,时间复杂度为 O(∣S∣2)O(|S|^2)O(∣S∣2),在极端情况下约为 10810^8108,可能勉强通过,但效率低下。
- 为了高效查询,我们可以使用 Trie\texttt{Trie}Trie(字典树) 存储所有单词,这样在遍历 SSS 时可以沿着 Trie\texttt{Trie}Trie 移动,快速判断当前前缀是否可能构成单词。
- 这是一个动态规划问题:设 dp[i]dp[i]dp[i] 表示处理完前 iii 个字符(即 S[0..i−1]S[0..i-1]S[0..i−1])能获得的最大得分。我们要求的是 dp[∣S∣]dp[|S|]dp[∣S∣]。
3. 动态规划状态转移
- 初始状态:dp[0]=0dp[0] = 0dp[0]=0(没有字符时得分为 000)。
- 对于每个位置 iii(0≤i<∣S∣0 \leq i < |S|0≤i<∣S∣),如果 dp[i]dp[i]dp[i] 已经计算过(即不是极小值),则从 iii 开始尝试所有可能的子串 S[i..j]S[i..j]S[i..j](i≤j<∣S∣i \leq j < |S|i≤j<∣S∣)。
- 设子串长度为 L=j−i+1L = j - i + 1L=j−i+1。
- 情况一:强制扣分。无论该子串是否在字典中,我们都可以选择将其视为“非单词”,直接扣分 P×LP \times LP×L,此时 dp[j+1]=max(dp[j+1],dp[i]−P×L)dp[j+1] = \max(dp[j+1], dp[i] - P \times L)dp[j+1]=max(dp[j+1],dp[i]−P×L)。
- 情况二:单词匹配。如果该子串在字典中,且权重为 WWW,则 dp[j+1]=max(dp[j+1],dp[i]+W)dp[j+1] = \max(dp[j+1], dp[i] + W)dp[j+1]=max(dp[j+1],dp[i]+W)。
- 最终答案是 dp[∣S∣]dp[|S|]dp[∣S∣]。
4. Trie\texttt{Trie}Trie 优化匹配过程
- 在枚举 jjj 从 iii 到 ∣S∣−1|S|-1∣S∣−1 时,我们同步在 Trie\texttt{Trie}Trie 中沿着字符 S[j]S[j]S[j] 移动。
- 如果当前字符在 Trie 中不存在子节点,说明从 iii 开始的所有后续子串都不可能构成单词,此时可以直接
break,不再继续向后扩展。 - 如果当前节点是某个单词的结尾(
weight != -1),说明 S[i..j]S[i..j]S[i..j] 是一个单词,可以进行加分转移。
5. 时间复杂度
- 构建 Trie:O(∑单词长度)O(\sum \text{单词长度})O(∑单词长度),最大约为 N×100=106N \times 100 = 10^6N×100=106。
- 动态规划:对于每个 iii,最坏情况下需要遍历到 i+100i+100i+100(因为单词长度不超过 100100100),因此总复杂度为 O(∣S∣×100)O(|S| \times 100)O(∣S∣×100),即约 10610^6106。
- 整体复杂度在可接受范围内。
解题思路总结
- 读入数据,包括字典和惩罚值 PPP。
- 构建 Trie\texttt{Trie}Trie,存储所有单词及其权重。
- 动态规划:
- 初始化 dpdpdp 数组为极小值(
LLONG_MIN),dp[0]=0dp[0] = 0dp[0]=0。 - 对于每个 iii,从 iii 开始在 Trie\texttt{Trie}Trie 中遍历,枚举可能的结束位置 jjj。
- 先计算扣分情况(因为即使不是单词也可以选择扣分)。
- 如果当前字符不在 Trie 中,则停止继续扩展。
- 如果当前位置是单词结尾,则计算加分情况。
- 更新 dp[j+1]dp[j+1]dp[j+1]。
- 初始化 dpdpdp 数组为极小值(
- 输出 dp[∣S∣]dp[|S|]dp[∣S∣] 作为答案。
参考代码
// Another Word Game
// UVa ID: 11539
// Verdict: Accepted
// Submission Date: 2025-12-05
// UVa Run Time: 2.480s
//
// 版权所有(C)2025,邱秋。metaphysis # yeah dot net
#include <bits/stdc++.h>
using namespace std;
const int MAX_S_LEN = 10010;
const int MAX_WORD_LEN = 110;
struct TrieNode {
int weight; // 单词结尾处的权重,-1表示不是单词结尾
TrieNode* children[26];
TrieNode() {
weight = -1;
for (int i = 0; i < 26; i++)
children[i] = nullptr;
}
};
void insertWord(TrieNode* root, const string& word, int w) {
TrieNode* cur = root;
for (char ch : word) {
int idx = ch - 'a';
if (cur->children[idx] == nullptr)
cur->children[idx] = new TrieNode();
cur = cur->children[idx];
}
cur->weight = w; // 记录该单词的权重
}
void destroy(TrieNode *root) {
if (root == nullptr) return;
for (int i = 0; i < 26; i++)
destroy(root->children[i]);
delete root;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
for (int caseNo = 1; caseNo <= t; caseNo++) {
int n, penalty;
cin >> n >> penalty;
TrieNode* root = new TrieNode();
for (int i = 0; i < n; i++) {
string word;
int w;
cin >> word >> w;
insertWord(root, word, w);
}
string s;
cin >> s;
int len = s.length();
vector<long long> dp(len + 1, LLONG_MIN); // dp[i]表示处理完前i个字符的最大得分
dp[0] = 0;
for (int i = 0; i < len; i++) {
if (dp[i] == LLONG_MIN) continue;
TrieNode* cur = root;
for (int j = i; j < len; j++) { // 枚举以i开头的子串
int idx = s[j] - 'a';
int wordLen = j - i + 1;
// 无论是否匹配单词,都可以选择按非单词处理并扣分
dp[j + 1] = max(dp[j + 1], dp[i] - penalty * wordLen);
// 尝试匹配单词
if (cur->children[idx] == nullptr) break; // 此后缀不在字典中,停止继续扩展
cur = cur->children[idx];
if (cur->weight != -1) // 构成一个字典中的单词
dp[j + 1] = max(dp[j + 1], dp[i] + cur->weight);
}
}
cout << "Case " << caseNo << ": " << dp[len] << "\n";
//destroy(root);
}
return 0;
}
提示:
- 本题使用 Trie\texttt{Trie}Trie + 动态规划 是标准解法。
- 注意 dpdpdp 数组应使用
long long类型,防止溢出。 - 对于每个测试用例,需要释放 Trie\texttt{Trie}Trie 树内存以避免内存泄漏(虽然在此题中因数据量不大可以省略,但在实际开发中应养成好习惯)。

5618

被折叠的 条评论
为什么被折叠?



