【暖*墟】 #洛谷省选网课# 7.31树上问题的进阶

73 篇文章 0 订阅
14 篇文章 0 订阅

【树上问题的进阶】

       目录

一. 树上前缀和

例题1 前缀和+lca

例题2 链式修改+树上差分

例题3 前缀和+异或

例题4 前缀和+Kth

关于主席树

二. dfs序

例题5 dfs序+线段树

三. 轻重链剖分

树链剖分之轻重链讲解

轻重链剖分的应用

例题6 查询u到v路径上的权值和

例题7 查询所有与u相邻点的权值和

 四.换根意义下的操作

换根求lca    

五. 树上启发式合并 (树上并查集)


 

一. 树上前缀和

注意:有根树;所有祖先求和+自身。

代码实现:(递归中忘记设置边界)

 

例题1 前缀和+lca

Solution

lca:最近公共祖先,在有根树中,找出某两个结点u和v最近的公共祖先。

lca代表被算多的地方,这里只被算多了一次。

lca以上的位置被多算了两倍。如图:

减去lca,减去lca的父亲,lca以上的路径被减了两遍。

 

例题2 链式修改+树上差分

Solution

差分后,每个点的现在值等于原值+子树中所有变化之和。

 

例题3 前缀和+异或

Solution

异或和性质:a^a=0。想方法求路径异或和。

求路径异或和,想方法用前缀异或和。

u到v的路径异或和 = sumu ^ sumv 。

路径异或和=0,当且仅当 sumu=sumv 。

枚举所有权值的出现次数,ans+=cnt(cnt-1)/ 2 。(组合数学)

代码实现:(递归中忘记设置边界)

↑first改成second

 

例题4 前缀和+Kth

Solution

复习:怎么通过权值线段树,求得排名为k的数?

怎么在区间内,求得排名为k的数?......求前缀,相减。

树上前缀和处理(如例题1),将路径序列变成权值线段树。

通过可持久化求kth,求得此题的答案。

 

关于主席树

对原来的数列[1..n]的每一个前缀[1..i](1≤i≤n)建立一棵线段树,

线段树的每一个节点存某个前缀[1..i]中属于区间[L..R]的数一共有多少个。

(比如根节点是[1..n],一共i个数,sum[root] = i;根节点的左儿子是[1..(L+R)/2],

若不大于(L+R)/2的数有x个,那么sum[root.left] = x)。若要查找[i..j]中第k大数时,设某结点x,

那么x.sum[j] - x.sum[i - 1]就是[i..j]中在结点x内的数字总数。

而对每一个前缀都建一棵树,会MLE,观察到每个[1..i]和[1..i-1]只有一条路是不一样的,

那么其他的结点只要用回前一棵树的结点即可,时空复杂度为O(nlogn)。

 

一个线段树在修改一个值的时候,它只要修改logn个节点就可以了,

那么我们只要每次增加logn个节点就可以记录它原来的状态了,

即你在更新一个值的时候仅仅只是更新了一条链,其他的节点都相同,即达到共用。

由于主席树每棵节点保存的是一颗线段树,维护的区间相同,结构相同,保存的信息不同,

因此具有了加减性。(这是主席树关键所在,当除笔者理解了很久很久,才相通的),

所以在求区间的时候,若要处区间[l, r], 只需要处理rt[r] - rt[l-1]就可以了,

