Loading

最短路相关

0. 一些前置习题

1. luoguP1608 路径统计

实际上只需要开一个 cnt 记录一下到当前点的最短路有几条就行了.
跑 dijkstra 的时候, 如果是严格大于就直接把答案覆盖上, 等于就将方案数相加.

if(dis[p]==dis[u]+e[i].val)ans[p]+=ans[u];
if(dis[p]>dis[u]+e[i].val)
{
	dis[p]=dis[u]+e[i].val;
	ans[p]=ans[u];
	q.push(make_pair(dis[p],p));
}

2. luoguP1462 通往奥格瑞玛的道路

最大值最小, 考虑二分这个最大值.
对于当前二分的值 mid, 每次只保留点权小于等于 mid 的点跑一遍最短路, 判断最短路是否小于等于 b 即可.
总体非常简单, 代码略.

3. luoguP5304 [GXOI/GZOI2019]旅行者

考虑每次将点分成两组, 新建点 \(S,T\), 从 \(S\) 向其中一组连边权为 \(0\) 的边, 从另一组向 \(T\) 连边权为 \(0\) 的边, 这时 \(S\)\(T\) 的最短路就是从 \(A\) 中一点到 \(B\) 中一点的距离的最小值. (实际上这样做不需要真正建出 \(S\)\(T\), 一开始把第一组的所有点入队, 最后统计另一组的最小值即可)
于是我们只需要枚举二进制位, 只需 \(O(\log k)\) 次即可得出答案, 时间复杂度 \(O(Tm\log n\log k)\).

但是我们还有更快的做法!
考虑每条边 \((u,v)\) 的贡献. 如果我们能对每个点 \(u\) 求出到该点最近的关键点 \(pre_u\) 和从该点能到达的最近的关键点 \(suf_u\) (注意这是张有向图), 那么这条边的贡献就是 \(dis(pre_u,u)+dis(v,suf_v)+(u,v), pre_u\neq suf_v\). 可以证明答案一定会被更新到.
\(pre_u\) 可以在跑 dijkstra 的时候更新. \(suf_u\) 只需要建反图再跑一遍即可.
时间复杂度 \(O(Tm\log n)\). 实现的时候需要注意细节.

//多测不清空, 爆零两行泪
memset(vis,0,sizeof(vis));
memset(dis1,0x3f,sizeof(dis1));
for(int i=1;i<=k;i++)
{
    int u=read();
    isv[u]=1;
    dis1[u]=0;
    q.push(make_pair(0,u));
    pre[u]=u;
}
while(!q.empty())
{
    int u=q.top().second;q.pop();
    if(vis[u])continue;
    vis[u]=1;
    for(int i=h[u];i;i=e[i].nxt)
    {
        int p=e[i].to;
        if(e[i].rev==1)continue;
        if(dis1[p]>dis1[u]+e[i].val)
        {
            dis1[p]=dis1[u]+e[i].val;
            pre[p]=pre[u];
            q.push(make_pair(dis1[p],p));
        }
    }
}
//求 suf 同理, 略
for(int u=1;u<=n;u++)
    for(int i=h[u];i;i=e[i].nxt)
    {
        int p=e[i].to;
        if(e[i].rev==1)continue;
        if(pre[u]==0||suf[p]==0)continue;
        if(pre[u]==suf[p])continue;
        ans=min(ans,dis1[u]+dis2[p]+e[i].val);
    }
printf("%lld\n",ans);

4. luoguP6545 [CEOI2014] The Wall

咕咕咕

5. luoguP3640 [APIO2013] 出题人

奇怪的题. 不过暂时不写.

1. 关于 Floyd 算法

众所周知 Floyd 算法实际上是一个很厉害的 dp.
有的时候我们就需要通过 Floyd 算法的本质来解决一些奇怪的问题.

0. Floyd 的原理

首先我们简单解释一下 Floyd 算法是如何工作的.

for(int k=1;k<=n;k++)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);

实际上虽然说这里的 dis 好像就表示了从 \(i\)\(j\) 的最短距离, 但其实 dis 数组有一维被省去了, 实际上应为 \(dis_{k,i,j}\), 表示从 \(i\)\(j\) 只经过 \(1,2,\cdots,k\) 的最短距离.
相应地, 状态转移方程实际上也应该是:

