数据结构总结

本文详细介绍了线段树的基本思想、模板及其应用,包括区间和修改、区间和查询、区间乘修改、区间加修改与区间最小值维护。此外,还探讨了分块算法,包括其思想、常见操作以及解决区间覆盖问题的例子。文章最后提到了主席树和莫队算法,强调了它们在离线区间查询问题中的作用。
摘要由CSDN通过智能技术生成

前言

  1. acm快退了,把写过的模板总结整理一下(总结在一起)。

线段树

前言碎碎念

  1. 之前学了一些东西,笔记写的还挺认真,可惜放在了电脑桌面上(基本不可能翻的那种,而且很多还给删了)emm。
  2. 慢慢总结回来吧,
  3. 竟然错在了这里:pushdown里面没有更新tag[p<<1]tag[p<<1|1]。。。——2021.10.15
    1. 还是要保持好状态,+熟练度的。

算法基本思想

  1. 挺熟悉的,略了。

模板(基于区间和修改&区间和查询)

  1. 这是我历来用的模板
#include <bits/stdc++.h>
// #define int long long
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 1e5 + 10;
int sum[N << 3], a[N << 3], tag[N << 3];
void pushup(int p) { sum[p] = sum[p << 1] + sum[p << 1 | 1]; }
void build(int p, int l, int r) {
    if (l == r) {
        sum[p] = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
    pushup(p);
}
void pushdown(int p, int l, int r) {
    int mid = (l + r) >> 1;
    sum[p << 1] += (mid - l + 1) * tag[p], tag[p << 1] += tag[p];
    sum[p << 1 | 1] += (r - (mid + 1) + 1) * tag[p], tag[p << 1 | 1] += tag[p];
    tag[p] = 0;
}
void update(int p, int l, int r, int x, int y, int k) {
    if (x <= l && r <= y) {
        sum[p] += (r - l + 1) * k, tag[p] += k;
        return;
    }
    pushdown(p, l, r);
    int mid = (l + r) >> 1;
    //有相交部分就搜索
    if (mid >= x) update(p << 1, l, mid, x, y, k);
    if (mid + 1 <= y) update(p << 1 | 1, mid + 1, r, x, y, k);
    pushup(p);
}
int query(int p, int l, int r, int x, int y) {
    if (x <= l && r <= y) return sum[p];
    pushdown(p, l, r);
    int res = 0;
    int mid = (l + r) >> 1;
    if (mid >= x) res += query(p << 1, l, mid, x, y);
    if (mid + 1 <= y) res += query(p << 1 | 1, mid + 1, r, x, y);
    return res;
}
signed main() { return 0; }

模板题

区间乘修改+单点查询

  1. 题目AQ酱的谎言
  2. 题意:现有一列数 a 1 , a 2 , . . . , a n a_1,a_2,...,a_n a1,a2,...,an,将会进行 m m m次操作,每次操作指定三个正整数 l , r , k l,r,k l,r,k,并将 a l a_l al a r a_r ar之间的所有数乘上 k k k,你需要回答在 m m m次操作后,这 n n n个数模 100000 100000 100000的余数分别是多少。
  3. 题解:就是裸的区间乘操作
    1. 区间乘:其他操作一样,再sum和tag那里改一下就好。sum[p]*=k,tag初始化为1,tag[p<<1],tag[p<<1|1]在pushdown的时候在原来基础上再乘上tag[p],继承之后tag[p]=1.
  4. 代码
#include <bits/stdc++.h>
#define int long long
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 1e5 + 10;
const int mod = 1e5;
int n, m, l, r, k;

int sum[N * 30], a[N], tag[N * 30];
void pushup(int p) { sum[p] = sum[p << 1] + sum[p << 1 | 1]; }
void build(int p, int l, int r) {
    tag[p] = 1;  //区间乘懒惰标记初始化
    if (l == r) {
        sum[p] = a[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid);
    build(p << 1 | 1, mid + 1, r);
    pushup(p);
}
void pushdown(int p, int l, int r) {
    int mid = (l + r) >> 1;
    sum[p << 1] = sum[p << 1] * tag[p] % mod;
    sum[p << 1 | 1] = sum[p << 1 | 1] * tag[p] % mod;
    tag[p << 1] = tag[p << 1] * tag[p] % mod;
    tag[p << 1 | 1] = tag[p << 1 | 1] * tag[p] % mod;
    tag[p] = 1;
}
void update(int p, int l, int r, int x, int y, int k) {
    if (x <= l && r <= y) {
        sum[p] = sum[p] * k % mod;
        tag[p] = tag[p] * k % mod;
        return;
    }
    pushdown(p, l, r);
    int mid = (l + r) >> 1;
    if (x <= mid) update(p << 1, l, mid, x, y, k);
    if (y > mid) update(p << 1 | 1, mid + 1, r, x, y, k);
    pushup(p);
}
int query(int p, int l, int r, int x, int y) {
    int res = 0;
    if (x <= l && r <= y) return sum[p];
    int mid = (l + r) >> 1;
    pushdown(p, l, r);
    if (x <= mid) res += query(p << 1, l, mid, x, y);
    if (y > mid) res += query(p << 1 | 1, mid + 1, r, x, y);
    return res;
}
signed main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) cin >> a[i];
    build(1, 1, n);  //初始化建树
    while (m--) {
        cin >> l >> r >> k;
        update(1, 1, n, l, r, k);
    }
    for (int i = 1; i <= n; i++) {
        printf("%lld ", query(1, 1, n, i, i));
    }
    return 0;
}

