首页 > 其他分享 >数据结构进阶

数据结构进阶

时间:2023-02-25 22:22:24浏览次数:35  
标签:进阶 树状 lowbit ll 数组 数据结构 节点 define

并查集

并查集,是一种树形数据结构,用于维护不相交的集合。

基本原理

每个集合用一棵树来表示,树根的编号就是整个集合的编号。
每个节点存储了它的父节点。

模板题(AcWing.836

一共有 n 个数,编号是 1∼n,最开始每个数各自在一个集合中。

现在要进行 m 个操作,操作共有两种:

  1. M a b ,将编号为 \(a\) 和 \(b\) 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b ,询问编号为 \(a\) 和 \(b\) 的两个数是否在同一个集合中;

并查集实现

思路

维护一个数组 \(p\) 来表示每个节点的父节点。

判断树根

显然,如果一个节点的父节点是它本身,那它就是根节点。

代码:

inline bool is_root(ll x)
{
    if(p[x]==x) return 1;
    else return 0;
}

这个在本题中用不着。

合并集合

可以发现,如果要将 \(a\) 并入 \(b\) (其实等同于将 \(b\) 并入 \(a\) ),仅需在 \(a\) 的根节点和 \(b\) 的根节点间加入一条边。(原创图)

image

Q:那如果题目数据特别毒瘤,不间断地合并很多集合怎么办?那不就被卡成 \(\operatorname{O(n)}\) 了吗?
A:路径压缩的 find() 只会跑一遍 \(\operatorname{O(n)}\) ,就会重新变为 \(\operatorname{O(1)}\) 。

代码:

p[find(a)]=find(b);

求 \(x\) 的集合编号

递归查找。

inline ll find(ll x)
{
    if(p[x]!=x) p[x]=find(p[x]);//不断查找父节点,直到找到根节点
    return p[x];
}

以上代码添加了路径压缩,也就是 p[x]=find(p[x]) 在找到父节点的同时顺带更新了 p[x] 的值。

图示(原创图):

image

相当于你问你爸爸你的祖先是谁,你爸爸也不知道,爸爸就去问爷爷,然后你的爷爷也不知道,爷爷就去问你的太爷爷,你的太爷爷年纪太大了,啥也不记得,就去问你的祖先。

代码

#include <bits/stdc++.h>
#define ll long long
#define rll register ll
#define cll const ll
#define N 100005
using namespace std;
inline ll read()
{
    rll x=0;bool f=1;static char c=getchar();
    while(!isdigit(c)){if(c=='-') f=0;c=getchar();}
    while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return (f==1)?x:-x;
}
inline void write(ll x)
{
    if(x<0) putchar('-'),x=-x;
    if(x>9) write(x/10);
    putchar(x%10+48);
}
ll n=read(),m=read(),p[N];
inline ll find(ll x)
{
    if(p[x]!=x) p[x]=find(p[x]);
    return p[x];
}
int main()
{
    for(ll i=1;i<=n;i++) p[i]=i;
    while(m--)
    {
        char op[2];ll a,b;
        scanf("%s%lld%lld",op,&a,&b);//这里用 read() 会死
        if(op[0]=='M') p[find(a)]=find(b);
        else
        {
            if(find(a)==find(b)) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

树状数组

树状数组(Binary Indexed Tree,BIT),即二叉索引树,适用于一些区间修改操作。

树状数组和线段树可是亲兄弟了,但他俩毕竟还有一些区别:树状数组能有的操作,线段树一定有;线段树有的操作,树状数组不一定有。

树状数组和线段树的复杂度同级,单次操作都是 \(\operatorname{O(log_2n)}\) ,但树状数组常数更小,速度比线段树快得多,代码简单清晰,因此,能用树状数组时要尽量用。

不过,树状数组不像线段树是那样要用 biuld() 建出一棵树,它是用数组模拟树形结构。

图解

下图解释了树状数组的存储模式,\(a\) 数组负责存储元素真正的值,\(c\) 数组就是树状数组,负责管理 \(a\) 数组, \(c_i\) 出发的箭头指向其管理的元素。比如要进行区间求和操作, \(c_i\) 就表示它出发的箭头指向的所有元素之和。

image

这就是树状数组的存储方式。

重点——lowbit函数

\(C_1=A_1\)
\(C_2=A_1+A_2\)
\(C_3=A_3\)
\(C_4=A_1+A_2+A_3+A_4\)
\(C_5=A_5\)
\(C_6=A_5+A_6\)
\(C_7=A_7\)
\(C_8=A_1+A_2+A_3+A_4+A_5+A_6+A_7+A_8\)

\(......\)

可以发现,这颗树是有规律的。

设k为i的二进制中从最低位到高位连续零的长度,有:
\(c_i=a_{i-2^k+1}+a_{i-2^k+2}+...+a_i\)

求和:根据上面的式子,可得:
\(sum_i=c_i+c+c_{(i-2^{k1})-2^{k2}}+...\)
\(sum_i=2^k\)

可得:

\(2^k=i\) & \(i^{i-1}\)
\(2^k=i\) & \(-i\)

这就是 lowbit() 函数,它可以找到 \(x\) 在二进制上的第一个 1 的位置。

lowbit函数代码

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

一维树状数组

在 \(c\) 数组中更新 \(a\) 数组的元素

树状数组的重点就是利用二进制的变化,动态地更新树状数组。

下图解释了树状数组的元素更新原理。

image

由图可知, \(a_i\) 如果要更新,那么首先管理它的直接上级 \(c_i\) 就需要最先更新,然后利用 lowbit() 找到 \(c_i\) 的父节点并更新,直到更新完所有的节点。

区间修改代码

写法1
void add(ll x,ll y)
{
    while(x<=n)
    {
        c[x]+=y;
        x+=lowbit(x);
    }
}
写法2
void add(ll x,ll y)
{
    for(ll i=x;i<=n;i+=lowbit(i))
        c[i]+=y;
}

如何建出树状数组

如果你看懂了上面的内容,再稍加思考,就可以想到。

其实只要在输入时将 \(a_i\) 更新进去即可。

初始化代码

for(ll i=1;i<=n;i++)
{
    a[i]=read();
    add(i,a[i]);
}

如何查询区间和

请看下面一个例子:

如果你有几张钱币,分别是 1、2、4、8 元,那么你就可以凑出十五元以内的任何钱数,如:

\(1=1\)
\(2=2\)
\(3=2+1\)
\(4=4\)
\(5=4+1\)
\(6=4+2\)
\(7=4+2+1\)
\(8=8\)
\(9=8+1\)
\(10=8+2\)
\(11=8+2+1\)
\(12=8+4\)
\(13=8+4+1\)
\(14=8+4+2\)
\(15=8+4+2+1\)

同样的,利用 \(c\) 数组的不同组合,也可以求出任何的区间和。

区间求和的顺序与区间更新有点区别,区间更新的顺序是通过子节点一级级更新祖先,区间查询是从后往前用 lowbit() 找到要加的元素。
首先定义一个 \(ret\) 来存储区间和。
然后用 lowbit() 找到要加的元素并加上它。

区间求和代码

写法1
ll sum(ll x)
{
    ll ret=0;
    while(x)
    {
        ret+=c[x];
        x-=lowbit(x);
    }
    return ret;
}
写法2
ll sum(ll x)
{
    ll ret=0;
    for(ll i=x;i;i-=lowbit(x))
        ret+=c[x];
    return ret;
}

代码

#include <bits/stdc++.h>
#define ll long long
#define rll register ll
#define cll const ll
#define N 500005
using namespace std;
inline ll read()
{
    rll x=0;bool f=1;static char c=getchar();
    while(!isdigit(c)){if(c=='-') f=0;c=getchar();}
    while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return f?x:-x;
}
inline void write(ll x)
{
    if(x<0) putchar('-'),x=-x;
    if(x>9) write(x/10);
    putchar(x%10+48);
}//卡常
cll n=read();
ll m=read(),c[N];
void add(ll i,ll x){while(i<=n) c[i]+=x,i+=(i&-i);}
ll answer(ll x)
{
    rll ret=0;
    while(x) ret+=c[x],x-=(x&-x);
    return ret;
}//树状数组
int main()
{
    for(rll i=1;i<=n;i++) add(i,read());
    while(m--)
    {
        cll op=read(),l=read(),r=read();
        if(op==1) add(l,r);
        else write(answer(r)-answer(l-1)),putchar('\n');
    }
    return 0;
}

二维树状数组

模板题(luogu.P4054/AcWing.2369

一个 \(n × m\) 的方格,初始时每个格子有一个整数权值。接下来每次有 2 种操作:

  1. 改变一个格子的权值
  2. 求一个子矩阵中某种特定权值出现的个数

思路

二维树状数组加一维就行,再多写一维 \(color\) ,表示权值。

代码

#include <bits/stdc++.h>
#define ll int
#define rll register ll
#define cll const ll
#define N 305
#define M 105
using namespace std;
inline ll read()
{
    rll x=0;bool f=1;register char c=getchar();
    while(c<48||c>57){if(c=='-') f=0;c=getchar();}
    while(c>=48&&c<=57){x=x*10+(c^48);c=getchar();}
    return f?x:-x;
}
inline void write(ll x)
{
    if(x<0) putchar('-'),x=-x;
    if(x>9) write(x/10);
    putchar(x%10+48);
}//卡常
cll n=read(),m=read();
ll q,a[N][N],c[N][N][M];
inline void add(ll x,ll y,ll k,ll color)
{
    for(rll i=x;i<=n;i+=(i&-i))
        for(rll j=y;j<=m;j+=(j&-j))
            c[i][j][color]+=k;
}
inline ll answer(ll x,ll y,ll color)
{
    rll ret=0;
    for(rll i=x;i;i-=(i&-i))
        for(rll j=y;j;j-=(j&-j))
            ret+=c[i][j][color];
    return ret;
}//二维树状数组
int main()
{
    for(rll i=1;i<=n;i++)
        for(rll j=1;j<=m;j++)
            a[i][j]=read(),add(i,j,1,a[i][j]);
    q=read();
    while(q--)
    {
        cll op=read();
        if(op==1)
        {
            cll x=read(),y=read(),c=read();
            add(x,y,-1,a[x][y]);a[x][y]=c;add(x,y,1,a[x][y]);
        }//修改,记得先减去之前的颜色
        else
        {
            cll x1=read(),x2=read(),y1=read(),y2=read(),c=read();
            cll num=answer(x2,y2,c),num2=answer(x1-1,y2,c);
            cll num3=answer(x2,y1-1,c),num4=answer(x1-1,y1-1,c);
            write(num-num2-num3+num4),putchar('\n');
        }//查询区间和,二维差分
    }
    return 0;
}

标签:进阶,树状,lowbit,ll,数组,数据结构,节点,define
From: https://www.cnblogs.com/wangxuzhou-blog/p/advanced-data-structure.html

相关文章