1 条题解
-
0
题目重述
对于数组 ,定义 为值 在 中任意两次出现之间的最大间隔:
若 出现少于两次,则 。
数组 的美丽值为:
给定数组 (),一个数组 称为 nice,如果:
- 对所有 成立
求所有 nice 数组 的最大可能美丽值。
解题思路
第一步:关键观察
对于任意值 ,设它在数组 中第一次出现的位置为 ,最后一次出现的位置为 (若出现至少两次)。那么:
中间的所有出现不会影响这个距离。因此,我们只需要关心每个值的第一次和最后一次出现位置。
第二步:将距离和转化为位置贡献
将 写成求和形式:
这意味着,对于每个位置 (),如果有值 满足 ,那么这个值 就会对位置 贡献 。
因此,总美丽值可以写成:
$$\text{beauty} = \sum_{k=1}^{n-1} \left|\{x : L_x \le k < R_x\}\right| $$定义 ,则:
第三步:问题转化
现在问题转化为:对于每个位置 ,我们希望最大化 ,即尽可能多的值 满足其开始位置 且结束位置 。
每个值 的 和 必须满足:
- 是一个位置 ,且 (因为可以在该位置放置 )
- 是一个位置 ,且 ,并且
第四步:标程的核心算法
标程使用贪心算法分别计算前缀和后缀的最优值。
4.1 计算前缀能开始的不同值数量
从左到右遍历位置 到 :
- 维护一个集合 ,初始包含所有值
- 对于位置 ,在 中找到小于等于 的最大值
- 如果找到这样的值,就把它从 中删除(表示这个值可以在位置 开始)
- ,表示前缀 中能开始的不同值的最大数量
贪心正确性:为了让前缀能开始尽可能多的不同值,应该优先让较小的值在较早的位置开始。较大的 可以容纳较大的值,而较小的 只能容纳较小的值,贪心选择小于等于 的最大值可以最小化浪费。
4.2 计算后缀能结束的不同值数量
从右到左遍历位置 到 :
- 维护一个集合 ,初始包含所有值
- 对于位置 ,在 中找到小于等于 的最大值
- 如果找到这样的值,就把它从 中删除(表示这个值可以在位置 结束)
- ,表示后缀 中能结束的不同值的最大数量
4.3 计算答案
对于每个切分点 ():
- 前缀 中能开始的不同值数量为
- 后缀 中能结束的不同值数量为
- 这两部分能配对的最大数量是 ,因为这受限于开始和结束的较小者
最终答案:
$$\text{ans} = \sum_{k=1}^{n-1} \min(pre[k], suf[k+1]) $$
第五步:正确性证明
上界性:对于任何位置 ,跨越它的不同值 必须满足 且 。因此,跨越 的值数量不超过前缀 中能开始的不同值数量,也不超过后缀 中能结束的不同值数量,所以不超过 。
可达性:可以通过构造证明存在一种方案,使得对于所有 同时达到这个上界。构造方法是将值按贪心配对,使得每个值的开始和结束位置分别取自前缀和后缀的最优位置。
贪心最优性:用 维护未使用值,每次取小于等于 的最大值,可以最大化每个前缀能开始的不同值数量。这是因为较大的 可以容纳较大的值,而较小的 只能容纳较小的值,贪心保证了最小化浪费。
算法复杂度
- 每个测试用例:
- 两次遍历,每次 ( 操作)
- 一次求和
- 总复杂度:$O(\sum n \log n) \le O(2 \times 10^5 \log (2 \times 10^5))$,可以通过。
示例验证
以第一个样例 为例:
计算 (前缀开始数):
可选值 删除的值 无 计算 (后缀结束数):
从右到左:
可选值 删除的值 无 计算答案:
与样例输出一致。
代码实现(带注释)
#include <bits/stdc++.h> using namespace std; const int MAXN = (int)1e6 + 7; int n, t, ans; int arr[MAXN], f[MAXN][2]; set<int> st; void solve() { cin >> n; for (int i = 1; i <= n; i++) cin >> arr[i]; // 计算前缀能开始的不同值数量(j=0)和后缀能结束的不同值数量(j=1) for (int j = 0; j < 2; j++) { st.clear(); // 初始所有值都可用 for (int i = 1; i <= n; i++) st.insert(i); // j=0: 从左到右遍历,计算前缀 // j=1: 从右到左遍历,计算后缀 for (int i = (j ? n : 1); (j ? i > 1 : i < n); (j ? i-- : i++)) { // 找到小于等于 arr[i] 的最大可用值 auto it = st.upper_bound(arr[i]); if (it != st.begin()) { it--; st.erase(*it); // 用掉这个值 } f[i][j] = n - st.size(); // 已用值的数量 } } // 计算答案:对每个切分点取 min 后求和 ans = 0; for (int i = 1; i < n; i++) { ans += min(f[i][0], f[i + 1][1]); } cout << ans << "\n"; } int main() { ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); cin >> t; while (t--) solve(); return 0; }
总结
这道题的核心技巧是:
- 将距离和转化为位置贡献:每个位置 统计有多少个值的区间覆盖了它
- 分别计算前缀和后缀的最优值:使用贪心算法,每次取小于等于 的最大可用值
- 取 min 后求和:每个位置的贡献受限于前缀开始数和后缀结束数的较小值
这种方法将原问题转化为了一个可以在 时间内解决的问题。
- 1
信息
- ID
- 6682
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 1
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者