图论——树上前缀和和树上差分(LCA应用)

树上前缀和

原理

在这里插入图片描述

应用

P4427 求和

P4427

在这里插入图片描述

#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;

typedef long long LL;
const LL mod=998244353;
const int N=3e5+10,M=2*N; 
int n,m;
int h[N],to[M],ne[M],tot;
int fa[N][22];//fa[u][i]表示从u向上跳2^i层的祖先结点
int dep[N];   //dep[v]表示v的深度
LL mi[60];   //mi[j]表示dep[v]的j次幂
LL s[N][60]; //s[v][j]表示从根到v的路径节点的深度的j次幂之和

void add(int a, int b){ 
  to[++tot]=b,ne[tot]=h[a],h[a]=tot;
}
void dfs(int u, int f){ //预处理节点信息
  for(int i=1; i<=20; i++) 
    fa[u][i]=fa[fa[u][i-1]][i-1];
  for(int i=h[u];i;i=ne[i]){
    int v=to[i];
    if(v==f) continue;
    fa[v][0]=u; dep[v]=dep[u]+1;
    for(int j=1;j<=50;j++) mi[j]=mi[j-1]*dep[v]%mod;
    for(int j=1;j<=50;j++) s[v][j]=(mi[j]+s[u][j])%mod;
    dfs(v, u);
  }
}
int lca(int u, int v){ //倍增法求lca
  if(dep[u]<dep[v])swap(u, v);
  for(int i=20; ~i; i--)
    if(dep[fa[u][i]]>=dep[v]) u=fa[u][i];
  if(u==v) return v;
  for(int i=20; ~i; i--)
    if(fa[u][i]!=fa[v][i]) u=fa[u][i], v=fa[v][i];
  return fa[u][0];
}
int main(){
  scanf("%d",&n); 
  for(int i=1,a,b; i<n; i++){
    scanf("%d%d",&a,&b);
    add(a,b); add(b,a);
  }
  mi[0]=1; dfs(1, 0);
  
  scanf("%d",&m);
  for(int i=1,u,v,k;i<=m;i++){
    scanf("%d%d%d",&u,&v,&k);
    int l=lca(u,v);
    LL ans=(s[u][k]+s[v][k]-s[l][k]-s[fa[l][0]][k]+2*mod)%mod;
    printf("%lld\n",ans);
  }
}

树上差分

原理

在这里插入图片描述

应用

P3128 Max Flow P

P3128

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=5e4+10;
int n,k,x,y;
vector<int> e[N];
int fa[N][20],dep[N],diff[N];
int ans=-1;
void dfs(int u,int f){
	fa[u][0]=f,dep[u]=dep[f]+1;
	for(int i=1;i<=19;i++){
		fa[u][i]=fa[fa[u][i-1]][i-1];
	}
	for(auto v:e[u]){
		if(v!=f){
			dfs(v,u);
		}
	}
}
int lca(int u,int v){
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=19;i>=0;i--){
		if(dep[fa[u][i]]>=dep[v])
			u=fa[u][i];
	}
	if(u==v) return u;
	for(int i=19;i>=0;i--){
		if(fa[u][i]!=fa[v][i])
			u=fa[u][i],v=fa[v][i];
	}
	return fa[u][0];
}

void dfs2(int u,int f){
	for(auto v:e[u]){
		if(v!=f){
			dfs2(v,u);
			diff[u]+=diff[v];
		}
	}
	ans=max(ans,diff[u]);
}
int main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	scanf("%d%d",&n,&k);//用scanf和printf比较好,
	for(int i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		e[x].push_back(y);
		e[y].push_back(x);
	}
	dfs(1,0);
	while(k--){
		scanf("%d%d",&x,&y);
		int l=lca(x,y);
		diff[x]++,diff[y]++,diff[l]--,diff[fa[l][0]]--;
	}
	dfs2(1,0);
	printf("%d",ans);
	return 0;
} 

砍树(蓝桥杯真题)

传送门
在这里插入图片描述
本题思路:将树上区间加操作,最后统计每条边总和,这马上想到差分,因为是树上,所以使用树上差分

#include <iostream>
#include <algorithm>
#include <vector>
#include<map>
using namespace std;
typedef long long ll;
typedef pair<int,int> pii;
const int maxn = 1e5 + 50;

vector<int> e[maxn];
ll fa[maxn], dep[maxn], son[maxn], siz[maxn], top[maxn];
ll diff[maxn];
map<pii,ll> id;
void dfs1(ll u, ll f) {
    fa[u] = f;
    dep[u] = dep[f] + 1;
    siz[u] = 1;
    for (auto x: e[u]) {
        ll v = x;
        if (v == f) continue;
        dfs1(v, u);
        siz[u] += siz[v];
        if (siz[v] > siz[son[u]]) son[u] = v;
    }
}