区间加修改&维护区间最小值

  1. 题目cf1555 E. Boring Segments(尺取&线段树&维护区间最小值)
  2. 题意:给定 n n n个线段,一个数 m m m。每个线段有 [ l , r ] [l,r] [l,r] w w w,分别表示可以通行的区间以及价值。求能在 [ 1 , m ] [1,m] [1,m]通行的线段组的 w w w的最小极差值。
    1. 1 ≤ n ≤ 3 e 5 , 2 ≤ m ≤ 1 e 6 1\le n\le 3e5,2\le m\le 1e6 1n3e5,2m1e6
    2. 1 ≤ l i < r i ≤ m , 1 ≤ w i ≤ 1 e 6 1\le l_i<r_i\le m,1\le w_i\le 1e6 1li<rim,1wi1e6
  3. 题解
    1. 差点忘了线段树还能维护最小值。
    2. [1,2]和[3,4]不能在[1,4]通行!所以不能维护原来的区间,区间应该做一些处理:r--(画一下图就能看出来)。然后我们维护的就是1-(m-1),如果这个区间的最小值为0,则说明不能通行,大于0则能通行。
    3. 尺取还是比较好像的,不多说了。
  4. 代码
#include <bits/stdc++.h>
// #define int long long
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 1e6 + 10;
struct node {
    int l, r, w;
    node(int l = 0, int r = 0, int w = 0) : l(l), r(r), w(w) {}
    void input() { scanf("%d%d%d", &l, &r, &w); }
    bool operator<(const node &b) const { return w < b.w; }
} s[N];
int n, m;
//区间加,区间查询。维护区间最小值
int mi[N << 3], tag[N << 3];
void pushup(int p) { mi[p] = min(mi[p << 1], mi[p << 1 | 1]); }
//这个题,建树这一步没必要
void build(int p, int l, int r) {
    if (l == r) return;
    int mid = (l + r) / 2;
    build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
    pushup(p);
}
void pushdown(int p, int l, int r) {
    if (tag[p] == 0) return;
    mi[p << 1] += tag[p], tag[p << 1] += tag[p];
    mi[p << 1 | 1] += tag[p], tag[p << 1 | 1] += tag[p]; // 竟然忘了加这一行emmmm:https://codeforces.ml/contest/1263/problem/E
    tag[p] = 0;
}
void update(int p, int l, int r, int x, int y, int k) {
    if (x <= l && r <= y) {
        mi[p] += k, tag[p] += k;
        return;
    }
    pushdown(p, l, r);
    int mid = (l + r) / 2;
    //有交集,就遍历
    if (mid >= x) update(p << 1, l, mid, x, y, k);
    if (mid + 1 <= y) update(p << 1 | 1, mid + 1, r, x, y, k);
    pushup(p);
}
int query(int p, int l, int r) { return mi[p]; }
signed main() {
    cin >> n >> m;
    build(1, 1, m - 1);
    for (int i = 1; i <= n; i++) s[i].input(), s[i].r--;
    sort(s + 1, s + 1 + n);
    // for (int i = 1; i <= n; i++) {
    // cout << i << ":::" << s[i].l << " " << s[i].r << " " << s[i].w << endl;
    // }
    int ans = 1e9, l, r = 0;
    for (int i = 1; i <= n; i++) {
        //不严格尺取,枚举左端点,找右端点即可
        while (query(1, 1, m - 1) == 0 && r < n)
            ++r, update(1, 1, m - 1, s[r].l, s[r].r, 1);
        // cout << ">>>" << i << " " << r << endl;
        if (query(1, 1, m - 1)) ans = min(ans, s[r].w - s[i].w);
        update(1, 1, m - 1, s[i].l, s[i].r, -1);
    }
    cout << ans << endl;
    return 0;
}

二维线段树&区间加&区间最小值&前缀和与差分

  1. 题目2021牛客暑期多校训练营6-H.Hopping Rabbit
  2. 题意:一个人每次可以走d步,上下左右都ok。给定n个矩形,求是否可以找到两个整数 x 0 , y 0 x_0,y_0 x0,y0使起点为 ( x 0 + 0.5 , y 0 + 0.5 ) (x_0+0.5,y_0+0.5) (x0+0.5,y0+0.5)时无论怎么走都不会落入矩形当中(可以从上面飞过)。
    2.1 输入: n , d , x 1 , y 1 , x 2 , y 2 n,d,x_1,y_1,x_2,y_2 n,d,x1,y1,x2,y2都是整数,数据范围见下图。
    在这里插入图片描述
    2.2 输出:如果可以找到这样的点,打印"YES"然后打印 x 0   y 0 x_0\ y_0 x0 y0;否则打印"NO"
  3. 题解
    3.1 首先很容易知道(处理一下)需要求 ( 0 , d − 1 ) ∗ ( 0 , d − 1 ) (0,d-1)*(0,d-1) (0,d1)(0,d1)的矩阵中是否有没有被给定矩阵覆盖的点,如果有,输出,否则就输出。那么就变成了二维区间覆盖问题,二维线段树&前缀和
    3.2 具体操作见代码?
  4. 代码
    4.1 define y1 y_1是因为,不然会报错emm
