HHU暑期第三弹——数据结构进阶(线段树+树状数组+并查集)

本文详细介绍了数据结构中的线段树、树状数组和并查集。线段树用于高效解决连续区间的动态查询问题,其基本操作包括创建和单点更新。树状数组擅长区间查询和修改,通过巧妙的二分策略实现了O(logN)的复杂度。并查集是一种处理不相交集合合并和查找的数据结构,常见操作包括建立集合、合并集合和查找集合。文章通过实例深入浅出地解析了这些数据结构的原理和实现方法。
摘要由CSDN通过智能技术生成

第三弹数据结构进阶的主要内容有以下几部分:线段树、树状数组、并查集。

一、线段树

一:线段树基本概念

1:概述

线段树,类似区间树,是一个完全二叉树,它在各个节点保存一条线段(数组中的一段子数组),使用线段树可以快速的查找某一个节点在若干条线段中出现的次数主要用于高效解决连续区间的动态查询问题,由于二叉结构的特性,它基本能保持每个操作的复杂度为O(lgN)!线段树的作用一般都不是对线段进行处理,所以即使不包含某个区间,但是其端点还是存在的,所以我们只需要对它的端点进行处理就好了。

性质:父亲的区间是[a,b],(c=(a+b)/2)左儿子的区间是[a,c],右儿子的区间是[c+1,b],线段树需要的空间为数组大小的四倍


2:基本操作

【以下以 求区间最大值为例】
先看声明:

#include <iostream>  
#include <cstring>  
#include <cstdio>  
#include <algorithm>  
#include <cmath>  
using namespace std;  
const int maxn=50000;  
struct node{  
    int l;  
    int r;  
    int sum;  
}tree[(maxn+5)*9];  
int a[maxn+5];


【创建线段树(初始化)】:

       由于线段树是用二叉树结构储存的,而且是近乎完全二叉树的,所以在这里我使用了数组来代替链表上图中区间上面的红色数字表示了结构体数组中对应的下标。

在完全二叉树中假如一个结点的序号(数组下标)为 I ,那么 (二叉树基本关系)

I 的父亲为 I/2,

I 的另一个兄弟为 I/2*2 或 I/2*2+1

I 的两个孩子为 I*2 (左)   I*2+1(右)

有了这样的关系之后,我们便能很方便的写出创建线段树的代码了。

void build(int l,int r,int index){  
    tree[index].l=l;  
    tree[index].r=r;  
    if(l==r){  
        tree[index].sum=a[l];  
        return;  
    }  
    int middle = (l+r)/2;  
    build(l, middle, 2*index);  
    build(middle+1, r, 2*index+1);  
    tree[index].sum=tree[2*index].sum+tree[2*index+1].sum;  
}  

【单点更新线段树】:

       由于我事先用 father[ ] 数组保存过 每单个结点 对应的下标了,因此我只需要知道第几个点,就能知道这个点在结构体中的位置(即下标)了,这样的话,根据之前已知的基本关系,就只需要直接一路更新上去即可。

void update(int number,int index,int ans)  //单点更新  
{   
    tree[index].sum += ans;  
    if(tree[index].l==tree[index].r&&tree[index].l==number){  
        return;  
    }  
    int middle = (tree[index].l+tree[index].r)/2;  
    if(middle>=number)//这个数在右边子树 
	{  
        update(number, 2*index, ans);  
    }
	else//这个数在左边子树 
	{  
        update(number, 2*index+1, ans);  
    }  
}  


【查询区间】: 
       将一段区间按照建立的线段树从上往下一直拆开,直到存在有完全重合的区间停止。对照图例建立的树,假如查询区间为 [1,6] 
                                                      

最下边的区间为完全重合的区间。

int getSum(int l,int r,int index)//index代表结点的编号,一般都是从1开始 
{  
    if(tree[index].l==l&&tree[index].r==r)
	{  //找到了这个区间 
        return tree[index].sum;  
    }  
    int middle = (tree[index].l+tree[index].r)/2;  
    if(middle<l)
	{  // 区间全在右半树 
        return getSum(l, r, 2*index+1);  
    }
	else if(middle>=r)
	{  //区间全在左半树 
        return getSum(l,r,2*index);  
    }
	else
	{  //区间左右树都有 
        return getSum(l, middle, 2*index)+getSum(middle+1, r, 2*index+1);  
    }  
}  
  

强烈推荐线段树博文--->夜深人静写算法


二、典型模板

线段树求和模板//只适用于区间都在左右同侧的情况。

<span style="font-size:14px;">#include<iostream>
#include<string.h>
#include<stdio.h>
#define lson id*2    //id的左子树编号
#define rson id*2+1    //id的右子树编号
using namespace std;
int ans;
//id是每个所在区间的编号
//l和r是id所代表区间的左右节点

