高阶数据结构

并查集

并查集是一种可以动态维护若干个不重叠的集合。具体来说善长动态维护许多具有传递性的关系。同时也可以在一张无向图中维护节点之间的连通性。

既然是高阶我们重点来讲带有‘’扩展域“和“边带权”的并查集。

“边代权的并查集”

在这类题中我们通常要维护的是集合中点的一些权值问题。如一个节点到父节点距离是x,另一个节点到父节点距离为y。那么它们两个的距离就是abs(x - y)。因此在该类问题上我们用一个数组d,用d【x】保存节点x到父节点fa【x】之间的边权。

如何在路径压缩的时候更新d数组呢?
我们回想路径压缩的过程。对于d数组,我们只需要在回溯的时候加上其父节点的d即可,即

int find(int x){
	if(x == fa[x])return x;
	int root = find(fa[x);
	d[x] += d[fa[x]];
	return fa[x] = root;
}

以下图为例,我们寻找5的父节点的时候,root 的值先为 3 ,当前层的x为4 。 d[x] += d[fa[x]] ; 由于d[fa[x]] 为0 , 所以d[x] 还是4 。然后到x为5的层数,root 由于 上一层返回值是 fa[x] = root;所以还是3 , 不过由于fa[3] 没有更新还是4 ,所以d[x] += d[fa[x]]; 后d[x] = 4 + 2 = 6 ,最后 d[5] = 3 。很好我们完成了路径的压缩和更新问题。
在这里插入图片描述
在这里插入图片描述
那合并什么说法呢?
为了方便能得到最全面的情况,以下面为例,假设现在多了 7 5 20
那么我们如何合并呢?我们看图很容易知道缺 1 和 3 之间的边 , 同时边权也容易知道是 5 。 那么如何能得到一个通用的公式呢。我们先回想一下一般的合并,我们先得到 7 和 5 的父节点分别为 1 和 3 ,我们让 1 为 3 的儿子节点。那么现在我们缺的是 1 到 3 的边的权值 ,即 d[1]。 对于在一个节点中的两个节点的距离为 abs(d[x] - d[y]),所以,现在20 = d[7] - d[5] ,对于现在以3为根节点后,而 d[7] = 之前的d[7] + d[1] . 所以我们得到
d [ 1 ] = 20 + d [ 5 ] − d [ 7 ] d[1] = 20 + d[5] -d[7] d[1]=20+d[5]d[7] . 加粗的地方请留意,不是 d[x] + d[y] 。
在这里插入图片描述
在这里插入图片描述

void merge(int x, int y, int z ) {
	int fx = get(x), fy = get(y);
	if (fx != fy) {
		fa[fx] = fy, d[fx] = z + d[y] - d[x];
	}
}

带“扩展域”的并查集

带权值的并查集和带“扩展域”的并查集都是为了解决传递关系不止一个的问题。带“扩展域”的并查集是扩展了多个域来应对多种关系。也就是说将原本的一个点拆分成几个点来进行处理。具体的话建立看一下 食物链 这个经典题目。

要正确理解好,就需要将题面上的表示关系的动作转化为关系。

就以食物链为例,将 一个节点分为三的目的是,能处理好多对关系。
这些是以关系建立起来的域,是关系
X: 本身域
X+n :捕食域
X + 2n : 天敌域

这是什么意思呢?本身域就是与自己同类的一个集合, x + n :域里构成的集合是由x的捕食的节点组成的。 x + 2n :域里构成的集合是由x的天敌的节点组成的。

对于每一种关系我们都需要在三个域中建立起相应的关系。
如 x 吃 y ,那么就应该有 x 是 y 的天敌 , y 是 x 的 捕食对象。同时由三边有 , y 的 捕食对象是 x 的天敌。

即, merge(x , y+ 2n) 的含义是 x 和 y + 2n 是同一个集合。
Merge(x + n , y ) ,
Merge(x + 2n , y + n)

下面这个题和食物链类似
题目转送们

由于是剪刀石头布这个比较常见的东西我们就可以更好的来理解扩展并查集了。 我们对于其中一个节点x , 与它同为一个域(集合)的是x本身 , 比它小的我们 用x + n , 那么x + 2n 就表示大于x的关系的集合。我们通过举一个例子来说明把。 x > y . 那么 表示什么呢? x 本社应该与 y的 大于关系的域(y+2n)在同一个集合 ,(对于扩展域我们有一个小技巧,我们对于每一个域中都要考虑),因此先我们考虑一下 x + n 这个域,这个里边是比x小的集合,那么在这里y比x小,因此应该是y本身这个域与x + n 同在一个集合。我们再来考虑一下 x + 2n ,我们由剪刀石头布很容易知道 , 比 x 大的 应该是哪些与y有小于关系的点。 在这个问题,还有一个点就是裁判的数量。我们就枚举一下裁判是谁 , 当且仅当我们在不讲i这个人的关系加入其中的时候任然没有矛盾就说明,这个i是一个裁判。

树状数组

树状数组的基本用途是维护一个序列的前缀和。首先我们需要理解这个数组的含义是什么。假设数组为c[x] , 这个里边维护的值是区间 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x) +1 , x] [xlowbit(x)+1,x]中数的和。有以下三个性质:

  1. 每一个内部节点c[x]保存以它为根的所有子树的和。
  2. 每一个内部节点c[x]的子节点个数等于lowbit(x)的位数
  3. 除树根外,每一个内部节点c[x]的父节点为c[x+lowbit(x)]
  4. 树的深度为 O(logn)

