树链剖分题集

树链剖分常用于处理静态树上操作,效率很高,写起来也就是固定的轻重边拆分+线段树,但是代码一般都是100行+,容易出错。
本题集中的树链剖分练习题,在解决思路上没什么难点,主要是如何设计线段树以及如何更新+询问,也就是说要想好怎么维护线段树,其它地方没什么太难的,但是代码长就容易出bug,多写写就好了。最后一题是线段树动态开点,这和可持久化权值线段树(动态主席树)一样的思想,只需要将普通的线段树给稍微修改一下即可。

树的统计

题意简述
原题来自:ZJOI 2008
一树上有 n 个节点,编号分别为 1 到 n,每个节点都有一个权值 w。我们将以下面的形式来要求你对这棵树完成一些操作:

1.CHANGE u t :把节点 u 权值改为 t;

2.QMAX u v :询问点 u 到点 v 路径上的节点的最大权值;

3.QSUM u v :询问点 u 到点 v 路径上的节点的权值和。

注意:从点 u 到点 v 路径上的节点包括 u 和 v 本身。

解题思路
树链剖分+线段树上单点修改,区间查询,需要注意查询操作,利用到了LCA倍增思想,树链剖分中很多区间修改和查询操作都是这样完成的。

代码示例

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int N = 3e4+10;
const int M = 2*N;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y, edge[tot] = z;
    nex[tot] = head[x], head[x] = tot;
}
int n,q,a[N];
/*父亲,深度,子树大小,重儿子,重路径顶部节点,
    树中节点在线段树中下标,线段树中节点对应树中位置*/
