咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕咕
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]\),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