【算法】静态仙人掌

· · 个人记录

前置知识: tarjan(割边)、LCA

题目大意:

给定一棵仙人掌,让你求这棵仙人掌上两点之间的最短路。

解题思路:

在我们开始学习静态仙人掌之前,先需要了解仙人掌到底是一个什么东西:

如上图,仙人掌是一个无向连通图,任意一条边最多只能出现在一个环当中。

因为仙人掌是一个图,我们可以按照传统思想求多源最短路,但是会爆。所以我们用圆方树来将其转化成一棵树再来解决。

圆方树:

圆方树是一个有向有根树,从父节点连向子节点,并且这个树当中有两种点,圆点方点

圆点就是之前在仙人掌中的点

而方点需要我们先将每个环的边全部断开,再找出每个环的,具体可以用 tarjan 求出。然后向方点连接一条有向边权值为 0,之后在方点向这个环的其他点连接一条有向边,因为我们要找的是最短路,所以将权值设为这个点到根的最小权值

而不在任何一个环当中的边边权不变,并且是一个割边,但是也都变成有向边

因为这是一个有根树,所以我们将 1 设为这个圆方树的

最后保证与原仙人掌中的路径权值相等。

以上图为例,将它转化为一棵圆方树

当我们构建完圆方树后,就可以将最短路转化为求这两个点的最近公共祖先与他们之间的权值

剩下的也就没什么了,对代码的解释的都在注释里了

贴代码

#include<bits/stdc++.h>
using namespace std;
struct edge{
    int to,next,w;
}ed[1000001];
int he[1000001],tot;
int he2[1000001];
int dfn[1000001];
int low[1000001],idx;
int s[1000001],stot[1000001];//表示权值的前缀和与每个环内的权值和
int fa[1000001],fw[1000001];//父亲和与父亲之间的权值
int f[1000001][15];
int dep[1000001];
int dis[1000001];//与根的权值
int qwq,homo,num;//方点的两个儿子以及方点个数
int n,m,q;
void insert(int u,int v,int w)//建仙人掌
{
    ed[tot].to=v;
    ed[tot].w=w;
    ed[tot].next=he[u];
    he[u]=tot++;
}
void insert2(int u,int v,int w)//建圆方树
{
    ed[tot].to=v;
    ed[tot].w=w;
    ed[tot].next=he2[u];
    he2[u]=tot++;
}
void build(int u,int v,int w)
{
    int sum=w;//从记录的这条边算起
    for(int i=v;i!=u;i=fa[i])
    {
        s[i]=sum;//前缀和吗每次加上它到它父亲那条边的权值
        sum+=fw[i];//更新sum
    }
    s[u]=stot[u]=sum;//因为u还没有被记录,所以赋值为更新完的sum。此时,sum加完环内所有权值。
    insert2(u,++num,0);//向方点连边。因为是多余的点,所以num++
    for(int i=v;i!=u;i=fa[i])
    {
        stot[i]=sum;//每个点都在这个环内
        insert2(num,i,min(s[i],sum-s[i]));//向方点连一条最短路径,分别是一个优弧和劣弧
    }
    return ;
}
void tarjan(int u,int from)//割边
{
    dfn[u]=low[u]=++idx;
    for(int i=he[u];i!=-1;i=ed[i].next)
    {
        int v=ed[i].to;
        if(!dfn[v])
        {
            fa[v]=u;fw[v]=ed[i].w;
            tarjan(v,i);
            low[u]=min(low[u],low[v]);
            if(low[v]>dfn[u])
                insert2(u,v,ed[i].w);//这条边是割边,直接在圆方树中建边
        }
        else if(i!=(from^1)) 
            low[u]=min(low[u],dfn[v]);
    }
    for(int i=he[u];i!=-1;i=ed[i].next)
    {
        int v=ed[i].to;
        if(dfn[u]<dfn[v]&&fa[v]!=u)//表示v在以u为根的分量中,且这条边不在圆方树中,从u出去的边
            build(u,v,ed[i].w);//建立方点再连接
    }
    return ;
}
void dfs(int u,int father)//预处理
{
    dep[u]=dep[father]+1;
    f[u][0]=father;
    for(int i=1;(1<<i)<=dep[u];i++)
        f[u][i]=f[f[u][i-1]][i-1];
    for(int i=he2[u];i!=-1;i=ed[i].next)
    {
        int v=ed[i].to;
        dis[v]=dis[u]+ed[i].w;
        dfs(v,u);
    }
    return ;
}
int LCA(int from,int to)//求LCA
{
    if(dep[from]<dep[to])
        swap(from,to);
    int deep=dep[from]-dep[to];
    for(int i=0;i<=13;i++)
    {
        if(deep&1)
            from=f[from][i];
        deep>>=1;
    }
    if(from==to) return to;
    for(int i=13;i>=0;i--)
        if(f[from][i]!=f[to][i])
        {
            from=f[from][i];
            to=f[to][i];
        }
    qwq=from;homo=to;
    return f[from][0];
}
int main()
{
    memset(he,-1,sizeof(he));
    memset(he2,-1,sizeof(he2));
    scanf("%d %d %d",&n,&m,&q);num=n;//圆方树中至少有n个点
    while(m--)
    {
        int x,y,z;
        scanf("%d %d %d",&x,&y,&z);
        insert(x,y,z);
        insert(y,x,z);
    }
    tarjan(1,0);
    dfs(1,0);
    while(q--)
    {
        int x,y;
        scanf("%d %d",&x,&y);
        int lca=LCA(x,y);
        if(lca<=n) 
            printf("%d\n",dis[x]+dis[y]-dis[lca]*2);//从源点到x+从源点到y-2*从源点到lca(不会建议画图)
        else
        {
            int p=dis[x]-dis[qwq];
            int q=dis[y]-dis[homo];
            int minx=min(abs(s[qwq]-s[homo]),stot[qwq]-abs(s[qwq]-s[homo]));//一条路径与整个环内权值和减去的路径的最小值
            printf("%d\n",p+q+minx);//输出
        }
    }
    return 0;
}