「K-Anonymous Sequence」 K匿名序列

GitHub跳转原题关系图返回列表

每个数至少有 k-1 个相同数:排序后连续分段 DP(段长≥k),转移化为直线族取 max 用单调队列维护上凸壳,斜率优化 O(n)。

OJ: roj

题目 ID: 3177

难度:省选/NOI-

标签:DP斜率优化单调队列凸壳

创建: 2026-10-01 23:20

更新: 2026-10-01 23:43

形式化题目

非严格递增序列 a1≤a2≤⋯≤ana_1\le a_2\le\dots\le a_n。每次操作可将某个数减 1,求最少操作数使每个数在序列中至少有 k−1k-1 个数与之相同(即每个取值的出现次数 ≥k\ge k)。

正解

思路

1. 排序 + 连续分段

只能减不能增,且排序后最优解一定把序列切成若干连续段,每段长度 ≥k\ge k,段内所有数最终取相同值(段内最小值 aja_j,减到它)。设前缀和 SS,段 (j,i](j,i] 的代价为 Si−Sj−(i−j)ajS_i-S_j-(i-j)a_j。

fi=min⁡j≤i−k(fj+Si−Sj−(i−j)aj),f0=0f_i=\min_{j\le i-k}\bigl(f_j+S_i-S_j-(i-j)a_j\bigr),\quad f_0=0

2. 斜率优化

整理转移:

fi=Si−max⁡j(i⋅aj−cj)⏟直线 y=ajx−cj 在 x=i,cj=fj−Sj+jajf_i=S_i-\max_{j}\underbrace{\bigl(i\cdot a_j-c_j\bigr)}_{\text{直线 } y=a_j x-c_j \text{ 在 } x=i},\qquad c_j=f_j-S_j+j a_j
  • 直线斜率 aja_j 随 jj 递增(序列非降),查询点 ii 也递增;
  • 维护上凸壳(取 max):双端队列存决策下标,入队时用叉乘判定弹掉被支配的队尾直线,查询时队头单调后移;
  • 同斜率的直线按截距取优,避免凸壳退化。

均摊 O(1)O(1) 转移,整体 O(n)O(n)。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 23:34
# update_at: 2026-10-01 23:34

import sys
from collections import deque


def min_ops(a: list[int], k: int) -> int:
    """排序后做连续分段 DP,单调队列(斜率优化)求最小总降序代价。

    f[i] = min{ f[j] + (S[i]-S[j]) - (i-j)*a[j] | j <= i-k, f[j] < +inf },
    整理成 f[i] = S[i] - max{ i*a[j] - c[j] },其中 c[j] = f[j]-S[j]+j*a[j];
    max{} 是斜率 a[j] 随 j 递增的直线族、查询点 i 也递增,
    所以用双端队列维护上凸壳,均摊 O(1) 完成一次转移。
    """
    n = len(a)
    S = [0] * (n + 1)
    for i, v in enumerate(a, 1):
        S[i] = S[i - 1] + v  # 前缀和:段 (j, i] 的原始和

    f = [0] * (n + 1)  # f[i]:前 i 个数分成若干长度 >= k 的段的最小代价
    c = [0] * n        # c[j] = f[j]-S[j]+j*a[j],直线 i*a[j]-c[j] 的截距
    q = deque()        # 决策下标 j;斜率 a[j] 从队头到队尾严格递增
    for i in range(k, n + 1):
        j = i - k  # 段长约束 i-j >= k:决策 j 到此刻才首次合法
        if j == 0 or j >= k:  # 0 < j < k 的前缀切不出合法段,f[j] = +inf,不入队
            cj = f[j] - S[j] + j * a[j]
            while q and a[q[-1]] == a[j] and c[q[-1]] >= cj:
                q.pop()  # 同斜率、截距不更小 → 队尾直线处处被支配
            if not (q and a[q[-1]] == a[j]):  # 同斜率但队尾更优 → 新直线全劣,不入队
                while len(q) >= 2:
                    l2, l1 = q[-2], q[-1]
                    # l1 永远取不到 max ⟺ 它与左右两直线的交点次序颠倒(叉乘免除法)
                    if (c[l1] - c[l2]) * (a[j] - a[l1]) >= (cj - c[l1]) * (a[l1] - a[l2]):
                        q.pop()
                    else:
                        break
                c[j] = cj
                q.append(j)

        # 查询点 i 递增 → 上凸壳的最优直线从队头单调后移
        while len(q) >= 2 and i * a[q[1]] - c[q[1]] >= i * a[q[0]] - c[q[0]]:
            q.popleft()
        f[i] = S[i] - (i * a[q[0]] - c[q[0]])

    return f[n]


def solve() -> None:
    data = iter(sys.stdin.buffer.read().split())
    out: list[str] = []
    T = int(next(data))
    for _ in range(T):
        n, k = int(next(data)), int(next(data))
        a = sorted(int(next(data)) for _ in range(n))  # 题面保证非降序,排序只是保险
        out.append(str(min_ops(a, k)))

    print('\n'.join(out))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-01 23:22
 * update_at: 2026-10-01 23:22
 */
#include <bits/stdc++.h>
using namespace std;
typedef  long long ll;
typedef  unsigned long long ull;

const int maxn = 2e6+5;
int n,m;
int a[maxn];

void read_data(){

}

signed main () {
    ios::sync_with_stdio(false); cin.tie(0);
    read_data();
    
    return 0;
}

复杂度

  • 时间复杂度:O(n)O(n)(每条直线至多入队出队一次)。
  • 空间复杂度:O(n)O(n)。

总结

  • 「相邻同值分组」类问题的标准变形:排序 + 连续段 DP + 段内取最小值的代价公式。
  • 转移形如 fi=min⁡j{i⋅kj+bj}f_i=\min_j\{i\cdot k_j+b_j\}(kjk_j、查询点均单调)时用单调队列凸壳斜率优化。
  • 叉乘写法避免浮点除法,同斜率直线的支配判定是常见的坑。