目前看到的做法都带一个 ,这里给一个严格的线性时间做法。
记数字 的出现次数为
。
需要删除的元素个数等于数组长度减去可以保留的元素个数,当支配数为 时,可以保留的元素个数为
。
记:
为 满足
且
的
的个数
为 满足
且
的
之和
通过分析哪些数被 “削平”,计算出可以保留的元素个数为
,因此问题归结于如何对每个
计算
。
通过前缀和技巧可以在
的总时间内计算,而
均具有二维偏序的形式,利用树状数组可以在
的总时间内计算,因此很容易得到一个最坏时间复杂度为
的算法。但通过挖掘更多性质,可以得到一个严格线性的做法。
当 且
时,显然以
作为支配数劣于以
作为支配数,所以可以通过构建单调栈确定一系列候选支配数,使得它们的值递增而出现次数递减,最优的支配数一定在这些候选支配数中产生。
按照值从小到大、出现次数从多到少的顺序遍历候选支配数,假设上一个考虑的支配数是 ,当前考虑的支配数是
。
作为前缀和, 的计算是简单的:
。
记集合 ,集合
,
,
,显然
是
三者的不交并。因此
,
,问题归结于如何求出
。
为了求出 ,可以将
按照出现次数排序(由于出现次数不超过
,因此可以用
复杂度的计数排序),记出现次数第
少的数为
。在遍历候选支配数
的过程中维护指针
,使其指向首个出现次数大于等于
的数,即
,则
包含了指针从
向左移动到
新扫过的位于
之间的数,即
。
按照定义直接求解即可。
最坏时间复杂度 ,提交链接,核心代码片段如下
int main() {
int t = read();
while (t--) {
int n = read();
vector<int> cnt(n);
for (int i = 0; i < n; i++) cnt[read() - 1]++;
vector<int> s(n);
ranges::iota(s, 0);
counting_sort(s, n, [&cnt](int x) { return cnt[x]; });
vector<int> stk;
for (int x = 0; x < n; x++) {
while (!stk.empty() && cnt[stk.back()] <= cnt[x]) stk.pop_back();
stk.push_back(x);
}
int f = 0, g = 0, h = 0;
int mx = 0;
int p = n;
for (int idx = 0; idx < stk.size(); idx++) {
int x = stk[idx], y = idx == 0 ? -1 : stk[idx - 1];
while (p - 1 >= 0 && cnt[s[p - 1]] >= cnt[x] - 1) {
p--;
if (s[p] <= y) {
g++;
h += cnt[s[p]];
}
}
for (int z = y + 1; z <= x; z++) {
f += cnt[z];
if (cnt[z] >= cnt[x] - 1) {
g++;
h += cnt[z];
}
}
mx = max(mx, f - h + g * (cnt[x] - 1) + 1);
}
cout << n - mx << '\n';
}
return 0;
}

京公网安备 11010502036488号