Distinctification 题解

完全凭自己想出来的,可喜可贺,记录一下。

不过数组没开二倍空间虚空调试了三个小时。

按照自己做题时的思路讲一遍,我认为这样会更加清晰。


首先通过手玩样例会发现:刚开始的时候这些数对互不相干,不断地加入数对会让原本互不影响的数对“连通”起来。

这里的“连通”的意思是,这些数通过一系列操作后,达到题目要求的状态时(也就是 $a_{i}$ 互不相同),它们的 $a_{i}$ 是连续的,为什么是连续的会在下面讲到。

这一点启示我们每次加入一个数对只用对其所在的“连通块”考虑。

那么哪些数对会互相影响呢?

有一点需要注意的是:通过题目给的操作改变 $a_{i}$ 后,每个“连通块”的最小值一定不变。

因为想要对一个数做减法的话一定得有一个正好比它小 $1$ 的数,如果没有就减不了了,所以最小值一定。

那么最小值确定了过后,最大值也会随之确定。不妨把最小值称为 $mn$ ,“连通块”中的数的个数为 $cnt$,那么最大值即为 $mn + cnt - 1$。

这一点可以用数学归纳法去证明,在一个最小值为 $mn$,元素个数为 $cnt$ 的“连通块”里加入一个新的数时,这个“连通块”的最大值最多会增加 $1$。

因为如果这个数能加进去的话一定是和原本的“连通块”里的某个数相等或是正好大 $1$,相等的情况可以通过第一种操作使其加 $1$,最后最多会比原本的最大值大 $1$,再大的话就和“能够加入这个‘连通块’的假设矛盾了”。而连通块只有一个数的时候最大值显然就是 $mn = mn + cnt - 1$。

考虑题目要求,要让所有的 $a_{i}$ 互不相同,那么一个连通块里最小值为 $mn$,最大值为 $mn + cnt - 1$ 并且正好有 $mn$ 个数,那么最后的状态肯定是 $mn \sim mn + cnt - 1$ 每个数出现一次。

接下来就是如何计算一个“连通块”里的答案。通过操作描述可知一个数对的代价只会与它加入时的 $a_{i}$ 和结束时的 $a_{i}$ 相关,因为 $+1$ 和 $-1$ 的代价可以正好抵消。

那么令一个数对 $(a_{i}, b_{i})$ 的最终状态为 $(c_{i}, b_{i})$,那么它的代价就为 $(c_{i} - a_{i})b_{i}$。

那么是不是我们可以任意安排“连通块”内的 $c_{i}$ 呢?会不会出现在将 $a_{i}$ 不断 $-1$ 或 $+1$ 的途中无法进行操作了呢?

不会,因为对于一个“连通块”一定可以通过操作使其中的 $a_{i}$ 在 $mn \sim mn + cnt - 1$ 中每个数恰好出现一次,接着基于这种状态进行操作,第一步肯定是将某个 $a_{i} - 1$,这样做了之后一定会出现两个 $a_{i} - 1$,再把另一个 $a_{i} - 1$ 加上 $1$ 仍然会保持合法状态。这样的过程类似于冒泡,所以通过若干次交换后一定可以达到我们希望的状态。

好了,现在我们来安排每个数对的 $c_{i}$。我们可以先将上面的贡献分成两部分 $c_{i}b_{i}$ 和 $-a_{i}b_{i}$,后面一部分是确定的,而我们想要代价最小,那么应该给较大的 $b_{i}$ 安排较小的 $c_{i}$,这个贪心策略的正确性是显然的。

也就是说,一个连通块最终的状态一定会是:$(mn, b_{1}), (mn + 1, b_{2}), (mn + 2, b_{3}) \cdots (mn + cnt - 1, b_{cnt})$,其中 $b_{1} \sim b_{cnt}$ 单调递减。

它的代价也就是 $\sum\limits_{i = 1}^{cnt}(mn + i - 1 - a_{i})b_{i}$。

考虑把它分成 $mn \sum\limits_{i = 1}^{cnt}b_{i}$,$\sum\limits_{i = 1}^{cnt}ib_{i}$ 和 $-(a + 1)\sum\limits_{i = 1}^{cnt}b_{i}$ 这三部分计算,最后一部分可以在加入的时候计算,所以我们只考虑前面两部分。

这一点本质上是要求我们维护一个“连通块”的权值和,最小权值,以及“权值乘上排名”的和。最难计算的肯定就是这个带有“排名”的部分了。