int tre[2000700];
int a,b,c,d,n,m,k;
int push(int id)
{
    tre[id]=tre[lson]+tre[rson];//把子叶的值拿来更新父亲节点的值
    return 0;
}


int build(int id,int l,int r)   //树的初始化,必须的操作,虽然我也不知道为什么,好像是为了给每个点编号
{
    if (l>r)    return 0;//去除非法状态,以下每个子函数都要写
    if (l==r)
                {
                tre[id]=0;
                return 0;

                }
    int mid=(l+r)/2;
    build(lson,l,mid);//递归左右子树
    build(rson,mid+1,r);//注意是mid+1
    push(id);
}
int add(int id,int l,int r,int pos,int num)     //在编号为id的区间【l,r】中,在pos位置的值增加num,同时更新它的所有祖宗节点的值都加num
{
    if(l>r) return 0;//判断非法状态
    if (l==r && r==pos)     //如果到了最后一层,更新值,记得return掉
        {
        tre[id]+=num;
        return 0;
        }
    int mid=(l+r)/2;
    if (pos<=mid)//如果pos在当前区间中点的左边,则在左子树中递归,反之在右子树中递归
        {
            add(lson,l,mid,pos,num);
        }
    if (pos>=mid+1)
        {
        add(rson,mid+1,r,pos,num);
        }
    push(id);//下一层递归完成后对本层进行更新
}
int query(int id,int l,int r,int L,int R)
{
    if (l>r || L>R)    return 0; //非法状态
    if (l>=L && r<=R)
        {
        ans+=tre[id];
        return 0;//一定记得return,都则id会大的飞起
        }
    int mid=(l+r)/2;
    if (L<=mid)    //如果所查询区间和【l,r】的左半边有重合,则递归求左半边,右半边同理
        {
        query(lson,l,mid,L,R);
        }
    if (mid+1<=R)
        {
        query(rson,mid+1,r,L,R);
        }
    return 0;
}
int main()
{

/******************
使用方法
1,初始化: build(1,1,maxx) maxx是所开数组的长度或者题目要求的长度(第二个1端点可以被更改)
2,给节点加值 add(1,1,maxx,pos,num)  pos是位置,num是数值</span><span style="font-family: Arial;"><span style="font-size:12px;">(第二个1是区间端点可以被更改)</span></span><span style="font-size:14px;">
3,查询区间[L,R]的和   query(1,1,maxx,L,R)</span><span style="font-family: Arial;">(第二个1是区间端点可以被更改)</span><span style="font-size:14px;">
******************/
build(1,1,1000);
int n,m,a,b;

cin>>n>>m;
for (int i=1;i<=n;i++)
        {
        scanf("%d%d",&a,&b);
        add(1,1,m,a,b);
        }
for (int i=1;i<=m;i++)
        {
        scanf("%d%d",&a,&b);
        ans=0;
        query(1,1,m,a,b);
        cout<<ans<<endl;
        }
}</span>

单点查询、区间查询最大值模板

<span style="font-family:microsoft yahei;font-size:18px;color:#555555;">/* 
线段树模板,先自学线段树原理
这里单点修改,区间查询最大值的代码
最小值只需要把max改成min即可
注意ans初始化
*/ 
#include<iostream>
#include<string.h>
#include<stdio.h>
#define lson id*2  //左儿子
#define rson id*2+1    //右儿子
using namespace std;
int tre[2000000];  //线段树
int ans=-19943;
int a,b,c,d,n,m;
int pushup(int id)   //儿子节点的信息传递到父亲节点
             //根据需要调节代码,下同
{
    tre[id]=max(tre[lson],tre[rson]);
    return 0;
}
int build(int id,int l,int r) //初始化
{
    if (l>r) return 0;
    if (l==r)
        {
        tre[id]=1;
        return 0;
        }
    int mid=(l+r)/2;
    build(lson,l,mid);
    build(rson,mid+1,r);//递归构造左右子树
    pushup(id);
    return 0;
}
int add(int id,int l,int r,int pos,int num)    //id是当前节点的编号,l&&r是id所代表区间的左右边界,pos是要增值的那个点(就比如要对区间[1,7]里的5所在位置增值,那么这个pos就是5),num是要增加的数值 
{ 
    if (l>r) return 0;//线段树中容易发生这种错误,因为每次mid+1可能大于r
    if (l==r && r==pos)    //如果l==r说明递归到了最底层,直接更新节点数值
        {
        tre[id]=num;
        return 0;
        }
    int mid=(l+r)/2;
    if (pos<=mid)    //如果要增值的点在mid左边,递归左边
        add(lson,l,mid,pos,num);
    if (pos>=mid+1)        //反之递归右边,注意这里是mid+1
        add(rson,mid+1,r,pos,num);
    pushup(id);    //更新当前节点的值
    return 0;
}
int
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值