小红的双排列删除得分
小红的双排列删除得分
问题描述
给定一个长度为 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;
}

浙公网安备 33010602011771号