洛谷P3128 [USACO15DEC] Max Flow P
洛谷P3128 [USACO15DEC] Max Flow P
洛谷题目传送门
题目描述
Farmer John 在他的谷仓中安装了 N − 1 N-1 N−1 条管道,用于在 N N N 个牛棚之间运输牛奶( 2 ≤ N ≤ 50 , 000 2 \leq N \leq 50,000 2≤N≤50,000),牛棚方便地编号为 1 … N 1 \ldots N 1…N。每条管道连接一对牛棚,所有牛棚通过这些管道相互连接。
FJ 正在 K K K 对牛棚之间泵送牛奶( 1 ≤ K ≤ 100 , 000 1 \leq K \leq 100,000 1≤K≤100,000)。对于第 i i i 对牛棚,你被告知两个牛棚 s i s_i si 和 t i t_i ti,这是牛奶以单位速率泵送的路径的端点。FJ 担心某些牛棚可能会因为过多的牛奶通过它们而不堪重负,因为一个牛棚可能会作为许多泵送路径的中转站。请帮助他确定通过任何一个牛棚的最大牛奶量。如果牛奶沿着从 s i s_i si 到 t i t_i ti 的路径泵送,那么它将被计入端点牛棚 s i s_i si 和 t i t_i ti,以及它们之间路径上的所有牛棚。
输入格式
输入的第一行包含 N N N 和 K K K。
接下来的 N − 1 N-1 N−1 行每行包含两个整数 x x x 和 y y y( x ≠ y x \ne y x=y),描述连接牛棚 x x x 和 y y y 的管道。
接下来的 K K K 行每行包含两个整数 s s s 和 t t t,描述牛奶泵送路径的端点牛棚。
输出格式
输出一个整数,表示通过谷仓中任何一个牛棚的最大牛奶量。
输入输出样例 #1
输入 #1
5 10
3 4
1 5
4 2
5 4
5 4
5 4
3 5
4 3
4 3
1 3
3 5
5 4
1 5
3 4
输出 #1
9
说明/提示
2 ≤ N ≤ 5 × 1 0 4 , 1 ≤ K ≤ 1 0 5 2 \le N \le 5 \times 10^4,1 \le K \le 10^5 2≤N≤5×104,1≤K≤105。
题目分析
我们把题目简化一下:
给定一颗无向图 E E E,输入 K K K个数 x , y x,y x,y,对于每个 x , y x,y x,y,将路径 [ x , y ] [x,y] [x,y]上的所有点权加 1,试求出
点权最大的点 u u u
由此,我们便很容易看出这是一道树链剖分的题,而且是重链剖分
由此,我们便有两个思路:
F i r s t : First: First:
我们将原来的树链剖分的线段树转换为一个差分,最后只需求出前缀和即可。
该思路的比较简单,我就不写了
S e c o n d : Second: Second:
由于要将一条链上的所有点权都加 1 ,那我们很自然就想到了线段树的区间修改,由于要用到懒标
记,我们的代码会很长很长,正好可以复习一下线段树区间修改
Code:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int h[N],to[N],ne[N],tot,a[N];
void add(int u,int v){
tot++;to[tot]=v;ne[tot]=h[u];h[u]=tot;
}
int n,m;
struct tre{
int data,l,r,lzy;//data为区间[l,r]的区间和,lzy为懒标记
};
class Segment_tree{//线段树模板(懒标记+区间修改+区间查询)
private:
int w[N*4];//建树时叶子节点点权
tre tree[N*4];
void pushup(int u){tree[u].data=tree[u*2].data+tree[u*2+1].data;}//由下而上更新data
void add(int u,int v){tree[u].data+=(tree[u].r-tree[u].l+1)*v;tree[u].lzy+=v;}//将 u 点对应区间中的每一个点都加上 v
void pushdown(int u){//懒标记下传到左右子树
if(!tree[u].lzy)return;
add(u*2,tree[u].lzy);add(u*2+1,tree[u].lzy);//左右子树分别加上 tree[u].lzy
tree[u].lzy=0;//易错点:懒标记下传后一定要清零
}
void _build(int u,int l,int r){//建树
tree[u]={0,l,r,0};
if(l==r){tree[u].data=w[l];return;}//注意:一定是区间对应实际值,而非节点编号
int mid=(l+r)/2;
_build(u*2,l,mid);_build(u*2+1,mid+1,r);//分别建立左右子树
pushup(u);//向上整合区间和
}
void modify(int u,int x,int y,int v){//将区间[x,y]的每一个值都加上 v
int l=tree[u].l,r=tree[u].r;
if(l>y||r<x)return;//如果当前节点对应区间与目标区间无交集,退出
if(x<=l&&r<=y){add(u,v);return;}//如果当前区间为目标区间的子集,直接加
pushdown(u);//注意:无论何时要访问子节点就要下传懒标记
modify(u*2,x,y,v);modify(u*2+1,x,y,v);//细分区间修改
pushup(u);//向上更新修改后的值
}
int getsum(int u,int x,int y){
int l=tree[u].l,r=tree[u].r;
if(l>y||r<x)return 0;//无交集,直接退(返回一个不影响答案的值)
if(x<=l&&r<=y){return tree[u].data;}//为自己,返回(全部)值
pushdown(u);//访问子(节点),传(懒)标记
return getsum(u*2,x,y)+getsum(u*2+1,x,y);//合答案
}
public://对外封装
void build(int l,int r,int a[]){//以a[]为叶子节点的值建树
memcpy(w,a,sizeof(w));
_build(1,l,r);
}
void update(int l,int r,int v){modify(1,l,r,v);}//将区间[l,r]的值加上v
int sum(int l,int r){return getsum(1,l,r);}//求区间[l,r]的和
};
class Heavy_chain_decomposition{//重链剖分
private:
int id[N],reid[N*4],siz[N],fa[N],dep[N],son[N],top[N],tim,w[N];
//重链剖分数组:
//id[u]:u节点在线段树中的编号
//reid[u]:线段树中u点对应的节点
//siz[u]:u节点及其子树大小
//fa[u]:u节点的父节点
//dep[u]:u节点的深度(dep越大,深度越大,id越大,节点越靠下)
//son[u]:u节点的重儿子(siz最大的儿子)
//top[u]:u节点所在链的链头(dep最小的节点)
//tim:访问时间,用来标号
//w:建树数组
Segment_tree tr;//剖分出的链对应线段树
void dfs1(int u,int fath,int dp){//第一次深搜求出dep,siz,fa,son
//u为当前节点,fath为父节点,dp为深度
dep[u]=dp;siz[u]=1;fa[u]=fath;//更新dep,siz,fa(siz初始值为1:包括u节点)
for(int i=h[u];i;i=ne[i]){//向下找子节点
int v=to[i];
if(v==fath)continue;//不能跑回去
dfs1(v,u,dp+1);//先向下搜,在整合siz
siz[u]+=siz[v];//整合siz[u]
if(siz[v]>siz[son[u]])son[u]=v;//求出重儿子
}
}
void dfs2(int u,int fath,int ori){//第二次深搜求出id,reid,top
//u为当前节点,fath为父节点,ori为当前节点所在链的链头
//Q:为什么要第二次深搜才能求出id?
//A:因为我们第一次深搜是乱访问的,没有遵循先重(儿子)后轻(儿子)的顺序
id[u]=++tim;reid[id[u]]=u;top[u]=ori;//更新id,reid,top值
if(son[u]){dfs2(son[u],u,ori);}//有重儿子就先访问,生成重链
//重链上的点都是重儿子,这样经过的点会更多(注意不是链上点更多)
for(int i=h[u];i;i=ne[i]){
int v=to[i];
if(v==fath||v==son[u])continue;
dfs2(v,u,v);//生成轻链
}
}
void build_tree(int u){//建树:建立线段树
dfs1(u,0,0);dfs2(u,0,u);
tr.build(1,n,w);
}
public://对外封装
void change(int a[]){//将a数组中每个点对应的值转化到线段树叶子节点对应的值上
for(int i=1;i<=n;i++)w[id[i]]=a[i];
}
void build(int u,int a[]){//以u点的点权为a[u]建树
change(a);
build_tree(u);
}
void update(int x,int y,int v){//将路径[x,y]上的每一个点的点权都加上v
while(top[x]!=top[y]){//一直向上跳,直到跳到同一条链上
if(dep[top[x]]<dep[top[y]])swap(x,y);//链头深(dep大)的先跳
tr.update(id[top[x]],id[x],v);//跳的时候将经过的点权都加v
x=fa[top[x]];//跳到链头的父节点(更高一条链)
}
if(dep[x]>dep[y])swap(x,y);//已经到同一条链上了,让x的id小(dep小),方便修改
tr.update(id[x],id[y],v);//修改
}
int sum(int x,int y){
int res=0;
while(top[x]!=top[y]){//跳(到链头)同高
if(dep[top[x]]<dep[top[y]])swap(x,y);//(深度)深先跳
res+=tr.sum(id[top[x]],id[x]);//累加(点权)和
x=fa[top[x]];//(向)上跳(到更高的)链
}
if(dep[x]>dep[y])swap(x,y);//(深度)小在前
res+=tr.sum(id[x],id[y]);
return res;
}
}tr;
int main(){
cin>>n>>m;
for(int i=1;i<=n-1;i++){
int x,y;
cin>>x>>y;add(x,y);add(y,x);
}
tr.build(1,a);//一开始为空树
for(int i=1;i<=m;i++){
int x,y;cin>>x>>y;
tr.update(x,y,1);
}
int ans=0;
for(int i=1;i<=n;i++)ans=max(ans,tr.sum(i,i));
cout<<ans<<'\n';
return 0;
}
总结
树链剖分和线段树告诉了我们一个道理:不要有畏难情绪,理解了再难的代码也能打出来