1 条题解
-
0
F. 数组缩减 题解
一、题意理解
给定一个长度为 的数组 。每次操作可以移除若干个元素,要求这些元素满足:
- 要么全部相等;
- 要么两两互不相等。
对每个目标大小 (),求将数组恰好缩减到 的最小操作次数。
二、问题转化
转化一:求每轮最多能移除多少元素
原问题求"达到某个大小所需最少操作次数",等价于求"在 次操作内,最多能移除多少元素"。
设 表示在恰好 次操作下最多能移除的元素数,则 就是满足 的最小 。且 单调不减,可以通过求 的前缀最优来得到所有答案。
转化二:压缩频次数组
由于元素的具体值不重要,只关心每种值的出现次数。设频次数组 ,其中 表示值 在原数组中出现的次数。只保留 的项,排序后得到长度为 的非降数组 。
三、操作的本质
一次操作对应两种模式:
- 类型一(全局减):从每种出现过的值中各移除一个元素,即将所有非零 减 (但不低于 )。
- 类型二(消除最大):移除某个值的全部出现,即将某个 清零。
由此, 次操作由 次"全局减"和 次"消除最大"组成()。且操作顺序不影响最终结果。
四、关键观察
假设固定 (消除了 个频次最大的元素),那么剩余的频次都会受到 次全局减的影响。
设未消除的频次中最大值为 ,则 超过 没有意义(再多减只会使已为 的项保持为 ),所以有效的 只取 中出现的值。
由于 中仅有 个不同的值,整个 对的枚举量实际为 。
五、算法流程
-
读入数组 ,统计频次,得到数组 (只含 的项),排序。
-
初始化答案数组 ,表示移除 个元素的最少操作数。
-
枚举 (全局减的次数 取 中的值递增):
-
用指针 维护第一个满足 的位置。前 个频次 ,会被完全清零;后续频次减去 。
-
令 从 遍历到 ,其中 表示第一个不被消除最大的元素的索引。这里的 对应消除最大的次数 。
-
对于一个确定的 ,操作总次数为:
移除的总元素数为:
$$s = \underbrace{\sum_{t=j}^{i-1} (c_t - k)}_{\text{前 } i-j \text{个会受全局减影响的部分}} + \underbrace{\text{被消除最大的部分之和}}_{\text{会在后续用前缀和计算}} $$但标程的循环逻辑里, 是通过累加 得到的,维护的是在消除 个最大频次后,剩余部分受全局减减少的元素数。
-
对于每个计算出的移除量 ,用 更新 。
-
-
最后对 取前缀最小值:,保证单调性。
六、标程详细解析
private fun solveTest() { val n = readInt() val a = readInts(n) val c = intarr(n) for (x in a) { c[x - 1]++ } val res = intarr(n, Int.MAX_VALUE) c.sort() var j = 0 for (k in 0..n) { while (j < n && c[j] <= k) j++ var s = 0 for (i in j..n) { val d = k + (n - i) if (s < n) res[s] = min(res[s], d) if (i < n) s += (c[i] - k) } } for (i in 1 .. n - 1) { res[i] = min(res[i], res[i - 1]) } println(res) }步骤拆解:
-
构建频次数组 :长度为 , 统计值 的出现次数。排序后得到非降序列。
-
外层循环 : 从 到 ,表示全局减的次数。 不会超过 (因为数组大小只有 )。
-
指针 :
c[j] <= k的频次在 次全局减下会归零, 是第一个 的位置。 -
内层循环 : 从 到 , 表示消除最大的次数。
- :总操作次数 = 全局减次数 + 消除最大次数。
- :维护当前方案下移除的元素总数(逐步累加各频次被减掉的部分)。
- 用 更新 。
-
前缀最小值:因为如果能用 次操作移除 个元素,那么移除少于 个元素也可以用不超过 次操作完成。
七、答案的还原
存储的是移除 个元素所需的最少操作数。
原问题要求输出 ——缩减到恰好 个元素的最少操作数,即移除 个元素。
因此最终输出为 (即 ,,...)。
八、复杂度分析
- 构建频次数组 。
- 排序频次数组 。
- 双指针枚举 总复杂度 ,因为每个频次只被访问有限次。
- 总时间复杂度 ,空间复杂度 。
所有测试用例 ,算法完全可行。
九、代码实现(C++)
#include <bits/stdc++.h> using namespace std; void solve() { int n; cin >> n; vector<int> a(n); vector<int> freq(n, 0); for (int i = 0; i < n; i++) { cin >> a[i]; freq[a[i] - 1]++; } vector<int> c; for (int i = 0; i < n; i++) { if (freq[i] > 0) c.push_back(freq[i]); } sort(c.begin(), c.end()); int m = c.size(); vector<int> res(n + 1, n + 1); // res[s] = min ops to remove s elements res[0] = 0; int j = 0; for (int k = 0; k <= n; k++) { while (j < m && c[j] <= k) j++; int s = 0; for (int i = j; i <= m; i++) { int d = k + (m - i); // total operations if (s <= n) { res[s] = min(res[s], d); } if (i < m) s += (c[i] - k); } } // 前缀取 min,保证单调性 for (int i = 1; i <= n; i++) { res[i] = min(res[i], res[i - 1]); } // 输出 c_0 到 c_{n-1},即 res[n] 到 res[1] for (int x = 0; x < n; x++) { cout << res[n - x] << " \n"[x == n - 1]; } } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int t; cin >> t; while (t--) { solve(); } return 0; }
十、样例验证(以第一个样例为例)
数组:,。
频次排序后:(对应值 的出现次数)。
经算法计算得到的 前缀最小值后,输出 到 依次为:
3 3 2 2 2 1 1 1 1 1 1与题目示例完全一致。
- 1
信息
- ID
- 6659
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者