int par[N],deep[N],size[N],son[N],top[N],seg[N],rev[N];
int vis[N];
void dfs1(int x,int fa){
    /*利用深搜更新par,size,deep,son数组*/
    vis[x] = true; size[x] = 1;
    par[x] = fa; deep[x] = deep[fa]+1;
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(y == fa || vis[y]) continue;
        dfs1(y,x);
        size[x] += size[y]; //累加子树大小
        if(size[y] > size[son[x]]) son[x] = y;//求重儿子
    }
}
void dfs2(int x,int fa){
    if(son[x]){ //先走重儿子,使得重路径在线段树中连续
        seg[son[x]] = ++seg[0];//0位置用不到,利用来计数
        top[son[x]] = top[x];
        rev[seg[0]] = son[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(top[y]) continue;
        /*若y没有被遍历过,即y不是x的重儿子或者父亲*/
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
        /*如果x-->y是轻边,那么y就是其所在重路径顶部节点*/
    }
}
struct SegmentTree{
    int l,r;
    ll mx, sum;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define mx(x) t[x].mx
    #define sum(x) t[x].sum
}t[N*4];
void BuildTree(int rt,int l,int r){
    l(rt) = l, r(rt) = r;
    if(l == r){
        mx(rt) = sum(rt) = a[rev[l]]; //线段树上l节点对应着树上rev[l]点
        return;
    }
    int mid = l+r>>1;
    BuildTree(rt*2,l,mid); BuildTree(rt*2+1,mid+1,r);
    mx(rt) = max(mx(rt<<1),mx(rt<<1|1));
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
void preHandle(){
    dfs1(1,0); //我们以1号节点为根
    /*根节点所在重路径的顶部节点也是根节点,赋初值*/
    seg[0] = seg[1] = top[1] = rev[1] = 1;
    dfs2(1,0);
    BuildTree(1,1,seg[0]);
}
ll tmx,tsum;//利用全局变量同时统计2个答案
void query(int rt,int l,int r){
    /*将以rt为根的区间内属于[l,r]部分的和累加到tsum上,并更新tmx*/
    if(l <= l(rt) && r(rt) <= r){
        tsum += sum(rt); tmx = max(tmx,mx(rt));
        return ;
    }
    int mid = l(rt)+r(rt)>>1;
    if(l <= mid) query(rt<<1,l,r);
    if(r > mid) query(rt<<1|1,l,r);
}
void ask(int x,int y){
    /*返回x与y之间路径上权值最大点的权值*/
    int fx = top[x] , fy = top[y];
    while(fx != fy){//先将x和y条整到同一个重链上
        if(deep[fx] < deep[fy]) swap(x,y),swap(fx,fy);
        query(1,seg[fx],seg[x]);
        x = par[fx]; fx = top[x];
    }
    if(deep[x] > deep[y]) swap(x,y);//路径浅的编号小
    query(1,seg[x],seg[y]); //再更新一次
}
void change(int rt,int x,int val){
    /*把线段树节点x的权值改为val*/
    if(l(rt) == r(rt)){
        mx(rt) = sum(rt) = val;
        return;
    }
    int mid = l(rt) + r(rt) >> 1;
    if(x > mid) change(rt<<1|1,x,val);
    else change(rt<<1,x,val);
    mx(rt) = max(mx(rt<<1),mx(rt<<1|1));
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
int main(){
    #ifdef LOCAL
        freopen("123.txt","r",stdin);
        freopen("222.txt","w",stdout);
    #endif
    scanf("%d",&n);
    for(int i = 1,x,y;i < n;i++){
        scanf("%d%d",&x,&y);
        addEdge(x,y,1); addEdge(y,x,1);
    }
    for(int i = 1;i <= n;i++) scanf("%d",a+i);
    scanf("%d",&q);
    preHandle();//树链剖分预处理
    char op[20];
    for(int i = 1,x,y;i <= q;i++){
        scanf("%s%d%d",op,&x,&y);
        if(op[0] == 'C') change(1,seg[x],y);
        else{
            tmx = -INF; tsum = 0;
            ask(x,y);//同时更新最大值与路径和
            if(op[1] == 'M') printf("%lld\n",tmx);
            else printf("%lld\n",tsum);
        }
    }
    return 0;
}
「HAOI2015」树上操作

题意简述
有一棵点数为 N 的树,以点 1 为根,且树有点权。然后有 M 个操作,分为三种:

1、把某个节点 x 的点权增加 a 。

2、把某个节点 x 为根的子树中所有点的点权都增加 a 。

3、询问某个节点 x 到根的路径中所有点的点权和。

解题思路
树链剖分+线段树上区间修改,区间查询。用到了线段树上的延迟标记,其实和普通线段树没啥区别,就是查询和修改时候要用LCA倍增思想将树上的链转化为线段树上的区间。只要理解了树上节点和线段树下标是通过seg[x] 和 rev[seg[x]]来转换的即可。

另外还需要明白线段树上重链的seg[]值是连续的,同一个子树上的所有节点下标也是连续的,这个结论可以通过观察dfs2的实现得到。

代码示例

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
const int M = 2e5+10;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y,edge[tot] = z;
    nex[tot] = head[x]; head[x] = tot;
}
int n,m,a[N];
int getInt() {
    int ans = 0;
    bool neg = false;
    char c = getchar();
    while (c!='-' && (c<'0' || c>'9')) c = getchar();
    if (c == '-') neg = true, c = getchar();
    while (c>='0' && c<='9')
        ans = ans*10 + c-'0', c = getchar();
    return neg ? -ans : ans; 
}
int deep[N],par[N],top[N],size[N],son[N],seg[N],rev[N];
int vis[N];
void dfs1(int x,int fa){
    vis[x] = true; par[x] = fa;
    deep[x] = deep[fa]+1; size[x] = 1;
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(vis[y] || y == fa) continue;
        dfs1(y,x);
        size[x] += size[y]; 
        if(size[y] > size[son[x]]) son[x] = y;
    }
}
void dfs2(int x,int fa){
    if(son[x]){
        seg[son[x]] = ++seg[0];
        rev[seg[0]] = son[x];
        top[son[x]] = top[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(top[y]) continue;
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
    }
}
struct SegmentTree{
    int l,r;
    ll sum,add;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define sum(x) t[x].sum
    #define add(x) t[x].add
}t[N*4];
void BuildTree(int rt,int l,int r){
    l(rt) = l,r(rt) = r,add(rt) = 0;
    if(l == r){
        sum(rt) = a[rev[l]]; return;
    }
    int mid = l+r>>1;
    BuildTree(rt<<1,l,mid); BuildTree(rt<<1|1,mid+1,r);
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
void preHandle(){
    dfs1(1,0);
    seg[0] = seg[1] = top[1] = rev[1] = 1;
    dfs2(1,0);
    BuildTree(1,1,seg[0]);
}
void spread(int rt){
    if(!add(rt)) return;
    sum(rt<<1) += (r(rt<<1)-l(rt<<1)+1)*add(rt);
    sum(rt<<1|1) += (r(rt<<1|1)-l(rt<<1|1)+1)*add(rt);
    add(rt<<1) += add(rt); add(rt<<1|1) += add(rt);
    add(rt) = 0;
}
void Add(int rt,int l,int r,ll val){
    if(l <= l(rt) && r(rt) <= r){
        sum(rt) += (r(rt) - l(rt)+1)*val;
        add(rt) += val; return;
    }
    spread(rt);
    int mid = l(rt)+r(rt)>>1;
    if(l <= mid) Add(rt<<1,l,r,val);
    if(r > mid) Add(rt<<1|1,l,r,val);
    sum(rt) = sum(rt<<1)+sum(rt<<1|1);
}
ll query(int rt,int l,int r){
    if(l <= l(rt) && r(rt) <= r) return sum(rt);
    spread(rt); ll res = 0;
    int mid = l(rt) + r(rt) >> 1;
    if(l <= mid) res += query(rt<<1,l,r);
    if(r > mid) res += query(rt<<1|1,l,r);
    return res;
}
ll ask(int x){
    /*从节点x到根节点的路径和*/
    ll res = 0;
    while(x){
        res += query(1,seg[top[x]],seg[x]);
        x = par[top[x]];
    }
    return res;
}
int main(){
    n = getInt(); m = getInt();
    for(int i = 1;i <= n;i++) a[i] = getInt();
    for(int i = 1,x,y;i < n;i++){
        x = getInt(); y = getInt();
        addEdge(x,y,1); addEdge(y,x,1);
    }
    preHandle();
    for(int i = 1,op,x,y;i <= m;i++){
        op = getInt();
        if(op == 1){
            x = getInt(); y = getInt();
            Add(1,seg[x],seg[x],y);
        }else if(op == 2){
            x = getInt(); y = getInt();
            Add(1,seg[x],seg[x]+size[x]-1,y);
        }else{
            x = getInt(); printf("%lld\n",ask(x));
        }
    }
    return 0;
}
「NOI2015」软件包管理器

题意简述
Linux 用户和 OSX 用户一定对软件包管理器不会陌生。通过软件包管理器,你可以通过一行命令安装某一个软件包,然后软件包管理器会帮助你从软件源下载软件包,同时自动解决所有的依赖(即下载安装这个软件包的安装所依赖的其它软件包),完成所有的配置。Debian/Ubuntu 使用的 apt-get,Fedora/CentOS 使用的 yum,以及 OSX 下可用的 Homebrew 都是优秀的软件包管理器。

你决定设计你自己的软件包管理器。不可避免地,你要解决软件包之间的依赖问题。如果软件包 A 依赖软件包 B,那么安装软件包 A 以前,必须先安装软件包 B。同时,如果想要卸载软件包 B,则必须卸载软件包 A。现在你已经获得了所有的软件包之间的依赖关系。而且,由于你之前的工作,除 0 号软件包以外,在你的管理器当中的软件包都会依赖一个且仅一个软件包,而 0 号软件包不依赖任何一个软件包。依赖关系不存在环(若有m(m≥2) 个软件包A1,A2,A3,…,Am ,其中 A1 依赖 A2,A2依赖 A3 依赖 A4 ,……,Am−1 依赖 Am ,而 Am 依赖 A1 ,则称这 m 个软件包的依赖关系构成环),当然也不会有一个软件包依赖自己。

现在你要为你的软件包管理器写一个依赖解决程序。根据反馈,用户希望在安装和卸载某个软件包时,快速地知道这个操作实际上会改变多少个软件包的安装状态(即安装操作会安装多少个未安装的软件包,或卸载操作会卸载多少个已安装的软件包),你的任务就是实现这个部分。注意,安装一个已安装的软件包,或卸载一个未安装的软件包,都不会改变任何软件包的安装状态,即在此情况下,改变安装状态的软件包数为 0。

解题思路
要画图理解题意,若A依赖B,则安装A之前要安装B,若卸载B要先卸载A。也就是说要安装A,就要确保A到根节点路径上软件都已安装;若卸载A,就要确保 A 的子树为空。

于是就是树上操作,若卸载一个软件 x ,就是查询它子树上已安装的软件个数(包括它自己),输出后再删除即可;若安装一个软件 x ,就是查询 x 到根节点路径上未安装的软件个数(包括它自己),输出后再安装。因为软件安装和卸载只是改变其状态(已安装:1,未安装:0),而不改变结构(彻底删除节点),所以可以用树链剖分+线段树解决。

但是需要注意的是,怎么把整棵子树都清零,怎么把到根节点的路径都置 1,显然要用延迟标记,但是延迟标记却不应该累加,那要怎么处理?
置 1 时延迟标记add = 1,置0时延迟标记add = -1,每一个节点如果延迟标记不为0,那么该节点为根的子树所有软件状态相同。我们下传延迟标记操作封装在spread函数内,可以参考如何实现延迟标记的更新。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = 2e5+10;
typedef int ll;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y, edge[tot] = z;
    nex[tot] = head[x], head[x] = tot;
}
int n,m;
int par[N],deep[N],size[N],son[N],top[N],seg[N],rev[N];
int vis[N];
void dfs1(int x,int fa){
    vis[x] = true; par[x] = fa;
    deep[x] = deep[fa]+1; size[x] = 1;
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(vis[y]) continue;
        dfs1(y,x);
        size[x] += size[y];
        if(size[y] > size[son[x]]) son[x] = y;
    }
}
void dfs2(int x,int fa){
    if(son[x]){
        seg[son[x]] = ++seg[0];
        rev[seg[0]] = son[x];
        top[son[x]] = top[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(top[y]) continue;
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
    }
}
struct SegmentTree{
    int l,r,sum,add;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define sum(x) t[x].sum
    #define add(x) t[x].add
}t[N*4];
void BuildTree(int rt,int l,int r){
    l(rt) = l,r(rt) = r,add(rt) = sum(rt) = 0;
    if(l == r) return;
    int mid = l+r>>1;
    BuildTree(rt<<1,l,mid); BuildTree(rt<<1|1,mid+1,r);
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
void preHandle(){
    dfs1(1,0);
    seg[0] = seg[1] = top[1] = rev[1] = 1;
    dfs2(1,0);
    BuildTree(1,1,seg[0]);
}
void spread(int rt){
    if(!add(rt)) return; 
    if(add(rt) == -1){
        sum(rt<<1) = sum(rt<<1|1) = 0;
        add(rt<<1) = add(rt<<1|1) = add(rt);
        add(rt) = 0; return;
    }
    sum(rt<<1) = (r(rt<<1)-l(rt<<1)+1)*add(rt);
    sum(rt<<1|1) = (r(rt<<1|1)-l(rt<<1|1)+1)*add(rt);
    add(rt<<1) = add(rt); add(rt<<1|1) = add(rt);
    add(rt) = 0;
}
void Add(int rt,int l,int r,ll val){
    if(l <= l(rt) && r(rt) <= r){
        if(val == -1) sum(rt) = 0;
        else sum(rt) = (r(rt) - l(rt)+1);
        add(rt) = val; 
        return;
    }
    spread(rt);
    int mid = l(rt)+r(rt)>>1;
    if(l <= mid) Add(rt<<1,l,r,val);
    if(r > mid) Add(rt<<1|1,l,r,val);
    sum(rt) = sum(rt<<1)+sum(rt<<1|1);
}
ll query(int rt,int l,int r){
    if(l <= l(rt) && r(rt) <= r) return sum(rt);
    spread(rt); ll res = 0;
    int mid = l(rt) + r(rt) >> 1;
    if(l <= mid) res += query(rt<<1,l,r);
    if(r > mid) res += query(rt<<1|1,l,r);
    return res;
}
int ask1(int x){
    return query(1,seg[x],seg[x]+size[x]-1);
}
int ask2(int x){
    int res = 0;
    while(x){
        res += query(1,seg[top[x]],seg[x]);
        x = par[top[x]];
    }
    return res;
}
void Add2(int x){
    while(x){
        Add(1,seg[top[x]],seg[x],1);
        x = par[top[x]];
    }
}
int main(){
    scanf("%d",&n);
    for(int i = 1,x;i < n;i++){
        scanf("%d",&x);
        addEdge(x+1,i+1,1); addEdge(i+1,x+1,1);
    }
    preHandle();
    scanf("%d",&m); char op[22];
    for(int i = 1,x;i <= m;i++){
        scanf("%s%d",op,&x); x++;
        if(op[0] == 'u'){
            printf("%d\n",ask1(x));
            Add(1,seg[x],seg[x]+size[x]-1,-1);
        }else{
            printf("%d\n",deep[x]-ask2(x));
            Add2(x);
        }
    }
    return 0;
}
染色

题意简述
原题来自:SDOI 2011
给定一棵有 n 个节点的无根树和 m 个操作,操作共两类。
1、将节点 a 到节点 b 路径上的所有节点都染上颜色;
2、询问节点 a 到节点 b 路径上的颜色段数量,连续相同颜色的认为是同一段,例如 112221 由三段组成:11 、 222、1。

请你写一个程序依次完成操作。

解题思路
如果不是在树上,是在区间上,该如何操作?用线段树解决。线段树上每个节点有lc:左端点颜色,rc:右端点颜色,same:区间颜色是否唯一(1表示唯一,0表示不唯一),sum:区间内颜色段数量。
那么更新显然要用延迟标记,标记在same上,而合并操作需要考虑中间相接的端点颜色是否相同,若相同则颜色段数量要-1。于是区间上“染色”问题就可以用线段树解决了。

在树上显然类似,因为我们可以通过树链剖分来将树给转化为区间。唯一麻烦的地方就是端点颜色问题,在区间上,我们只需要判断左子树的右端点、右子树的左端点是否颜色相同即可,但是树上的路径是通过倍增来找的,显然需要记录每一条链的端点颜色,麻烦在于如何记录并判断。

树链剖分后节点越浅,其seg越小,于是需要判断颜色是否相同的是seg[fx]和 seg[par[fx]],这是由于我们在倍增调整深度时候是自底向上的,自然要判断深度浅的节点。为了美观以及简便,设 getcol(x) 返回该节点的颜色。

代码示例

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5+10;
const int M = 2e5+10;
const int INF = 0x3f3f3f3f;
int head[N],edge[M],ver[M],nex[M], tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y, edge[tot] = z;
    nex[tot] = head[x], head[x] = tot;
}
int deep[N],seg[N],rev[N],top[N],son[N],size[N],par[N];
int vis[N],a[N];
void dfs1(int x,int fa){
    vis[x] = true; par[x] = fa;
    deep[x] = deep[fa]+1; size[x] = 1;
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(vis[y] || y == fa) continue;
        dfs1(y,x);
        size[x] += size[y];
        if(size[y] > size[son[x]]) son[x] = y;
    }
}
void dfs2(int x,int fa){
    if(son[x]){
        seg[son[x]] = ++seg[0];
        rev[seg[0]] = son[x];
        top[son[x]] = top[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(top[y]) continue;
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
    }
}
struct SegmentTree{
    /*lc:区间左端点颜色,rc:区间右端点颜色,sum:区间内颜色段数*/
    int l,r,lc,rc,sum;
    int same;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define lc(x) t[x].lc
    #define rc(x) t[x].rc
    #define sum(x) t[x].sum
    #define same(x) t[x].same
}t[N*4];
void Updata(int rt){//更新节点rt的same、rc、lc和sum
    same(rt) = same(rt<<1) && same(rt<<1|1) && rc(rt<<1) == lc(rt<<1|1);
    sum(rt) = sum(rt<<1)+sum(rt<<1|1);
    if(rc(rt<<1) == lc(rt<<1|1)) sum(rt)--;
    lc(rt) = lc(rt<<1); rc(rt) = rc(rt<<1|1);
}
void BuildTree(int rt,int l,int r){
    l(rt) = l,r(rt) = r,same(rt) = 0;
    if(l == r){
        lc(rt) = rc(rt) = a[rev[l]];
        sum(rt) = 1; return;
    }
    int mid = l + r >> 1;
    BuildTree(rt<<1,l,mid); BuildTree(rt<<1|1,mid+1,r);
    Updata(rt);
}
void preHandle(){
    dfs1(1,0);
    seg[0] = seg[1] = rev[1] = top[1] = 1;
    dfs2(1,0);
    BuildTree(1,1,seg[0]);
}
void spread(int rt){
    if(!same(rt)) return;//将懒惰标记下传,即对子树染相同颜色
    sum(rt<<1) = sum(rt<<1|1) = 1;
    lc(rt<<1) = rc(rt<<1) = lc(rt<<1|1) = rc(rt<<1|1) = lc(rt);
    same(rt<<1) = same(rt<<1|1) = 1; same(rt) = 0;
}
void modify(int rt,int l,int r,int col){
    if(l <= l(rt) && r(rt) <= r){
        sum(rt) = 1; lc(rt) = rc(rt) = col;
        same(rt) = 1; return;
    }
    spread(rt);//懒惰标记
    int mid = l(rt)+r(rt)>>1;
    if(l <= mid) modify(rt<<1,l,r,col);
    if(r > mid) modify(rt<<1|1,l,r,col);
    Updata(rt);
}
void change(int x,int y,int z){
    int fx = top[x],fy = top[y];
    while(fx != fy){
        if(deep[fx] < deep[fy]) swap(x,y),swap(fx,fy);
        modify(1,seg[fx],seg[x],z);
        x = par[fx]; fx = top[x];
    }
    if(deep[x] > deep[y]) swap(x,y);
    modify(1,seg[x],seg[y],z);
}
int query(int rt,int l,int r){
    if(l <= l(rt) && r(rt) <= r) return sum(rt);
    spread(rt);
    int mid = l(rt)+r(rt)>>1 ,res= 0;
    if(l <= mid) res += query(rt<<1,l,r);
    if(r > mid) res += query(rt<<1|1,l,r);
    /*合并时中间颜色相同,要减去一段*/
    if(l <= mid && mid < r && rc(rt<<1) == lc(rt<<1|1)) res--;
    return res;
}
int getcol(int rt,int x){//这个函数太好用了,哭哭
    if(l(rt) == r(rt)) return lc(rt);
    spread(rt);
    int mid = l(rt)+r(rt)>>1;
    if(x <= mid) return getcol(rt<<1,x);
    else return getcol(rt<<1|1,x);
}
int ask(int x,int y){
    int fx = top[x], fy = top[y], res = 0;
    while(fx != fy){
        if(deep[fx] < deep[fy]) swap(fx,fy),swap(x,y);
        res += query(1,seg[fx],seg[x]);
        if(getcol(1,seg[fx]) == getcol(1,seg[par[fx]])) res--;
        x = par[fx]; fx = top[x];
    }
    if(deep[x] > deep[y]) swap(x,y);
    res += query(1,seg[x],seg[y]);
    return res;
}
int n,m;
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++) scanf("%d",a+i);
    for(int i = 1,x,y;i < n;i++){
        scanf("%d%d",&x,&y); 
        addEdge(x,y,1); addEdge(y,x,1);
    }
    preHandle(); char op[4];
    for(int i = 1,x,y,z;i <= m;i++){
        scanf("%s",op);
        if(op[0] == 'Q'){
            scanf("%d%d",&x,&y);
            printf("%d\n",ask(x,y));
        }else{
            scanf("%d%d%d",&x,&y,&z);
            change(x,y,z);
        }
    }
    return 0;
}
「SDOI2014」旅行

