总复杂度 (O(nsqrt{n})) 的算法,跑到落谷 (rk2)。
一道题调了一年系列。
Description
给 (n) 个车的坐标 (a_i),(n) 个加油站的坐标 (b_i),(m) 次修改操作。
-
每次修改,将第 (i) 辆车的位置修改到 (x)
-
在初始情况及每次修改后:要求不重不漏给每一个车匹配一个停车场(即可以将 (a) 数组打乱顺序),最小化 (sum_{i=1}^{n} |a_i - b_i|),输出这个最小值。
Solution
比较显然的是车和加油站其实在询问的时候是对称的,具有对称美。
暴力思想
这种最小化曼哈顿距离和的问题我们比较熟悉的做法是把 (a, b) 都暴力 ( ext{sort}) 一遍,对位相减,这里用邻项交换的放法给出一个理性的证明:
设 (a, b (a le b)) 作为两个车坐标 ,(c, d (c le d)) 作为两个加油站的坐标,即证明 (a Rightarrow c, b Rightarrow d) 第一种匹配方式的答案不会比第二种匹配方式 (a Rightarrow d, b Rightarrow c) 差,这样见到一个无序数组我们将其排序答案不会变差,即完成证明。
-
当 (a le b le c le d) 时:
- 第一种方式代价:(c - a + d - b = -a-b+c+d)
- 第二种方式代价:(d - a + c - b = -a-b+c+d)
- 这种情况下两者代价相等
-
当 (a le c le b le d) 时:
- 第一种方式代价:(c - a + d - b = -a-b+c+d)
- 第二种方式代价:(d - a + b - c = -a+b-c+d)
- 由 (c le b) 可知 (-b+c le 0 le b - c),故前者一定不差于后者。
-
当 (c le d le a le b) 时:
- 第一种方式代价:(a - c + b - d = a + b - c - d)
- 第二种方式代价:(a - d + b - c = a + b - c - d)
- 这种情况下两者代价相等
-
当 (c le a le d le b) 时:
- 第一种方式代价:(a - c + b - d = a + b - c - d)
- 第二种方式代价:(d - a + b - c = - a + b - c + d)
- 由 (a le d) 可知 (a - d le 0 le -a +d),故前者一定不差于后者。
已经不是简要证明了,以后还是记住这个性质吧。
感性理解一下,当车与加油站没有交叉时,怎么搞都无所谓,因为总要从一个车出发扑通扑通跑到一个停车场,而坐标是定值所以相等。当有交叉时,抽象的理解为一个车向右走(或者车库向右是对称问题),遇到的第一个停车场就停下来,否则如果继续跑,但总有一个车要匹配这个停车场,所以如果右边的车匹配过来就不是最优了。(感觉还是图比较好理解,但是我懒得画了)。
暴力每次修改要 ( ext{sort}) 一次,所以复杂度是 (O(nq)) 的,需要优化。
优化
考虑对于一次修改,原来位置和现在位置所形成的区间内的车都循环移位了,如果要用数据结构维护 (|a[i] - b[i]|),似乎不太可能,因为是对位相减取绝对值,不符合结合律,也没法用分块之类的毒瘤数据搞(就是说对于每一组数据都要看他的正负性然后取值,但是呢一一看就是暴力了。。。)。
算贡献!
考虑换一种统计答案的方式:算贡献。用每条边的贡献(经过这条边的车的数量)来进行计算答案。
将数据离散化(设该数组为 (d),设 (W_i = d_{i + 1} - d_i),可理解为从 (i) 走到 (i + 1) 的实际距离 )后,考虑先处理两个:(SumA_i, SumB_i),分别表示在 ([1, i]) 位置的区间内用多少个车 (/) 停车场(即前缀和数组),令 (S_i = |SumA_i - SumB_i|),答案就是 (sum S_i W_i),或者说 (sum |(SumA_i - SumB_i) imes W_i|)。
怎么理解这个东西呢?(S_i) 的实际意义是走 (i Rightarrow i + 1) 这条边的车的数量。感性理解一下,有 (min(SumA_i,SumB_i)) 已经在 (i) 位置之前完成了互相匹配,但是存在 (|SumA_i - SumB_i|) 在之前无车 / 加油站匹配,所以一定要经过这条边,到 (i) 位置之后寻找匹配。
经过这部转化,考虑修改一次的影响(设原位置为 (x) ,新位置为 (y)):
- 若 (x < y),即给 (SumA) 的 ([x, y - 1]) 区间带来 (-1) 偏移量
- 若 (x > y) 即给 (SumA) 的 ([y, x - 1]) 区间带来 (+1) 的偏移量
那么问题转化为了一个数据结构,支持:
- (A) 数组区间加和修改
- 对应位置询问 (|A_i-B_i|)
不会做。不能用线段树维护的原因在于,每次加入一个数时,你无法区分每个数的数值 (ge 0) 还是 (< 0),你又没发把数组排序做到有序然后可以二分这个东西。(即没有区间零界限的能力,因为数值是不断增加的)。。
经过上面的思考加上 (n, q le 50000) 的提示,应该这是个分块。秉承大块朴素,小块暴力的思想,考虑在存在 ( ext{tag}) 标记的情况下,如何快速找到 (0) 界点呢?我们有两个思路:
- 修改的时候,大块打 ( ext{tag}),小块按照下方进行暴力重构。每个块里面预处理按原值 (A_i - B_i), ( ext{sort}) 一下,之后查询二分 (0) 的位置,前面取反,后面不变,相加即可。这也是目前大多题解的做法,时间复杂度 (O(nsqrt{n} log n))
- 修改的时候,大块打 ( ext{tag}),小块按照下方进行暴力重构。每个块里,把值域当下标搞一个桶
由于大多数题解都是第一种写法这里也就说说 & 写写第二种方法。
把 ((SumA_i - SumB_i)) 当做下标,值 ((SumA_i - SumB_i) imes W_i) 打进桶里,把桶再搞个前缀和,即 (cnt_i) 表示 (le i) 的数的和 。同理,把边权单独搞出来弄前缀和形成 (g_i)(这个东西为了计算 ( ext{tag}) 的应先规定)。询问枚举每个块,设当前的 ( ext{tag}) 标记为 (x),答案即 (-cnt[-x] - x imes g[-x] + (cnt[INF] - cnt[x]) + x imes (g[INF] - G[-x]))((0) 前面取反,两部分加和,在各自记录。
但是由于这题内存紧,所以开不下 (nsqrt{n}) 的数组大小,不妨考虑每次重构块时的有值域的区间平移一下,用 ( ext{vector}) 搞即可,有一种感性的理解,这样搞总空间是 (O(n)) 的。因为考虑只有一个位置加入车 / 停车场,值域才会 (+1/-1),而全数组最多加减 (n) 次,所以总空间 (O(n))。
时空复杂度
(O(nsqrt{n})) 。
Tips
-
这题中间出现的位置可能之前没有导致离散化找不到,所以可以离线先读进来离散化,再搞搞。
-
由于有负数的值,搞桶的时候搞个偏移量。
#include <iostream>
#include <cstdio>
#include <cmath>
#include <vector>
#include <algorithm>
#define rint register int
using namespace std;
typedef long long LL;
const int N = 50005, S = 390;
int n, Q, t, a[N], b[N], d[N * 3], tot;
int pos[N * 3], L[S], R[S], len[S], Min[S], Max[S], tag[S], sum[N * 3];
// sum[i] = sumA[i] - sumB[i]
vector<LL> cnt[S];
vector<int> g[S];
struct Q{
int i, x;
} q[N];
int inline get(int x) {
return lower_bound(d + 1, d + 1 + tot, x) - d;
}
// 建立 / 重构块为 i 的 id
void inline build(int id) {
Min[id] = 2e9, Max[id] = 0;
for (rint i = L[id]; i <= R[id]; i++) Min[id] = min(Min[id], sum[i]), Max[id] = max(Max[id], sum[i]);
len[id] = Max[id] - Min[id] + 1;
cnt[id].clear(); g[id].clear();
cnt[id].resize(len[id], 0); g[id].resize(len[id], 0);
for (rint i = L[id]; i <= R[id]; i++) {
int v = sum[i] - Min[id];
cnt[id][v] += (LL)sum[i] * (d[i + 1] - d[i]);
g[id][v] += d[i + 1] - d[i];
}
for (rint i = 1; i < len[id]; i++)
cnt[id][i] += cnt[id][i - 1], g[id][i] += g[id][i - 1];
}
// 修改操作
void inline change(int l, int r, int k) {
if (pos[l] == pos[r]) {
for (rint i = l; i <= r; i++) sum[i] += k;
build(pos[l]);
return;
}
int p = pos[l], q = pos[r];
for (rint i = p + 1; i < q; i++) tag[i] += k;
for (rint i = l; i <= R[p]; i++) sum[i] += k;
for (rint i = L[q]; i <= r; i++) sum[i] += k;
build(p); build(q);
}
// 查询操作
LL inline calc() {
LL res = 0;
for (rint i = 1; i <= t; i++) {
int x = min(len[i] - 1, max(-1, - Min[i] - tag[i]));
if (x != -1) res += abs(cnt[i][x] + (LL)tag[i] * g[i][x]);
if (x != -1) res += abs((cnt[i][len[i] - 1] - cnt[i][x]) + (LL)tag[i] * (g[i][len[i] - 1] - g[i][x]));
else res += abs(cnt[i][len[i] - 1] + (LL)tag[i] * g[i][len[i] - 1]);
}
return res;
}
int main() {
// 读入 + 离散化
scanf("%d", &n);
for (rint i = 1; i <= n; i++) scanf("%d", a + i), d[++tot] = a[i];
for (rint i = 1; i <= n; i++) scanf("%d", b + i), d[++tot] = b[i];
scanf("%d", &Q);
for (rint i = 1; i <= Q; i++)
scanf("%d%d", &q[i].i, &q[i].x), d[++tot] = q[i].x;
sort(d + 1, d + 1 + tot);
tot = unique(d + 1, d + 1 + tot) - d - 1;
d[tot + 1] = d[tot];
for (rint i = 1; i <= n; i++) sum[a[i] = get(a[i])]++, sum[b[i] = get(b[i])]--;
for (rint i = 2; i <= tot; i++) sum[i] += sum[i - 1];
// 分块
t = sqrt(tot);
for (rint i = 1; i <= t; i++) L[i] = (i - 1) * t + 1, R[i] = i * t;
if (R[t] < tot) R[t] = tot;
for (rint i = 1; i <= t; i++) {
for (rint j = L[i]; j <= R[i]; j++) pos[j] = i;
build(i);
}
printf("%lld
", calc());
for (rint i = 1; i <= Q; i++) {
int id = q[i].i, x = a[id], y = get(q[i].x);
if (x < y) change(x, y - 1, -1);
else if (x > y) change(y, x - 1, 1);
a[id] = y;
printf("%lld
", calc());
}
return 0;
}