<福州集训之旅Day4> | 数据结构II |

目录

基础数据结构 II

提高数据结构

并查集
树状数组
线段树


<更新提示>

<第一次更新>主要内容为:特点与性质,用法,及功能代码,不附例题


<正文>

并查集

特点与性质:并查集,即一种用于快速“合并集合”和“查找集合”的算法。“并”就是可以将集合合并,“查”就是可以查询元素,集合直接的关系,“集”就说明这个算法使用了处理集合问题的。在并查集中,每个集合都有一个代表元,代表元用于代表和索引出集合内的其他元素。一个集合内的元素以树形结构存储下来,代表元即根节点,节点之间的父子关系代表着数据被引入集合的关系。并查集中用一个father数组,用于记录树形结构中节点的父亲,如果x位代表元,那么father[x]=x。

功能代码:
初始化:
起初先将每一个元素构建为一个仅包含自己本身的集合,每个集合的代表元就是自己,自己的父亲也是自己,就算是对并查集初始化了。

for(int i=1;i<=n;++i)
father[i]=i;

判断数据所属集合:
判断数据所属集合,找到该集合的代表元即可,所有只要不断查找当前节点的祖先即可,直到找到father[x]=x,x即代表元。

int findfather(int x)
{
    if(father[x]==x)return x;
    else return findfather(father[x]);
}

这是对判断数据所属集合的普通算法,但在需要频繁查询时,递归查询就显得效率低下,我们需要对查询算法进行改进:每一次查询时,对查询的结果进行保存,下次再查询时直接调用即可,这样就能达到路径压缩的效果。

int findfather(int x)
{
    if(father[x]==x)return x;
    else {father[x]=findfather(x);return father[x];}
}

合并集合:
集合内的元素以树的形式储存下来,标志就是根节点代表元,如果需要合并两个集合,只需要让其中一个集合的代表元承认另一集合的代表元是两集合合并后新的代表元即可,这样所有元素也相当于承认了新的代表元。

int fu,fv;
fu=findfather(u);
fv=findfather(v);
father[fu]=fv;

使用:考察并查集的题目,除了要求能快速实现集合上的操作,更重要的是从题目中归纳出集合的模型,查找,合并的依据,才能用并查集解题。

树状数组

特点与性质:树状数组时一种支持快速数据修改的求前缀和算法。树状数组时一种值记录部分再二进制上具有管理的数据的前缀和算法。作为一种前缀和,它和前缀和一样,需要一个等级大小的空间用于存前缀和。树状数组不同于普通的前缀和在于不连续记录每一个位置的数据的和,而是跳跃这记录数据的和,这个跳跃间隔同时适用于查询前缀和时的向前跳跃和修改数据后的向后跳跃。这个跳跃间隔被定义为:lowbit(x),即用二进制表示(仅保留最低位1得到的数字),至于定义的原因不必深究,有兴趣自行了解查找。

功能代码:
计算移动距离:
即计算lowbit(x),没有过多的解释,即利用计算机补码进行运算:

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

求前缀和:
由于树状数组不记录所有的前缀和,所有需要把当前位置没有记录到的数字和记录下来,每次向前条lowbit(x)的距离记录即可。

int getsum(int k)
{
    int sum=0;
    while(k>0)
    {
        sum+=s[k];
        k-=lowbit(k);
    }
    return sum;
}

动态修改:
对树状数组做动态修改时,由于不记录所有前缀和的特点,只需要将更改的变量向后同步即可,每次依然向后移动lowbit(x)。

void modify(int x,int delta)
{
    while(x<=n)
    {
        s[x]+=delta;
        x+=lowbit(x);
    }
}

初始化:
初始化实际是将每个数据插入到树状数组中

for(int i=1;i<=n;i++)
{
    modify(i,d[i]);
}

线段树

特点与性质:线段树是一个树形结构,每个节点主要存放的时某个区间的特征值,每个节点的左儿子存放的时左区间的特征值,右儿子存放的时右区间的特征值,。为了方便数组的存储,即t[i]节点的左右儿子分别为t[2i],t[2i+1],以树的形式存储。因此,线段树需要开4倍元素个数(非满二叉树性质)容量存放所有节点数据。每个节点最少需要记录当前节点的区间范围,待求特征值两个量。建议使用struct定义类型。叶子节点存放的则是单个元素数据本身。

功能代码:
根据数据建树:
依照线段树的定义,使用左右儿子存放左右区间特征值,递归向下定义建树。