#include <bits/stdc++.h>
// #define int long long
#define x1 x_1
#define y1 y_1
#define x2 x_2
#define y2 y_2
#define x0 x_0
#define y0 y_0
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 1e5 + 10;
int n, d, x1, x2, y1, y2;
struct node {
    int y1, y2, k;
    node(int y1 = 0, int y2 = 0, int k = 0) : y1(y1), y2(y2), k(k) {}
};
vector<node> g[N];
void add(int x1, int x2, int y1, int y2) {
    g[x1].push_back(node(y1, y2, 1));
    g[x2 + 1].push_back(node(y1, y2, -1));
}
// 区间最小值
int mi[N << 2], tag[N << 2];  //统一N<<2,逆十字
void pushup(int p) { mi[p] = min(mi[p << 1], mi[p << 1 | 1]); }
void build(int p, int l, int r) {
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
    pushup(p);
}
void pushdown(int p) {
    mi[p << 1] += tag[p], tag[p << 1] += tag[p];
    mi[p << 1 | 1] += tag[p], tag[p << 1 | 1] += tag[p];
    tag[p] = 0;
}
void update(int p, int l, int r, int x, int y, int k) {
    if (x <= l && r <= y) {
        mi[p] += k, tag[p] += k;
        return;
    }
    pushdown(p);  //使用下一层之前,自然要先处理
    int mid = (l + r) >> 1;
    if (mid >= x) update(p << 1, l, mid, x, y, k);
    if (mid + 1 <= y) update(p << 1 | 1, mid + 1, r, x, y, k);
    pushup(p);
}
int query(int p, int l, int r, int x, int y) {
    if (x <= l && r <= y) return mi[p];
    pushdown(p);
    int res = 1e9;
    int mid = (l + r) >> 1;
    if (mid >= x) res = min(res, query(p << 1, l, mid, x, y));
    if (mid + 1 <= y) res = min(res, query(p << 1 | 1, mid + 1, r, x, y));
    return res;
}
int x0 = -1, y0 = -1;
bool solve() {
    build(1, 0, d - 1);
    for (int i = 0; i < d; i++) {
        for (auto j : g[i]) {
            update(1, 0, d - 1, j.y1, j.y2, j.k);
        }
        if (query(1, 0, d - 1, 0, d - 1) == 0) {
            x0 = i;
            for (int j = 0; j < d; j++) {
                if (query(1, 0, d - 1, j, j) == 0) {
                    y0 = j;
                    return true;
                }
            }
        }
    }
    return false;
}
signed main() {
    cin >> n >> d;
    for (int i = 1; i <= n; i++) {
        cin >> x1 >> y1 >> x2 >> y2;
        x2--, y2--;
        //这里一开始想错了
        // if (x2 - x1 >= d || y2 - y1 >= d) f = false;
        if (x2 - x1 >= d) x1 = 0, x2 = d - 1;
        if (y2 - y1 >= d) y1 = 0, y2 = d - 1;
        x1 = (x1 % d + d) % d, x2 = (x2 % d + d) % d;
        y1 = (y1 % d + d) % d, y2 = (y2 % d + d) % d;
        if (x1 <= x2) {
            if (y1 <= y2)
                add(x1, x2, y1, y2);
            else
                add(x1, x2, y1, d - 1), add(x1, x2, 0, y2);
        } else {
            if (y1 <= y2)
                add(x1, d - 1, y1, y2), add(0, x2, y1, y2);
            else
                add(x1, d - 1, y1, d - 1), add(x1, d - 1, 0, y2),
                    add(0, x2, y1, d - 1), add(0, x2, 0, y2);
        }
    }
    if (solve()) {
        puts("YES");
        cout << x0 << " " << y0;  // << endl;
    } else
        puts("NO");
    return 0;
}

线段树维护区间GCD&单点修改&区间查询

  1. 题目小阳的贝壳
  2. 题意
    1. 题目描述
      在这里插入图片描述
    2. 数据范围:n为数组长度,m为查询次数
      在这里插入图片描述
  3. 题解这里只说说怎么维护区间gcd
    1. a b c da b-a c-a d-a的gcd一样、我们求gcd(b,c,d)的时候相当于求gcd(a,b-a,c-b,d-c)用到差分
    2. 然后我们只需要维护差分的前缀和以及gcd就ok了,差分前缀和用于求当时的col[l],gcd用于求 g c d ( a l + 1 , a l + 2 , . . . , a r ) gcd(a_{l+1},a_{l+2},...,a_r) gcd(al+1,al+2,...,ar)。然后gcd([l,r])=gcd(col[l],gcd[l-1,r])。不能直接用后者的原因就不多说了(3.1的解释??)
    3. 具体的看代码emm。也是比较模板型的
  4. 代码
    1. 一些注意:mx,g维护的是绝对值;单点修改不需要懒惰标记;这题的关键是理解怎么维护区间gcd的(总的来说就是维护差分数组的区间gcd,修改的时候只改变两个地方的差分值,最后查询的时候用3.2的操作
#include <bits/stdc++.h>
// #define int long long
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 1e5 + 10;
int n, m, op, l, r, a[N], c[N], x;
// mx:最大相邻差值
//单点修改,不需要tag!!
int mx[N << 2], g[N << 2], sum[N << 2];
void pushup(int p) {
    mx[p] = max(mx[p << 1], mx[p << 1 | 1]);
    g[p] = __gcd(g[p << 1], g[p << 1 | 1]);
    sum[p] = sum[p << 1] + sum[p << 1 | 1];
}
void build(int p, int l, int r) {
    if (l == r) {
        sum[p] = c[l];
        mx[p] = g[p] = abs(c[l]);  //一定得为正数
        return;
    }
    int mid = (l + r) >> 1;
    build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r);
    pushup(p);
}
//单点更新差分数组c,不需要tag
//单点就该有单点的样子?
void update(int p, int l, int r, int x, int k) {
    if (l == r) {
        // mx[p] += k, g[p] += k, sum[p] += k;
        sum[p] += k;
        mx[p] = g[p] = abs(sum[p]);  //注意并不是单纯的像sum[p]一样相加减
        return;
    }
    int mid = (l + r) >> 1;
    if (mid >= x)
        update(p << 1, l, mid, x, k);
    else
        update(p << 1 | 1, mid + 1, r, x, k);
    pushup(p);
}
//一下几个query可以合并,但不是必要的
//[x,y]区间最大相邻值
int query_mx(int p, int l, int r, int x, int y) {
    //以下,不需要考虑!
    // if (x > y) return 0;  //[x,y]长度为0
    if (x <= l && r <= y) return mx[p];
    int res = 0;
    int mid = (l + r) >> 1;
    if (mid >= x) res = max(res, query_mx(p << 1, l, mid, x, y));
    if (mid < y) res = max(res, query_mx(p << 1 | 1, mid + 1, r, x, y));
    return res;
}
//[x,y]区间和查询
int query_sum(int p, int l, int r, int x, int y) {
    if (x <= l && r <= y) return sum[p];
    int res = 0;
    int mid = (l + r) >> 1;
    if (mid >= x) res += query_sum(p << 1, l, mid, x, y);
    if (mid < y) res += query_sum(p << 1 | 1, mid + 1, r, x, y);
    return res;
}
int query_gcd(int p, int l, int r, int x, int y) {
    // if (x > y) return 0;  //[x,y]长度可能为0
    if (x <= l && r <= y) return g[p];
    int res = 0;
    int mid = (l + r) >> 1;
    if (mid >= x) res = __gcd(res, query_gcd(p << 1, l, mid, x, y));
    if (mid < y) res = __gcd(res, query_gcd(p << 1 | 1, mid + 1, r, x, y));
    return res;
}
signed main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) {
        scanf("%d", &a[i]);
        c[i] = a[i] - a[i - 1];  //差分
    }
    build(1, 1, n);
    while (m--) {
        scanf("%d%d%d", &op, &l, &r);
        if (op == 1) {
            scanf("%d", &x);
            //查询都是小问题,我们最应该关注的是怎样修改
            update(1, 1, n, l, x);
            if (r + 1 <= n) update(1, 1, n, r + 1, -x);
        } else if (op == 2) {
            printf("%d\n", query_mx(1, 1, n, l + 1, r));
        } else {
            printf("%d\n", __gcd(query_sum(1, 1, n, 1, l),
                                 query_gcd(1, 1, n, l + 1, r)));
        }
    }
    return 0;
}