(rt[l-1]处理的是[1,l-1]的数,rt[r]处理的是[1,r]的数,相减即为[l, r]这个区间里面的数。

比如以区间第k大为例,hdu2665:

1.建树

首先需要建立一棵空的线段树,也是最原始的主席树,此时主席树只含一个空节点,

此时设根节点为rt[0],表示刚开始的初值状态,然后依次对原序列按某种顺序更新,

即将原序列加入到对应位置。此过程与线段树一样,时间复杂度为O(nlogn),

空间复杂度O(nlog(n))(保守情况下,线段树不会超过4*n)

2.更新

 我们知道,更新一个叶节点只会影响根节点到该叶节点的一条路径,

故只需修改该路径上的信息即可。每个主席树的节点即每棵线段树的结构完全相同,

只是对应信息(可以理解为线段树的结构完全一样,只是对应叶子节点取值不同,

从而有些节点的信息不同,本质是节点不同),此时可以利用历史状态,

即利用相邻的上一棵线段树的信息。相邻两颗线段树只有当前待处理的元素不同,

其余位置完全一样。因此,如果待处理的元素进入线段树的左子树的话,右子树是完全一样的,

可以共用,即直接让当前线段树节点的右子树指向相邻的上一棵线段树的右子树;

若进入右子树,情况可以类比。此过程容易推出时间复杂度为O(logn),空间复杂度为 O(logn)。

 

3.查询

先附上处理好之后的主席树, 如图:

是不是看着很晕。。。。。。笔者其实也晕了,我们把共用的节点拆开来,看下图:

如果早就这样写估计很快就明白了,rt[i]表示处理完前i个数之后所形成的线段树,

即具有了前缀和的性质,那么 rt[r] - rt[l-1] 即表示处理的 [l, r] 区间喽。

当要查询区间[1,3]的时候,我们只要将rt[3] 和 rt[0]节点相减即可得到。如图:

这样我们得到区间[l, r]的数要查询第k大便很容易了,

设左节点中存的个数为cnt,当k<=cnt时,我们直接查询左儿子中第k小的数即可,

如果k>cnt,我们只要去查右儿子中第k-cnt小的数即可,这边是一道很简单的线段树了。

就如查找[1, 3]的第2小数(图上为了方便,重新给节点标号),从根节点1向下搜,

发现左儿子2的个数为1,1<2,所有去右儿子3中搜第2-1级第1小的数,

然后再往下搜,发现左儿子6便可以了,此时已经搜到底端,

所以直接返回节点6维护的值3即可就可以了。

主席树代码实现:

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <string>
#include <iostream>
#include <vector>
#include <map>
#include <set>
#include <queue>
using namespace std;
typedef long long LL;
typedef pair<int,int> pii;
#define pb push_back
#define lson l,m,rt<<1
#define rson m+1,r,rt<<1|1
#define calm (l+r)>>1
const int INF=1e9+7;

const int maxn=100000;
int n,m,tot;
int a[maxn+10],rt[maxn+10];
struct node{
    int l,r,sum;
}tree[maxn*20];
vector<int> v;
inline int getid(int x){
    return lower_bound(v.begin(),v.end(),x)-v.begin()+1;
}
void build(int l,int r,int &x){
    x=++tot;
    tree[x].sum=0;
    if(l==r)return;
    int m=(l+r)>>1;
    build(l,m,tree[x].l);
    build(m+1,r,tree[x].r);
}
void update(int l,int r,int &x,int y,int k){
    x=++tot;tree[x]=tree[y];tree[x].sum++;
    if(l==r){
        return;
    }
    int m=(l+r)>>1;
    if(k<=m)update(l,m,tree[x].l,tree[y].l,k);
    else update(m+1,r,tree[x].r,tree[y].r,k);
}
int query(int l,int r,int x,int y,int k){
    if(l==r)return l;
    int m=(l+r)>>1;
    int sum=tree[tree[y].l].sum-tree[tree[x].l].sum;
    if(k<=sum)return query(l,m,tree[x].l,tree[y].l,k);
    else return query(m+1,r,tree[x].r,tree[y].r,k-sum);
}
int main(){
    //freopen("D://input.txt","r",stdin);
    int T;scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m);
        v.clear();
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
            v.pb(a[i]);
        }
        sort(v.begin(),v.end());
        v.erase(unique(v.begin(),v.end()),v.end());
        int cnt=v.size();
        tot=0;build(1,cnt,rt[0]);
        for(int i=1;i<=n;i++){
            update(1,cnt,rt[i],rt[i-1],getid(a[i]));
        }
        while(m--){
            int l,r,x;scanf("%d%d%d",&l,&r,&x);
            printf("%d\n",v[query(1,cnt,rt[l-1],rt[r],x)-1]);
        }
    }
    return 0;
}

 

*例题4的详细步骤分析*

第一步:离散化

即:把节点的点权换成它在所有点权中的排名(它是第几小的)

将存储点权的数组复制一份之后排序,去重,

然后将原先的每个点权在去重后的数组里进行二分查找,就可以得到它的排名。

第二步:建主席树

每个节点维护它到根的路径上的权值线段树,所以可以利用它的父节点更新

所以将整棵树dfs一遍,在此过程中建主席树。

第三步:求解

用u点的主席树+v点的主席树-lca(u,v)的主席树-lca(u,v)父节点的主席树,在这样产生的主席树上查找第k小的排名,最后输出它原来的点权。

