AcWing算法提高课 Level-3 第四章 高级数据结构

并查集

1250. 格子游戏

在这里插入图片描述

  • 并查集解决的是连通性(无向图联通分量)和传递性(家谱关系)问题,并且可以动态的维护。抛开格子不看,任意一个图中,增加一条边形成环当且仅当这条边连接的两点已经联通,于是可以将点分为若干个集合,每个集合对应图中的一个连通块。
  • 并查集(典型并查集判断是否存在环的问题)
  • 并查集中一般用一维的坐标会方便一些,所以这个题要把二维坐标转化成一维坐标,有个很常用的方式,(x,y) -> x*n+y,前提是x和y都从0开始
  • 将每个坐标看成一个点值,为了方便计算,将所有的坐标横纵坐标都减1,第一个位置即(1,1)看成是0,(1,2)看成是1,依次类推,将所有的坐标横纵坐标都减1后,假设当前点是(x,y),则该点的映射值是a = (x * n + y),若向下画,则b = [(x + 1) * n + y],若向右画,则b = [x * n + y - 1]
  • 并查集操作复杂度接近O(1),本来应该这题是O(n+m),但由于实现时只用了路径压缩,没有用什么什么合并,所以本质这道题复杂度是mlogn的,但这个logn非常小,因为并查集常数很小,
#include <iostream>

using namespace std;

const int N = 40010;        // 200 * 200

int n, m;
int fa[N];

int get(int x, int y)
{
    return x * n + y;
}

int find(int x)
{
    if (fa[x] != x) fa[x] = find(fa[x]);
    return fa[x];
}

int main()
{
    cin >> n >> m;
    
//    for (int i = 1; i <= n; i ++ ) fa[i] = i;
    for (int i = 0; i < n * n; i ++ ) fa[i] = i;  // wa!!!! [0, n * n - 1]
    
    int res = 0;
    for (int i = 1; i <= m; i ++ )
    {
        int x, y;
        char d;
        cin >> x >> y >> d;
        x -- , y -- ;
        int a = get(x, y);
        int b;
        if (d == 'D') b = get(x + 1, y);
        else b = get(x, y + 1);
        
        int pa = find(a), pb = find(b);
        if (pa == pb)
        {
            res = i;
            break;
        }
        fa[pa] = pb;
    }
    if (!res) cout << "draw" << endl;
    else cout << res << endl;
    
    return 0;
}

树状数组

  • 树状数组相较于数组和前缀和数组的作用

在这里插入图片描述

  • 采取的是二进制思想,例如求1~x的和,就可以用下图的方案在log(x)中算出;即可以在log(n)时间内算出前n项的前缀和
    在这里插入图片描述
  • 那如何求每个区间的和呢?先看看区间的性质,第一个区间包含 2 i 1 2^{i_1} 2i1个数,第二个区间包含 2 i 2 2^{i_2} 2i2个数,以此类推,然后发现 2 i 1 2^{i_1} 2i1是x的二进制表示的最后一位1, 2 i 2 2^{i_2} 2i2是x - 2 i 1 2^{i_1} 2i1的最后一位1。因此,在这样的(L, R]区间中,这个区间长度一定是R的二进制表示的最后一位1所对的次幂。(lowbit(x)是O(1)的),那么这个区间形式就可以改写为[R-lowbit( R)+1,R],那么就可以用数组,C[R]来表示这个区间的总和了,这样的区间最多有n个。
  • C[x] = a[x - lowbit( x) + 1, x],对于每个x而言,1~x的所有数的和,最多可以拆成log(x)个C[x]的和,所以是log(n)
    在这里插入图片描述
    在这里插入图片描述
  • 这样就挖掘出了所有不同C之间的关系

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

241. 楼兰图腾

在这里插入图片描述

#include <iostream>
#include <cstring>

using namespace std;

typedef long long ll;

const int N = 2e5 + 10;

int n;
int a[N], tr[N];
int Greater[N], lower[N];

int lowbit(int x)
{
    return x & (-x);
}