分块

前言

  1. 参考博客
    1. oi-wiki分块
    2. Pecco算法学习笔记(23): 分块

分块思想

  1. 分块是一种思想,而不是一种数据结构。
  2. 分块是一种很灵活的思想,相较于树状数组和线段树。
    1. 优点:是通用性更好,可以维护很多树状数组和线段树无法维护的信息。
    2. 缺点:是渐进意义的复杂度,相较于线段树和树状数组不够好。
    3. 分块的时间复杂度比不上线段树和树状数组这些对数级算法。但由此换来的,是更高的灵活性。与线段树不同,块状数组并不要求所维护信息满足结合律,也不需要一层层地传递标记。但它们又有相似之处,线段树是一棵高度约为 log ⁡ 2 n \log_2n log2n的树,而块状数组则被看成一棵高度为3的树。

分块常见的操作

在这里插入图片描述

题目1:

  1. LibreOJ 6280 数列分块入门 4

题目2:线段树模板题&区间加&区间求和(分块解决)

  1. 题目链接P3372 【模板】线段树 1
  2. 题意:给定一个长度为n的数组a,m个查询,(n,m<=1e5).
    1. 查询1:1 x y k在区间[x,y]内所有数加上k。
    2. 查询2:2 x y求区间[x,y]内所有数之和。
    3. 注意:保证数列中任意时刻所有值的和在-263~263。
  3. 题解:分块
    1. sum记录每个块的和,tag[i]标记块i中每个数需要加的值。
    2. 熟悉一下分块的操作啦。
  4. 代码
/*
1. 题目链接:https://www.luogu.com.cn/problem/P3372
2. 题意:给定一个长度为n的数组a,m个查询,(n,m<=1e5).
	1. 查询1:1 x y k在区间[x,y]内所有数加上k。
	2. 查询2:2 x y求区间[x,y]内所有数之和。
	3. 注意:保证数列中任意时刻所有值的和在-2^63~2^63。
3. 题解:分块
	1. sum记录每个块的和,tag[i]标记块i中每个数需要加的值。
	2. 熟悉一下分块的操作啦。
*/
#include<bits/stdc++.h>
#define int long long
#define dbg(x) cout<<#x<<"==="<<x<<endl
using namespace std;
template<class T>
void read(T &x) {
	T res=0,f=1;
	char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) res=(res<<3)+(res<<1)+(c-'0'),c=getchar();
	x=res*f;
}
const int N=1e5+5;

int n,a[N],m,op,x,y,k;

int num,st[N],ed[N],id[N],sz[N];
int tag[N],sum[N];
void init_block(int n) {
	int num=sqrt(n);
	for(int i=1; i<=num; i++) st[i]=n/num*(i-1)+1,ed[i]=n/num*i;
	ed[num]=n;
	for(int i=1; i<=num; i++) {
		for(int j=st[i]; j<=ed[i]; j++) id[j]=i,sum[i]+=a[j];
		sz[i]=ed[i]-st[i]+1;
	}
}
void update(int x,int y,int k) {
	int ix=id[x],iy=id[y];
	if(ix==iy) {
		for(int j=x; j<=y; j++) a[j]+=k,sum[ix]+=k;
	} else {
		for(int j=x; j<=ed[ix]; j++) sum[ix]+=k,a[j]+=k;
		for(int i=ix+1; i<=iy-1; i++) sum[i]+=sz[i]*k,tag[i]+=k;
		for(int j=st[iy]; j<=y; j++)sum[iy]+=k,a[j]+=k;
	}
}
void pushdown(int id) {
	if(tag[id]) {
		for(int j=st[id]; j<=ed[id]; j++) a[j]+=tag[id];
		tag[id]=0;
	}
}
int query(int x,int y) {
	int ix=id[x],iy=id[y];
	int res=0;
	pushdown(ix),pushdown(iy);
	if(ix==iy) {
		for(int j=x; j<=y; j++) res+=a[j];
	} else {
		for(int j=x; j<=ed[ix]; j++) res+=a[j];
		for(int i=ix+1; i<=iy-1; i++) res+=sum[i];
		for(int j=st[iy]; j<=y; j++) res+=a[j];
	}
	return res;
}
signed main() {
	read(n),read(m);
	for(int i=1; i<=n; i++) read(a[i]);
	init_block(n);
	while(m--) {
		read(op);
		if(op==1) {
			read(x),read(y),read(k);
			update(x,y,k);
		} else {
			read(x),read(y);
//			cout<<">>>>"; 
			printf("%lld\n",query(x,y));
		}
	}
	return 0;
}
/*
input:::
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
output:::
11
8
20
*/