\[dis_{k,i,j}=\min\{dis_{k-1,i,j},dis_{k-1,i,k}+dis_{k-1,k,j}\} \]

这样 Floyd 的原理就变得清晰了. 我们通过先在外层枚举 \(k\) 将这一维压掉了.

一道例题: luoguP1119 灾后重建

就是上面对 Floyd 理解的直接应用. 对于每个询问 \(T\), 我们跑只经过重建时间小于等于 \(T\) 的点的 Floyd 即可.

for(int i=1;i<=q;i++)
{
    int x=read(),y=read(),T=read();
    while(t[now]<=T&&now<n)
    {
        for(int u=0;u<n;u++)
            for(int v=0;v<n;v++)
                a[u][v]=min(a[u][v],a[u][now]+a[now][v]);
        now++;
    }
    if(t[x]>T||t[y]>T||a[x][y]==inf)printf("-1\n");
    else printf("%d\n",a[x][y]);
}

1. 传递闭包

说人话: 求出从每个点能到达的所有点.

很明显我们只需要将 Floyd 算法稍加改造就行.

for(int k=1;k<=n;k++)
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			map[i][j]|=map[i][k]&map[k][j];

2. Floyd 最小环

Floyd 可以用来求无向图最小环.
核心代码只有下面几行:

for(int k=1;k<=n;k++)
{
    for(int i=1;i<k;i++)
        for(int j=i+1;j<k;j++)
            ans=min(ans,dis[i][j]+mp[i][k]+mp[k][j]);
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);
}

具体解释一下.
首先一个环至少要有三个点. 不妨设环上编号最大的点为 \(k\), 它在环上相邻的两个点为 \(i,j\), 不妨设 \(i<j\).
我们考虑枚举 \(i,j,k\), 这样包含 \((i,k),(k,j)\) 的最小环就为 \(dis(i,j)+(i,k)+(k,j)\).
但其实上面的式子还有一点问题. 我们必须保证 \(dis(i,j)\) 这一项里的路径不经过 \(k\).
于是我们考虑利用 Floyd 的转移顺序, 当枚举到 \(k\) 的时候, 在转移之前, 我们只计算了从 \(i\)\(j\) 经过点 \(1,2,\cdots,k-1\) 所得到的的最短路径. 这样 \(k\) 及之后的部分就都被排除了. (注意到我们钦定了 \(k\) 是最大的, 因此比 \(k\) 大的也不需要统计)

3. 反 向 Floyd

