小红的双排列删除得分

小红的双排列删除得分

问题描述

给定一个长度为 2n 的双排列(每个数字 1..n 恰好出现两次),每次可选择一对相同元素作为区间端点 [l, r],删除该区间并获得区间内所有元素之和,剩余元素保持原序拼接。求能获得的最高总分。


思路一:前后缀分割 DP(原长代码)

1. 基本转化

  • 每次操作删除一个“匹配对”及其之间的所有元素。
  • 最优操作中,选择的区间不会交叉(交叉会使两个操作无法同时执行;嵌套则内层不增加收益)。
  • 因此可以看作:在数组中选出若干互不重叠的区间,每个区间两端数字相同,目标是被覆盖元素的总和最大。
  • 等价于:求最大可删除元素和,即用这些区间覆盖尽可能大的元素和。

2. 状态定义

  • L[x]、R[x]:数字 x 第一次和第二次出现的位置(因为每个数字恰好出现两次)。

  • pref[i]:原数组前缀和,$$pref[i] = \sum_{j=1}^{i} A[j]$$。

  • suf[i]:后缀和,$$suf[i] = \sum_{j=i}^{2n} A[j]$$。

  • pre_dp[i]:仅考虑前 i 个元素,能获得的最大删除元素和(即前 i 个元素中,选出互不重叠的合法区间,覆盖元素和的最大值)。

  • suf_dp[i]:仅考虑从 i 到 2n 的后缀,能获得的最大删除元素和。

3. 转移方程

从左到右计算 pre_dp

  • 初始 pre_dp[0] = 0。
  • 对于每个位置 i($$1 \le i \le 2n$$):
    • 先继承 pre_dp[i] = pre_dp[i-1](不选以 i 为右端点的区间)。
    • 若 A[i] 是第二次出现(即 L[A[i]] \ne i,因为 L 存储第一次位置),则可以删除区间 [L[A[i]], i],获得价值 pref[i] - pref[L[A[i]]-1],并且这个区间与之前的区间不重叠,所以加上 pre_dp[L[A[i]]-1]。
    • 取最大:

      \[pre_dp[i] = \max(pre_dp[i-1],\; pref[i] - pref[L[A[i]]-1] + pre_dp[L[A[i]]-1]) \]

从右到左计算 suf_dp

  • 对称地,suf_dp[2n+1] = 0。
  • 对于位置 i 从 2n 到 1:
    • 继承 suf_dp[i] = suf_dp[i+1](不选以 i 为左端点的区间)。
    • 若 A[i] 是第一次出现(即 R[A[i]] \ne i,因为 R 存储第二次位置),则可以删除区间 [i, R[A[i]]],获得价值 suf[i] - suf[R[A[i]]+1],并加上 suf_dp[R[A[i]]+1]。
    • 取最大:

      \[suf_dp[i] = \max(suf_dp[i+1],\; suf[i] - suf[R[A[i]]+1] + suf_dp[R[A[i]]+1]) \]

4. 合并答案

  • 最优解中,必然存在一个“分割点” i,使得所有选中区间要么完全在左侧($$\le i$$),要么完全在右侧($$> i$$),且左右两侧独立最优(因为区间不交叉,可以找到这样一个分界)。
  • 因此枚举 i($$0 \le i \le 2n$$),答案 $$ans = \max(ans, pre_dp[i] + suf_dp[i+1])$$。
  • 实际上代码枚举 i 作为右侧起点,使用 pre_dp[i-1] + suf_dp[i] 等价。

5. 复杂度

  • 时间:$$O(n)$$,扫描若干次。
  • 空间:$$O(n)$$。

6. 正确性说明

  • 由于最优区间互不重叠,它们自然将数组划分为若干段,存在一个分界线使得左侧区间全部在分界左边,右侧在右边。
  • 左右两侧独立计算最大删除和,再枚举分界线即可得到全局最优。

思路二:最小剩余和 DP(简化代码)

1. 问题转化

  • 设整个数组总和为 $$S = 2 \times (1+2+\cdots+n) = n(n+1)$$。
  • 每次操作删除一段区间并得分,最终得分为所有被删除元素之和。
  • 因为每个元素最多删除一次,所以 最高得分 = S – 最小可能剩余元素和。
  • 问题转化为:选择若干互不重叠的合法区间,使得未被覆盖的元素和最小。

2. 状态定义

  • dp[i]:处理完前 i 个元素后,最小的剩余元素和(即未被删除的元素之和)。
  • last[x]:数字 x 最近一次出现的位置(扫描过程中维护)。