题目3:线段树不能维护&区间加&查询区间大于等于x的数(这一类查询,分块才能做)。

  1. 题目链接P2801 教主的魔法
  2. 题意:给定一个长为n的数组a,以及m次操作。
    1. 操作1:M l r x区间[l,r]内的数全部增加x
    2. 操作2:A l r c查询区间[l,r]内大于等于c的数的个数
    3. n<=1e6,m<=3000,1<=x<=1000,1<=c<=1e9
  3. 题解:查询区间大于等于x的数的个数(这一类查询不能用线段树,“只能用”分块儿
    1. 每个块内排个序
    2. 更新的时候非完整块的话就更新数组a,否则就更新tag
    3. 查询的时候非完整块就一个一个判断,完整块就直接二分(一个块所有数加上一个数,数的顺序不变)
  4. 代码
/*
1. **题目链接**:[P2801 教主的魔法](https://www.luogu.com.cn/problem/P2801) 
2. **题意**:给定一个长为n的数组a,以及m次操作。
	1. 操作1:M l r x区间[l,r]内的数全部增加x
	2. 操作2:A l r c查询区间[l,r]内大于等于c的数的个数
	3. n<=1e6,m<=3000,1<=x<=1000,1<=c<=1e9
3. **题解**:查询区间大于等于x的数的个数(==这一类查询不能用线段树,“只能用”分块儿==)
	1. 每个块内排个序
	2. 更新的时候非完整块的话就更新数组a,否则就更新tag
	3. 查询的时候非完整块就一个一个判断,完整块就直接二分(一个块所有数加上一个数,数的顺序不变) 
*/
#include<bits/stdc++.h>
//#define int long long
#define lc p<<1
#define rc p<<1|1
#define Add(x,y) (x=(x+y>=1e9)?1e9:(x+y))
#define dbg(x) cout<<#x<<"==="<<x<<endl
using namespace std;
template<class T>
void read(T &x) {
	T res=0,f=1;
	char c=getchar();
	while(!isdigit(c)) {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(isdigit(c)) res=(res<<3)+(res<<1)+(c-'0'),c=getchar();
	x=res*f;
}
const int N=1e6+5;

int n,a[N],m,l,r,x;
char c;
int st[N],ed[N],sz[N],id[N];
int tag[N],b[N];//a[i]为修改之后的值,b[i]为排序之后的值
void init_block(int n) {
	int num=sqrt(n);
	for(int i=1; i<=num; i++) {
		st[i]=n/num*(i-1)+1,ed[i]=n/num*i;
	}
	ed[num]=n;
	for(int i=1; i<=num; i++) {
		for(int j=st[i]; j<=ed[i]; j++) id[j]=i, b[j]=a[j];
		sz[i]=ed[i]-st[i]+1;
		sort(b+st[i],b+ed[i]+1);
	}
}
//维护每一块的顺序和值
void update(int l,int r,int x) {
	if(id[l]==id[r]) {
		for(int j=st[id[l]]; j<=ed[id[l]]; j++) {
			if(l<=j&&j<=r) Add(a[j],x);
			b[j]=a[j];
		}
		sort(b+st[id[l]],b+ed[id[l]]+1);//重新排序
	} else {
		//左边
		for(int j=st[id[l]]; j<=ed[id[l]]; j++) {
			if(l<=j&&j<=r) Add(a[j],x);
			b[j]=a[j];
		}
		sort(b+st[id[l]],b+ed[id[l]]+1);//重新排序
		//中间
		for(int i=id[l]+1; i<=id[r]-1; i++) Add(tag[i],x);
		//右边
		for(int j=st[id[r]]; j<=ed[id[r]]; j++) {
			if(l<=j&&j<=r) Add(a[j],x);
			b[j]=a[j];
		}
		sort(b+st[id[r]],b+ed[id[r]]+1);//重新排序
	}
}
int query(int l,int r,int x) {
//	cout<<":::"<<l<<" "<<r<<" "<<x<<" "<<id[l]<<" "<<id[r]<<endl;
	int res=0;
	if(id[l]==id[r]) {
		for(int j=l; j<=r; j++) {
			if(a[j]+tag[id[l]]>=x) res++;
		}
	} else {
		//左边
		for(int j=l; j<=ed[id[l]]; j++) {
			if(a[j]+tag[id[l]]>=x) res++;//,dbg(j);
		}
		//中间
		for(int i=id[l]+1; i<=id[r]-1; i++) {
			int pos=lower_bound(b+st[i],b+ed[i]+1,x-tag[i])-b;
//			for(int j=st[i]; j<=ed[i]; j++) cout<<b[j]<<" ";
//			cout<<endl;
			res+=ed[i]-pos+1;
//			dbg(i),dbg(ed[i]-pos+1);
		}
		//右边
		for(int j=st[id[r]]; j<=r; j++) {
			if(a[j]+tag[id[r]]>=x) res++;//,dbg(j);
		}
	}
//	dbg(res);
	return res;
}
signed main() {
	read(n),read(m);
	for(int i=1; i<=n; i++) read(a[i]);//,cout<<a[i]<<" ";
//	cout<<endl;
//	build(1,1,n);
	init_block(n);
	while(m--) {
		cin>>c;
		read(l),read(r),read(x);
		if(c=='M') {
			update(l,r,x);
		} else {
//			cout<<">>>>>>>>>>";
			printf("%d\n",query(l,r,x));
		}
	}
	return 0;
}
/*
input:::
5 3

1 2 3 4 5

A 1 5 4

M 3 5 1

A 1 5 4

output:::
2
3

*/

块状数组

模板

/*
1. 若查询时遇到两边不完整的块直接暴力查询。
2. (num+1)^2=num^2+2*num+1,所以最后一块最长为3*num
*/
num=sqrt(n);//均值不等式 
for(int i=1; i<=num; i++) {
	st[i]=n/num*(i-1)+1;//st[i]表示第i块第一个元素的下标
	ed[i]=n/num*i;//ed[i]表示第i块最后一个元素的下标
}
ed[num]=n;//漏掉的一块并入最后一块
for(int i=1; i<=num; i++) {
	for(int j=st[i]; j<=ed[i]; j++) id[j]=i;//j属于的块号
	sz[i]=ed[i]-st[i]+1;//第i块的大小 
}
/*
1. 以上操作使数组分好块,接下来就好操作了。 ——接下来就又是考察数学思维了。 
*/ 

主席树

参考博客

  1. 【学习笔记】主席树

感受

  1. 应该记算法的具体思想@!!!,而不是记模板。只有这样,才能够熟练应用一种算法。
  2. 但是总结模板还是必要的。自己的模板——复习时最好的资料
  3. 之前学的平衡树、字符串等等等等,都没有理解或是没有自己实现,现在啥都不记得了emmm
  4. 树状数组——一个放弃了的基础算法(可以用线段树完全取代,问题是线段树我都多用模板emmm)
  5. (2021.10.02)突然有种感悟:可以先把刷过的好题都以不同的知识点总结起来,等到觉得将这个知识点理解的差不多了,再来总结一下。也就不用非得一下子就将这个知识点的东西学完(os:毕竟我还有挺多题要补的)。

算法总结

前置知识

  1. 线段树、权值线段树、前缀和等

具体思想

静态区间求第k小值

P3834 【模板】可持久化线段树 2(主席树)
1.参考博客: 【学习笔记】主席树

  • 只是想要了解主席树的话这一个博客就ok了
  • 想有更深入的了解的话,这里面还给了题目,建议学会静态区间第k小值的思想之后,解决其他题目时尽量先自己想想(代码如无必要,只总结kth问题就ok了——莫依赖模板嘛!)

2.要求:给定一个数组a,长度为n(主席树节点数组长度建议N*32>=n(logn)^2),然后m次询问,l,r,k。求区间[l,r]前的第k小数。

3.说明

  1. 离散化:b[]=a[],离散后数组长度q=unique(b+1,b+1+n)-b-1。
    • unique返回的时数组去重之后的尾地址;
    • sz=unique(a+1,a+1+n)-(a+1);
    • sz=unique(a,a+n)-a;
    • 调用之前不排序的话(结果数组:相邻相同元素只留一个)
  2. 主席树:以rt[i]为根的树表示的是b[1]->b[i]所构成的线段树(其中b[i+1]->b[n]的)
    • 数组我们用离散化之后的b[]
    • 权值线段树以值域作为区间:rt[x]表示的区间是值域区间而不是数组区间。
    • 线段树rt[i]是有rt[i-1]更新来的@!!!:(最开始先建一个空树),rt[i]与rt[i-1]结构是完全一样的,唯一不一样的是值不一样(当然节点也不一样哈)
    • 线段树前缀和:值域为b[l]->b[r]的线段树=>sum[rt[r]]-sum[rt[l-1]](rt[r]每个节点的值都减去rt[l-1]的值即得到值域为b[l]->b[r]的线段树)
    • 除此之外就和线段树的操作是一样的了

4.步骤

  1. 离散化: 先离散化数组a[],得到数组b[]以及大小sz
  2. 建空树:先把结构搞起来
  3. 修改/查询操作:
    • 自己写写试试

5.复杂度分析

  1. 时间复杂度
    • 建树 O(nlogn)
    • 询问 O(mlogn)
    • 总复杂度 O((n+m)logn)
  2. 空间复杂度:一般为O(n(logn)^2)

6.代码

  1. 总结:
    • build,update,query函数都要有的参数:根节点(query为rt[v]-rt[u]),范围(包括l,r)
    • 重点:访问线段树的时候从根节点开始
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 10;

int n, m, a[N], b[N];
int l, r, k;
int rt[N * 32], lc[N * 32], rc[N * 32], sum[N * 32];
int sz, id;

void build(int &rt, int l, int r) {
    rt = ++id, sum[rt] = 0;
    if(l==r) return ;
    int mid = (l + r) >> 1;
    build(lc[rt], l, mid);
    build(rc[rt], mid + 1, r);
}
int update(int o, int l, int r, int x) {
    int oo = ++id;
    //<-能进入update,就表示根节点有更新->
    lc[oo] = lc[o], rc[oo] = rc[o], sum[oo] = sum[o] + 1;
    if (l == r) return oo;
    int mid = (l + r) >> 1;
    if (x <= mid)
        lc[oo] = update(lc[o], l, mid, x);
    else
        rc[oo] = update(rc[o], mid + 1, r, x);
    return oo;
}
//表示在值域为[u,v]构成的线段树中查询
int query(int u, int v, int l, int r, int k) {
    if (l == r) return l;  //我们求的是下标,而不是大小
    int mid = (l + r) >> 1, x = sum[lc[v]] - sum[lc[u]];
    if (k <= x)
        return query(lc[u], lc[v], l, mid, k);
    else
        return query(rc[u], rc[v], mid + 1, r, k - x);
}
signed main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]), b[i] = a[i];
    sort(b + 1, b + 1 + n);
    sz = unique(b + 1, b + 1 + n) - b - 1;
    build(rt[0], 1, sz);
    // 1~n加点
    for (int i = 1; i <= n; i++) {
        int x = lower_bound(b + 1, b + 1 + sz, a[i]) - b;
        rt[i] = update(rt[i - 1], 1, sz, x);
    }
    while (m--) {
        scanf("%d%d%d", &l, &r, &k);
        int ans = b[query(rt[l - 1], rt[r], 1, sz, k)];
        //线段树:函数中的l,r最初调用总是1~sz
        printf("%d\n", ans);
    }
    return 0;
}