void dfs2(ll u, ll t) {
    top[u] = t;
    if (!son[u]) return;
    dfs2(son[u], t);
    for (auto x: e[u]) {
        ll v = x;
        if (v != son[u] && v != fa[u]) dfs2(v, v);
    }
}

ll lca(ll x, ll y) {
    while (top[x] != top[y]) {
        if (dep[top[x]] < dep[top[y]]) swap(x, y);
        x = fa[top[x]];
    }
    return dep[x] > dep[y] ? y : x;
}

void dfs(int x, int fx) {
    for (auto y: e[x]) {
        if (y == fx) continue;
        dfs(y, x);
        diff[x] += diff[y];
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);
    int n, m;
    cin >> n >> m;
    for (int i = 1; i < n; i++) {
        int u, v;
        cin >> u >> v;
        e[u].emplace_back(v);
        e[v].emplace_back(u);
        id[{u,v}]=i;
        id[{v,u}]=i;
    }
    dfs1(1, 1);
    dfs2(1, 1);
    for (int i = 1; i <= m; i++) {
        int u, v;
        cin >> u >> v;
        diff[u]++, diff[v]++, diff[lca(u, v)] -= 2;
    }
    dfs(1, 0);
    ll ans = -1;
    for (int i = 0; i <= n; i++){
        if (diff[i] >= m){
            ans=max(ans,id[{i,fa[i]}]);
        }
    }
    cout << ans << endl;
    return 0;
}

注意:以两个点存储边号,标记。边差分原理是,将边差分转换为点差分,除根节点外,每个点都代表点上面的那条边。

U143800 暗之连锁

传送门

在这里插入图片描述

难点:

1.当边累积超过1次,则说明删除此边不能形成满足条件,所以不会影响答案。
2.当累积为1次时,答案加一,删除此边和构成环的附加边。
3.当累积为0次时,答案加m,说明删除此边就满足条件,然后随便删除一条附加边即可。

然后用set存有哪些点,最好答案还要减去m,因为根节点为0,但是其不表示边。注意set适合去重,存点,但是有顺序的时候得慎重考虑,因为set会自动排序。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+10;
int n,k,x,y;
vector<int> e[N];
set<int> st;
int fa[N][20],dep[N];
ll diff[N];
ll ans=0;
void dfs(int u,int f){
	fa[u][0]=f,dep[u]=dep[f]+1;
	for(int i=1;i<=19;i++){
		fa[u][i]=fa[fa[u][i-1]][i-1];
	}
	for(auto v:e[u]){
		if(v!=f){
			dfs(v,u);
		}
	}
}
int lca(int u,int v){
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=19;i>=0;i--){
		if(dep[fa[u][i]]>=dep[v])
			u=fa[u][i];
	}
	if(u==v) return u;
	for(int i=19;i>=0;i--){
		if(fa[u][i]!=fa[v][i])
			u=fa[u][i],v=fa[v][i];
	}
	return fa[u][0];
}

void dfs2(int u,int f){
	for(auto v:e[u]){
		if(v!=f){
			dfs2(v,u);
			diff[u]+=diff[v];
		}
	}
}
int main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	scanf("%d%d",&n,&k);
	int m=k;
	for(int i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		e[x].push_back(y);
		e[y].push_back(x);
		st.insert(x);
		st.insert(y);
	}
	dfs(1,0);
	while(k--){
		scanf("%d%d",&x,&y);
		int l=lca(x,y);
		diff[x]++,diff[y]++,diff[l]-=2;
	}
	dfs2(1,0);
	for(auto x:st){
		if(diff[x]==1) ans++;
		else if(!diff[x]) ans+=m;
	}
	printf("%d",ans-m);
	return 0;
} 

P3258 松鼠的新家

传送门
在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=3e5+10;
int n,k,x,y;
vector<int> e[N];
int fa[N][22],dep[N],a,b,t[N];
ll diff[N];
ll ans=0;
void dfs(int u,int f){
	fa[u][0]=f,dep[u]=dep[f]+1;
	for(int i=1;i<=21;i++){
		fa[u][i]=fa[fa[u][i-1]][i-1];
	}
	for(auto v:e[u]){
		if(v!=f){
			dfs(v,u);
		}
	}
}
int lca(int u,int v){
	if(dep[u]<dep[v]) swap(u,v);
	for(int i=21;i>=0;i--){
		if(dep[fa[u][i]]>=dep[v])
			u=fa[u][i];
	}
	if(u==v) return u;
	for(int i=21;i>=0;i--){
		if(fa[u][i]!=fa[v][i])
			u=fa[u][i],v=fa[v][i];
	}
	return fa[u][0];
}

