题解 [THUPC2017]天天爱射击

题目链接 (洛谷)

题意简述

在一维空间内给定 \(n\) 块木板(区间)和 \(m\) 颗子弹(点)的位置,并给出每块木板需要被多少颗子弹打穿才会破碎。

每颗子弹能贯穿所有挡住了其位置的木板,问每颗子弹能打碎几颗木板。\(( n, m \le 200000 )\)

分析

一眼开出数点问题

维度:一维空间 + 自然的时间维

将时间维离线下来后,本题可抽象描述成:

给定 \(m\) 个二维点 \((x_i, y_i)\)\(n\) 个区间 \([l_i, r_i]\), 求每个二维点满足几个区间使得 \(l_i \le x_i \le r_i\)\(y_i\) 为所有在该区间内的点中第 \(k_i\) 小的 \(y\)\(k_i\) 就是该木板的耐久)

\(x\) 为位置,\(y_i\) 表示第 \(i\) 颗子弹是第 \(y_i\) 个输入的(也就是说 \(i\) 是第 \(y_i\) 颗打出去的子弹)

题目虽然问的是每个子弹能打碎多少个木板,但很显然这个问题是长在木板上的,因为 \(k_i\) 是木板的属性,所以我们应该考虑求出每个木板是被谁打碎的

(当然,只要稍微学过一点点 DS 的看完上面的问题转换应该都能发现这就是个裸的区间 k 小

到这里问题就解决了

解法

update:笔者写这篇博客的时候水平太菜了,整体二分应该能做到 \(O(n \log n)\)

不带修区间k小可以做到 \(O(n \log n)\) ( 好像还有 \(O(\frac{n \log n}{\log \log n})\) 的做法 ) ,经 (wo) 典 (zhi) 解 (hui) 法 (de) 是用主席树建立前缀线段树,查询时在两个前缀线段树上同时二分

由于本题数据范围较小,这里再推荐两种更好写的解法

1.整体二分 \(O(n \log^2 n)\)
2.莫队 + 简单的根号平衡 \(O(n\sqrt{n})\)

代码 (整体二分)

#include<bits/stdc++.h>
#define lowbit(x) x&-x
using namespace std;

const int N = 200010, inf = 1 << 29;

int n, m, cnt, boa_l[N], boa_r[N], boa_s[N], ans[N], tree[N];

struct bullet{
    int pos, num;
    bool operator < (const bullet a)const{
        return pos < a.pos;
    }
}bul[N];

struct question{
    int l, r, k, opt;
    /*
    opt = 1为插入操作(将原数组理解为插入)在l处插入一个值为r的点
    opt = 2为询问
    */
}q[4 * N], q1[4 * N], q2[4 * N];

void add(int pos, int val) { for(; pos < N; pos += lowbit(pos)) tree[pos] += val; }

int que(int pos)
{
    int res = 0;
    for(; pos; pos -= lowbit(pos)) res += tree[pos];
    return res;
}

void solve(int l, int r, int L, int R)
{
    if(l > r || L > R) return ;
    if(l == r){
        for(int i = L; i <= R; i++) if(q[i].opt == 2) ans[l]++;
        return ;
    }
    int mid = (l + r) >> 1, cnt1 = 0, cnt2 = 0;
    for(int i = L; i <= R; i++){
        if(q[i].opt == 1){
            if(q[i].r <= mid){
                add(q[i].l, 1);
                q1[++cnt1] = q[i];
            }
            else q2[++cnt2] = q[i];
        }
        else{
            int res = que(q[i].r) - que(q[i].l - 1);
            if(res >= q[i].k) q1[++cnt1] = q[i];
            else q[i].k -= res, q2[++cnt2] = q[i];
        }
    }

    for(int i = 1; i <= cnt1; i++) if(q1[i].opt == 1) add(q1[i].l, -1);
    for(int i = 1; i <= cnt1; i++) q[L + i - 1] = q1[i];
    for(int i = 1; i <= cnt2; i++) q[L + cnt1 + i - 1] = q2[i];
    solve(l, mid, L, L + cnt1 - 1);
    solve(mid + 1, r, L + cnt1, R);
}

inline int read()
{
    int x = 0, f = 1;
    char c = getchar();
    while(c < '0' || c > '9') { if(c == '-') f = -1; c = getchar(); }
    while(c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}

int main()
{
    n = read(), m = read();
    for(int i = 1; i <= n; i++) boa_l[i] = read(), boa_r[i] = read(), boa_s[i] = read();
    for(int i = 1; i <= m; i++) bul[i].pos = read(), bul[i].num = i;
    bul[m + 1] = (bullet){inf, m + 1};

    sort(bul + 1, bul + m + 1);

    for(int i = 1; i <= m; i++) q[++cnt] = (question){i, bul[i].num, 0, 1};
    for(int i = 1; i <= n; i++){
        int ql = lower_bound(bul + 1, bul + m + 2, (bullet){boa_l[i], 0}) - bul,
            qr = upper_bound(bul + 1, bul + m + 2, (bullet){boa_r[i], 0}) - bul - 1;
	    //处理并生成新的区间

        if(qr - ql + 1 >= boa_s[i]) q[++cnt] = (question){ql, qr, boa_s[i], 2};
    }
    solve(1, N, 1, cnt);
    for(int i = 1; i <= m; i++) printf("%d\n", ans[i]);
    return 0;
}

简单的总结

1.数点问题的通用解法就是数维度,在思考的时候不妨将全部维度都离线下来思考
2.k 大,k 小这类问题变换成时间后会非常的诡异,如本题的"木板耐久为 k"
(如果这题空间也是二维的话我可能还真就想不出来了
3.如果输出数字的总和加起来并不大(如本题的 \(\le n\)),说不定这题每个输出数字的每个 1 都要单独计算

posted @ 2021-05-17 23:46  sgweo8ys  阅读(175)  评论(0)    收藏  举报