题解:洛谷 P14075 划分字符串

【题目来源】

洛谷:P14075 [GESP202509 六级] 划分字符串 - 洛谷

【题目描述】

小 A 有一个由 \(n\) 个小写字母组成的字符串 \(s\)。他希望将 \(s\) 划分为若干个子串,使得子串中每个字母至多出现一次。例如,对于字符串 street 来说,str + e + e + t 是满足条件的划分;而 s + tree + t 不是,因为子串 treee 出现了两次。

额外地,小 A 还给出了价值 \(a_1,a_2,…,a_n\),表示划分后长度为 \(i\) 的子串价值为 \(a_i\)。小 A 希望最大化划分后得到的子串价值之和。你能帮他求出划分后子串价值之和的最大值吗?

【输入】

第一行,一个正整数 \(n\),表示字符串的长度。

第二行,一个包含 \(n\) 个小写字母的字符串 \(s\)

第三行,\(n\) 个正整数 \(a_1,a_2,…,a_n\),表示不同长度的子串价值。

【输出】

一行,一个整数,表示划分后子串价值之和的最大值。

【输入样例】

6
street
2 1 7 4 3 3

【输出样例】

13

【核心思想】

  1. 问题分析:给定字符串 \(s\)(长度 \(n\))和长度价值数组 \(a[1..n]\),要求将 \(s\) 划分为若干子串,每个子串中字母至多出现一次,最大化各子串长度对应价值之和。这是一个线性 DP 问题,核心在于枚举以 \(i\) 结尾的无重复字符子串,利用集合快速判重确定合法子串范围。

  2. 算法选择

    • 一维线性 DP\(dp[i]\) 表示前 \(i\) 个字符的最大价值之和
    • 集合判重:用 set 维护当前子串中的字符,遇到重复字符即终止扩展,确保子串合法
  3. 关键步骤

    • 初始化:读取 \(n\)、字符串 \(s\)、价值数组 \(a[1..n]\)\(s = ' ' + s\) 使下标从 \(1\) 开始
    • 边界\(dp[1] = a_1\)(第一个字符单独成段)
    • DP 计算\(i\)\(2\)\(n\)):
      • 初始值\(dp[i] = dp[i-1] + a_1\)(将 \(s_i\) 单独划分为长度为 \(1\) 的子串)
      • 枚举扩展\(j\)\(i-1\) 递减到 \(1\)
        • set 维护子串 \(s[j..i]\) 中的字符集合
        • \(s_j\) 已在集合中:\(break\)(出现重复,子串 \(s[j..i]\) 不合法)
        • \(s_j\) 加入集合
        • 状态转移\(dp[i] = \max(dp[i], dp[j-1] + a_{i-j+1})\)(将 \(s[j..i]\) 作为长度为 \(i-j+1\) 的子串)
    • 输出答案\(dp[n]\)
  4. 时间/空间复杂度

    • 时间复杂度:\(O(n^2)\),外层 \(O(n)\),内层最坏 \(O(n)\),集合操作 \(O(1)\)
    • 空间复杂度:\(O(n)\)\(dp\) 数组
  5. 线性 DP 的核心思想

    • 无后效性分割\(dp[i]\) 仅依赖于 \(dp[j-1]\)\(j \leq i\)),前面的划分不影响后续,满足 DP 最优子结构
    • 合法子串约束:通过从 \(i\) 向左扩展并用集合判重,确保子串 \(s[j..i]\) 中字符不重复,遇到重复立即终止(更短的 \(j\) 必然也包含该重复字符)
    • 枚举所有可能分割点:对每个 \(i\),尝试所有以 \(i\) 结尾的合法子串长度,取价值最大者
    • 贪心不可行:价值数组 \(a\) 非单调,无法贪心选择最长或最短子串,必须枚举所有合法分割
    • 适用于字符串分割、带约束的子串选择、一维序列划分类 DP 问题

【算法标签】

普及 #线性DP-一维

【代码详解】

#include <bits/stdc++.h>
using namespace std;

#define int long long  // 使用长整型防止溢出
const int N = 100005;  // 定义最大数组长度

int n;                  // 字符串长度
string s;               // 输入的字符串(处理后前面加空格)
string t[N];           // 未使用的字符串数组(保留原代码结构)
int a[N];               // 存储每个位置的分值
int dp[N];              // dp数组,dp[i]表示前i个字符的最大得分

signed main()
{
    // 输入字符串长度和字符串
    cin >> n >> s;

    // 在字符串前添加空格,使索引从1开始
    s = " " + s;

    // 输入每个位置的分值
    for (int i = 1; i <= n; i++)
    {
        cin >> a[i];
    }

    // 初始化dp数组
    dp[1] = a[1];  // 第一个字符的最大得分就是它本身的分值

    // 动态规划计算最大得分
    for (int i = 2; i <= n; i++)
    {
        set<char> st;  // 用于记录当前子串中的字符

        // 初始值:将当前字符单独作为一个子串
        dp[i] = dp[i - 1] + a[1];

        // 将当前字符加入集合
        st.insert(s[i]);

        // 尝试将当前字符与前面的字符组成更长的子串
        for (int j = i - 1; j >= 1; j--)
        {
            // 如果遇到重复字符,则终止循环
            if (st.count(s[j]))
            {
                break;
            }

            // 将字符加入集合
            st.insert(s[j]);

            // 更新dp值:考虑从j到i的子串
            dp[i] = max(dp[i], dp[j - 1] + a[i - j + 1]);
        }
    }

    // 输出前n个字符的最大得分
    cout << dp[n] << endl;

    return 0;
}

【运行结果】

6
street
2 1 7 4 3 3
13
posted @ 2026-08-10 12:26  团爸讲算法  阅读(21)  评论(0)    收藏  举报