void dfs2(int u,int f){
	for(auto v:e[u]){
		if(v!=f){
			dfs2(v,u);
			diff[u]+=diff[v];
		}
	}
}
int main(){
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	scanf("%d",&n);
	for(int i=0;i<n;i++){
		scanf("%d",&t[i]);
	}
	for(int i=1;i<n;i++){
		scanf("%d%d",&x,&y);
		e[x].push_back(y);
		e[y].push_back(x);
	}
	dfs(1,0);
	for(int i=1;i<n;i++){
		x=t[i-1],y=t[i];
		int l=lca(x,y);
		diff[x]++,diff[y]++,diff[l]--,diff[fa[l][0]]--;
	}
	dfs2(1,0);
	for(int i=1;i<n;i++) diff[t[i]]--;//要先减,看清楚题意要求的输出
	for(int i=1;i<=n;i++){
		printf("%d\n",diff[i]);
	}
	return 0;
} 

P2680 运输计划

传送门

题意:让一条边长度(时间)变为0,求所有运输计划所需最大时间最小。求最大值最小——二分答案。

在这里插入图片描述

#include<bits/stdc++.h>
using namespace std;
const int N=3e5+10,M=2*N;

int h[N],e[M],w[M],ne[M],idx;//链式向前星 
int fa[N],son[N],sz[N],top[N],dep[N];//树剖求最近公共祖先 
int n,m,R,L,cnt;//R表示最大航道飞行时间,L表示最大每条边的飞行时间
//diff树上差分数组,sum树上前缀和数组 
int diff[N],sum[N],dnf[N],val[N];//dnf从祖宗节点重新映射每个树节点,val[i]存储点i上面的边长度。 
void add(int a,int b,int c){
	e[idx]=b,ne[idx]=h[a],w[idx]=c,h[a]=idx++;
}
struct node{
	int u,v,l,d;//存储运输计划的两个端点和最近公共祖先节点,所需耗时时间 
}edge[N];

void dfs1(int u,int f,int len){//预处理除了top的数组,和前缀和数组 
	fa[u]=f,dep[u]=dep[f]+1,sz[u]=1,sum[u]=len,dnf[++cnt]=u;//dnf和val这个技巧要学会 
	for(int i=h[u];~i;i=ne[i]){
		int j=e[i];
		if(j==f) continue;
		dfs1(j,u,len+w[i]);
		val[j]=w[i];
		sz[u]+=sz[j];
		if(sz[son[u]]<sz[j]) son[u]=j;
	}
}

void dfs2(int u,int t){//预处理top数组,重链 
	top[u]=t;
	if(!son[u]) return;
	dfs2(son[u],t);
	for(int i=h[u];~i;i=ne[i]){
		int v=e[i];
		if(v==fa[u]||v==son[u]) continue;
		dfs2(v,v);
	}
}
int lca(int u,int v){
	while(top[u]!=top[v]){
		if(dep[top[u]]<dep[top[v]]) swap(u,v);
		u=fa[top[u]];
	}
	return dep[u]<dep[v]?u:v;
}
void dfs3(int u){//树上差分求和 
	for(int i=h[u];~i;i=ne[i]){
		int v=e[i];
		if(v==fa[u]) continue;
		dfs3(v);
		diff[u]+=diff[v];
	}
}
bool check(int x){
	memset(diff,0,sizeof diff);
	int count=0;
	for(int i=1;i<=m;i++){
		if(edge[i].d>x){
			count++;
			int u=edge[i].u,v=edge[i].v,l=edge[i].l;
			diff[u]++,diff[v]++,diff[l]-=2;
		}
	}
	dfs3(1);
	for(int i=1;i<=n;i++){
		if(val[dnf[i]]>=R-x&&diff[dnf[i]]>=count)//有一条边长度大于等于缺少的部分,并且被覆盖次数大于count次 
			return true;
	}
	return false;
}
int main(){
	memset(h,-1,sizeof h);
	int a,b,c;
	scanf("%d%d",&n,&m);
	for(int i=1;i<n;i++){
		scanf("%d%d%d",&a,&b,&c);
		add(a,b,c),add(b,a,c);
		L=max(L,c);
	}
	dfs1(1,0,0),dfs2(1,1);
	for(int i=1;i<=m;i++){
		scanf("%d%d",&a,&b);
		int l=lca(a,b);
		int d=sum[a]+sum[b]-sum[l]*2;
		edge[i]={a,b,l,d};
		R=max(R,d);
	}
	int l=R-L-1,r=R+1;
	while(l+1<r){
		int mid=l+r>>1;
		if(check(mid)) r=mid;
		else l=mid;
	} 
	printf("%d\n",r);
}

开心,一遍敲过,想代码时间应该占完成题目总时间的七成,想好了,bug才很少。
经验:
1.用点表示点上面边的长度
2.二分答案技巧。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值