名字是乱起的(
考虑 这个 奇奇怪怪的题.
首先我们发现不合法当且仅当存在 \(a_{i,j}>a_{i,k}+a_{k,j}\).
另外, 如果有 \(a_{i,j}=a_{i,k}+a_{k,j}\), 那就说明实际上我们是 \(i\rarr k\rarr j\), 因此 \(i\rarr j\) 这条边就可以没有.
剩下的情况都是至少要有一条对应权值的边的.

for(int k=1;k<=n;k++)
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
        {
            if(a[i][j]>a[i][k]+a[k][j])
            {
                printf("-1\n");
                return 0;
            }
            if((a[i][j]==a[i][k]+a[k][j])&&i!=k&&j!=k)flag[i][j]=1;
        }
for(int i=1;i<=n;i++)
    for(int j=1;j<=n;j++)
        if(!flag[i][j])
            ans+=a[i][j];
printf("%lld\n",ans/2);

2. 最短路径树

最短路径树 \((\text{Shortest Paths Tree, SPT})\) 就是从源点 \(s\) 到其它每个点的最短路径构成的树.

求法也很简单, 就是在跑 dijkstra 的时候记录一下是从哪条边转移过来的.

void dijkstra()
{
	memset(dis,0x7f,sizeof(dis));
	dis[s]=0;q.push(make_pair(0,s));
	while(!q.empty())
	{
		int u=q.top().second;q.pop();
		if(vis[u])continue;
		vis[u]=1;
		for(int i=h[u];i;i=e[i].nxt)
		{
			int p=e[i].to;
			if(dis[p]>=dis[u]+e[i].val)//这一句加等号的原因接下来会解释
			{
				dis[p]=dis[u]+e[i].val;
				pre[p]=i;//记录是从哪条边转移过来的
				q.push(make_pair(dis[p],p));
			}
		}
	}
}

注意到上面的代码在转移的时候多增加了一个等号.

这是因为一般来说我们都是求最小最短路径树 \(\text{MSPT}\), 即权值和最小的最短路径树.

要做到这一点, 只需要在扩展的时候选择到当前点 \(p\) 边权最小的边就行了.

注意到在 dijkstra 中到源点最短路长度小的点会先被扩展, 因此如果在之后又出现了一个点到当前点的最短路和之前的相同, 这个点到当前点的这条边的边权一定更小.

因此我们只需要加一个等号就能使得到的最短路径树的边权和最小了.

另外一点是为什么要记录前驱边而不是记录前驱点. 一方面是因为有很多题都需要按照边的编号进行输出方案, 记录前驱边会好写很多, 但更加重要的是如果有重边, 只记点不记边就会挂的很惨.

具体地, 对于双向边, 开始存边时令 \(\text{cnt=1}\), 那么最后链式前向星中的编号除以 \(2\) 下取整就是按照读入顺序边的编号.

板子题: CF545E

比较好的一道题是 CF1005F. 我们来讨论这道题的做法.

题意略. 很明显就是让我们统计最短路径树的个数. 因为边权都是 \(1\), 根据我们上面的写法, 我们能够遍历到所有可以选择的边.

于是只需要把

pre[p]=i;

改成

pre[p].push_back(i);

就行了. 方案数就是所有点的 pre 的数量乘起来.

关于输出答案:

很明显可以跑一遍 dfs.

void dfs(int u)
{
	if(u==n+1)
	{
		now++;
		for(int i=1;i<=m;i++)printf("%d",vis[i]);
		printf("\n");
		if(now==sum)flag=1;
		return;
	}
	for(int i=0;i<qwq[u].size();i++)
	{
		if(flag)return;
		if(vis[qwq[u][i]/2])continue;
		vis[qwq[u][i]/2]=1;
		dfs(u+1);
		vis[qwq[u][i]/2]=0; 
	}
}

3. 删边最短路

讨论两种情况, 无向正权图和有向无权图.
需要注意的是一般有向图上并没有复杂度正确的算法.

1. 无向正权图删边最短路

只有删边的 模板题

实际上可以加强一下, 不只是删边, 每次更改一条边的权值也能做.
删边就可以看成是把一条边的权值改成 \(\inf\).
直接看题: CF1163F Indecisive Taxi Fee

简明题意: 给定一个无向图, 每次修改一条边的权值 (互相独立) 并询问 \(1\)\(n\) 的最短路的长度.

我们讨论几种情况.

  1. 将不是最短路上的边的权值增大.
    显然答案还是原来的最短路.
  2. 将不是最短路上的边的权值减小.
    预处理每个点到 \(1\)\(n\) 的最短路的长度 \(dis(1,u), dis(u,n)\).
    设边为 \((u,v)\), 答案就是原来的最短路长度, \(dis(1,u)+w'(u,v)+dis(v,n)\), \(dis(1,v)+w'(u,v)+dis(u,n)\) 三者的最小值.
  3. 将最短路上的边的权值减小.
    显然答案为原来最短路的长度减去减小量.
  4. 将最短路上的边的权值增大.
    此时答案就为原来最短路修改后的长度和不经过修改边的最短路的最小值.
    问题来了怎么求出不经过修改边的最短路的最小值.
    我们反过来考虑, 对于任意一条边, 求出经过它的最短路可以不经过哪些部分.
    有一个简单的性质, 删边后的最短路一定包含一段最短路的前缀和一段最短路的后缀. 因此可以不经过的部分就是最短路上连续的一段.
    经过任意一条边的最短路是易求的, 就是情况 2 中的做法.
    于是就简单了, 我们如果能求出这条边对应的可以不经过的最短路的部分, 那么对于这部分上的任意一条边, 对不经过它的最短路取一个 \(\min\) 就行了.
    但是要如何求出这条边对应的可以不经过的最短路的部分? 我们的做法是, 考虑每个点 \(u\), 求出从 \(1\)\(u\) 的最短路离开最短路的位置 \(l(u)\) 和从 \(u\)\(n\) 进入最短路的位置 \(r(u)\).
    注意到在以上定义中 \(l(u)\)\(r(u)\) 可能是不唯一的. 我们考虑让 \(l(u)\) 尽量靠近 \(1\), \(r(u)\) 尽量靠近 \(n\), 这样包含的最短路的边就会尽量多.
    要求出 \(l(u)\)\(r(u)\), 我们只需要仿照最短路树的思路, 按照最短路的顺序进行 dp 即可.
    具体地, 以求 \(l(u)\) 为例, 若 \(dis(1,u)+w(u,v)=dis(1,v)\), 则令 \(l(v)=\min(l(u),l(v))\).
    需要注意, 为了避免回到最短路上, 不要用最短路上的边 \((u,v)\) 更新 dp 值.同时, 初始化为最短路上的点, \(l(u)=u\).
    另外 dp 的时候就不用写最短路了, 按照 \(dis(1,u)\) 的顺序跑就行了.
    于是这道题就基本上做完了. 对于每条边, 它对应的区间就是 \([l(u),r(v)]\)\([l(v),r(u)]\).
    至于维护区间 \(\min\) 和单点求值, 开一棵线段树就行了.

温馨提示: CF 上的数据非常毒瘤, 保证了 \(1\)\(n\) 连通但未保证整张图是连通的, 需要注意.

#define int long long
//建图和 dijkstra 略去
inline int dis(int i)//经过 i 这一条边的最短路
{
	int uu=eds[i].u,vv=eds[i].v,vval=eds[i].val;
	if(dis1[uu]==inf)return inf;//特判不连通
	return vval+min(dis1[uu]+disn[vv],dis1[vv]+disn[uu]);
}
bool cmp1(int xx,int yy){return dis1[xx]<dis1[yy];}
bool cmpn(int xx,int yy){return disn[xx]<disn[yy];}
//线段树略去
signed main()
{
	//建图略
	dijkstra1();dijkstran();//先求 dis(1,u) 和 $dis(u,n)$ 并求出最短路
	memset(l,0x7f,sizeof(l));
	memset(r,0x7f,sizeof(r));
	for(int u=1;pre[u];u=e[pre[u]^1].to)//标记出最短路
	{
		path[++pcnt]=u;
		ispath[pre[u]]=ispath[pre[u]^1]=1;
	}
	path[++pcnt]=n;
	for(int pos=1;pos<=pcnt;pos++)//dp 预处理边界
	{
		int u=path[pos];
		l[u]=pos;r[u]=pos-1;//这里的 l,r 是最短路的边对应的区间的端点, 因此 r 要减 1
	}
	for(int i=1;i<=n;i++)order[i]=i;
	sort(order+1,order+n+1,cmp1);//按照 dis(1,u) 的顺序进行 dp
	for(int now=1;now<=n;now++)
	{
		int u=order[now];
		if(dis1[u]==inf)break;//不连通特判
		for(int i=h[u];i;i=e[i].nxt)
		{
			if(ispath[i])continue;//不用最短路更新 dp 值
			int p=e[i].to;
			if(dis1[p]==dis1[u]+e[i].val)l[p]=min(l[p],l[u]);
		}
	}
	//计算 r 的部分和 l 基本一样, 略去
	build(1,1,pcnt-1);
	for(int now=1;now<=m;now++)//枚举每条边贡献到线段树上
	{
		int i=2*now;//建的是双向边
		if(ispath[i])continue;
		int uu=eds[i].u,vv=eds[i].v;
		if(dis1[uu]==inf)continue;
		int ll=l[uu],rr=r[vv];
		if(ll<=rr)modify(1,ll,rr,dis(i));
		ll=l[vv];rr=r[uu];
		if(ll<=rr)modify(1,ll,rr,dis(i));
	}
	for(int i=1;i<=Q;i++)//分类讨论输出
	{
		int t=read(),x=read();t*=2;
		if(ispath[t])
		{
			if(x>e[t].val)
			{
				int uu=eds[t].u,vv=eds[t].v,now=min(l[uu],l[vv]);
				printf("%lld\n",min(dis1[n]-e[t].val+x,query(1,now,now)));
			}
			else printf("%lld\n",dis1[n]-e[t].val+x);
		}
		else
		{
			if(x>e[t].val||dis(t)==inf)printf("%lld\n",dis1[n]);
			else printf("%lld\n",min(dis1[n],dis(t)-e[t].val+x));
		}
	}
	return 0;
}

2. 有向无权图删边最短路

参考资料:
EI 的博客

有向图上的删边最短路做起来要困难得多. 不过如果有向图是无权的 (也可以认为边权是 \(1\)), 我们有一个期望时间复杂度不错的随机化算法.
首先根据前面无向图删边最短路的铺垫, 我们知道有这样的结论:

  • 考虑求出 \(s\)\(t\) 的最短路, 那么我们只需要处理这条最短路上的删边就行.
  • 如果最短路上的边被删掉了, 那么我们在中间会从原来的最短路上离开, 并且因为最短路的性质和每次只删一条边, 我们只会离开一次.

我们约定一些记号: 记一开始求出的最短路为 \(p_1\rarr p_2\rarr\cdots\rarr p_k\).

(未完待续, 可能会长期不填)

4. 次短路和 k 短路

1. 次短路

这里有两种情况.

1. 可重复经过点和边

P2865 [USACO06NOV]Roadblocks G
什么是可重复经过点和边?
比如说我们有这样一个图:
image
这个时候从 \(1\)\(2\) 的次短路是 \(3\) 而不是 \(4\), 路径 \(1\rarr2\rarr1\rarr2\) 是最短的.
但是我们发现显然最多只会有一次折返, 我们只需要每次枚举边 \((u,v)\), 若有 \(dis(1,u)+(u,v)+dis(v,n)\) 大于最短路长度, 就将 \(1\rarr u\rarr v\rarr n\) 统计为一个可能的次短路. 比如对于上面那张图, 次短路会在我们枚举边 \((2,1)\) 时取到.
代码极其好写.

dijkstra1();dijkstran();shortest=dis1[n];
for(int u=1;u<=n;u++)
    for(int i=h[u];i;i=e[i].nxt)
    {
        int p=e[i].to;
        if(shortest==dis1[u]+disn[p]+e[i].val)continue;
        ans=min(ans,dis1[u]+disn[p]+e[i].val);
    }

好像还有一种在跑最短路的过程中求次短路的方法, 不过我个人认为又难写又不容易保证正确性和时间复杂度. (反正 luogu 上的题解一车堆优化 SPFA)

2. 不能重复经过点和边

就是 这道题, 虽然题面没说.
我们只需要每次删掉最短路上的一条边跑最短路即可.
还是很好写的(

dijkstra1();
int u=n;
while(u!=1)
{
    int i=pre[u];
    e[i].del=e[i^1].del=1;
    dijkstra2();ans=min(ans,dis[n]);//这道题是非严格次短路, 直接取 min 就行
    e[i].del=e[i^1].del=0;
    u=prever[u];
}

2. k短路

咕咕咕.
哦对了 k 短路就不要求是简单路了, 因为若要求是简单路则没有时间复杂度正确的算法.

5. 网格图最短路

luoguP3350 [ZJOI2016]旅行者

最短路和分治结合的奇怪题目.
考虑分治, 将询问离线下来, 每次将矩形的长边切一半, 我们发现一共有两类询问, 两个点在两侧和在一侧. 在两侧的最短路一定经过中线, 在一侧的最短路可能经过中线, 也可能完全包含在两个子矩形内.
对中线上的每个点跑一遍最短路, 这样就能更新所有经过中线的最短路. 然后向两侧分治统计子矩形内的答案即可.
时间复杂度比较玄学, 好像是 \(O(S\sqrt{S}\log S)\), 其中 \(S\) 大概是 \(nm\).
实现起来感觉比较像整体二分.

code:

//头文件略
const int maxn=20010,maxq=100010,inf=0x7f7f7f7f;
int n,m,Q,cnt=1,h[maxn],dis[maxn],vis[maxn],ans[maxq];
struct edge{int to,nxt,val;}e[maxn<<2];
void addedge(int u,int v,int val)
{
    e[++cnt]=(edge){v,h[u],val};
    h[u]=cnt;
}
inline int p(int x,int y){return (x-1)*m+y;}
inline void rev(int u,int &x,int &y){y=(u-1)%m+1;x=(u-y)/m+1;}
inline bool isin(int x,int y,int u,int d,int l,int r){return x>=u&&x<=d&&y>=l&&y<=r;}
struct query{int ord,qx1,qy1,qx2,qy2;}qs[maxq],lq[maxq],rq[maxq];
priority_queue<pii,vector<pii >,greater<pii > > q;
void dijkstra(int x,int y,int u,int d,int l,int r)//以 (x,y) 为源点, (u,d,l,r) 范围内的最短路
{
    for(int i=u;i<=d;i++)//不要用 memset
        for(int j=l;j<=r;j++)
        {
            vis[p(i,j)]=0;
            dis[p(i,j)]=inf;
        }
    dis[p(x,y)]=0;q.push(make_pair(0,p(x,y)));
    while(!q.empty())
    {
        int nowu=q.top().second;q.pop();
        if(vis[nowu])continue;
        vis[nowu]=1;
        for(int i=h[nowu];i;i=e[i].nxt)
        {
            int p=e[i].to,nowx,nowy;
            rev(p,nowx,nowy);
            if(!isin(nowx,nowy,u,d,l,r))continue;
            if(dis[p]>dis[nowu]+e[i].val)
            {
                dis[p]=dis[nowu]+e[i].val;
                q.push(make_pair(dis[p],p));
            }
        }
    }
}
void solve(int ql,int qr,int u,int d,int l,int r)//询问范围 [ql,qr], 当前矩形为 (u,d,l,r)
{
    if(ql>qr)return;
    if(u==d&&l==r)
    {
        for(int i=ql;i<=qr;i++)ans[qs[i].ord]=0;
        return;
    }
    if(d-u>r-l)//选长的一条边
    {
        int mid=(d+u)>>1;
        for(int i=l;i<=r;i++)//先以中线上的点为源点做最短路更新答案
        {
            dijkstra(mid,i,u,d,l,r);
            for(int j=ql;j<=qr;j++)
                ans[qs[j].ord]=min(ans[qs[j].ord],dis[p(qs[j].qx1,qs[j].qy1)]+dis[p(qs[j].qx2,qs[j].qy2)]);
        }
        //然后保留最短路可能不经过中线的询问, 向两侧分治
        int lcnt=0,rcnt=0;
        for(int i=ql;i<=qr;i++)
        {
            if(isin(qs[i].qx1,qs[i].qy1,u,mid,l,r)&&isin(qs[i].qx2,qs[i].qy2,u,mid,l,r))lq[++lcnt]=qs[i];
            if(isin(qs[i].qx1,qs[i].qy1,mid+1,d,l,r)&&isin(qs[i].qx2,qs[i].qy2,mid+1,d,l,r))rq[++rcnt]=qs[i];
        }
        for(int i=1;i<=lcnt;i++)qs[i+ql-1]=lq[i];
        for(int i=1;i<=rcnt;i++)qs[ql+lcnt+i-1]=rq[i];
        solve(ql,ql+lcnt-1,u,mid,l,r);
        solve(ql+lcnt,ql+lcnt+rcnt-1,mid+1,d,l,r);
    }
    else//同理, 略
}
int main()
{
    //建图略
    Q=read();
    for(int i=1;i<=Q;i++)qs[i]=(query){i,read(),read(),read(),read()};
    memset(ans,0x7f,sizeof(ans));
    solve(1,Q,1,n,1,m);
    for(int i=1;i<=Q;i++)printf("%d\n",ans[i]);
    return 0;
}
posted @ 2022-11-20 00:18  pjykk  阅读(28)  评论(0编辑  收藏  举报