HDU-5044 Tree 树上差分 好题
树上差分 运用 普通差分思想,在树上用一个数组模拟差分,能实现 区间修改 (边和点都可以)和 单点查询 操作,能比线段树快。
既然是模拟一维差分,实现的原理,就是在线段头部+1,线段尾部-1;这样能实现 O(1) 修改, O(n) 查询的功能。
先讲点权的区间修改:
比如 让 点u 到 v 之间路径上所有点+1,想象以 u-v 路径上深度最小的点(即最近公共祖先 lca ) 把线段分成两段,这样一条线段就分成了两个线段,这两个线段分别处于左子树和右子树;那么只需要让tree[u]+=1,tree[v]+=1,同时让tree[ lca(u,v) ] - = 1,
tree[ fa[ lca(u,v) ] ] - = 1;学过差分的同学应该容易想象,一个结点其子树包含自身的tree[ ]值和即为当前结点修改后的最终值,可以用dfs遍历的方式实现。
ll lca(ll x,ll y){
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]])swap(x,y);
y=fa[top[y]];
}
if(dep[x]>dep[y])swap(x,y);
return x;
}
void update1(ll x,ll y,ll k){
tree1[x]+=k;tree1[y]+=k;
tree1[lca(x,y)]-=k;
tree1[fa[lca(x,y)]]-=k;
}
void sum1(ll x,ll pre){
for(int i=head[x];i!=-1;i=z[i].next){
if(z[i].e==pre)continue;
sum1(z[i].e,x);
tree1[x]+=tree1[z[i].e];
}
}
再讲边权的区间修改:
这里的处理方式采用了,把边权下放给点权的方式,统一把边权给 这条边连接的两个点中 深度较大的点。
这样在差分的时候,只要令tree[ u ]+=1,tree[ v ]+=1;同时让
tree[ lca(u,v) ] - = 2;不难想象,在一次修改中,让 u-v 这条链上除了 lca 的所有点+1;结合点权的实际意义,lca 的值代表连接 lca 与其父亲这条边的边权,是不包含在 u-v 链中的。如此看来上面的操作正好实现了这个功能。
ll lca(ll x,ll y){
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]])swap(x,y);
y=fa[top[y]];
}
if(dep[x]>dep[y])swap(x,y);
return x;
}
void update2(ll x,ll y,ll k){
tree2[x]+=k;tree2[y]+=k;
tree2[lca(x,y)]-=2*k;
}
void sum2(ll x,ll pre){
for(int i=head[x];i!=-1;i=z[i].next){
if(z[i].e==pre)continue;
sum2(z[i].e,x);
tree2[x]+=tree2[z[i].e];
}
}
这里给出例题 HDU-5044 是一道需要用树上差分同时维护 点权和边权的题目。题目大致意思是,有两种操作:1.令 u-v 链上所有结点的权值 +k; 2.令 u-v 链上所有边的权值 +k;
这里给出我的AC代码:
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
#include<vector>
#include <iostream>
#pragma comment(linker, "/STACK:102400000,102400000")//此处采用手动扩栈,不然会T 呜呜呜~
using namespace std;
#define ll long long
#define pb push_back
#define rep(i,j,k) for(ll i=j;i<=k;i++)
#define per(i,j,k) for(ll i=j;i>=k;i--)
const ll mod=998244353;
const ll maxn=1e6+10;
ll qp(ll x,ll y){
ll ans=1;
while(y){
if(y%2)ans=ans*x%mod;x=x*x%mod;y/=2;}return ans;}
inline ll qread(){
ll s=0,w=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')w=-1;
for (;ch>='0'&&ch<='9';ch=getchar())s=(s<<1)+(s<<3)+(ch^48);
return (w==-1?-s:s);}
struct node{
ll s,e,next;
}z[2*maxn];
ll n,m,len,cnt,num=1;
ll w[maxn],son[maxn],top[maxn],fa[maxn],dep[maxn],siz[maxn];
ll tree1[maxn],tree2[maxn],head[maxn];
void add(ll a,ll b){
z[len].s=a;z[len].e=b;z[len].next=head[a];head[a]=len++;
}
void dfs1(ll x,ll pre){
fa[x]=pre;dep[x]=dep[pre]+1;son[x]=0;
for(int i=head[x];i!=-1;i=z[i].next){
if(z[i].e==pre)continue;
dfs1(z[i].e,x);
siz[x]+=siz[z[i].e];
if(siz[z[i].e]>=siz[son[x]])son[x]=z[i].e;
}
}
void dfs2(ll x,ll pre){
if(son[pre]==x)top[x]=top[pre];
else top[x]=x;
if(son[x])dfs2(son[x],x);
for(int i=head[x];i!=-1;i=z[i].next){
if(z[i].e==pre)continue;
if(z[i].e==son[x])continue;
dfs2(z[i].e,x);
}
}
ll lca(ll x,ll y){
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]])swap(x,y);
y=fa[top[y]];
}
if(dep[x]>dep[y])swap(x,y);
return x;
}
void update1(ll x,ll y,ll k){
tree1[x]+=k;tree1[y]+=k;
tree1[lca(x,y)]-=k;
tree1[fa[lca(x,y)]]-=k;
}
void update2(ll x,ll y,ll k){
tree2[x]+=k;tree2[y]+=k;
tree2[lca(x,y)]-=2*k;
}
void sum1(ll x,ll pre){
for(int i=head[x];i!=-1;i=z[i].next){
if(z[i].e==pre)continue;
sum1(z[i].e,x);
tree1[x]+=tree1[z[i].e];
}
}
void sum2(ll x,ll pre){
for(int i=head[x];i!=-1;i=z[i].next){
if(z[i].e==pre)continue;
sum2(z[i].e,x);
tree2[x]+=tree2[z[i].e];
}
}
int main(){
ll t;
scanf("%lld",&t);
while(t--){
printf("Case #%lld:\n",num++);
len=0,cnt=0;
scanf("%lld%lld",&n,&m);
memset(head,-1,sizeof(head));
rep(i,1,n-1){
ll a,b;
scanf("%lld%lld",&a,&b);
add(a,b);add(b,a);
siz[i]=1;
}
siz[n]=1;
dfs1(1,0);
dfs2(1,0);
memset(tree1,0,sizeof(tree1));
memset(tree2,0,sizeof(tree2));
rep(i,1,m){
char op[10];
scanf("%s",op);
ll a,b,c;
scanf("%lld%lld%lld",&a,&b,&c);
if(op[3]=='1'){
update1(a,b,c);
}else{
update2(a,b,c);
}
}
sum1(1,0);sum2(1,0);
rep(i,1,n-1)printf("%lld ",tree1[i]);
printf("%lld\n",tree1[n]);
for(int i=0;i<len-2;i+=2){
ll a=z[i].e;ll b=z[i].s;
if(dep[a]>dep[b])swap(a,b);
printf("%lld ",tree2[b]);
}
if(len!=0){
ll a=z[len-2].e,b=z[len-2].s;
if(dep[a]>dep[b])swap(a,b);
printf("%lld",tree2[b]);
}
printf("\n");
}
return 0;
}
我的代码 4020ms 卡过的…亲测,这道题用线段树会T到死,然后开个手动扩栈,尽量少用数组初始化,然后这道题还卡了输出格式…很坑就对了0.0
洛谷P3995 树链剖分(伪模板)
简单讲解下题意, 已知 普通的树链剖分是以子树节点的个数为依据(即 siz【】)划分一个节点的中儿子与重链,题目会给出 1e5 个节点与 2e5 个询问,我们需要以一个新的法则来划分重链,使得对于所有询问的 从节点U到节点V的路径中轻链和重链的总数和 sum 最小。
这道题的第一想法就是贪心,令所有询问中,经过边数最多的边作为一条重链边。那么问题就转化为了找节点U到节点V之间的路径,以及使路径中所有的节点权值加1。这个功能怎么实现呢,找两点间的路径,我选择使用树链剖分的 top 数组来找到两点的LCA;至于区间修改嘛,我用的是树上差分,感觉用线段树可以的~~吧。
但是直接的树链剖分只能得82分,亲测。问题出在区间修改上。首先我们维护的是边权:
正常操作是:
tree1[x]++,tree1[y]++,tree1[lca]-=2;
但是如果fa【U】||fa【V】==LCA;那么这条边不论是不是重链,都只会经过一次,那么就可以不用操作,避免影响最优解;
同时在最后一次dfs中,判断条件要变为: <=
if(tree1[son[x]]<=tree1[t])son[x]=t;
如果用<那么,就有可能出现 非叶子节点没有重儿子的情况;
附上代码:
#include<cstdio>
#include<cmath>
#include<cstring>
#include<iostream>
#include<queue>
#include<stack>
#include<map>
#include<set>
#include<algorithm>
using namespace std;
#define ll long long
#define rep(i,j,k) for(ll i=j;i<=k;i++)
#define per(i,j,k) for(ll i=j;i>=k;i--)
const ll mod=1e9+7;
const ll maxn=1e6+5;
ll qp(ll x,ll y){
ll ans=1;
if(y<0)return 0;
while(y){
if(y%2)ans=ans*x%mod;x=x*x%mod;y/=2;}return ans;}
inline ll qread(){
ll s=0,w=1;char ch=getchar();
for(;!isdigit(ch);ch=getchar())if(ch=='-')w=-1;
for (;ch>='0'&&ch<='9';ch=getchar())s=(s<<1)+(s<<3)+(ch^48);
return (w==-1?-s:s);}
ll n,m,len=0;
ll siz[maxn],dep[maxn],fa[maxn],top[maxn]={0},son[maxn]={0};
ll head[maxn];
ll tree1[maxn]={0};
struct node{
ll s,e,next;
}z[maxn];
void add(ll a,ll b){
z[len].s=a;z[len].e=b;z[len].next=head[a];head[a]=len++;
}
void dfs1(ll x,ll pre){
fa[x]=pre;dep[x]=dep[pre]+1;tree1[x]=0;
son[x]=0;
for(int i=head[x];i!=-1;i=z[i].next){
ll t=z[i].e;
if(t==pre)continue;
dfs1(t,x);
siz[x]+=siz[t];
if(siz[t]>siz[son[x]])son[x]=t;
}
}
void dfs2(ll x,ll pre){
if(son[pre]==x)top[x]=top[pre];
else top[x]=x;
if(son[x])dfs2(son[x],x);
for(int i=head[x];i!=-1;i=z[i].next){
ll t=z[i].e;
if(t==pre)continue;
if(t==son[x])continue;
dfs2(t,x);
}
}
ll lca(ll x,ll y){
while(top[x]!=top[y]){
if(dep[top[x]]>dep[top[y]])swap(x,y);
y=fa[top[y]];
}
if(dep[x]>dep[y])swap(x,y);
return x;
}
void update1(ll x,ll y){
ll w=lca(x,y);
if(fa[x]!=w)tree1[x]++,tree1[w]--;
if(fa[y]!=w)tree1[y]++,tree1[w]--;
}
void sum1(ll x,ll pre){
son[x]=0;
for(int i=head[x];i!=-1;i=z[i].next){
ll t=z[i].e;
if(t==pre)continue;
sum1(t,x);
tree1[x]+=tree1[t];
if(tree1[son[x]]<=tree1[t])son[x]=t;
}
}
int main(){
n=qread(),m=qread();
memset(head,-1,sizeof(head));
rep(i,1,n-1){
ll a,b;
scanf("%lld%lld",&a,&b);
add(a,b);add(b,a);
siz[i]=1;
}
siz[n]=1;
dfs1(1,0);dfs2(1,0);
for(int i=1;i<=m;i++){
ll a=qread(),b=qread();
update1(a,b);
}
sum1(1,0);
for(int i=1;i<=n;i++){
printf("%lld\n",son[i]);
}
return 0;
}
我试过用vector存边,但是只能拿82分,但是用链式前向星就100 了,这里存疑。
对于链式前向星和vector的区别,我也不是很明白,之前做到过一道题目,用 vector 就是会T,但是链式前向星就过了,吸取教训,以后都用链式前向星存边吧。