P14075 [GESP202509 六级] 划分字符串

记录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)问题。

  1. 状态定义
    我们定义 dp[i] 表示字符串的前 i 个字符(即 s[0] 到 s[i-1])在满足划分条件下的最大价值之和。

  2. 状态转移
    为了求出 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])

  3. 合法性约束与剪枝
    题目要求每个子串中的字母至多出现一次。这意味着当我们从第 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 核心递推公式将复杂的字符串划分问题,转化为自底向上的最优子结构求解
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值