查询前缀和

对于每一个数x , 可以将区间[1,x] 利用二进制拆分为log(x)个小区间。具体来说,好比12 = 1100 , 那么可以分为两个区间,[1 , 8 ] , [9 , 12]. 回想c[x] 的定义 , 那么c[12] 存储的是区间[9 , 12] 的和,那么还缺[1,8]的什么办,而这正好是12 - lowbit(12) = 8 , 的c[8]对应的区间。因此我们由如下操作:

int ask(int x){
	int sum = 0 ;
	for( ; x ; x-=lowbit(x)) sum += c[x];
	return sum;
}

因为树的深度为log(n)因此我们就可以在O(logn)的时间内求出前缀和了。
当然我们要求[ l ,r ] 区间内数的和的时候 , 只需要aks(r) - ask(l - 1 )即可。

单点增加

就是说我们要对原序列a[i] + y , 同时又要正确的维护前缀和和。有与包含a[i]的区间只有c[i]以及其父节点。由性质3我们就可以得到以下代码:

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

初始化树状数组

树状数组求逆序对

利用树状数组求序列的逆序对个数是利用在集合a上的数值进行树状数组的技巧。为什么这么说呢?我们对于位置i , 查找后边比它小的个数。我们不一定需要询问具体的a[i]的值,我们只需要查询,后边是否有值为[0,a[i] - 1 ] 这个区间内。
因此我们就可以后序遍历整个数组,每一次进行一次询问,之后将a[i]值得个数加1.在这里树状数组记录c[x] 记录的是 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x) +1 , x] [xlowbit(x)+1,x]中数出现过的总次数。

代码:

int res = 0;
for(int i = n ; i ; --i){
	res += ask(a[i] - 1 );
	add(a[i], 1);
}

树状数组的扩展应用

区间修改和区间查询

  1. 对于区间修改:因为维护的是前缀和,那么我们就可以利用前缀和的技巧,对于修改一个区间[l,r] ,我们只需要在l处加上d , 在r+1处减去d即可。将区间修改转变为两个单点修改。具体来说我们维护的不再是具体的值而是维护指令(修改)的累积影响.具体来说,我们维护的这个数组初始为0,b数组对于位置x的前缀和就是经过这些指令后a[x]添加的值。
    对于询问第x个数的值我们只需要 a[x] + ask(x) ;
  2. 上面说的是区间修改后单点询问,那么如何区间修改和区间询问呢,对于区间询问也就是a[1~x]的前缀和如何求,如果直接用上面的,我们也是可以求的,这不过时间复杂度为上升。我们想想如何优化,对于区间 a[1~x] 整体增加的值为 1 ~ x 没一个b的前缀和.

维护一个01序列

这是一个很经典的一类题目,很具有思维性,因此就拿出来讲一下。
题目转送们

解题思路

