记录120
#include<bits/stdc++.h>
using namespace std;
const int MAXN=30;// 计分序列最大长度 n <= 20
const int MAXM=1e5+10;// 字符串最大长度 m <= 10^5
int a[MAXN],dp[MAXM];// 计分序列 a[i], dp[i] 表示前 i 个字符的最大得分
int main(){// 线性DP
ios::sync_with_stdio(false);
cin.tie(0);
int n,m;
string s;
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
cin>>m;
cin>>s;
for(int i=1;i<=m;i++){
// 情况1:第 i 个字符不作为任何计分子串的结尾
dp[i]=dp[i-1];// 保底策略(不选当前位置)
// 情况2:尝试以第 i 个字符为结尾,向前匹配 k 个连续的 "abc"
// k 代表当前匹配了 k 个 "abc",每个 "abc" 长度为 3
for(int k=1;k<=n;k++){
// 计算当前 k 个 "abc" 的起始位置(在字符串 s 中的下标)
// i 是第几个字符,对应 s 的下标是 i-1
// k 个 "abc" 总长度为 3*k,所以起始下标为 (i - 1) - (3 * k - 1) = i - 3 * k
int start_id=i-3*k;
// 如果起始位置超出了字符串开头,说明无法凑出 k 个 "abc",跳出循环
if(start_id<0) break;
// 检查从 start_id 开始的 3 个字符是否为 "abc"
// 如果匹配成功,更新最大得分
// 得分 = 起始位置之前的最大得分 + 当前 k 个 "abc" 的分数 a[k]
if(s.substr(start_id,3)=="abc") dp[i]=max(dp[i],dp[start_id]+a[k]);// 进攻策略(尝试用当前位置得分)
// 如果当前这 3 个字符不是 "abc",说明无法凑出更长的连续 "abc" 串了
// 例如想凑 2 个 abc,但倒数第二个 abc 位置不对,那 3 个、4 个肯定也不对
else break;
}
}
cout<<dp[m]<<"\n";
return 0;//结束程序
}
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的优快云账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
题目传送门
https://www.luogu.com.cn/problem/P10721
💡 核心思路分析
1. 题意理解与数学建模
- 目标:在一个字符串中,找出若干个不重叠的、由 kk 个连续的
"abc"组成的子串,使得这些子串的得分总和最大。 - 计分规则:一个由 kk 个
"abc"组成的子串得分为 akak 。注意, kk 个"abc"必须严格连续,且字符不能重复使用(即选取的子串之间不能有交集)。
2. 算法选择:线性动态规划 (DP)
- 题目要求我们在字符串上“切”出若干段合法的子串,这是非常经典的线性 DP / 序列划分模型。
- 状态定义:设
dp[i]表示处理完字符串的前i个字符时,能够获得的最大总得分。 - 状态转移:对于第
i个字符,我们有两种决策:- 决策一(不选):第
i个字符不作为任何计分子串的结尾。那么前i个字符的最大得分就等于前i-1个字符的最大得分,即dp[i] = dp[i-1]。 - 决策二(选):第
i个字符恰好是某个合法子串的结尾。假设这个子串是由 kk 个"abc"组成的(长度为 3k3k ),那么这个子串的起始位置就是i - 3*k + 1(对应代码中的下标start_id = i - 3*k)。如果这一段确实是连续的"abc",那么dp[i]就可以从dp[start_id] + a[k]转移而来。我们只需要枚举所有可能的 kk ,取最大值即可。
- 决策一(不选):第
3. 关键优化:利用连续性剪枝
- 在枚举 kk 时,由于我们要找的是连续的
"abc",所以当我们检查倒数第 kk 个"abc"发现它不是"abc"时,就没必要继续检查 k+1,k+2...k+1,k+2... 了,直接break跳出循环。这极大地减少了无效的字符串匹配操作。
📝 代码逐行深度解析
第一部分:头文件、常量与全局变量定义
#include<bits/stdc++.h>
using namespace std;
const int MAXN=30; // 计分序列最大长度 n <= 20,开 30 足够
const int MAXM=1e5+10; // 字符串最大长度 m <= 10^5,防止越界稍微开大一点
int a[MAXN], dp[MAXM]; // a[i] 存储得分为 a_i;dp[i] 表示前 i 个字符的最大得分
解析:这里定义了基础的常量和数组。dp 数组是一维的,因为线性 DP 只需要依赖前面的状态,空间复杂度为 O(m)O(m) 。
第二部分:主函数入口与数据读入
int main(){
ios::sync_with_stdio(false); // 关闭 C++ 标准输入输出流与 C 标准库的同步
cin.tie(0); // 解除 cin 和 cout 的绑定
int n, m;
string s;
cin >> n; // 读入计分序列的长度
for(int i = 1; i <= n; i++) cin >> a[i]; // 读入每个 k 对应的得分 a_k
cin >> m; // 读入字符串的长度
cin >> s; // 读入字符串
解析:使用了标准的 IO 加速技巧,这在处理高达 105105 级别的数据时非常重要,能有效避免因读写耗时导致的超时(TLE)。
第三部分:DP 状态初始化与保底策略
for(int i = 1; i <= m; i++){
// 情况1:第 i 个字符不作为任何计分子串的结尾
dp[i] = dp[i-1]; // 保底策略(不选当前位置)
解析:外层循环遍历字符串的每一个位置 i。首先执行“决策一”,无论当前位置能否构成 "abc",我们至少可以继承前一个位置的最优解。这保证了 dp[i] 具有单调不减的性质。
第四部分:枚举连续 "abc" 的个数并尝试状态转移
// 情况2:尝试以第 i 个字符为结尾,向前匹配 k 个连续的 "abc"
// k 代表当前匹配了 k 个 "abc",每个 "abc" 长度为 3
for(int k = 1; k <= n; k++){
// 计算当前 k 个 "abc" 的起始位置(在字符串 s 中的下标)
// i 是第几个字符(从1开始),对应 s 的下标是 i-1
// k 个 "abc" 总长度为 3*k,所以起始下标为 (i - 1) - (3 * k - 1) = i - 3 * k
int start_id = i - 3 * k;
// 边界判断:如果起始位置超出了字符串开头,说明无法凑出 k 个 "abc",跳出循环
if(start_id < 0) break;
解析:进入内层循环,枚举以当前位置结尾可能包含的 "abc" 的数量 kk 。通过简单的数学推导得出待检查子串的起始下标 start_id,并进行防越界的边界判断。
第五部分:合法性校验、状态更新与剪枝
// 检查从 start_id 开始的 3 个字符是否为 "abc"
// 如果匹配成功,更新最大得分
// 得分 = 起始位置之前的最大得分 + 当前 k 个 "abc" 的分数 a[k]
if(s.substr(start_id, 3) == "abc")
dp[i] = max(dp[i], dp[start_id] + a[k]); // 进攻策略(尝试用当前位置得分)
// 如果当前这 3 个字符不是 "abc",说明无法凑出更长的连续 "abc" 串了
// 例如想凑 2 个 abc,但倒数第二个 abc 位置不对,那 3 个、4 个肯定也不对
else break;
}
}
cout << dp[m] << "\n"; // 输出整个字符串的最大得分
return 0; // 结束程序
}
解析:这是代码的核心灵魂所在。利用 C++ 的 substr 函数快速截取并比对字符串。如果匹配成功,则进行状态转移(dp[start_id] + a[k]);如果匹配失败,立刻 break 终止内层循环。这种基于连续性的剪枝,让算法在最坏情况下也能保持极高的运行效率。最终,dp[m] 即为所求的全局最优解。
364



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