题意简述
S 国有 N 个城市,编号从 1 到 N。城市间用 N−1 条双向道路连接,满足从一个城市出发可以到达其它所有城市。每个城市信仰不同的宗教,如飞天面条神教、隐形独角兽教、绝地教都是常见的信仰。为了方便,我们用不同的正整数代表各种宗教,S 国境内总共有 C 种不同的宗教。
S 国的居民常常旅行。旅行时他们总会走最短路,并且为了避免麻烦,只在信仰和他们相同的城市留宿。当然旅程的终点也是信仰与他相同的城市。S 国政府为每个城市标定了不同的旅行评级,旅行者们常会记下途中(包括起点和终点)留宿过的城市的评级总和或最大值。
在 S 国的历史上常会发生以下几种事件:

1、CC x c:城市 x 的居民全体改信了 c 教;

2、CW x w:城市 x 的评级调整为 w;

3、QS x y:一位旅行者从城市 x 出发,到城市 y,并记下了途中留宿过的城市的评级总和;

4、QM x y:一位旅行者从城市 x 出发,到城市 y,并记下了途中留宿过的城市的评级最大值。
由于年代久远,旅行者记下的数字已经遗失了,但记录开始之前每座城市的信仰与评级,还有事件记录本身是完好的。请根据这些信息,还原旅行者记下的数字。