首先我们容易知道,对于同一个编号,在最后出现的,肯定是放在编号对应的位置的。难点在于哪些和它同编号的但是在被插队之后往后移动的情况。对于这些编号,我们知道一个是它受限于前边的编号,即它不可能出现在自己初始编号的前边,第二个就是后边与其同一编号的人。它会因为这些人而被一直往后移动。由于该题编号是0开始我们可以在读入的时候就都加上1.之后考虑维护一个01序列,起始全是1。然后后序遍历,对于每一个编号,二分来查看01前缀和恰好为其编号的位置是多少,然后将这个位置add(pos , -1);

线段树

以下是线段树讲得比较好的一篇博客了就不继续讲了。
线段树
下面我们主要讲的是线段树维护区间最大连续子段和 ,以及维护区间的众数和扫描线。

区间最大连续子段和

先以下篇博客,
区间最大子段和
下面就来具体的对博客进行补充一下。首先是ls和rs,由于两者类似因此就讲讲ls。想想我们在求一个以左端点为起始点的最大连续字段和会什么求呢?由于线段树是将该区间分为了左右两部分,那么我们想到的一个就是左半边的ls和左半边的sum加右半边的ls(是为了保证区间的连续性)。
而m s msms有三种情况:

该区间内的ms是左儿子的ms
该区间内的ms是右儿子的ms
该区间内的ms是左儿子的rs+右儿子的ls.(也是为了保证区间的连续性)

具体来说我们一个线段树的节点维护了四个信息:区间和,紧靠左端的最大连续子段和lmax , 紧靠右端的最大连续子段和rmax , 以及区间最大字段和。

t[u].sum = t[u<<1].sum + t[u<<1|1].sum;
t[u].lmax = max(t[u<<1].lmax , t[u<<1].sum + t[u<<1|1].lmax];
t[u]rmax = max(t[u<<1|1].rmax , t[u<<1|1].sum + t[u<<1].rmax];
t[u].dat = max(max(t[u<<1].dat , t[u<<1|1].dat) , t[u<<1].rmax + t[u<<1|1].lmax);

维护长度为n的01串

题目转送们

与上一题类似的思想因此决定要进行总结一下。
题目大意:

然我们动态的维护一些区间,之后询问是否有连续为0(即没有人住)的长度为n,有的话取出开头最小的编号。

解题思路:

  • 对于线段树的题目我们首先需要想的是我们的线段树维护的是区间的什么?且维护的这个东西要满足区间可加性。首先对于每一个区间我们感兴趣的是这个区间中0字符的长度,因此我们就维护这个东西。那么如何维护呢?
    首先,我们类似上题的思想,我们用len 表示这个区间最长的0子串长度,用ld表示靠近左端点最长的0子串长度,用rd表示靠近右端点最长的0子串长度。这里可能有人会疑惑如果最长的在中间什么办?这其实没有关系,中间这个部分在拆分为区间的时候一定会出现在某一个区间的左端点开始或者右端点开始。因此我们采用左,右两端一个是可以涵盖中间的情况,另一个是了可以更好的利用线段树来进行维护。
  • 那么这个长度是什么得到的呢?这个最大长度我们一般是在左区间的最大长度,右区间的最大长度和左区间的右端点开始的最大长度和右区间左端点开始的最大长度(满足连续)三者中取一个最大值。
    讲完区间最大长度我们就来讲讲,以左右端点开始的最大长度。我我们就以左端点为例。 对于左端点,首先我们赋值为其左区间的ld,当左区间的ld = 最区间的节点个数的时候,说明左区间是全为0的,那么我们就将右区间的ld与其合并。
  • 同样在这里涉及到了区间修改,因此我们利用lazy标记来维护。我们将lazy标记分为两种,因为这个维护的是区间是否被覆盖的问题(不需要加减值)。例如当为1的时候就表明这个区间为空,那么就让这个区间的len = ld = rd = r - l + 1 .因为全为0l,如果为2就表示被覆盖了那么就都赋值为0.
  • 还有一个难点是

扫描线

我们需要知道扫描线维护的是什么
假设有以下矩形我们要求出它们的面积和。
在这里插入图片描述
现在我们用扫描线划分后的图形为下
在这里插入图片描述
当我们扫到黄颜色的坐标的是后我们需要知道打问号的线段的长度是多少,很明显是前一个矩形的边长加上第一个的边长然后减去公共部分。我们要维护的就是当前线段的长度。那么我们要如何维护呢一个就是直接利用区间来进行维护,也就是魔改线段树,另一个就是离散化y坐标后再进行。这里我们讲讲如何离散后用线段树维护能得到正确的线段长度。
魔改线段树版本