可以解决的问题

1.静态区间求第k小值

例题1:P3834 【模板】可持久化线段树 2(主席树)

  1. 题意题解见本博文算法总结部分

2.静态区间求众数及其个数

例题1:P3567 [POI2014]KUR-Couriers

  1. 题意:在这里插入图片描述
  2. 提示:大题和静态区间求第k小值一样,都是静态且都是值域区间
  3. 代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 5e5 + 5;

int n, m, a[N], l, r;
int b[N];
int rt[N * 32], lc[N * 32], rc[N * 32], sum[N * 32];
int sz, id;

void build(int &rt, int l, int r) {
    rt = ++id, sum[rt] = 0;  //能进入函数,就表示能更新
    if (l == r) return;
    int mid = (l + r) >> 1;
    build(lc[rt], l, mid);
    build(rc[rt], mid + 1, r);
}
int update(int o, int l, int r, int x) {
    int oo = ++id;
    lc[oo] = lc[o], rc[oo] = rc[o],
    sum[oo] = sum[o] + 1;  //能进来(进入函数),就能更新
    if (l == r) return oo;
    int mid = (l + r) >> 1;
    if (x <= mid)
        lc[oo] = update(lc[o], l, mid, x);
    else
        rc[oo] = update(rc[o], mid + 1, r, x);
    return oo;  //好像根本就到不了这里
}
int query(int u, int v, int l, int r, int x) {
    if (l == r) return (sum[v] - sum[u] > x) ? l : 0;
    int mid = (l + r) >> 1;  //, res = 0;
    int numl = sum[lc[v]] - sum[lc[u]], numr = sum[rc[v]] - sum[rc[u]];
    if (numl > x) return query(lc[u], lc[v], l, mid, x);
    if (numr > x) return query(rc[u], rc[v], mid + 1, r, x);
    return 0;  //但是这里是必不可少的
}
signed main() {
    cin >> n >> m;
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]), b[i] = a[i];
    sort(b + 1, b + 1 + n);
    sz = unique(b + 1, b + 1 + n) - b - 1;
    build(rt[0], 1, sz);
    for (int i = 1; i <= n; i++) {
        int x = lower_bound(b + 1, b + 1 + sz, a[i]) - b;
        rt[i] = update(rt[i - 1], 1, sz, x);
    }
    while (m--) {
        scanf("%d%d", &l, &r);
        int len = r - l + 1;
        int x = len / 2;  //严格大于一半,即要严格大于x
        int ans = b[query(rt[l - 1], rt[r], 1, sz, x)];  // b[0]=0
        //注意不要混淆值域区间和数组区间,调用的不是:query(rt[l-1],rt[r],l,r,x);
        printf("%d\n", ans);
    }
    return 0;
}

