咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕
洛谷 P2934 安全路径
KONO题面哒……然而并没有完整题面
题目大意:
给出一个N节点、M条边的无向图,求出到每个节点的安全路径的最小长度。
安全路径定义:不经过1号点到当前节点的最短路的最后一条边的路径。
数据范围:
\(N\leq10^5,M\leq2*10^5\)
提供可并堆(中的左偏树)因为只学了左偏树的做法。
吐槽,题面中样例说明的迫真连通图稍微有那么一点点难看。
当然,要是解析出了题目的要点,这道题和样例说明还是挺好理解的。但做法稍微有点绕。
首先我们想到把整个连通图浓缩成一个最短路径树(即每条边都位于原图最短路上的树)。
然后原连通图就会变成介个样子。
那么,我们把原图缩成最短路径树有什么用呢?由于安全路径的定义,所以在寻找到一个节点的安全路径时,肯定有一条非树边连接在那个节点上。而又由于要求长度最小,所以整个安全路径只存在一条非树边。这样的话就好展开情况讨论了。
补:安全路径并非次短路,因为次短路也有可能经过不能通过的那条边。
(由于画的丑而且懒,就不补图了)
-
目标点(A)为叶子结点——找到所有以A为端点的线和与A相连的点,答案就为\(min(dis[x]+d)\))(d为边权)
\(Ans=min(dis[x]+d+dis[y])-dis[A]\)(此情况下\(A=y\))
-
目标点为非叶节点:
需要分两种情况——
- 从子树中节点走到自己:直接找通向子孙的最短路径。所以可以直接从上一种情况继承长度。\(Ans=min(dis[x]+d+dis[y])-dis[A]\)(y为子树中节点)
- 从其他节点走到自己:从最短路径树的构造很容易看出,肯定不能从自己的子树中绕上来,那样会消耗更多的点数。此时,就枚举所有不在自己子树中且与自己相连的点(同时也不是自己的父亲节点),并前往计算到x的最短路,再从x到A。\(Ans=min(dis[x]+d+dis[y])-dis[A]\)(此情况下\(A=y\))
至此,所有情况讨论完毕。整理得:
\(Ans=min(dis[x]+d+dis[y])-dis[A]\),x为子树外一点,y为子树内一点(包括自己).
这样的话,就跟可并堆扯上关系了。在整棵树中从下向上合并(DSU),并在计算某一节点的答案时删掉自己子树中的点。此即为最终解。
注:由于使用到了左偏树,需要像链表那样把整个多叉树转化为左儿子右兄弟(同层同父节点)的二叉树。一次DFS即可。判断是否在子树中使用DFN计数。
KONO代码哒!
#include <cstdio> #include <iostream> #include <algorithm> #include <queue> #include <cstring> using namespace std; const int maxn = 200010; struct edge { int to, next, dis; } g[maxn << 1]; int head[maxn], cnt; void add(int from, int to, int dis) { g[++cnt].to = to; g[cnt].next = head[from]; g[cnt].dis = dis; head[from] = cnt; } struct node { int val, dis, ls, rs; } sum[maxn << 1]; int n, m; struct _node { int dis, id; }; bool operator<(_node a, _node b) { return a.dis > b.dis; } priority_queue<_node> q; int root[maxn], dfi[maxn][2], son[maxn], bro[maxn], f[maxn]; int dis[maxn]; bool mark[maxn]; int merge(int x, int y) { if (!x || !y) return x + y; if (sum[x].val > sum[y].val) swap(x, y); sum[x].rs = merge(sum[x].rs, y); if (sum[sum[x].ls].dis < sum[sum[x].rs].dis) swap(sum[x].ls, sum[x].rs); sum[x].dis = sum[sum[x].rs].dis + 1; return x; } void dij(int x) { memset(dis, 0x3f, sizeof(dis)); dis[x] = 0; mark[x] = 1; _node tmp; tmp.dis = 0; tmp.id = x; q.push(tmp); while (!q.empty()) { int u = q.top().id; q.pop(); // printf("%d %d\n",u,dis[u]); for (int i = head[u]; i; i = g[i].next) { int v = g[i].to, d = g[i].dis; if (!mark[v] && dis[u] + d < dis[v]) { dis[v] = dis[u] + d; tmp.dis = dis[v]; tmp.id = v; f[v] = u; q.push(tmp); } } } } int DFN; void build_dfs(int x) { dfi[x][0]=++DFN; for(int i=son[x];i;i=bro[i]){ build_dfs(i); } dfi[x][1]=DFN; } void build() { for(int i=2;i<=n;i++){ bro[i]=son[f[i]]; son[f[i]]=i; } build_dfs(1); } int ans[maxn]; void pt() { int i=100000000; while(i--); } void dfs(int x) { // printf("%d\n",x); for(int i=head[x];i;i=g[i].next) { int v=g[i].to,d=g[i].dis; // printf("%d\n",i); if(v!=f[x]&&f[v]!=x){ // printf("www"); sum[i].dis=1; sum[i].val=dis[v]+d+dis[x]; // printf("%d %d\n",v,x); root[x]=merge(root[x],i); } } // printf("www"); for(int i=son[x];i;i=bro[i]) { dfs(i); root[x]=merge(root[x],root[i]); // printf("%d %d\n",x,i); // pt(); } int l=dfi[x][0],r=dfi[x][1]; while(root[x]) { int v=g[root[x]].to; // printf("%d %d\n",sum[root[x]].ls,sum[root[x]].rs); // pt(); if(l<=dfi[v][0]&&dfi[v][0]<=r) { root[x]=merge(sum[root[x]].ls,sum[root[x]].rs); } else break; } ans[x]=root[x]?sum[root[x]].val-dis[x]:-1; // printf("--%d %d\n",x,ans[x]); } int main() { scanf("%d%d",&n,&m); int i,j; for(int i=1;i<=m;i++) { int a,b,c; scanf("%d%d%d",&a,&b,&c); add(a,b,c); add(b,a,c); } dij(1); build(); // for(i=1;i<=n;i++)printf("-%d %d\n",dfi[i][0],dfi[i][1]); dfs(1); for(i=2;i<=n;i++)printf("%d\n",ans[i]); return 0; }
说说个人犯的错吧。
1.太久没写最短路差点忘了\(Dij\)怎么写了(捂脸
2.在合并操作时需要把边当作节点编号,不小心打成点编号了(捂脸
最后,感谢nodgd老师带来的讲解!
来源:https://www.cnblogs.com/cooper233/p/12657362.html