序列划分与最大化函数值 | 豆包MarsCode AI 刷题

156 阅读1分钟

序列划分与最大化函数值

问题描述

小U拿到了一个长度为n的序列S,他想通过某种方法将这个序列划分为k段,并且使每一段都不为空。他发现,通过对每一段应用一个unique操作(即将相邻且相同的元素合并成一个)之后,可以得到一个新的序列,并定义函数ff表示经过unique操作后的序列的长度。小U想知道,如何划分才能使所有段的f(A)f(A)函数值之和最大化?

例如,给定序列 [1,1,1,2,2,3,3,1],小U可以将其划分为三段,分别为 [1,1,1][2,2] 和 [3,3,1]unique 之后得到的序列分别是 [1][2] 和 [3,1],那么最终的ff函数值之和为 6。


测试样例

样例1:

输入:n = 8 ,k = 3 ,a = [1, 1, 1, 2, 2, 3, 3, 1]
输出:6

样例2:

输入:n = 6 ,k = 2 ,a = [1, 2, 3, 3, 2, 1]
输出:6

样例3:

输入:n = 5 ,k = 1 ,a = [1, 1, 1, 1, 1]
输出:1

解题思路

目标与难点

此问题要求在对序列进行划分的同时,最大化每段 unique 操作后长度的总和。需要注意的是:

  1. 去重后的长度并不直接依赖于原始序列的长度,而取决于区间中不连续的相同元素个数。
  2. 我们需要高效地计算每个划分方案的 unique 长度和,以便从中找到最优解。

由于可能的划分方案数目较多,直接枚举所有分段组合不可行。因此我们可以使用动态规划(DP)结合前缀信息来优化计算过程。

动态规划状态定义

我们定义 dp[r][i]dp[r][i] 为将序列的前 rr 个元素划分为 ii 段时,所有段的 ff 值之和的最大值。划分后第 ii 段的长度由 dp[r][i]dp[r][i] 的转移状态决定。

前缀去重长度数组 ss

为了简化去重长度的计算,我们先构建一个辅助数组 ss,其中 s[i]s[i] 表示从序列开头到第 ii 个元素,经过 unique 操作后的长度。这样,我们就可以用差分来高效计算任意区间的去重长度。

构建 ss 数组的过程如下:

  • 初始化 s[1]=1s[1] = 1,表示第一个元素 unique 后的长度为1。
  • 对于每个元素 ii
    • a[i]a[i1]a[i] \neq a[i - 1](即当前元素和前一个元素不同),则 s[i]=s[i1]+1s[i] = s[i - 1] + 1
    • 否则 s[i]=s[i1]s[i] = s[i - 1]

最终,s[i]s[i] 数组为:[1,1,1,2,2,3,3,1] 时,ss 数组为 [1, 1, 1, 2, 2, 3, 3, 4]

通过 ss 数组,区间 [l,r][l, r] 的 unique 长度(即经过 unique 操作后的长度)可以表示为:

f(l,r)=s[r]s[l1]+(a[l1]==a[l2]?1:0)f(l, r) = s[r] - s[l - 1] + (a[l - 1] == a[l - 2] ? 1 : 0)

其中,若左边界元素与其前一个元素相同,需要加1以确保从前一段继承。

DP转移方程的设计

对于序列 SS 划分为 ii 段时的 DP 递推,可以表示为:

dp[r][i]=max(dp[r][i],dp[l1][i1]+f(l,r))dp[r][i] = \max(dp[r][i], dp[l - 1][i - 1] + f(l, r))

其中 f(l,r)f(l, r) 表示区间 [l,r][l, r] 的去重长度。

实现过程中,我们可以枚举所有区间的左右边界,并递推更新 dpdp 表。

初始代码实现

以下代码实现了上述 DP 方案,通过递推计算每个子区间的去重长度,最终得到最优解。

int solution(int n, int k, vector<int> a) {
    vector dp(n + 1, vector<int>(k + 1, 0));
    vector<int> s(n + 1, 0);
    s[1] = 1;
    for (int i = 2; i <= n; ++i) {
        if (a[i - 1] != a[i - 2]) {
            s[i] = s[i - 1] + 1;
        } else {
            s[i] = s[i - 1];
        }
    }

    auto cal = [&](int l, int r) {
        return s[r] - s[l - 1] + (l >= 2 && a[l - 1] == a[l - 2]);
    };

    for (int r = 1; r <= n; ++r) {
        for (int l = 1; l <= r; ++l) {
            for (int i = k; i > 0; --i) {
                dp[r][i] = max(dp[r][i], dp[l - 1][i - 1] + cal(l, r));
            }
        }
    }
    return dp[n][k];
}