代码实现:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cstdlib>
using namespace std;
const int e=1e5+5;
struct point
{
    int l,r,w;
}c[e*30];
int fa[e][21],a[e],h[e],tot,num,deep[e],n,m,rt[e],xxx,val[e];
int next[e<<1],head[e],go[e<<1];
inline int read()
{
    char ch;
    int res=0;
    bool f=false;
    while(ch=getchar(),(ch<'0'||ch>'9')&&ch!='-');
    if(ch=='-')f=true;
    else res=ch-48;
    while(ch=getchar(),ch>='0'&&ch<='9')
    res=(res<<3)+(res<<1)+ch-48;
    return f? -res:res;
}
inline void insert(int y,int &x,int l,int r,int p)
{
    c[x=++num]=c[y];
    c[x].w++;
    if(l==r)return;
    int mid=l+r>>1;
    if(p<=mid)insert(c[y].l,c[x].l,l,mid,p);
    else insert(c[y].r,c[x].r,mid+1,r,p);
    c[x].w=c[c[x].l].w+c[c[x].r].w;
}
inline int query(int x,int y,int z,int d,int l,int r,int k)
{
    if(l==r)return l;
    int ret=c[c[x].l].w+c[c[y].l].w-c[c[z].l].w-c[c[d].l].w;
    int mid=l+r>>1;
    if(k<=ret)
    return query(c[x].l,c[y].l,c[z].l,c[d].l,l,mid,k);
    else return query(c[x].r,c[y].r,c[z].r,c[d].r,mid+1,r,k-ret);
}
inline void add(int x,int y)
{
    next[++tot]=head[x];
    head[x]=tot;
    go[tot]=y;
    next[++tot]=head[y];
    head[y]=tot;
    go[tot]=x;
}
inline void dfs2(int u)
{
    insert(rt[fa[u][0]],rt[u],1,n,val[u]);
    for(int i=head[u];i;i=next[i])
    {
        int v=go[i];
        if(v==fa[u][0])continue;
        dfs2(v);
    }
}
inline void dfs(int u,int father)
{
    deep[u]=deep[father]+1;
    for(int i=0;i<=19;i++)
    fa[u][i+1]=fa[fa[u][i]][i];
    for(int i=head[u];i;i=next[i])
    {
        int v=go[i];
        if(v==father)continue;
        fa[v][0]=u;
        dfs(v,u);
    }
}
inline int lca(int x,int y)
{
    if(deep[x]<deep[y])swap(x,y);
    for(int i=19;i>=0;i--)
    {
        if(deep[fa[x][i]]>=deep[y])
        x=fa[x][i];
        if(x==y)return x;
    }
    for(int i=19;i>=0;i--)
    {
        if(fa[x][i]!=fa[y][i])
        {
            x=fa[x][i];
            y=fa[y][i];
        }
    }
    return fa[x][0];
}
inline int find(int x)
{
    int l=1,r=xxx,mid;
    while(l<=r)
    {
        mid=l+r>>1;
        if(x>h[mid])
        l=mid+1;
        else r=mid-1;
    }
    return l;
}
int main()
{
    int i,j,u,v,k;
    n=read();
    m=read();
    for(i=1;i<=n;i++)
    {
        val[i]=read();
        a[i]=val[i];
    }
    for(i=1;i<n;i++)
    {
        u=read();
        v=read();
        add(u,v);
    }
    sort(a+1,a+n+1);
    h[1]=a[1];
    xxx=1;
    for(i=2;i<=n;i++)
    if(a[i]!=a[i-1])h[++xxx]=a[i];
    for(i=1;i<=n;i++)
    val[i]=find(val[i]);
    dfs(1,0);
    int ans=0;
    dfs2(1);
    for(i=1;i<=m;i++)
    {
        u=read();
        v=read();
        k=read();
        u^=ans;
        int z=lca(u,v);
        int last=query(rt[u],rt[v],rt[z],rt[fa[z][0]],1,n,k);
        ans=h[last];
        printf("%d",ans);
        if(i!=m)putchar('\n');
    }
    return 0;
}

 

二. dfs序

注意,树的dfs序可能有多种。

例题5 dfs序+线段树

Solution

 

三. 轻重链剖分

注意是 log n 的重链和轻边。先dfs求每个子树的大小,再来确定重边。

优先访问最大的子树 ,再记录dfs序。

上述代码中,最好预处理出 top(链头)值。

 

树链剖分之轻重链讲解

首先我们有一颗树每个点(或者边)有权值,

我们要做的就是询问两个点之间路径上各点(边)权值的最大、最小,权值和(线段树作用),

然后我们还要支持在线更改任意节点(边)的权值。

我们要做的是轻重链剖分,首先我们看几个定义:

size:和SBT里的一样,size[i]为以该点为根节点的子树一共有几个节点。

重儿子:一个节点当不为叶子节点的时候有且只有一个重儿子,

             重儿子为该点的儿子中size最大的,有多个最大时任选一个。

重链:由根节点开始,每个点每次都访问自己的重儿子,一直访问到叶子节点,就组成了一条重链。

那么对于一个点的非重儿子来说,以他为根节点,可以重新访问出一条重链。

如图所示,用红色的线画出的为重链,其中6号点自己为一条重链,

那么对于每条重链,我们需要记下他的顶标top,就是该重链中深度最小的节点的标号

比如链1-3-4-9-10,的top为1,链2-8的top为2。

重链几个明显的性质就是互不重合且所有重链覆盖所有点,

重链之间由一条不在重链上的边(我们称作轻边)连接,然后对于每一条重链来说,

我们定义他的深度,顶标为根节点的重链的深度为1,顶标的父亲在深度为x的重链上,

那么该重链深度为x+1,如图链1-3-4-9-10的深度为1,链2-8,链5-7的深度为2,链6的深度为3。 

先DFS求出每个点的size,然后再深搜一遍可得到每个点的 top(链头),和处理出每一条链。

 

轻重链剖分的应用

LA:在logn的复杂度内查询x的深度为d的祖先是谁。

方法:跳链头,在重链上利用dfs序的递增性求出某位置的节点。

 

例题6 查询u到v路径上的权值和

 

例题7 查询所有与u相邻点的权值和

 

 四.换根意义下的操作

一般只用于题目要求换根。

换根求lca

例题8

 

五. 树上启发式合并 (树上并查集)

 

 

                                                ——时间划过风的轨迹,那个少年,还在等你。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值