void add(int x, int c)
{
    for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

int sum(int x)
{
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int main()
{
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
    
    // 从左到右
    for (int i = 1; i <= n; i ++ )
    {
        int y = a[i];
        Greater[i] = sum(n) - sum(y);   // [y + 1, n]   // y + 1 ~ n之间已出现的数的个数
        lower[i] = sum(y - 1);  // [1, y - 1]    // 1 ~ y - 1之间已出现的数的个数
        add(y, 1);  // 记录y这个数已经出现了
    }
    
    memset(tr, 0, sizeof tr);   //  清空,重新建树,之间是从左向右遍历数组建树,后面要从右向左了
    ll res1 = 0, res2 = 0;
    
    for (int i = n; i; i -- )
    {
        int y = a[i];
        res1 += Greater[i] * (ll)(sum(n) - sum(y));
        res2 += lower[i] * (ll)(sum(y - 1));
        add(y, 1);
    }
    
    printf("%lld %lld", res1, res2);
    
    return 0;
}

线段树

// 简单线段树的操作 :

//传入节点编号,用子节点信息来算父节点信息
push up(int u)

//将一段区间初始化为一颗线段树
build()

//修改操作,修改某一个点或者某一个区间(懒标记)
modify()

//查询某一段区间的信息
query()
  • 定义 :线段树是一颗满二叉树,以一颗长度为10的序列为例 :
    在这里插入图片描述
  • N个叶结点的满二叉树有N + N/2 + N/4 + … = 2N-1个结点,因为在上述存储方式下,最后还有一层产生了空余,最后一层最坏情况下是上一层的两倍,也就是还有2N个,所以保存线段树的数组长度要不小于4N
  • 线段树的查询操作,例如查询【5, 9】,有8个区间要查 。且可以发现为log(n),不过最多有4logn,而树状数组只有1logn,所以线段树复杂度更大
    在这里插入图片描述
  • 线段树的结构 :
//一般使用结构体来存储线段树,空间大小开四倍
struct Node{
    int l,r;  //维护的区间
    int v;   //维护的信息...
} tree[N*4];
  • 线段树的建树 :
//build
void build(int u,int l,int r){ //构建节点u,其维护的是区间[l,r]
    tr[u]={l,r};
    if(l==r) return ; //已经是叶子节点
    int mid=l+r>>1;
    build(u<<1,l,mid),build(u<<1|1,mid+1,r);
}
  • push_up操作 :
//push_up操作,用子节点信息来更新父节点信息,以维护最大值为例
void push_up(int u){
    tree[u].v=max(tree[u<<1].v,tree[u<<1|1].v);
}
  • 查询操作 :
//query操作,用来查询某一段区间内的信息,以最大值为例
int query(int u,int l,int r){  //从u节点开始查询[l,r]区间内的某一信息
    if(tree[u].l>=l&&tree[u].r<=r) return tree[u].v;  //说明这一段的信息已经被完全包含,因此不需要继续向下递归,直接返回即可
    int res=0;
    //否则需要判断该递归那一边
    int mid=tree[u].l+tree[u].r >> 1;
    if(l<=mid) res=max(res,query(u<<1,l,r));  //递归左边并更新信息
    if(mid<r) res=max(res,query(u<<1|1,l,r));  //递归右边并更新信息,切记是mid<r,无等号
    return res;
}
  • 修改操作
//query操作,用来查询某一段区间内的信息,以最大值为例
int query(int u,int l,int r){  //从u节点开始查询[l,r]区间内的某一信息
    if(tree[u].l>=l&&tree[u].r<=r) return tree[u].v;  //说明这一段的信息已经被完全包含,因此不需要继续向下递归,直接返回即可
    int res=0;
    //否则需要判断该递归那一边
    int mid=tree[u].l+tree[u].r >> 1;
    if(l<=mid) res=max(res,query(u<<1,l,r));  //递归左边并更新信息
    if(mid<r) res=max(res,query(u<<1|1,l,r));  //递归右边并更新信息,切记是mid<r,无等号
    return res;
}

1275. 最大数

在这里插入图片描述
在这里插入图片描述

  • 这道题的两个操作可以被转化成上述两个操作,而这两个操作就是线段树的经典操作,动态修改某个位置上的数,动态查询某个区间内的最大值
  • 不过,这个问题比较特殊,每个位置只会被修改一次,因此这题可以用RMQ算法做,可以看作静态问题,不过局限性比较大,只能处理静态数据。但其实不是如此,这题由于每次修改的时候依赖上一次查询的,是动态的问题,不能把它全部读进来然后预处理去做的,所以不能用RMQ
  • 注意!!凡是只要修改某一个点,单点,的,都不需要用到懒标记,本身都可以做到Logn的复杂度;凡是涉及到区间的,修改整一个区间的,这样的操作,一般都需要加上懒标记,否则复杂度会退化
  • pushup由儿子算父节点的信息,pushdown是父节点的修改更新到儿子上面
#include <iostream>

using namespace std;

typedef long long ll;

const int N = 2e5 + 10;

int m, p;
struct Node
{
    int l, r;
    int v;  // 区间[l, r]中的最大值
}tr[N * 4];

void pushup(int u)  // 由子结点的信息,来计算父节点的信息
{
    tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
}

void build(int u, int l, int r)
{
    tr[u] = {l, r};
    if (l == r) return ;    // 叶子结点
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}

int query(int u, int l, int r)
{
    if (tr[u].l >= l && tr[u].r <= r) return tr[u].v;   // 树中结点,已经被完全包含在[l, r]中了
    
    int mid = tr[u].l + tr[u].r >> 1;
    int v = 0;
    if (l <= mid) v = query(u << 1, l, r);
    if (r > mid) v = max(v, query(u << 1 | 1, l, r));
    
    return v;
}

void modify(int u, int x, int v)
{
    if (tr[u].l == x && tr[u].r == x) tr[u].v = v;   // 叶节点,递归出口
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if (x <= mid) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);
        pushup(u);      // 回溯,用子结点的信息更新父节点信息
    }
}