为了方便,我们认为事件之间的间隔足够长,以致在任意一次旅行中,所有城市的评级和信仰保持不变。
解题思路
如果没有信仰,那就是简单的树链剖分模板题,但这题多了个信仰,正常思路是开数组存放对应信仰的评级,但是显然不现实,从空间角度来看。
这题用到了动态开点,和可持久化线段树类似,只需要稍微改一下线段树模板即可。

代码示例

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1e5+10;
const int M = 2e5+10;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y,edge[tot] = z;
    nex[tot] = head[x], head[x] = tot;
}
int n,m;
int deep[N],par[N],size[N],top[N],seg[N],rev[N],son[N];
int vis[N],a[N],c[N];//a:初始评级 c:初始信仰
void dfs1(int x,int fa){
    deep[x] = deep[fa]+1;par[x] = fa;
    size[x] = 1;vis[x] = true;
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(vis[y] || y == fa) continue;
        dfs1(y,x);
        size[x] += size[y];
        if(size[y] > size[son[x]]) son[x] = y;
    }
}
void dfs2(int x,int fa){
    if(son[x]){
        seg[son[x]] = ++seg[0];
        rev[seg[0]] = son[x];
        top[son[x]] = top[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i],z = edge[i];
        if(top[y]) continue;
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
    }
}
struct SegmentTree{
    int ls,rs;
    ll mx,sum;
    SegmentTree(){
        ls = rs = mx = sum = 0;
    }
    #define ls(x) t[x].ls
    #define rs(x) t[x].rs
    #define mx(x) t[x].mx
    #define sum(x) t[x].sum
}t[N*20];
int root[N],sz = 1;//最多有多少个根节点
void preHandle(){
    dfs1(1,0);
    seg[0] = seg[1] = rev[1] = top[1] = 1;
    dfs2(1,0);
}
void Add(int &rt,int l,int r,int pos,ll val){
    if(!rt) rt = ++sz;
    mx(rt) = max(mx(rt),val); sum(rt) += val;
    if(l == r) return; int mid = l+r>>1;
    if(pos <= mid) Add(ls(rt),l,mid,pos,val);
    else Add(rs(rt),mid+1,r,pos,val);
}
void Delete(int rt,int l,int r,int pos){
    if(l == r){
        sum(rt) = mx(rt) = 0; return;
    }
    int mid = l+r>>1;
    if(pos <= mid) Delete(ls(rt),l,mid,pos);
    else Delete(rs(rt),mid+1,r,pos);
    sum(rt) = sum(ls(rt)) + sum(rs(rt));
    mx(rt) = max(mx(ls(rt)),mx(rs(rt)));
}
ll mx,sum;
void query(int rt,int L,int R,int l,int r){
  //  printf("%d %d %d %d %d\n",rt,L,R,l,r);
    if(!rt) return;
    if(l <= L && R <= r){
      //  printf("%d %d\n",mx(rt),sum(rt));
        mx = max(mx,mx(rt)); sum += sum(rt); return;
    }
    int mid = L+R>>1;
    if(l <= mid) query(ls(rt),L,mid,l,r);
    if(r > mid) query(rs(rt),mid+1,R,l,r);
}
void ask(int rt,int x,int y){
    /*返回x到y路径上信仰rt的评级最大值以及总和*/
    int fx = top[x],fy = top[y];
    while(fx != fy){
        if(deep[fx] < deep[fy]) swap(x,y),swap(fx,fy);
        query(rt,1,seg[0],seg[fx],seg[x]);
        x = par[fx]; fx = top[x];
    }
    if(deep[x] > deep[y]) swap(x,y);
    query(rt,1,seg[0],seg[x],seg[y]);
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++) scanf("%d%d",a+i,c+i);
    for(int i = 1,x,y;i < n;i++){
        scanf("%d%d",&x,&y);
        addEdge(x,y,1); addEdge(y,x,1);
    }
    preHandle();
    for(int i = 1;i <= n;i++)
        Add(root[c[i]],1,seg[0],seg[i],a[i]);
    char op[10];
    for(int i = 1,x,y;i <= m;i++){
        scanf("%s%d%d",op,&x,&y);
        if(op[1] == 'C'){
            Delete(root[c[x]],1,seg[0],seg[x]);
            Add(root[c[x] = y],1,seg[0],seg[x],a[x]);
        }else if(op[1] == 'W'){
            Delete(root[c[x]],1,seg[0],seg[x]);
            Add(root[c[x]],1,seg[0],seg[x],a[x] = y);
        }else{
            mx = sum = 0;//一起更新,少写代码,美滋滋
            ask(root[c[x]],x,y);
            if(op[1] == 'S') printf("%lld\n",sum);
            else printf("%lld\n",mx);
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

迷亭1213

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

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

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

打赏作者

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

抵扣说明:

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

余额充值