📌  相关文章
📜  在数组上执行给定操作后,S模M的值的最大计数位于[L,R]范围内(1)

📅  最后修改于: 2023-12-03 15:37:42.915000             🧑  作者: Mango

在数组上执行给定操作后,S模M的值的最大计数位于[L,R]范围内

简介

这个问题的意思是:给定一个长度为$n$的整数数组$A$,和一个正整数$M$。然后可以执行若干次以下操作:

  • 将一个元素$A_i$加上$1$;
  • 将一个元素$A_i$减去$1$。

执行完操作后,问$S=\sum_{i=1}^{n}A_i \bmod M$的值最大值出现在数组$A$的$[L,R]$范围内的方案。

这是一道较为典型的区间操作问题,可以使用前缀和和贪心策略来解决。

算法
  1. 前缀和

首先我们需要快速求出$S$的值。注意到$S=\sum_{i=1}^{n}A_i \bmod M=\sum_{i=1}^{n}(A_i \mod M)$。那么只需要求出每个数模$M$的值即可,这个可以通过前缀和来计算,复杂度为$O(n)$。

  1. 对于答案的分析

假设容易证明一下答案有如下性质:最有策略中,每次变化都是选数组$L$和$R$中最大值和次大值,将最大值减$1$,次大值加$1$,直到最大值小于次大值。

这个可以用反证法证明。如果最优策略中某个时刻存在一种操作将值$x$增加一个数,但它不是采取选最大值减少一个的方式,那么我们考虑将其换成选最大值减少一个的方式。我们令原来被操作的数为$A,B$,新操作的数为$C$。显然,$A,B,C$的大小关系只有如下$8$种情况:

$$ \begin{aligned} A<B<C\ A<C<B\ B<A<C\ B<C<A\ C<A<B\ C<B<A\ A=B<C\ B=C<A \end{aligned} $$

第$7$和第$8$种情况不合理,因为这样不会增加$S$的值。而第$1$,$2$,$3$,$4$,$5$和$6$种情况都可以通过选最大值减少一个的方式得到更优解。如下表所示:

| 操作前的大小关系 | 操作后的大小关系 | 操作后的结果 | | :---------------: | :---------------: | :----------: | | $A<B<C$ | $A-1=B+1<C$ | $S$不变 | | $A<C<B$ | $A+C-1<B+1<C$ | $S+C-A$ | | $B<A<C$ | $C-1<A+1<B$ | $S$不变 | | $B<C<A$ | $B+1<C-1<A$ | $S$不变 | | $C<A<B$ | $C-1<A+1<B$ | $S$不变 | | $C<B<A$ | $B+1<C-1<A$ | $S$不变 | | $A=B<C$ | $A+C-1=B+1<C$ | $S$不变 | | $B=C<A$ | $C-1<A+1=B$ | $S$不变 |

由于所有的情况都可以通过选最大值减少一个的方式得到更优解,所以该性质成立。

  1. 贪心

有了上面的性质,我们就可以得到一种$O(n)$的贪心算法。我们可以用一个双端队列来维护当前最大值和次大值以及它们在数组中的位置。然后我们从$[L,R]$这个区间的最左端开始扫描,每个数都维护一个相应的前缀和并且加入双端队列中。如果队列中存在不小于当前数的最大值,我们就将这些值不停地与当前数做操作,直到最大值小于当前值而且队列非空。对于每次操作,我们需要将对应的数字右移$1$位,这相当于将其除以$2$,也同时维护了前缀和。然后我们将当前数加入队列尾,如果队列的长度超过二,我们就将队列头部的数从队列中删除。

假设队列中既有最大值又有次大值,那么我们可以实现如下两个操作,对于操作中寻找的数,它一定在前面的位置:

  • 给最大值减$1$,给次大值加$1$;
  • 给队列头部的值减$1$,给最大值加$1$。

注意到题目约束了最大值和次大值必须在数组的$[L,R]$范围内,所以还要加一些额外的保护,具体见实现中的注释。最后,遍历完整个数组后,我们就可以得到最大的$S$值。

代码

下面给出一个使用C++语言实现的伪代码,其中关键代码使用了deque作为双端队列,时间复杂度满足$O(n)$的要求。

int S = 0;
deque<pair<long long, int>> q;
for (int i = L; i <= R; i++) {
    S += A[i];
    while (q.size() >= 2 && q[q.size() - 2].first >= A[i]) {
        int pos = q[q.size() - 2].second;
        int delta = q[q.size() - 1].first - A[i];
        if (pos < L) delta = 0;
        if (q[q.size() - 2].second >= L && q[q.size() - 2].second <= R) {
            S += q[q.size() - 2].first / 2 - A[pos] / 2;
            A[pos] = q[q.size() - 2].first / 2;
        }
        A[i] += delta;
        q.pop_back();
        if (pos >= L && pos <= R) {
            q[q.size() - 1].first = A[pos];
            break;
        }
    }
    if (q.empty() || q[q.size() - 1].first != A[i]) {
        q.push_back(make_pair(A[i], i));
        if (q.size() == 3) {
            if (q[0].second >= L && q[0].second <= R) {
                S += q[0].first / 2;
                A[q[0].second] = q[0].first / 2;
            }
            q.pop_front();
        }
    }
}
while (q.size() >= 2) {
    int pos = q[q.size() - 2].second;
    int delta = q[q.size() - 1].first - 0;
    if (pos < L) delta = 0;
    if (q[q.size() - 2].second >= L && q[q.size() - 2].second <= R) {
        S += q[q.size() - 2].first / 2 - A[pos] / 2;
        A[pos] = q[q.size() - 2].first / 2;
    }
    A[q[q.size() - 1].second] += delta;
    q.pop_back();
}
int tmpS = 0;
for (int i = L; i <= R; i++) tmpS += A[i] % M;
S = (S % M + M) % M;
tmpS = (tmpS % M + M) % M;
if (tmpS > S) S = tmpS;
return S;
总结

这题最难的地方在于对于最优策略的理解,一旦理解了这个之后实现还是比较简单的。这个题目的思路可以借鉴到很多别的区间操作问题中去。