• 【ACWing】273. 分级(配数学证明)


    题目地址:

    https://www.acwing.com/problem/content/275/

    给定长度为 n n n的序列 a a a,构造一个长度为 n n n的序列 b b b,满足: b b b非严格单调,即 b 1 ≤ b 2 ≤ … ≤ b n b_1≤b_2≤…≤b_n b1b2bn b 1 ≥ b 2 ≥ … ≥ b n b_1≥b_2≥…≥b_n b1b2bn。最小化 s = ∑ i = 1 n ∣ a i − b i ∣ s=∑^n_{i=1}|a_i−b_i| s=i=1naibi。只需要求出这个最小值 s s s

    输入格式:
    第一行包含一个整数 N N N
    接下来 N N N行,每行包含一个整数 A i A_i Ai

    输出格式:
    输出一个整数,表示最小 S S S值。

    数据范围:
    1 ≤ N ≤ 2000 1≤N≤2000 1N2000
    0 ≤ A i ≤ 1 0 6 0≤A_i≤10^6 0Ai106

    只考虑非降情形,可以证明最优解是存在的,这个较为显然,因为 S S S必然是非负整数,而任意自然数集合一定是有最小值的。

    法1:这个问题需要用到Slope Trick的技巧。先给几个定义:
    考虑一元实值函数 f ( x ) f(x) f(x),如果 f f f满足以下三条,我们就称其Slope Trickable:
    1、 f ( x ) f(x) f(x)连续;
    2、 f ( x ) f(x) f(x)可以分解为有限的若干子段,使得每一段里导数 f ′ ( x ) f'(x) f(x)恒等于一个整数;
    3、 f ( x ) f(x) f(x)下凸,即 f ′ ( x ) f'(x) f(x)单调增(严格的下凸函数定义并不等价导函数单调增,甚至可以导数不存在。这里不需要细究)。

    不妨称这种函数为“好函数”。最经典的好函数是 ∣ x − a ∣ |x-a| xa,其满足上面的三个条件。容易证明,若干个好函数之和依然是好函数。

    回到题目。设 f i ( x ) f_i(x) fi(x)是只考虑 a 1 ∼ i a_{1\sim i} a1i的情况下,并且 a i ≤ x a_i\le x aix,求得的 s s s最小值。令 f 0 ( x ) = 0 f_0(x)=0 f0(x)=0。设 g i ( x ) g_i(x) gi(x)也是只考虑 a 1 ∼ i a_{1\sim i} a1i的情况下,并且 a i = x a_i=x ai=x,求得的 s s s最小值。显然有: { f i ( x ) = min ⁡ { g i ( t ) : t ≤ x } g i ( x ) = f i − 1 ( x ) + ∣ x − a i ∣ {fi(x)=min{gi(t):tx}gi(x)=fi1(x)+|xai|

    {fi(x)=min{gi(t):tx}gi(x)=fi1(x)+xai我们证明 f i ( x ) f_i(x) fi(x)也是好函数。可以用数学归纳法。 f 0 f_0 f0是好函数。假设对于 i ≤ k − 1 i\le k-1 ik1 f i f_i fi是好函数,那么可以知道 g i g_i gi也是好函数(好函数之和依然是好函数)。只需要证明如果 g g g是好函数,则 h ( x ) = min ⁡ { g ( t ) : t ≤ x } h(x)=\min\{g(t):t\le x\} h(x)=min{g(t):tx}也是好函数。直观理解是很显然的,其实 h h h满足,当 g g g单调下降的时候, h = g h=g h=g,当 g g g开始不下降的时候, h h h把那一段开始“拉平”,即斜率拉平为 0 0 0。综上 f i f_i fi是好函数。我们定义 f i f_i fi的拐点多重集(下面简称“拐点集”),为其所有导数从左向右增加 1 1 1的点,如果某个点处导数增加 k k k,则将该点计入 k k k次。例如 ∣ x − 1 ∣ |x-1| x1∣的拐点集为 { 1 , 1 } \{1,1\} {1,1},因为在 1 1 1的位置斜率由 − 1 → 1 -1\to 1 11,变了 2 2 2,所以 1 1 1出现两次。

    s i = ∣ a 1 − b 1 ∣ + ∣ a 2 − b 2 ∣ + ⋅ ⋅ ⋅ + ∣ a i − b i ∣ s_i=|a_1−b_1|+|a_2−b_2|+···+|a_i−b_i| si=a1b1+a2b2+⋅⋅⋅+aibi,那么 s i s_i si能取到的最小值即为 t i = lim ⁡ x → + ∞ f i ( x ) t_i=\lim_{x\to +\infty}f_i(x) ti=limx+fi(x),所以我们本质上只需要每次求出 t i t_i ti即可,最后返回 s = t n s=t_n s=tn。注意到 f i − 1 f_{i-1} fi1右边充分远处导数一定为 0 0 0 g i g_i gi右边充分远处导数一定为 1 1 1。考虑 f i − 1 f_{i-1} fi1的最右边的不可导点(即 t i − 1 t_{i-1} ti1),分两种情况:
    1、如果 a i ≥ t i − 1 a_i\ge t_{i-1} aiti1,则 t i = t i − 1 t_i=t_{i-1} ti=ti1,比较显然。那么 f i f_i fi的拐点集即在 f i − 1 f_{i-1} fi1的拐点集中加上 a i a_i ai即可;
    2、如果 a i < t i − 1 a_iai<ti1,由于是加上了 ∣ x − a i ∣ |x-a_i| xai这个函数,这个时候 t i t_i ti的增加值其实就是 t i − 1 − a i t_{i-1}-a_i ti1ai。那么 f i f_i fi的拐点集中, f i − 1 f_{i-1} fi1的最大拐点实际上拐的次数会少 1 1 1(因为 ∣ x − a i ∣ |x-a_i| xai在那个点的斜率为 1 1 1),所以要将这个拐点从 f i − 1 f_{i-1} fi1拐点集中删除一次;而在 ( a i , t i − 1 ) (a_i,t_{i-1}) (ai,ti1)这个区间内的拐点,次数是不变的,因为此区间内斜率都是增加 1 1 1;然后 a i a_i ai的拐点次数增加 2 2 2 ( − ∞ , a i ) (-\infty, a_i) (,ai)里的拐点次数也是不变,因为斜率都是减 1 1 1

    所以具体算法就是用一个最大堆维护拐点集,以求 s i s_i si。代码如下:

    #include 
    #include 
    #include 
    using namespace std;
    
    const int N = 2010;
    int n, a[N];
    
    int solve() {
      int res = 0;
      priority_queue<int> heap;
      for (int i = 1; i <= n; i++) {
        heap.push(a[i]);
        if (a[i] < heap.top()) {
          res += heap.top() - a[i];
          heap.pop();
          // a[i]拐点次数需要增加2,所以这里要额外增加1次
          heap.push(a[i]);
        }
      }
      return res;
    }
    
    int main() {
      scanf("%d", &n);
      for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
      int res = solve();
      reverse(a + 1, a + 1 + n);
      printf("%d\n", min(res, solve()));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    时间复杂度 O ( n log ⁡ n ) O(n\log n) O(nlogn),空间 O ( n ) O(n) O(n)

    法2:动态规划。可以证明,存在一个最优解 b b b使得 b b b中只出现 a a a中的数字。证明参考https://blog.csdn.net/qq_46105170/article/details/126434918。设 a a a从小到大排好序之后是 a ′ a' a,设 f [ i ] [ j ] f[i][j] f[i][j]是只考虑 a 1 ∼ i a_{1\sim i} a1i的,且 b i = a j ′ b_i=a'_j bi=aj的时候,差的绝对值之和的最小值。那么可以按照 b i − 1 b_{i-1} bi1等于多少来分类,那么 b i − 1 ∈ { a 1 ′ , . . . , a i ′ } b_{i-1}\in\{a'_1,...,a'_i\} bi1{a1,...,ai},所以 f [ i ] [ j ] = min ⁡ k ≤ j { f [ i − 1 ] [ k ] } + ∣ a j − a j ′ ∣ f[i][j]=\min_{k\le j}\{f[i-1][k]\}+|a_j-a'_j| f[i][j]=kjmin{f[i1][k]}+ajaj可以用一个变量维护 f [ i − 1 ] [ . ] f[i-1][.] f[i1][.]的前缀最小值。代码如下:

    #include 
    #include 
    using namespace std;
    
    const int N = 2010, INF = 2e9;
    int n, a[N], b[N];
    int f[N][N];
    
    int work() {
      for (int i = 1; i <= n; i++) b[i] = a[i];
      sort(b + 1, b + 1 + n);
      for (int i = 1; i <= n; i++) {
        int minv = INF;
        for (int j = 1; j <= n; j++) {
          minv = min(minv, f[i - 1][j]);
          f[i][j] = minv + abs(b[j] - a[i]);
        }
      }
      int res = INF;
      for (int i = 1; i <= n; i++) res = min(res, f[n][i]);
      return res;
    }
    
    int main() {
      scanf("%d", &n);
      for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    
      int res = work();
      reverse(a + 1, a + 1 + n);
      res = min(res, work());
      printf("%d\n", res);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    时空复杂度 O ( n 2 ) O(n^2) O(n2)

  • 相关阅读:
    list部分接口模拟实现(c++)
    lambda stream流处理异常的方法/不终止stream流处理异常
    Day30_路由的query参数
    Scanner、Random、stirng
    【C++】STL容器——list类的使用指南(含代码演示)(13)
    【python学习】-列表运算(列表元素均加减乘除某个数、两个列表间的运算、遍历列表等)
    LInux系统特殊权限
    Pandas处理dataframe的文本数据列:使用str属性获取数据列的字符串方法类、contains函数判断数据列是否包含指定字符串生成布尔值序列
    C++——类和对象3|日期类型|Cout运算符重载|Cin运算符重载|const成员|
    Volatile及原理(黑马)
  • 原文地址:https://blog.csdn.net/qq_46105170/article/details/126434434