CF545E Paths and Trees(最短路径树)

LawrenceSivan

2021-06-07 11:18:07

题解

CF545E Paths and Trees

前言:

学这个的时候看了一些博客,感觉其实这一块网上很多博客和题解其实一些细节没有说太清楚。于是今天主要想说一下关于最短路径树的一些细节与处理问题。

最短路径树

$SBT$ 有以下特点: - 树上任意不属于根的节点 $x$,$dis(root,x)=$ 原图走到 $x$ 的最短路。 - 全图联通,且有 $n-1$ 条边。 与最小生成树的区别:最小生成树只需要满足全图联通就可以了。 这个问题[这篇日报](https://xyzl.blog.luogu.org/Shortest-Path-Tree-SPT)写的很清楚。 一般用 _Dijkstra_ 实现。 大家都知道, _Dijkstra_ 实现过程就像是一条一条把边拉进来,于是我们只需要在松弛操作的时候记录一下前驱就可以了。 这个与一般的 _Dijkstra_ 无异。 ```cpp int pre[maxn]; bool vis[maxn]; ll dis[maxn],ans[maxn]; #define P pair<ll,int> #define mp make_pair inline void Dijkstra(int s){ priority_queue <P,vector<P>,greater<P> > q; memset(vis,0,sizeof(vis)); memset(dis,0x3f,sizeof(dis)); dis[s]=0; q.push(mp(0,s)); while(!q.empty()){ int x=q.top().second;q.pop(); if(vis[x])continue; vis[x]=true; for(re int i=head[x];i;i=nxt[i]){ int v=to[i]; if(dis[v]>=dis[x]+w[i]){ dis[v]=dis[x]+w[i]; pre[v]=i;//记录前驱 q.push(mp(dis[v],v)); } } } } ``` 复杂度 $O((n+m)logn)

细节部分

1:关于这道题的题目要求(权值和最小):

如何考虑

给定一张带正权的无向图和一个源点,求边权和最小的最短路径树。

要求我们求出边权和最小的。

可以画图理解:

显然,从节点 1 到 节点 5 有两条路径都是最短的。

如果选择 1->2->3->5 这一条,那么最短路树长这个样子:

权值和是 1+1+1+1=4.

如果选择 1->4->5 这一条,最短路树长这样:

权值和是 1+1+1+2=5.

根据题意,选择上面那一种。

为什么?

我们反向考虑:

当我们找到一个节点,发现有两条路径都是最短路,为了求最短路径树,必然只有一条路能被选到,也就是说,一个边会成为树边,另一个就是非树边,而最短路径树权值和最小是针对树边而言的,于是我们选择的问题就集中在连接这个点的两条边哪一个更小,并使之成为树边。

有的题解会这么写:

为了满足本题的要求,还要生成树边权最小。自然是两点间路程相等情况下,经过的边数越多越好

其实这是不正确的。

通过一张图来看:

可以发现,从节点 1 到节点 4 的最短路有两条。

路线一 1->2->3->4 ,边数是 3

路线二 1->5->6->7->8->9->4 , 边数是 6.

若选择路线一:

最短路树:

权值和为 10+10+10+2+2+2+2+2=40.

若选择路线二:

最短路树:

权值和为 10+10+2+2+2+2+2+20=50.

代码实现

在满足这个问题的时候,大部分题解都会这么写:

for(re int i=head[x];i;i=nxt[i]){
    int v=to[i];
    if(dis[v]>dis[x]+w[i]){
        dis[v]=dis[x]+w[i];
        pre[v]=i;
        q.push(mp(dis[v],v));
    }
    else if(dis[v]==dis[x]+w[i]&&w[i]<w[pre[v]])pre[v]=i;
}

就是说他们把权值相等时单独拿出来进行判断,进行松弛的时候如果松弛前的结果与松弛后的结果相等即 dis[now]=dis[next]+w,比较连接这条点的边的大小,即 w[i]w[pre[next]] ,如果 w[i]<w[pre[next]],那么更新当前的 pre

其实这是完全没必要的。

画图理解:

假如现在只有一个点没有在最短路树中。

由于我们使用的是堆优化的 Dijkstra ,所以率先被扩展出来的点的 dis 一定是更小的。

比如上图中,如果想要扩展到 5 ,那么只能从 3 或者 4 而来。

又因为他们的路径权值是一样的,这只能说明 连接 $3,5$ 的边的权值是小于连接 $4,5$ 的边的权值的。 根据上面的内容,我们需要选择 $3->5$ 这个边。 于是可以发现: **在最短路相等的情况下,扩展到同一个节点,后出堆的点连的边权值一定更小** 所以我们直接就可以省去分类讨论的功夫,直接把他们写在一起: ```cpp for(re int i=head[x];i;i=nxt[i]){ int v=to[i]; if(dis[v]>=dis[x]+w[i]){//这里加上等号 dis[v]=dis[x]+w[i]; pre[v]=i; q.push(mp(dis[v],v)); } //else if(dis[v]==dis[x]+w[i]&&w[i]<w[pre[v]])pre[v]=i; //上面这行直接删掉 } ``` ### $2$:关于边的 $id$ 记录 也有一些同志写的时候喜欢记录一下边的 $id$ ,虽然这样做也可以,但是我个人更喜欢另外一种方式:在循环的时候直接用 $i$ 来作为 $id$ ,这样做会省下一些空间~~虽然这些空间完全没有省下的必要~~ 单独开 $id$ : ```cpp int head[maxn],to[maxn<<1],nxt[maxn<<1],id[maxn<<1],cnt; ll w[maxn<<1]; inline void add(int u,int v,ll val,int i){ nxt[++cnt]=head[u]; to[cnt]=v; w[cnt]=val; id[cnt]=i; head[u]=cnt; } int main(){ n=read();m=read();k=read(); for(re int i=1,u,v;i<=m;i++){ ll val; u=read();v=read();val=read(); add(u,v,val,i); add(v,u,val,i); } ... } ``` 用循环变量索引: ```cpp int head[maxn],to[maxn<<1],nxt[maxn<<1],cnt; ll w[maxn<<1]; inline void add(int u,int v,ll val){ nxt[++cnt]=head[u]; to[cnt]=v; w[cnt]=val; head[u]=cnt; } int main(){ n=read();m=read();k=read(); for(re int i=1,u,v;i<=m;i++){ ll val; u=read();v=read();val=read(); add(u,v,val); add(v,u,val); } ... } ``` 但是需要注意的是,如果单独开一个 $id$ 数组用来记录 $id$ 的话,那么最后 $dfs$ 的时候 $id$ 就直接是答案了。 但是如果使用循环变量 $i$ 来进行索引,那么最后得出答案时需要 $(ans[i]+1)/2)$. 具体原因也很好想。 单独记录 $id$ 的话,双向边的 $id$ 是一样的,而循环变量索引却导致两条双向边 $id$ 不一样。 单独开 $id$ : ```cpp void dfs(int u){ if(tot>=k)return; vis[u]=true; for(re int i=head[u];i;i=nxt[i]){ int v=to[i],pos=id[i]; if(vis[v])continue; if(dis[v]==dis[u]+w[i]){ ans[++h]=pos; tot++; dfs(v); if(tot>=k)return; } } } int main(){ ... dfs(1); for(re int i=1;i<=h;i++){ printf("%d ",ans[i]); } } ``` 用循环变量索引: ```cpp void dfs(int u){ if(tot>=k)return; vis[u]=true; for(re int i=head[u];i;i=nxt[i]){ int v=to[i]; if(vis[v])continue; if(dis[v]==dis[u]+w[i]){ ans[++h]=i; tot++; dfs(v); if(tot>=k)return; } } } int main(){ ... dfs(1); for(re int i=1;i<=h;i++){ printf("%d ",(ans[i]+1)/2); } } ``` ### $3$:关于输出问题 这个题完全不用排序啊。 >You may print the numbers of the edges in any order. >If there are multiple answers, print any of them. 虽然题目翻译没说这个问题,但是关于输入输出的英文原文还是要看看的吧。 不太懂为什么别的题解要排一遍序。 _long long_ 还是要开的。 ## CODE: ```cpp //#define LawrenceSivan #include<bits/stdc++.h> using namespace std; typedef long long ll; #define re register const int maxn=3e5+5; #define INF 0x3f3f3f3f3f3f3f3f int head[maxn],to[maxn<<1],nxt[maxn<<1],cnt; ll w[maxn<<1]; inline void add(int u,int v,ll val){ nxt[++cnt]=head[u]; to[cnt]=v; w[cnt]=val; head[u]=cnt; } int pre[maxn]; bool vis[maxn]; ll dis[maxn],ans[maxn]; #define P pair<ll,int> #define mp make_pair inline void Dijkstra(int s){ priority_queue <P,vector<P>,greater<P> > q; memset(vis,0,sizeof(vis)); memset(dis,0x3f,sizeof(dis)); dis[s]=0; q.push(mp(0,s)); while(!q.empty()){ int x=q.top().second;q.pop(); if(vis[x])continue; vis[x]=true; for(re int i=head[x];i;i=nxt[i]){ int v=to[i]; if(dis[v]>=dis[x]+w[i]){ dis[v]=dis[x]+w[i]; pre[v]=i; q.push(mp(dis[v],v)); } //else if(!vis[v]&&dis[v]==dis[x]+w[i]&&w[i]<w[pre[v]])pre[v]=i; } } } int n,m,s; inline ll read(){ ll x=0,f=1;char ch=getchar(); while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();} while(isdigit(ch)){x=x*10+(ch^48);ch=getchar();} return x*f; } int main(){ #ifdef LawrenceSivan freopen("aa.in","r",stdin); freopen("aa.out","w",stdout); #endif n=read();m=read(); for(re int i=1,x,y;i<=m;i++){ ll val; x=read();y=read();val=read(); add(x,y,val); add(y,x,val); } s=read(); Dijkstra(s); ll sum=0,tot=0; for(re int i=1;i<=n;i++){ if(i==s)continue; int pos=pre[i]; sum+=w[pos]; ans[++tot]=pos; } printf("%lld\n",sum); for(re int i=1;i<=tot;i++){ printf("%lld ",(ans[i]+1)/2); } return 0; } ```