总复杂度 \(O(n\sqrt{n})\) 的算法,跑到落谷 \(rk2\)。
一道题调了一年系列。
Description
给 \(n\) 个车的坐标 \(a_i\),\(n\) 个加油站的坐标 \(b_i\),\(m\) 次修改操作。
-
每次修改,将第 \(i\) 辆车的位置修改到 \(x\)
-
在初始情况及每次修改后:要求不重不漏给每一个车匹配一个停车场(即可以将 \(a\) 数组打乱顺序),最小化 \(\sum_{i=1}^{n} |a_i - b_i|\),输出这个最小值。
Solution
比较显然的是车和加油站其实在询问的时候是对称的,具有对称美。
暴力思想
这种最小化曼哈顿距离和的问题我们比较熟悉的做法是把 \(a, b\) 都暴力 \(\text{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\),故前者一定不差于后者。
已经不是简要证明了,以后还是记住这个性质吧。
感性理解一下,当车与加油站没有交叉时,怎么搞都无所谓,因为总要从一个车出发扑通扑通跑到一个停车场,而坐标是定值所以相等。当有交叉时,抽象的理解为一个车向右走(或者车库向右是对称问题),遇到的第一个停车场就停下来,否则如果继续跑,但总有一个车要匹配这个停车场,所以如果右边的车匹配过来就不是最优了。(感觉还是图比较好理解,但是我懒得画了)。
暴力每次修改要 \(\text{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) \times 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\) 的提示,应该这是个分块。秉承大块朴素,小块暴力的思想,考虑在存在 \(\text{tag}\) 标记的情况下,如何快速找到 \(0\) 界点呢?我们有两个思路:
- 修改的时候,大块打 \(\text{tag}\),小块按照下方进行暴力重构。每个块里面预处理按原值 \(A_i - B_i\), \(\text{sort}\) 一下,之后查询二分 \(0\) 的位置,前面取反,后面不变,相加即可。这也是目前大多题解的做法,时间复杂度 \(O(n\sqrt{n} \log n)\)
- 修改的时候,大块打 \(\text{tag}\),小块按照下方进行暴力重构。每个块里,把值域当下标搞一个桶
由于大多数题解都是第一种写法这里也就说说 & 写写第二种方法。
把 \((SumA_i - SumB_i)\) 当做下标,值 \((SumA_i - SumB_i) \times W_i\) 打进桶里,把桶再搞个前缀和,即 \(cnt_i\) 表示 \(\le i\) 的数的和 。同理,把边权单独搞出来弄前缀和形成 \(g_i\)(这个东西为了计算 \(\text{tag}\) 的应先规定)。询问枚举每个块,设当前的 \(\text{tag}\) 标记为 \(x\),答案即 \(-cnt[-x] - x \times g[-x] + (cnt[INF] - cnt[x]) + x \times (g[INF] - G[-x])\)(\(0\) 前面取反,两部分加和,在各自记录。
但是由于这题内存紧,所以开不下 \(n\sqrt{n}\) 的数组大小,不妨考虑每次重构块时的有值域的区间平移一下,用 \(\text{vector}\) 搞即可,有一种感性的理解,这样搞总空间是 \(O(n)\) 的。因为考虑只有一个位置加入车 / 停车场,值域才会 \(+1/-1\),而全数组最多加减 \(n\) 次,所以总空间 \(O(n)\)。
时空复杂度
\(O(n\sqrt{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\n", 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\n", calc());
}
return 0;
}