3. 转移方程

  • 初始化 dp[0] = 0。
  • 扫描 i = 1..2n,读入 a = A[i]:
    • 不删除当前元素:则它必然保留,剩余和增加 a,故 dp[i] = dp[i-1] + a。
    • 若 a 之前出现过(last[a] \ne -1),我们可以选择删除区间 [last[a], i]。删除后,该区间内所有元素都不保留,剩余情况等价于前 last[a]-1 个元素的最优剩余,故 dp[i] = \min(dp[i], dp[last[a]-1])。
    • 两者取较小值。
    • 更新 last[a] = i(记录当前位置,以便后续使用)。
  • 最终最小剩余和为 dp[2n],答案为 $$S - dp[2n]$$。

4. 正确性说明

  • 最优解中,对于最后一个位置 i,要么它被保留(不删),要么它属于某个以当前元素为右端点的区间,且该区间左端点是该元素上一次出现的位置(因为区间不能交叉,若选择更早的位置作为左端点,则中间会有冗余操作,不会更优)。
  • 无后效性:dp[i] 仅依赖 dp[i-1] 和 dp[last[a]-1],满足最优子结构。
  • 利用双排列性质(每个数字恰好两次),last[a] 在第二次出现时存储的就是第一次位置,删除区间为 [第一次, 当前],是唯一可能的以当前为右端点的区间,因此转移正确。

5. 复杂度

  • 时间:$$O(n)$$,单次扫描。
  • 空间:$$O(n)$$。

6. 注意事项

  • 总和 $$S = n(n+1)$$ 可能超过 32 位整型,需使用 long long 存储。
  • dp 数组也需用 long long,因为剩余和最大为 $$S$$,约为 $$4 \times 10^{10}$$(当 $$n=2\times10^5$$ 时)。

两种思路对比

方面 前后缀分割 DP 最小剩余和 DP
核心思想 将问题拆分为左右独立,枚举分界 转化为最小保留和,直接 DP
状态维度 一维前缀 + 一维后缀,再合并 单一从左到右的 DP
转移次数 多次遍历(前后缀各一次,合并一次) 单次遍历
常数 较大(需维护前缀和、后缀和、多个数组) 极小(仅一个 dp 数组和一个 last 数组)
代码长度 较长 简洁
是否容易溢出 注意总和用 long long 同样注意总和用 long long
通用性 适用于任何每个数字出现两次的数组 利用双排列特性,同样适用于一般出现次数≤2的数组(稍加修改可支持多次出现)
时间复杂度 $$O(n)$$ $$O(n)$$
空间复杂度 $$O(n)$$ $$O(n)$$

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

void solve() {
    int n; cin >> n;
    vector<int> A(2 * n + 1);
    
    for(int i = 1; i <= n*2; i ++) cin >> A[i];

    vector<ll> L(2*n + 1, -1), R(2*n+1, -1);//左右的相同数字
    vector<ll> pref(2*n + 1, 0), suf(2*n+2, 0);//前后缀最大可能

    for(int i = 1; i <= n*2; i ++) {
        int id = A[i];
        if(L[id] == -1) L[id] = i;
        else R[id] = i;

        pref[i] = pref[i-1] + A[i];
        int f = 2*n+1 - i;
        suf[f] = suf[f+1] + A[f];
    }
    vector<ll> pre_dp(2*n+1, 0), suf_dp(2*n+2, 0);
    for(int i = 1; i <= n*2; i ++) {
        pre_dp[i] = pre_dp[i-1];
        int id = A[i];
        if(L[id] != i) pre_dp[i] = max(pref[i]-pref[L[id]-1]+pre_dp[L[id]-1], pre_dp[i]);
    }
    ll ans = pre_dp[2*n];
    for(int i = n*2; i >= 1; i --) {
        suf_dp[i] = suf_dp[i+1];
        int id = A[i];
        if(R[id] != i) suf_dp[i] = max(suf_dp[i], suf[i]-suf[R[id]+1]+suf_dp[R[id]+1]);
        
        ans = max(ans, pre_dp[i-1]+suf_dp[i]);
    }
    cout << ans << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
    int _=1;
    while(_--) solve();
    return 0;
}

解法二

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll INF = 1e17;

void solve() {
    ll n; cin >> n;
    vector<ll> dp(2 * n + 1, INF); dp[0] = 0;
    vector<int> last(n + 1, -1); 
    
    for(int i = 1; i <= n*2; i ++) {
        int a; cin >> a;
        dp[i] = dp[i-1] + a;
        if(last[a] != -1) dp[i] = min(dp[last[a] - 1], dp[i]);
        last[a] = i;
    }
    ll ans = (1 + n) * n - dp[2 * n];
    cout << ans << "\n";
}

int main() {
    ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr);
    int _=1;
    while(_--) solve();
    return 0;
}
posted @ 2026-10-03 10:18  thenrify  阅读(7)  评论(0)    收藏  举报