优化

1. 三重循环的性能瓶颈

在起初的代码实现中,动态规划部分的核心是三重循环:

  • 外层循环枚举每个位置 rr(表示划分到当前元素的右端点)。
  • 中间循环枚举每个位置 ll(表示当前段的左端点)。
  • 内层循环依次计算每一段对应的划分数量 ii,并尝试更新对应的 dp[r][i]dp[r][i] 值。

在这种三重循环下,时间复杂度约为 O(n2×k)O(n^2 \times k),其中 nn 为序列长度,kk 为段数。在数据规模较大时,效率成为瓶颈。于是,我们从多个角度逐步优化。

2. 引入辅助数组 ma 减少冗余计算

从优化方向来看,由于内层循环每次在不同的分段数量 ii 下执行类似操作,存在大量重复计算。例如,对于某个 dp[r][i]dp[r][i] 的更新过程,大量计算依赖之前的状态。我们可以通过引入一个辅助数组 ma 来存储当前最优的状态,从而减少中间计算。

辅助数组 ma 的作用
  • ma[i] 表示在分段数量为 ii 的情况下,之前的某些状态的最大值。
  • 通过存储这些中间结果,我们可以避免再次循环查找。
  • 这个数组在每次外层循环的更新过程中进行滚动维护,帮助我们随时获取最优解。

通过引入 ma 数组,可以将复杂度降为 O(n×k)O(n \times k)

3. 优化DP转移方程中的冗余计算

在初始的 DP 转移方程中,我们反复调用 cal(l, r) 函数来计算从 llrr 的去重长度。通过前缀数组 ss,我们已简化了去重长度的计算,但仍然存在进一步优化的空间。利用 ma 数组后,可以在 dp[r][i] 更新时直接使用最优状态,无需再次遍历。

在这种思路下,我们重新设计 DP 更新为:

dp[r][i]=max(dp[r][i],s[r]+ma[i1])dp[r][i] = \max(dp[r][i], s[r] + ma[i - 1])

其中,ma[i] 用来记录最优值的滚动状态,使得无需逐段查找最大值。

4. 代码实现与详细注释

优化后代码如下,添加了注释以便理解每一步的优化意图:

int solution(int n, int k, vector<int> a) {
    // 定义 DP 表和前缀去重长度数组 s
    vector dp(n + 1, vector<int>(k + 1, 0));
    vector<int> s(n + 1, 0);
    
    // 计算前缀去重长度数组 s
    s[1] = 1;
    for (int i = 2; i <= n; ++i) {
        if (a[i - 1] != a[i - 2]) s[i] = s[i - 1] + 1;
        else s[i] = s[i - 1];
    }

    // 引入滚动数组 ma 以优化 DP 转移方程
    vector<int> ma(k + 1, 0);
    
    // 外层循环遍历每个右端点 r
    for (int r = 1; r <= n; ++r) {
        // 倒序更新分段数量,确保 DP 转移方程的正确性
        for (int i = k; i > 0; --i) {
            // 更新当前 dp 值,直接使用前缀和 s[r] 与最优状态 ma[i-1]
            dp[r][i] = max(dp[r][i], s[r] + ma[i - 1]);
            // 更新 ma[i],维护当前最优值
            ma[i] = max(ma[i], dp[r][i] - s[r] + (r < n && a[r - 1] == a[r]));
        }
    }
    
    // 返回结果,即划分为 k 段的最大 f 值之和
    return dp[n][k];
}

优化总结

  • 滚动维护最优值ma 数组作为滚动数组,避免了冗余计算,使每次的 DP 更新只需常数时间完成。
  • 复杂度分析:通过减少冗余的状态更新,算法的时间复杂度从 O(n2×k)O(n^2 \times k) 优化为 O(n×k)O(n \times k),适合较大规模数据。

总结

在本题中,通过前缀数组 ss 和滚动数组 ma 的巧妙应用,使得问题的求解过程高效而准确。