int main()
{
    int n = 0, last = 0;    // n表示树中结点个数,last保存上一次查询的结果
    scanf("%d%d", &m, &p);
    
    // 初始化线段树,结点的区间最多为[1, m]
    build(1, 1, m);
    
    int x;
    char op[2]; // 可以读空字符
    while (m -- )
    {
        scanf("%s%d", op, &x);
        if (*op == 'Q')
        {
            last = query(1, n - x + 1, n);  // 查询[n - x + 1, n]内的最大值,u = 1,即从根结点开始查询
            printf("%d\n", last);
        }
        else
        {
            modify(1, n + 1, ((ll)last + x) % p);   // 在n + 1处插入  // 需要将last + x的结果强制转换成ll,否则可能会溢出
            n ++ ;  // 结点个数 ++
        }
    }
    
    return 0;
}

可持久化数据结构

  • 可持久化的前提 :这个数据结构本身的拓扑结构在操作过程中保持不变。
  • 可持久化数据结构 :1.trie的可持久化。2.线段树的可持久化-主席树。比如线段树,只会变线段树里面的信息,线段树本身是不会发生变化的。树状数组每个结点的儿子也是固定不变的,变的只是里面存的信息。堆也是,结构不会发生变化,完全二叉树;而有些数据结构会发生拓扑序的变化,比如平衡树,左旋和右旋,操作后结点之间的拓扑序会发生变化。
  • 可持久化解决的问题是什么 :可以存下来数据结构的所有历史版本
  • 核心思想 :只会记录当前版本和前一个版本不一样的地方。比如线段树,每次修改,如果n个结点,操作涉及最多4logn个结点,每次操作最多改变的节点数只有o(logn)个。即,最多只会记录mlogn个结点
  • 可持久化trie :
    在这里插入图片描述

1
在这里插入图片描述

  • 凡是有变化的点就裂开成一个新的点,比如根结点就每次必然有变化

256. 最大异或和

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 原始数据最多有3e5,再加上3e5个操作,因此整个序列长度最大是6e5
  • 2^24正好大于10 ^7,所以trie长度最多为24,再加上根结点,所以每次最多建立25个新结点
    在这里插入图片描述
#include <iostream>

using namespace std;

// 30w初始数据和30w新增,  而10的7次方小于2的24次方, 再加上根节点, 就是说每个数最多需要25位;
const int N = 6e5, M = N * 25;

int n, m;
int s[N];   // 前缀和序列
int tr[M][2];
int max_id[M];  // 用于记录当前根结点版本的最大id范围
int root[N], idx;

// i是第i个插入的数的i,k是现在取到第k位,p是上一个插入的数的结点号,q是当前节点号
void insert(int i, int k, int p, int q)
{
    // 如果记录结束了
    if (k < 0)
    {
        max_id[q] = i;  // 记录当前节点(可能会被后面公用)所能到达的最大范围i
        return ;
    }
    
    int v = s[i] >> k & 1;
    
    // 如果前一个节点存在当前节点没有的分支, 那就把当前节点的这个空的路径指过去, 这就相当于复制! v ^ 1指的是与v相反的一侧
    if (p) tr[q][v ^ 1] = tr[p][v ^ 1];
    
    tr[q][v] = ++ idx;  // 现在才是正常trie树插入,给v开辟一个新结点
    
    insert(i, k - 1, tr[p][v], tr[q][v]);   // 以v这个结点为父节点,继续往下处理
    
    max_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]);
}

int query(int root, int C, int L) // L为限制  即L ~ root之间的版本是符合要求的
{
    int p = root;
    for (int i = 23; i >= 0; i -- )
    {
        int v = C >> i & 1;
        
        if (max_id[tr[p][v ^ 1]] >= L) p = tr[p][v ^ 1];
        else p = tr[p][v];
    }
    
    return C ^ s[max_id[p]];
}

int main()
{
    scanf("%d%d", &n, &m);
    
    s[0] = 0;
    max_id[0] = -1;
    root[0] = ++ idx;
    
    insert(0, 23, 0, root[0]);
    
    for (int i = 1; i <= n; i ++ )
    {
        int x;
        scanf("%d", &x);
        s[i] = s[i - 1] ^ x;
        root[i] = ++ idx;
        insert(i, 23, root[i - 1], root[i]);
    }
    
    char op[2];
    int l, r, x;
    while (m -- )
    {
        scanf("%s", op);
        if (*op == 'A')
        {
            scanf("%d", &x);
            n ++ ;
            s[n] = s[n - 1] ^ x;
            root[n] = ++ idx;
            insert(n, 23, root[n - 1], root[n]);
        }
        else
        {
            scanf("%d%d%d", &l, &r, &x);
            // 至少要包住第r个点, 所以用r-1, 否则会因为异或把root[r]抵消掉
            // l也同理
            printf("%d\n", query(root[r - 1], s[n] ^ x, l - 1));
        }
    }
    
    return 0;
}

平衡树Treap

253. 普通平衡树

在这里插入图片描述

AC自动机

1282. 搜索关键词

在这里插入图片描述

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值