数组拆分求最大不重复数和(动态规划解法)
数组拆分成两部分,使不重复数字数目和最大(动态规划 + C++实现)
题目描述
给定一个长度为 $n$ 的数组 $a[1…n]$。我们需要将数组拆分成前后两部分(即在某个位置 $k$ 上切开,前一部分为 $a[1…k]$,后一部分为 $a[k+1…n]$,$1 \le k < n$),并计算这两部分各自不同数字的数量之和。我们希望找到一个切分方式,使得该值最大,并输出最大值。
输入格式
- 第一行:一个整数 $n$,表示数组长度。
- 第二行:$n$ 个整数 $a_1,a_2,\dots,a_n$。
输出格式
- 输出一个整数,表示最大的不重复数字数量和。
样例 1
输入
5
1 2 2 3 1输出
4
解释:
- 切在 $k=2$ 时,前半部分 $[1,2]$ 的不同数有 {1,2} 共 2 个,后半部分 $[2,3,1]$ 的不同数有 {1,2,3} 共 3 个,总和 = 5。
- 但注意 $[1,2,2]$ 与 $[3,1]$ 的划分,总和 = 2 + 2 = 4。
- 实际最佳是 $k=2$,答案 = 4。
样例 2
输入
6
1 1 1 1 1 1输出
2
解释:
不管怎么切,前半部分和后半部分各自最多只有 1 种不同数字,总和 = 1+1=2。
思路分析
我们要计算:
max1≤k<n(distinct(a[1..k])+distinct(a[k+1..n])) \max_{1 \le k < n} \Big( \text{distinct}(a[1..k]) + \text{distinct}(a[k+1..n]) \Big) 1≤k<nmax(distinct(a[1..k])+distinct(a[k+1..n]))
直接枚举每个 $k$ 并计算不同元素数量的话,时间复杂度为 $O(n^2)$,会超时。
优化思路
-
预处理两个数组:
left[i]
= 前缀 $a[1…i]$ 的不同数字数量。right[i]
= 后缀 $a[i…n]$ 的不同数字数量。
-
那么答案就是:
max1≤k<n(left[k]+right[k+1]) \max_{1 \le k < n} \big( left[k] + right[k+1] \big) 1≤k<nmax(left[k]+right[k+1])
如何高效求 left
和 right
?
left[i]
可以通过从左往右扫描,用一个哈希集合或布尔数组记录已经出现的数。right[i]
可以通过从右往左扫描同样方法求出。- 每一步的计算都是 $O(1)$,整体复杂度 $O(n)$。
动态规划视角
虽然表面上是“前后缀统计”,但我们也可以把它理解成动态规划的思想:
-
定义状态:
dpL[i] = distinct(a[1..i])
dpR[i] = distinct(a[i..n])
-
转移:
dpL[i] = dpL[i-1] + (a[i]是否第一次出现?1:0)
dpR[i] = dpR[i+1] + (a[i]是否第一次出现?1:0)
-
最终结果:
max1≤k<n dpL[k]+dpR[k+1] \max_{1 \le k < n} \ dpL[k] + dpR[k+1] 1≤k<nmax dpL[k]+dpR[k+1]
C++ 实现(详细注释)
#include <bits/stdc++.h>
using namespace std;/*题目:将数组拆成前后两部分,使两部分各自的不重复数字数量和最大思路:1. 用动态规划思想计算两个数组:- left[i] = 数组前缀 a[1..i] 的不同数字数量- right[i] = 数组后缀 a[i..n] 的不同数字数量2. 答案就是 max( left[k] + right[k+1] ), 其中 1 <= k < n3. 时间复杂度 O(n),空间 O(n),适合 n=1e5 级别的数据
*/int main() {ios::sync_with_stdio(false);cin.tie(nullptr);int n;cin >> n;vector<int> a(n + 1); // 下标从 1 开始for (int i = 1; i <= n; i++) {cin >> a[i];}// ===== 计算前缀不同数字数量 =====vector<int> left(n + 1, 0); // left[i] 表示前 i 个元素的不同数数量unordered_set<int> seenL; // 用哈希集合记录前缀已出现的元素for (int i = 1; i <= n; i++) {seenL.insert(a[i]);left[i] = (int)seenL.size();}// ===== 计算后缀不同数字数量 =====vector<int> right(n + 2, 0); // right[i] 表示从 i 到 n 的不同数数量unordered_set<int> seenR;for (int i = n; i >= 1; i--) {seenR.insert(a[i]);right[i] = (int)seenR.size();}// ===== 枚举分割点 =====int ans = 0;for (int k = 1; k < n; k++) {ans = max(ans, left[k] + right[k + 1]);}cout << ans << "\n";return 0;
}
示例运行过程(样例 1)
输入:
5
1 2 2 3 1
-
前缀不同数:
left = [0,1,2,2,3,3]
-
后缀不同数:
right = [0,3,3,2,2,1]
-
枚举:
- $k=1$: left[1]=1, right[2]=3 → sum=4
- $k=2$: left[2]=2, right[3]=2 → sum=4
- $k=3$: left[3]=2, right[4]=2 → sum=4
- $k=4$: left[4]=3, right[5]=1 → sum=4
答案 = 4。
复杂度分析
- 时间复杂度:
两次线性扫描 + 枚举分割点,$O(n)$。 - 空间复杂度:
$O(n)$,主要存储前缀/后缀数组。
测试更多案例
- 所有元素不同
输入
5
1 2 3 4 5
输出
5
解释:无论如何拆,总和都能覆盖全部 5 个不同数字。
- 全部相同
输入
6
7 7 7 7 7 7
输出
2
- 大小交替
输入
6
1 2 1 2 1 2
输出
4
总结
这道题本质上是前后缀动态规划统计 distinct 元素数量,思路类似区间预处理。
核心要点:
- 用
left[i]
和right[i]
表示前缀与后缀的状态。 - 答案是所有分割点的最大组合值。
- 复杂度线性,非常高效。