考虑使用权值线段树来优化,我们在加入一个 $b_{i}$ 的同时可以在线段树上二分得到它的排名,具体的做法是每次进入左儿子的时候加上右儿子的节点个数。

注意,我们在求得一个数的排名后计算了 $b_{i}rank_{i}$ 这一代价,但是因为这个数会使其它的数的排名发生改变,也要把改变的代价一并计算。具体地,它会使原本排名大于等于 $rank$ 的数的排名增加 $1$,也就是说可以记录一个 $sum$,在线段树上二分时每进入右儿子就将 $sum$ 加上左儿子的权值和,最后的 $sum$ 就是排名增加了 $1$ 的元素的权值和。

但是还有一点没有解决:在两个“连通块”合并的时候怎么计算新的代价?

先看什么时候两个“连通块”会合并,不妨将两个“连通块”标号为 $x$ 和 $y$,那么合并的条件就是 $mn_{x} + cnt_{x} = mn_{y}$ 或 $mn_{y} + cnt_{y} = mn_{x}$,直观地讲,就是两个“连通块”的最值相邻的时候可以合并,这个时候可以通过第二个操作来改变连通块里的 $c_{i}$,同时也符合“连通块”形成的条件。

而合并的时候改变的依然是“排名”这一属性,同样地,可以使用线段树合并,并且在合并的时候记录大于当前数的数的个数,它的排名改变的量就是这么大。

对于“连通块”的维护,可以使用并查集,每次加入一个数的时候将这个“连通块”和下一个“连通块”合并即可。

说起来很复杂,但只需要明白我们需要维护“权值和”,“权值与排名的乘积和”以及“最小权值”即可。

代码很短也很清晰,不算注释只有不到 1.5k。

要注意虽然 $a_{i}$ 不超过 $2 \times 10^{5}$,但是 $mn + cnt - 1$ 可能超过 $2 \times 10^{5}$,所以要开二倍空间!

时间复杂度 $\mathcal{O}(n \log n)$,线段树合并的常数可能有点大但是可以通过此题。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int n, a, b, x, y, idx, father[400005], rt[400005], root[400005], lc[20000005], rc[20000005], cnt[20000005];
ll ans, sum[20000005];
//sum表示一段区间的权值和,cnt表示一段区间的元素个数
#define mid ((l + r) >> 1)
void push_up(int k) {
    cnt[k] = cnt[lc[k]] + cnt[rc[k]];
    sum[k] = sum[lc[k]] + sum[rc[k]];
}
int merge(int p, int q, ll pct = 0, ll qct = 0) {
    if(!p || !q) {//
        ans += qct * sum[p] + pct * sum[q];
        //计算排名更改引起的代价变化
        return p | q;
    }
    //因为题目限制了b_i互不相等所以不用考虑l=r时怎么合并
    lc[p] = merge(lc[p], lc[q], pct + cnt[rc[p]], qct + cnt[rc[q]]);//只有在进入做儿子的时候排名才会增加
    rc[p] = merge(rc[p], rc[q], pct, qct);
    push_up(p);
    return p;
}
void change(int& k, const int& pos, int l = 1, int r = n, ll ct = 0, ll sm = 0) {
    if(!k) k = ++idx;
    if(l == r) {
        cnt[k] = 1, sum[k] = pos;
        ans += (ct + 1) * pos + sm;
        //ct是比当前数大的个数,它的排名还得+1
        return;
    }
    if(pos <= mid) change(lc[k], pos, l, mid, ct + cnt[rc[k]], sm);
    else change(rc[k], pos, mid + 1, r, ct, sm + sum[lc[k]]);
    push_up(k);
}
int findset(int x) {return father[x] == x ? x : father[x] = findset(father[x]);}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n;
    for(int i = 1; i <= 400001; ++i) father[i] = rt[i] = i;
    for(int i = 1; i <= n; ++i) {
        cin >> a >> b;
        x = findset(a);
        y = rt[x] + 1;
        //x,y是两个待合并的连通块
        ans -= x * sum[root[x]] + y * sum[root[y]] + (a + 1ll) * b;//先把原来的代价减掉,后面再加上
        change(root[x], b);
        root[x] = merge(root[x], root[y]);
        father[y] = x, rt[x] = rt[y];
        ans += x * sum[root[x]];
        cout << ans << '\n';
    }
    return 0;
}
posted @ 2023-12-22 20:41  A_box_of_yogurt  阅读(1)  评论(0编辑  收藏  举报  来源
Document