记录130
#include<bits/stdc++.h>
using namespace std;
#define ll long long//定义长整型别名ll,因为价值a[i]最大可达1e9,累加容易溢出int
const int N=1e5+10;//定义常量N,表示字符串的最大长度
int a[N];//定义数组a,a[i]表示长度为i的合法子串的价值
ll dp[N];//定义dp数组,dp[i]表示前i个字符划分后的最大价值之和
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
int n;//表示字符串的长度
string s;//定义字符串s,存储输入的n个小写字母
cin>>n;//读入字符串的长度n
cin>>s;//读入由n个小写字母组成的字符串s
for(int i=1;i<=n;i++){//循环读入不同长度子串的价值
cin>>a[i];//读入长度为i的子串的价值
}
for(int i=1;i<=n;i++){//外层循环:遍历字符串的每一个位置(计算前i个字符的最优解)
bool vis[26]={false};//定义布尔数组vis,用来标记当前子串中26个字母是否出现过
for(int j=1;j<=i;j++){//内层循环:尝试以第i个字符结尾,向前截取长度为j的子串
int char_idx=s[i-j]-'a';//计算当前向前截取到的字符在字母表中的下标(0-25)
if(vis[char_idx])break;//如果这个字符在当前子串中已经出现过,说明再往前延伸不合法,直接跳出循环
vis[char_idx]=true;//如果没出现过,将其标记为已出现
dp[i]=max(dp[i],dp[i-j]+a[j]);//状态转移:更新前i个字符的最大价值
}
}
cout<<dp[n];//输出前n个字符(即整个字符串)划分后的最大价值之和
return 0;//程序结束
}
题目传送门
https://www.luogu.com.cn/problem/P14075
前言
我是一名专注信奥赛(CSP-J/S、NOIP)的教练。
- 如果你觉得这篇题解对你有帮助,欢迎点击关注我的CSDN账号,我会持续更新高质量算法解析。
- 我深知算法思维的构建远比单纯通过题目更重要,本系列题解不局限于AC代码的堆砌,而是致力于拆解题目背后的逻辑链条与核心知识点
- 备赛路上若遇瓶颈,欢迎随时评论或私信,我将甄选典型疑难问题,通过视频讲解或撰写专项文章的形式,为你提供深度答疑。
核心解题思路
这道题是一道结合了字符串合法性约束的线性动态规划(DP)问题。
-
状态定义:
我们定义dp[i]表示字符串的前i个字符(即s[0]到s[i-1])在满足划分条件下的最大价值之和。 -
状态转移:
为了求出dp[i],我们需要考虑最后一步划分。假设最后一个子串的长度为j(即子串为s[i-j]到s[i-1]),那么前i-j个字符的最优解是dp[i-j],当前子串的价值是a[j]。
状态转移方程为:dp[i] = max(dp[i], dp[i-j] + a[j])。 -
合法性约束与剪枝:
题目要求每个子串中的字母至多出现一次。这意味着当我们从第i个字符向前枚举子串长度j时,如果遇到重复字母,不仅当前的j不合法,任何比j更长的子串(继续向前延伸)也必然包含这个重复字母,因此都不合法。
核心优化:在内层循环中,使用一个大小为 26 的布尔数组 vis 记录当前子串内的字母。一旦发现重复字母,立即 break 跳出内层循环。这个剪枝操作保证了内层循环最多只会执行 26 次(因为只有 26 个小写字母),从而将总时间复杂度从 O(N^2) 降维打击到了 O(26N),完美适配 N=10^5 的数据规模。
代码分块详细解释
1. 头文件、常量与全局变量定义
#include<bits/stdc++.h>
using namespace std;
#define ll long long // 定义长整型别名 ll,因为价值 a[i] 最大可达 1e9,累加容易溢出 int
const int N=1e5+10; // 定义常量 N,表示字符串的最大长度
int a[N]; // 定义数组 a,a[i] 表示长度为 i 的合法子串的价值
ll dp[N]; // 定义 dp 数组,dp[i] 表示前 i 个字符划分后的最大价值之和
- 详细分析:这部分完成了基础数据结构的搭建。特别需要注意的是
dp数组使用了long long类型。题目中单个子串的价值a[i]可达 10^9,在极端情况下(例如 N=10^5),总价值可能会达到 10^14 级别,远超int的存储上限(约 2 × 10^9),使用long long是防止数据溢出的关键。
2. 输入与 IO 优化
int main(){
ios::sync_with_stdio(false);
cin.tie(0); // 关闭 C++ 标准流同步,大幅提升大量数据输入输出的效率
int n; // 表示字符串的长度
string s; // 定义字符串 s,存储输入的 n 个小写字母
cin>>n; // 读入字符串的长度 n
cin>>s; // 读入由 n 个小写字母组成的字符串 s
for(int i=1;i<=n;i++){ // 循环读入不同长度子串的价值
cin>>a[i]; // 读入长度为 i 的子串的价值
}
- 详细分析:在 N=105 的数据规模下,IO 效率至关重要。
ios::sync_with_stdio(false);和cin.tie(0);是竞赛中的标准加速操作。随后,程序依次读入字符串长度、字符串本体以及长度为 1 到 N 的子串价值数组a,为接下来的动态规划做好数据准备。
3. 核心逻辑:带剪枝的动态规划
for(int i=1;i<=n;i++){ // 外层循环:遍历字符串的每一个位置(计算前 i 个字符的最优解)
bool vis[26]={false}; // 定义布尔数组 vis,用来标记当前子串中 26 个字母是否出现过
for(int j=1;j<=i;j++){ // 内层循环:尝试以第 i 个字符结尾,向前截取长度为 j 的子串
int char_idx=s[i-j]-'a'; // 计算当前向前截取到的字符在字母表中的下标(0-25)
if(vis[char_idx]) break; // 核心剪枝:如果这个字符在当前子串中已经出现过,说明再往前延伸不合法,直接跳出循环
vis[char_idx]=true; // 如果没出现过,将其标记为已出现
dp[i]=max(dp[i],dp[i-j]+a[j]); // 状态转移:更新前 i 个字符的最大价值
}
}
- 详细分析:这是整个算法的灵魂。
- 外层循环:自底向上,依次计算
dp[1]到dp[n]。 - 内层循环与合法性检查:内层循环从当前字符
s[i-1]开始向前回溯。vis数组在每次外层循环开始时都会被重置。s[i-j]-'a'巧妙地将字符映射为 0-25 的整数下标。 - 剪枝的威力:一旦
vis[char_idx]为真,说明遇到了重复字母。此时break直接终止内层循环。因为字母表只有 26 个字母,所以无论字符串多长,内层循环最多只会运行 26 次。这使得原本可能超时的 O(N^2) 算法变成了极其高效的 O(26N) 线性算法。 - 状态转移:在确保子串合法的前提下,执行
dp[i] = max(dp[i], dp[i-j] + a[j]),将当前子串的价值与历史最优解结合。
4. 输出结果
cout<<dp[n]; // 输出前 n 个字符(即整个字符串)划分后的最大价值之和
return 0; // 程序结束
}
- 详细分析:
dp[n]存储了将整条长度为n的字符串进行最优划分后的最大总价值,直接输出即可。
核心逻辑总结表
| 代码模块 | 核心变量/操作 | 精炼作用 | 解决的痛点 |
|---|---|---|---|
| 数据类型 | #define ll long long | 使用 64 位长整型 | 防止价值累加时超出 int 范围导致数据溢出 |
| IO 优化 | ios::sync_with_stdio(false) | 提升输入输出效率 | 解决 105 级别数据量下 cin/cout 可能导致的超时问题 |
| 合法性标记 | bool vis[26]={false} | 记录当前子串内的字母 | 快速判断子串是否包含重复字母,保证划分合法 |
| 核心剪枝 | if(vis[char_idx]) break | 遇重复字母立即终止内层循环 | 将内层循环限制在常数级 (26次),将复杂度从 O(N2) 降为 O(N) |
| 状态转移 | dp[i] = max(dp[i], dp[i-j]+a[j]) | 线性 DP 核心递推公式 | 将复杂的字符串划分问题,转化为自底向上的最优子结构求解 |
669

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



