有一个树形的水系,由 N−1 条河道和 N 个交叉点组成。
我们可以把交叉点看作树中的节点,编号为 1∼N,河道则看作树中的无向边。
每条河道都有一个容量,连接 xx 与 yy 的河道的容量记为 c(x,y)。
河道中单位时间流过的水量不能超过河道的容量。
有一个节点是整个水系的发源地,可以源源不断地流出水,我们称之为源点。
除了源点之外,树中所有度数为 1 的节点都是入海口,可以吸收无限多的水,我们称之为汇点。
也就是说,水系中的水从源点出发,沿着每条河道,最终流向各个汇点。
在整个水系稳定时,每条河道中的水都以单位时间固定的水量流向固定的方向。
除源点和汇点之外,其余各点不贮存水,也就是流入该点的河道水量之和等于从该点流出的河道水量之和。
整个水系的流量就定义为源点单位时间发出的水量。
在流量不超过河道容量的前提下,求哪个点作为源点时,整个水系的流量最大,输出这个最大值。
输入格式
输入第一行包含整数 T,表示共有 T 组测试数据。
每组测试数据,第一行包含整数 N。
接下来 N−1 行,每行包含三个整数 x,y,z,表示 x,y之间存在河道,且河道容量为 z。
节点编号从 1 开始。
输出格式
每组数据输出一个结果,每个结果占一行。
数据保证结果不超过 2^31−1。
数据范围
N≤2×10^5
输入样例:
1
5
1 2 11
1 4 13
3 4 5
4 5 10
输出样例:
26
题目大意:以树的形式给出一个水系,由N个结点和N-1条边组成,边表示河道,每条河道有一个单位时间最大水流量,点表示河道的交叉点,交叉点中有两类比较特殊的点一类叫做源点,从该点可以留出无穷多的水,一类叫做汇点,可以有无穷多的水流入该点,让我们选择一个点作为源点,得到单位时间内的最大水流量。
下面是我一开始写的一个暴力做法,最终会T掉。
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N=2E5+10;
int h[N],e[N<<1],w[N<<1],ne[N<<1],idx,f[N];
int n,ans;
void add(int u,int v,int val)
{
e[idx]=v;
ne[idx]=h[u];
w[idx]=val;
h[u]=idx++;
}
void dfs(int u,int fa)
{
int t;
int flag=0;
if(fa==-1)//如果说当前结点是源点
t=0x3f3f3f3f;//t记录的是从u的父节点到u的河道所能承载的最大水流量
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)
{
t=w[i];
continue;
}
flag=1;
dfs(j,u);
f[u]+=f[j];//f[u]暂时表示u的所有子节点所能通过的最大水流量之和
}
if(flag==0)//如果u是叶子结点那么其叶子结点所能通过的最大水流量是无穷大
f[u]=0x3f3f3f3f;
f[u]=min(f[u],t);//更新f[u],得到能通过u点的最大水流量
}
int main()
{
int t;
cin>>t;
while(t--)
{
cin>>n;
memset(h,-1,sizeof h);
idx=0;
ans=0;
for(int i=1;i<n;i++)
{
int x,y,z;
cin>>x>>y>>z;
add(x,y,z);
add(y,x,z);
}
for(int i=1;i<=n;i++)//枚举每个点作为源点,所有情况中取一个最大值
{
memset(f,0,sizeof f);
dfs(i,-1);
ans=max(ans,f[i]);
}
cout<<ans<<endl;
}
return 0;
}
现在我们的任务就是如何对其进行优化,这段暴力代码中,时间复杂度最高的是枚举每一个点作为源点进行搜索,所以我们要在这个地方进行优化,优化方法y总讲的是用换根法。换根法的一般思路如下:(1)、首先从底向上递推,将对问题求解有帮助的信息先计算出来,并进行存储。(2)、从上往下递推,这一步就是要得到我们最终的解,这一步会用的信息已经在上一步中求出并记录,这里只需要通过一定的计算公式即可得到答案。
对于这个题来说,第一步中我们需要记录 一个d[u],表示不考虑u和父节点之间河道的承载量,能经过u点流向u点孩子结点的最大流量,d[u]的值等于所有min(d[son],c(u,son))之和,c(u,son)表示u和son之间河道的最大承载量,如果当前结点是汇点,那么能够通过该节点的最大流量记为无穷大。第二步要求出f[u],表示以u为源点所能通过的最大流量,在第一步中我们首先要选择一个点作为根节点开始扩展求解,记该点为root,所以在第二步中f[root]=d[root],然后从root点开始向下扩展,扩展的时候分两种情况,一种情况是如果新扩展出来的结点j是叶子结点,那么f[j]=min(f[u]-w[i],w[i]),如果j不是叶子结点,那么f[j]=d[j]+min(f[u]-min(d[j],w[i],w[i]),至于为什么要分开求解因为如果j为叶子结点,那么d[j]就为正无穷,从不是叶节点的递推公式来看,d[j]显然是不能再加了,然后min(d[j],w[i])=w[i],所以才有了第一个递推公式。
最后再来说一个比较坑的点,在第一步中我们首先要选择一个根节点,这个根节点的选择其实是有一定讲究的,就是这个根节点不能选择度为1的点作为根节点,否则就会被下面两组数据给卡掉。
Input:
1
3
1 2 1
2 3 1
Ans
2(如果随便选择根节点,答案就是无穷大,下面的样例结果同样如此)
Input:
1
2
1 2 1
Ans
1
如果最终我们发现所有节点的度都是1,说明这棵树一共就两个结点,一条边,直接输出w[0]即可。
上正确代码:
#include <iostream>
#include <cstring>
#include <cstdio>
#include <algorithm>
using namespace std;
const int N =2e5+10;
int h[N],e[N<<1],ne[N<<1],w[N<<1],idx,f[N],n,d[N];
int deg[N];//记录每个点的度
void add(int u,int v,int val)
{
e[idx]=v;
ne[idx]=h[u];
w[idx]=val;
h[u]=idx++;
}
int dfs_d(int u,int fa)
{
if(deg[u]==1)//u为叶子结点
{
d[u]=0x3f3f3f3f;
return d[u];
}
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)
continue;
d[u]+=min(w[i],dfs_d(j,u));
}
return d[u];
}
void dfs_f(int u,int fa)
{
for(int i=h[u];i!=-1;i=ne[i])
{
int j=e[i];
if(j==fa)
continue;
if(deg[j]==1)
f[j]=min(f[u]-w[i],w[i]);
else
{
f[j]=d[j]+min(f[u]-min(d[j],w[i]),w[i]);
dfs_f(j,u);
}
}
}
int main()
{
int t;
cin>>t;
while(t--)
{
cin>>n;
memset(h,-1,sizeof h);
memset(deg,0,sizeof deg);
memset(d,0,sizeof d);
memset(f,0,sizeof f);
idx=0;
for(int i=1;i<n;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
deg[a]++;
deg[b]++;
}
int root=1;//找到度不为1的点作为叶子结点
while(root<=n&°[root]==1)
root++;
if(root>n)
{
cout<<w[0]<<endl;
continue;
}
dfs_d(root,-1);
f[root]=d[root];
dfs_f(root,-1);
int res=0;
for(int i=1;i<=n;i++)
res=max(res,f[i]);
cout<<res<<endl;
}
return 0;
}