题目3:查询区间不同数的个数&主席树&莫队(2021.10.02)

  1. 传送门D-query SPOJ - DQUERY
  2. 题意:给一个长为 n 的数组 a ,以及 q 次询问,每次询问两个整数 l,r 表示询问区间 [ l , r ] ,要求每次询问输出 [ l , r ] 之间的数的不同个数。
    1. 1 <= n <= 3e4
    2. 1 <= ai <= 1e6
    3. 1 <= q <=2e5
    4. 1 <= l <= r <= n
  3. (主席树)题解
    1. 关键:每一棵树我们都只维护某个值的最后一个位置,令这个位置的值为 1 ,其他也为这个值的地方全部为 0 ,比如 1 2 3 2 4 3第 6 棵线段树的每个值为1 0 0 1 1 1,然后我们可以发现查询区间[l,r]的时候可以直接查询第r棵树的[l,r]区间即可。
  4. 学习总结:主席树与线段树的区别
    1. 主席树至少有 n 棵“完整的”线段树(这个题因为有时候有两次更新,所以不止 n 棵“完整的”线段树,但是我们也只需要 n 棵。)
    2. 主席树的节点数的个数: 2 ∗ n − 1 + m ∗ ( [ log ⁡ 2 n ] + 1 ) 2*n-1+m*([\log_2n]+1) 2n1+m([log2n]+1)。其中 m 为更新整个主席树的次数(最后结果是有 m+1 棵“完整”线段树),每一次更新会多 [ log ⁡ 2 n ] + 1 [\log_2n]+1 [log2n]+1个节点。——节点个数一般不会超过 n ∗ 32 n*32 n32,对于空间,我们没有必要吝啬,直接 N < < 5 N<<5 N<<5即可。(当然还是要知道怎么计算线段树的节点树的啦)。
    3. 与线段树操作的不同建树查询基本一样,都是从“根节点”一直操作即可,更新的时候有一点不一样——要建立新节点继承上一棵树的所有信息,然后就与普通线段树的更新一样了。(os:还没有试过区间更新)。
  5. 代码
#include <bits/stdc++.h>
// #define int long long
#define ll long long
#define pii pair<int, int>
#define x first
#define y second
using namespace std;
template <class T>
void read(T &x) {
    T res = 0, f = 1;
    char c = getchar();
    while (!isdigit(c)) {
        if (c == '-') f = -1;
        c = getchar();
    }
    while (isdigit(c)) res = (res << 3) + (res << 1) + (c - '0'), c = getchar();
    x = res * f;
}
const int N = 3e4 + 5;
const int M = 1e6 + 5;
const int Q = 2e5 + 5;
int n, a[N], q, l, r;
int pre[M];
int rt[N];
int lc[N << 5], rc[N << 5], sum[N << 5];  //一般要乘上32
int id = 0;
// vector<pair<int, pii> > v;
int build(int p, int l, int r) {
    p = ++id, sum[p] = 0;
    // v.push_back({p, {l, r}});
    if (l == r) return p;
    int mid = (l + r) >> 1;
    lc[p] = build(lc[p], l, mid), rc[p] = build(rc[p], mid + 1, r);
    return p;
}
int update(int p, int l, int r, int x, int k) {
    int pp = ++id;  //每进入一层添加一个新节点
    //先要继承上一颗树的所有信息,然后就是线段树的正常修改了
    lc[pp] = lc[p], rc[pp] = rc[p], sum[pp] = sum[p];
    // v.push_back({pp, {l, r}});
    if (l == r) {
        sum[pp] += k;
        return pp;
    }
    int mid = (l + r) >> 1;
    if (x <= mid)
        lc[pp] = update(lc[p], l, mid, x, k);
    else
        rc[pp] = update(rc[p], mid + 1, r, x, k);
    sum[pp] = sum[lc[pp]] + sum[rc[pp]];
    return pp;
}

int query(int p, int l, int r, int x, int y) {
    if (x <= l && r <= y) return sum[p];
    int mid = (l + r) >> 1, res = 0;
    if (x <= mid) res += query(lc[p], l, mid, x, y);
    if (y > mid) res += query(rc[p], mid + 1, r, x, y);
    return res;
}
signed main() {
    read(n);
    rt[0] = build(1, 1, n);  //注意这个rt[0]特别重要
    for (int i = 1; i <= n; i++) {
        read(a[i]);
        if (pre[a[i]] == 0)
            rt[i] = update(rt[i - 1], 1, n, i, 1), pre[a[i]] = i;
        else {
            rt[i] = update(rt[i - 1], 1, n, pre[a[i]], -1);
            rt[i] = update(rt[i], 1, n, i, 1);
            pre[a[i]] = i;
        }
    }
    read(q);
    while (q--) {
        read(l), read(r);
        printf("%d\n", query(rt[r], 1, n, l, r));
    }
    return 0;
}
/*
Input
5
1 1 2 1 3
3
1 5
2 4
3 5

Output
3
2
3
*/
变形1:查询[1,l]并上[r,n]区间内的不同数的个数
  1. 传送门牛客多校2018第一场J
  2. 提示:即查询区间[r,l+n]内的不同数的个数。
    1. 另外,注意,如果多组样例要记得id=0

