目前看到的做法都带一个 ,这里给一个严格的线性时间做法。

记数字 的出现次数为

需要删除的元素个数等于数组长度减去可以保留的元素个数,当支配数为 时,可以保留的元素个数为

记:

  • 为 满足 的个数
  • 为 满足 之和

通过分析哪些数被 “削平”,计算出可以保留的元素个数为 ,因此问题归结于如何对每个 计算 通过前缀和技巧可以在 的总时间内计算,而 均具有二维偏序的形式,利用树状数组可以在 的总时间内计算,因此很容易得到一个最坏时间复杂度为 的算法。但通过挖掘更多性质,可以得到一个严格线性的做法。

时,显然以 作为支配数劣于以 作为支配数,所以可以通过构建单调栈确定一系列候选支配数,使得它们的值递增而出现次数递减,最优的支配数一定在这些候选支配数中产生。

按照值从小到大、出现次数从多到少的顺序遍历候选支配数,假设上一个考虑的支配数是 ,当前考虑的支配数是

作为前缀和, 的计算是简单的:

记集合 ,集合 ,显然 三者的不交并。因此 ,问题归结于如何求出

为了求出 ,可以将 按照出现次数排序(由于出现次数不超过 ,因此可以用 复杂度的计数排序),记出现次数第 少的数为 。在遍历候选支配数 的过程中维护指针 ,使其指向首个出现次数大于等于 的数,即 ,则 包含了指针从 向左移动到 新扫过的位于 之间的数,即

按照定义直接求解即可。

最坏时间复杂度 提交链接,核心代码片段如下

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;
}