//根据数组a[]建立一棵同时记录区间和与区间最大值的线段树tree 
void build(int id,int l,int r)
{
    tree[id].left=l;tree[id].right=r;
    if(l==r)
    {
        tree[id].sum=a[1];
        tree[id].max=a[1];
    }
    else
    {
        int mid=(l+r)/2;
        build(id*2,l,mid);
        build(id*2+1,mid+1,r);
        tree[id].sum=tree[2*id].sum+tree[id*2+1].sum;
        tree[id].max=max(tree[2*id].max,tree[id*2+1].max;)

查询区间特征值:
更加查询区间递归向下查找;
如果查询区间内包含整个节点,直接返回当前区间值;
如果查询区间内只包含当前节点的左(右)区间,则返回对应区间的查询结果;
如果查询区间再当前节点的左右区间,则在左右区间分别询问,并在返回结果后将结果组合。

void query(int id,int l,int r)
{
    if(tree[id].left==l&&tree[id].right==r)
    return tree[id].sum;
    else
    {
        int mid=(tree[id].left+terr[id].right)/2;
        if(r<=mid)return query(id*2,l,r);
        else if(l>mid)return query(id*2+1,l,r);
        else return query(id*2,l,mid)+query(id*2+1,mid+1,r);
    } 
} 

修改单个数据的值:
类似于查询操作,递归查找,直到找到需要修改的节点,修改数据后返回,回溯过程中对一路的节点进行更新。如果修改数据下标等于当前节点,直接修改本节点的值后返回。在子节点完成修改后,需要更新当前节点的特征值。

void update(int id,int pos,int val)
{
    if(tree[id].left==tree[id].right)
    {
        tree[id].sum=tree[id].max=val;
    }
    else
    {
        int mid=(tree[id].left+tree[id].right)/2;
        if(pos<=mid)update(id*2,pos,val);
        else update(id*2+1,pos,val);
        tree[id].sum=tree[id*2].sum+tree[id*2+1].sum;
        tree[id].max=max(tree[id*2].max,tree[id*2+1].max); 
    }
}

区间修改操作:
如果使用单个修改的方式修改区间,时间复杂度将太大,不适用!我们效仿区间查询操作,如果当前区间被修改区间包含,就不再往下更新。但如果当前节点的区间被修改区间包含,显然它的儿子也一定要修改,为了减轻时间复杂度,我们只修改当前节点的值,并记录二线需要修改,而不递归向下修改。我们为每个节点添加一个lazytag标签,记录其区间的儿子是否需要修改,已经改动值的大小,以便在需要时同步数值。当不得不访问具体的儿子区间时,在对儿子进行同步。

对此,我们构造id节点的变动情况的下传函数:

void pushdown(int id)//下传变动情况 
{
    if(tree[id].tag==true)
    {
        tree[id*2].tag=tree[id*2+1].tag=true;//下传标记到左右儿子

        tree[id*2].delta+=tree[id].delta; //下传变动到左儿子 
        tree[id*2].max+=tree[id].delta; //更改左儿子数值 
        tree[id*2].sum+=(tree[id*2].r-tree[id*2].l+1)*tree[id].delta;

        tree[id*2+1].delta+=tree[id].delta; //下传变动到右儿子 
        tree[id*2+1].max+=tree[id].delta; //更改右儿子数值 
        tree[id*2+1].sum+=(tree[id*2+1].r-tree[id*2+1].l+1)*tree[id].delta;

        tree[id].tag=false;//儿子已被修改,清空标记 
        tree[id].delta=0;
    }
}

所有,我们需要对区间特征值的查询代码进行修改:

void query(int id,int l,int r)
{
    if(tree[id].left==l&&tree[id].right==r)
    return tree[id].sum;
    else
    {
        pushdown(id);//不得不访问儿子,需要下传
        int mid=(tree[id].left+terr[id].right)/2;
        if(r<=mid)return query(id*2,l,r);
        else if(l>mid)return query(id*2+1,l,r);
        else return query(id*2,l,mid)+query(id*2+1,mid+1,r);
    } 
} 

这样就能得出区间动态修改的代码了

void modify(int id,int l,int r,int x)
{
    if(tree[id].left>=l&&tree[id].right<=r)
    {
        tree[id].max+=x;
        tree[id].sum+=x*(tree[id].r-tree[id].l+1);
        tree[id].tag=true;tree[id].delta=x;//标记儿子需要修改但未修改 
    }
    else
    {
        pushdown(id);//不得不访问儿子,需要下传
        int mid=(tree[id].left+tree[id].right)/2;
        if(r<=mid)modify(id*2,l,r);
        else
        {
            if(i>mid)modify(id*2+1,l,r);
            else
            {
                modify(id*2,l,mid);
                modify(id*2+1,mid+1,r);
            }
        } 
        tree[id].sum=tree[id*2].sum+tree[id*2+1].sum;
        tree[id].max=max(tree[id*2].max,tree[id*2+1].max);
    } 
} 

<后记>
这几天博客实在是越来越长了,本来向放在这篇博客下的c++STL数据结构容器模板的使用又推延了,还是另开一篇博客好了。


<废话>

猜你喜欢

转载自blog.csdn.net/prasnip_/article/details/79302285