逛森林
这是一道模板题
首先,对任意时刻,\(u\)->\(v\)这条路径上的点都是不会变动的(就是说,比如,如果某时刻从\(1\)到\(4\)的路径为\(1\)->\(3\)->\(4\),那么对之后的任意时刻,这条路径都是这个,既不会改变顺序,也不会新增节点,更不会删除已有节点),所以我们可以把所有有效的操作一存起来最后再建边
那么这里利用倍增优化建边
倍增的分治结构是什么?
这个分治的状态是不是就出来了?注意绿边的边权为0
比如说我现在有一个传送门,是从\(x_0\)->\(x_1\)这条链到\(x_2\)->\(x_4\)这条链,那么我就会从\(f[x_0][0]\)的out点连一条边权为\(w\)的出边到虚拟节点\(T\),再从\(T\)连一条边权为0的入边到\(f[x_2][1]\)的in点,由于绿边的边权为0,显然就等价于从\(x_0\)->\(x_1\)这条链上的每一个点都连了一条边权为\(w\)的出边到\(x_2\)->\(x_4\)这条链上的每一个点
那么这个的复杂度是多少?首先考虑树上的边,总共是\(O(m)\)条,然后考虑绿边,因为每个树上的点顶多向上跳\(logn\)次,所以in点和out点的个数就是\(O(nlogn)\),由前文描述可得每个in点和out点至多会连三条有向边,所以绿边的个数就是\(O(nlogn)\),然后在考虑虚拟节点的边,对每一次操作,我们最多跳\(logn\)步,每跳一步就会连边,所以一共是\(O(mlogn)\)的条边,最后跑dij,总共的时间复杂度为\(O((nlogn+mlogn)logn)\)
考虑优化,利用st表的思想,对一条长度为\(2^k+p\)的链,我们将其分成两步,前\(2^k\)和后\(2^k\)个节点的in点和out点分别按照上述方法连边,尽管有重复连边,但是不影响答案,时间复杂度被优化到了\(O((nlogn+m)logn)\),如下图
这两个区间代表的节点进行连边就好了
其他的优化建边
update 2024.5.12
我们来说一下代码的细节
首先,千万不要认为我们对每个点都会建一颗有\(O(nlogn)\)个节点的树,从而导致节点总数是\(O(n^2logn)\),因为我们dfs到某一个点的时候,这条链上的其余点的树都已经建好了,不用再重新建,听不懂的话就看代码
然后,在求连边的节点时,不要用st表的写法(预处理出Log数组然后求出最大长度),因为这不是链,而是树;我们应该直接对代码中LCA的部分进行改动(然而这样的话时间复杂度就没办法优化了)
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=5e4+10,M=1e6+10,INF=0x3f3f3f3f;
int n,m,s,cnt,tot;
int fa[N];
int End[M<<5],Len[M<<5],Last[M<<2],Next[M<<5],dis[M<<2];
int dep[N],f[N][20],in[N][20],out[N][20];
bool vis[M<<2],mark[M];
int u[M][3],v[M][3],w[M],op[M];
vector<int> G[N];
struct Node
{
int Num,dis;
bool operator<(const Node &a) const
{
return a.dis<dis;
}
};
priority_queue<Node> q;
int getfa(int x)
{
return fa[x]==x?x:fa[x]=getfa(fa[x]);
}
void merge(int x,int y)
{
fa[getfa(x)]=getfa(y);
}
void add(int x,int y,int z)
{
End[++tot]=y,Len[tot]=z,Next[tot]=Last[x],Last[x]=tot;
}
void init(int x,int fa)
{
dep[x]=dep[fa]+1;
for(int i=0;i<=17;i++)
{
f[x][i+1]=f[f[x][i]][i];
in[x][i+1]=++cnt,add(cnt,in[x][i],0),add(cnt,in[f[x][i]][i],0);
out[x][i+1]=++cnt,add(out[x][i],cnt,0),add(out[f[x][i]][i],cnt,0);
//in表示出点,out表示入点
}
for(int i=1;i<=G[x].size();i++)
{
int v=G[x][i-1];
if(v!=fa)
{
f[v][0]=x;
in[v][0]=++cnt,add(cnt,v,0),add(cnt,x,0);
out[v][0]=++cnt,add(v,cnt,0),add(x,cnt,0);
init(v,x);
}
}
}
void LCA(int ver,int id)//直接对LCA的代码进行改动连边
{
int x=u[ver][id],y=v[ver][id];
if(x==y)
{
if(id==1) add(x,cnt,0);
else add(cnt,x,0);
return;
}
if(dep[x]<dep[y]) swap(x,y);
for(int i=17;i>=0;i--)
{
if(dep[f[x][i]]>=dep[y])
{
if(id==1) add(out[x][i],cnt,0);
else add(cnt,in[x][i],0);
x=f[x][i];
}
if(x==y) return;
if(dep[x]==dep[y]) break;
}
for(int i=17;i>=0;i--)
{
if(f[x][i]!=f[y][i])
{
if(id==1) add(out[x][i],cnt,0),add(out[y][i],cnt,0);
else add(cnt,in[x][i],0),add(cnt,in[y][i],0);
x=f[x][i];
y=f[y][i];
}
}
if(id==1) add(out[x][0],cnt,0),add(out[y][0],cnt,0);
else add(cnt,in[x][0],0),add(cnt,in[y][0],0);
return;
}
void Dij()
{
Node temp;
temp.Num=s;
temp.dis=0;
dis[s]=0;
q.push(temp);
while(!q.empty())
{
int u=q.top().Num;
q.pop();
if(vis[u]==1) continue;
vis[u]=1;
for(int i=Last[u];i;i=Next[i])
{
int v=End[i];
if(dis[v]>dis[u]+Len[i])
{
dis[v]=dis[u]+Len[i];
temp.Num=v;
temp.dis=dis[v];
q.push(temp);
}
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&s);
memset(dis,0x3f,sizeof(dis));
cnt=n;//cnt表示节点总数
for(int i=1;i<=n;i++) fa[i]=i;
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&op[i],&u[i][1],&v[i][1]);
if(op[i]==1)
{
scanf("%d%d%d",&u[i][2],&v[i][2],&w[i]);
mark[i]=1;//mark表示这次操作是否有效
for(int j=1;j<=2;j++)
if(getfa(u[i][j])!=getfa(v[i][j]))
{
mark[i]=0;
break;
}
}
else
{
scanf("%d",&w[i]);
if(getfa(u[i][1])!=getfa(v[i][1]))
{
mark[i]=1;
merge(u[i][1],v[i][1]);
G[u[i][1]].push_back(v[i][1]);
G[v[i][1]].push_back(u[i][1]);
//注意树中的边一定要和最终图的边分开存
//如果存一起的话
//在init过程中,由于我们要加边
//就会把边加进去,破坏树的结构
}
}
}
for(int i=1;i<=n;i++)
if(!dep[i]) init(i,0);
//注意森林的每一颗树都要init
//因为加了传送门之后可能连通
for(int i=1;i<=m;i++)
if(op[i]==1&&mark[i])
{
for(int j=1;j<=2;j++)
{
cnt++;//创建虚拟节点
LCA(i,j);
}
add(cnt-1,cnt,w[i]);//两个虚拟节点连边
}
else if(op[i]==2&&mark[i])
add(u[i][1],v[i][1],w[i]),add(v[i][1],u[i][1],w[i]);//注意树中的边也要添加到最终图中
Dij();
for(int i=1;i<=n;i++)
printf("%d ",dis[i]>INF/2?-1:dis[i]);
return 0;
}