首先我们离散化后用来建树的下标是不同纵坐标的个数。
在这里插入图片描述
上面是离散化后的坐标,不过为了正确维护线段的长度,我们每一个节点存储的长度是 ys[t[p].r + 1] - ys[t[p].l] 也就是区间 [l,r+1]的和。好处是叶子节点存储的就不是0这个无用的信息了,就以 第一个边长( 6 , 8) 和 (1,7) , 在修改的时候变为了(6,7),(1,6)(是便于读取下标从0开始)之后为例,那么它更新后的线段树如下,
在这里插入图片描述
当询问到我在上边标记黑色的线段的长得的时候,在询问的时候就会将我在图中标出的蓝色的区域的长度进行累加到根节点,而这就是黑色线段的长度了。

代码如下:

#include<stdio.h>
#include<vector>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;

const int N = 1E5 + 10;
int n ,  m;
double res;
struct segment{
    double x , y1,y2;
    int k;
     bool operator < (const segment & t)const{
        return x < t.x;
    }
}seg[N*2];

struct stree{
    int l , r , cnt ;
    double len;
}tr[N*8];

vector<double>ys;

void build(int p , int l , int r){
    tr[p].l = l , tr[p].r = r;
    if(l == r){
        tr[p].cnt = tr[p].len = 0;
        return ;
    }
    int mid = (l + r )>> 1;
    build(p<<1 , l , mid);
    build(p<<1|1 , mid+1,r);

}

// 二分查找一个值对应的离散后的值 , 即线段树中的下标
int find(double y){
    return lower_bound(ys.begin() , ys.end() , y) - ys.begin();
}


void pushup(int p){
    //tr[p].r  和 tr[p].l 就是下标所有是直接拿来取值
    if(tr[p].cnt) tr[p].len = ys[tr[p].r + 1] - ys[tr[p].l];
    else if(tr[p].l == tr[p].r)tr[p].len = 0;
    else tr[p].len = tr[p<<1].len + tr[p<<1|1].len ; // 如果没有被覆盖过肯定可以直接通过左右两边求
}


void modify(int u , int l , int r, int k){
    if(l <= tr[u].l &&tr[u].r <= r){
        tr[u].cnt += k ;
        pushup(u);
        return ;
    }else{
        int mid = (tr[u].l + tr[u].r) >>1;
        if(l <= mid) modify(u*2 , l , r , k);
        if(r > mid) modify(u<< 1 | 1 , l , r ,k);
        pushup(u);
    }
}


int main(){
    int T =  1;
    while(scanf("%d",&n) , n ){
        ys.clear();
        for(int i = 0 ; i < n ; ++i){
            double x1 , x2 , y2 ,y1;
            scanf("%lf%lf%lf%lf",&x1,&y1 , &x2,&y2);
            ys.push_back(y1) , ys.push_back(y2); // ys 里边存的是值,到时候去建线段树用的是元素个数
            seg[i*2] = {x1 , y1,y2 , 1} , seg[i<< 1 | 1] = {x2 , y1,y2,-1};
        }
        sort(ys.begin() , ys.end());
        ys.erase(unique(ys.begin() , ys.end()) , ys.end()); // 去重
        m = ys.size();
        build(1 , 0 , m - 2); // 那去重后的元素个数来建立线段树
        sort(seg , seg + n*2); //
        res = 0;
        for(int i = 0 ; i < 2*n ;){  // 遍历所有的节点
            int j = i ;
            while(j < 2*n&& seg[i].x == seg[j].x) j ++;
            if(i) res += tr[1].len * (seg[i].x- seg[i - 1].x); // 根节点是当前区间的高度?
            while(i < j){
                modify(1 , find(seg[i].y1) , find(seg[i].y2) - 1  , seg[i].k);
                i++;
            }
        }
        printf("Test case #%d\n", T ++ );
        printf("Total explored area: %.2lf\n\n", res);
    }

    return 0;
}

分块

博主先咕咕下,需要去训练了。训练完会尽快更新的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

落春只在无意间

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值