莫队

前言

  1. 参考资料
    1. 引入&普通莫队【算法讲堂】【电子科技大学】【ACM】莫队算法

简介:优雅的暴力

  1. 莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。
  2. 只能应用于离线,强制在线(上一次询问的答案,作为下一次询问的内容。即这一次询问的内容,比如用到上一次询问的答案)的话就不能用莫队。
  3. 分类
    1. 普通莫队
    2. 树上莫队
    3. 带修改莫队

普通莫队

  1. 题型:能够 O ( 1 ) O(1) O(1)转移就能用普通莫队。
    在这里插入图片描述

  2. 原理:优雅暴力

    1. 怎样优化的:关键是排序。
      在这里插入图片描述

题目1:询问区间不同数的个数&普通莫队&(主席树||线段树||树状数组也都可以解决)&不要吝啬空间(所有数组都1e7+5都没问题,只要不MLE)

  1. 传送门D-query SPOJ - DQUERY
  2. 题意:给定一个长度为 n n n的数组 a a a q q q次询问,每次询问区间 [ l i , r i ] [l_i,r_i] [li,ri],要求输出区间 [ l i , r i ] [l_i,r_i] [li,ri]内不同数的个数。
    1. 1 ≤ n ≤ 3 e 4 1\le n\le 3e4 1n3e4
    2. 1 ≤ a i ≤ 1 e 6 1\le a_i\le 1e6 1ai1e6
    3. 1 ≤ q ≤ 2 e 5 1\le q\le 2e5 1q2e5
    4. 1 ≤ l i ≤ r i ≤ n 1\le l_i\le r_i\le n 1lirin
  3. 题解:查询区间 [ l , r ] [l,r] [l,r]
    1. 主席树和线段树/树状数组的题解:同一个数都只记录最后位置(标记为1),其他位置标记为0。
      1. 主席树:查询第 r r r棵线段树的区间 [ l , r ] [l,r] [l,r]的和即可。
      2. 线段树/树状数组:也是离线处理, r r r为第一关键字进行排序,一直更新到 r r r然后查询区间 [ l , r ] [l,r] [l,r]的和即可。
    2. 莫队:就没多少思维难度了。
      1. 维护任意状态 [ L , R ] [L,R] [L,R],然后暴力更新即可。
      2. 排序是关键:如果 l l l在同一块,那就按 r r r从小到大排序,否则就按 l l l从小到大排序。——复杂度分析:因为可以 O ( 1 ) O(1) O(1)更新,所以移动次数即为复杂度, l l l最多移动 n ∗ n n*\sqrt{n} nn 次, r r r最多也是移动 n ∗ n n*\sqrt{n} nn 次,所以复杂度是 O ( n ∗ n ) O(n*\sqrt{n}) O(nn )级别的。
  4. 代码
#include <bits/stdc++.h>
// #define int long long
using namespace std;
template <class T>
void read(T &x) {
    T res = 0, f = 1;
    char c = getchar();
    while (!isdigit(c)) {
        if (c == '-') f = -1;
        c = getchar();
    }
    while (isdigit(c)) res = (res << 3) + (res << 1) + (c - '0'), c = getchar();
    x = res * f;
}
const int N = 1e7 + 5;
int n, a[N], q;
int id[N], st[N], ed[N], block;
int cnt[N], nowans = 0;
int ans[N];
struct node {
    int l, r, i;
    bool operator<(const node &b) const {
        if (id[l] == id[b.l])
            return r < b.r;
        else
            return id[l] < id[b.l];
    }
} s[N];

void init_block(int n) {
    block = sqrt(n);
    for (int i = 1; i <= block; i++)
        st[i] = n / block * (i - 1) + 1, ed[i] = n / block * i;
    ed[block] = n;
    for (int i = 1; i <= block; i++)
        for (int j = st[i]; j <= ed[i]; j++) id[j] = i;
}
void update(int x, int k) {
    if (k == 1) {
        if (cnt[a[x]] == 0) nowans++;
        cnt[a[x]]++;
    } else {
        cnt[a[x]]--;
        if (cnt[a[x]] == 0) nowans--;
    }
}
signed main() {
    read(n);
    for (int i = 1; i <= n; i++) read(a[i]);
    read(q);
    for (int i = 1; i <= q; i++) read(s[i].l), read(s[i].r), s[i].i = i;
    init_block(n);
    sort(s + 1, s + 1 + q);
    // int L = 1, R = n;
    // for (int i = L; i <= R; i++) update(i, 1);
    int L = 0, R = 0;
    //虽然任意区间都ok,但是上面复杂度还会大一点->但是上面不会出现负数的情况->但是出现负数好像也没事,而且上面也是可能出现负数的
    for (int i = 1; i <= q; i++) {
        while (L < s[i].l) update(L++, -1);
        while (L > s[i].l) update(--L, 1);
        //注意加减
        while (R > s[i].r) update(R--, -1);
        while (R < s[i].r) update(++R, 1);
        ans[s[i].i] = nowans;
    }
    for (int i = 1; i <= q; i++) printf("%d\n", ans[i]);
    return 0;
}
拓展题目1:查询区间 [ l , r ] [l,r] [l,r] ∑ i = 1 i = k c [ i ] 2 \sum_{i=1}^{i=k}c[i]^2 i=1i=kc[i]2,其中 c [ i ] c[i] c[i]表示值为 i i i的个数。
  1. 传送门P2709 小B的询问
  2. 提示能O(1)转移->也是普通莫队模板题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值