• BJOI2017 开车


    总复杂度 (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) 差,这样见到一个无序数组我们将其排序答案不会变差,即完成证明。

    1. (a le b le c le d) 时:

      • 第一种方式代价:(c - a + d - b = -a-b+c+d)
      • 第二种方式代价:(d - a + c - b = -a-b+c+d)
      • 这种情况下两者代价相等
    2. (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),故前者一定不差于后者。
    3. (c le d le a le b) 时:

      • 第一种方式代价:(a - c + b - d = a + b - c - d)
      • 第二种方式代价:(a - d + b - c = a + b - c - d)
      • 这种情况下两者代价相等
    4. (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;
    }
    
  • 相关阅读:
    IntelliJ破解
    IDEA的配置
    已解决No compiler is provided in this environment. Perhaps you are running on a JRE rather than a JDK?
    逆向工程,调试Hello World !程序(更新中)
    一种离谱到极致的页面侧边栏效果探究
    前端H5如何实现分享截图
    我女儿说要看雪,但是我家在南方,于是我默默的拿起了键盘,下雪咯。
    Web基本教程~05.CSS属性
    送你一朵小红花,CSS实现一朵旋转的小红花
    Vue 项目性能优化 —实战—面试
  • 原文地址:https://www.cnblogs.com/dmoransky/p/12578757.html
Copyright © 2020-2023  润新知