本题就是一道单源最短路问题。由于是稀疏图,我们采用Dijkstra算法。

Dijkstra算法原理

Dijkstra算法的步骤

我们把所有的节点分为两个集合:被选中的(visited==1) 和 未被选中的(visited==0),对于每个点,我们在操作中更新其到源点的距离。这个算法中用到贪心思想。
我们进行如下操作:
1.在所有未选中的节点中,找出目前距离源点距离最近的点,记为now,并将now移动到“被选中”集合(visited【now】=1).
2.把所有和now相连的节点next的距离进行更新,取dis【now】+l和dis【next】的最小值,从而始终维护dis数组使得其中存储了目前选取点集合中的路径最小值。
3.从s开始循环上述步骤直至进行到d。
这样,我们就得到了最短路的长度dis【d】,同时可以记录最短路的路径:只需在上述2步骤中记录下next节点的最短路是从哪个节点找到了,再回溯回去即可。

Dijkstra贪心的证明

对于贪心算法,我们在使用的时候应该考虑其正确性。
首先,dijkstra算法只适用于正边权图,图中不能存在负值边(因为dijk算法只能对未被选中的点进行距离更新,而已经选中的点在存在负边的情况下可能存在更短路)
这里不给出详细的证明,证明参见:Dijkstra算法介绍+正确性证明+性能分析_月本_诚的博客-CSDN博客_dijkstra算法正确性证明我们只需要知道,对于一条最短路,它的从s1到s2的一部分就是s1到s2的最短路。这样,前面的算法就很好理解了。
可以采用归纳法证明,假设在Dijkstra算法中找到的最短路为V:v1v2.....vn,对于i<k原算法都正确,而在i=k时不正确。
这时我们考虑到我们在维护数组时保证了i=k继承了i=k-1的最短路,因此不可能存在前半部分是最短路,后面不是的情况。

Dijkstra的代码实现

这里直接给出本题的ac代码,再分别对不同的坑点进行解释。

#include<cstdio>
#include<cstdlib>
#include<vector>
#include<queue>
#include<utility>
using namespace std;
typedef pair<int,int> P;
struct node{vector<int> next;vector<int> length;}node[30005];
vector<int> path[30005][2];
vector<int> pre[30005][2];
int sum[2],n,m,s,d,pathsum=0;
void print(int i){
printf("start\n");
int li=path[1][i].size();for(int j=li-1;j>=0;j--)printf("%d\n",path[1][i][j]);
printf("end\n");printf("%d\n",sum[i]);
}
void push(int now,int i,int ii){
pathsum=i>pathsum?i:pathsum;
path[i][ii].push_back(now);
int ll=pre[now][ii].size();
for(int j=0;j<ll;j++){
int last=pre[now][ii][j];
if(j)path[i+j][ii].push_back(now);
push(last,i+j,ii);
}
}
int dijk(int i){
bool v[30005]={0};int dis[30005];priority_queue<P,vector<P>,greater<P>> calc;
for(int i=0;i<=30000;i++){dis[i]=100000000;P x(100000000,i);calc.push(x);}
int now;P x(0,s);calc.push(x);dis[s]=0;
while(!v[d]){
int min=100000000,minj=0;
P x=calc.top();min=x.first;minj=x.second;calc.pop();
while(v[minj]){P x=calc.top();min=x.first;minj=x.second;calc.pop();}
now=minj;v[now]=1;if(min>=100000000)return 100000000;
int l=node[now].next.size();
for(int j=0;j<l;j++){
int xx=node[now].next[j],ll=node[now].length[j];
P x(dis[now]+ll,xx);calc.push(x);
if(dis[now]+ll<dis[xx]){dis[xx]=dis[now]+ll;pre[xx][i].clear();pre[xx][i].push_back(now);}
else if(dis[now]+ll==dis[xx])pre[xx][i].push_back(now);
}
}
push(d,1,i);return dis[d];
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y,k;scanf("%d%d%d",&x,&y,&k);
node[x].next.push_back(y);node[x].length.push_back(k);
node[y].next.push_back(x);node[y].length.push_back(k);
}
scanf("%d%d",&s,&d);
sum[1]=dijk(1);
for(int q=1;q<=pathsum;q++){
int l=path[q][1].size();
for(int i=1;i<l;i++){
int ss=path[q][1][i],dd=path[q][1][i-1];
int ll=node[ss].next.size();int deltaj=0;
for(int j=0;j<ll;j++){if(node[ss].next[j]==dd){deltaj=j;break;}}
node[ss].length[deltaj]=100000000;
int ll1=node[dd].next.size();int deltaj1=0;
for(int j=0;j<ll;j++){if(node[dd].next[j]==ss){deltaj1=j;break;}}
node[dd].length[deltaj1]=100000000;
}
}
sum[0]=dijk(0);print(1);if(sum[0]<100000000&&sum[0]>sum[1])print(0);
}

首先这道题并不是要求我们找到最短路,而是要求找到所谓的“近似最短路”
它的要求是:不与任何一条最短路的任何一条边重合
因此我们的思路是:找到所有的最短路,并删除这些边。
下面我介绍一下我在做这道题的时候遇到的一些坑点:

如何找到所有的最短路?

我发现在我的dijk算法中,只能得到一条最短路,因为我起初用一个pre数组存储n个顶点在最短路中的上一个顶点,数组大小开了n,因此每个顶点只能存唯一一个pre点,也就不能得到所有的路。
后来,我在每个顶点处都开了一个vector,来记录所有可能的pre节点。

        int l=node[now].next.size();
for(int j=0;j<l;j++){
int xx=node[now].next[j],ll=node[now].length[j];
P x(dis[now]+ll,xx);calc.push(x);
if(dis[now]+ll<dis[xx]){dis[xx]=dis[now]+ll;pre[xx][i].clear();pre[xx][i].push_back(now);}
else if(dis[now]+ll==dis[xx])pre[xx][i].push_back(now);
}

然后我设计了一个递归函数来得到所有最短路中的边。但我并没有得到左右最短路,更像是得到了一棵由最短路组成的树,树根在终点d处。

void push(int now,int i,int ii){
pathsum=i>pathsum?i:pathsum;
path[i][ii].push_back(now);
int ll=pre[now][ii].size();
for(int j=0;j<ll;j++){
int last=pre[now][ii][j];
if(j)path[i+j][ii].push_back(now);
push(last,i+j,ii);
}
}

然后我再删掉这些边进行dijk寻路就可以了。

如何优化Dijkstra算法?

然而算法逻辑正确并不足够通过oj,目前的Dijkstra算法复杂度达到了惊人的O(n^2),tle也是必然的。
仔细思考之后,我想到每次寻找最近节点的步骤非常浪费时间:
原代码:

        int min=100000000,minj=0;
for(int j=0;j<n;j++){if(min>dis[j]&&!v[j]){min=dis[j],minj=j;}}

而我们完全可以将当前的距离写进一个优先队列中,每次在这个最小堆中取最小值就可以了。至于更新的时候,如果有更小的值可以直接加入,无需删去原来的值,毕竟我们始终只会用到最小的值。
优化后代码如下:

P x=calc.top();min=x.first;minj=x.second;calc.pop();
while(v[minj]){P x=calc.top();min=x.first;minj=x.second;calc.pop();}

(其实是一个dowhile结构)
然后复杂度就降低到lgn了

彩蛋:附著名oier的ac代码如下

#include<bits/stdc++.h>
using namespace std;
#define N 33333
#define M 833333
#define ll long long
#define PI pair<ll,int>
#define pb push_back
#define mp make_pair
priority_queue<PI,vector<PI>,greater<PI>>q;
int fir[N],l[M],to[M],w[M],ec=1,ban[M],S,T;
void add(int a,int b,int v){l[++ec]=fir[a];fir[a]=ec;w[ec]=v;to[ec]=b;}
int n,m,pre[N],sta[N],tp; ll d[N],D[N];
int inSP[N];
void dijk(ll*d,int S,int T,int o){
memset(d,0x3f,N<<3);
q.push(mp(d[S]=0,S));
while(q.size()){
int x=q.top().second;
ll D=q.top().first;
q.pop();
if(D^d[x])continue;
for(int i=fir[x],y;i;i=l[i]){
y=to[i];
if(d[y]>D+w[i]&&!ban[i])
q.push(mp(d[y]=D+w[i],y)),pre[y]=x;
}
}
if(o&&d[T]<1e16){
puts("start");
int p=T;
while(p^S) sta[++tp]=p=pre[p];
while(tp) printf("%d\n",sta[tp--]);
printf("%d\nend\n%lld\n",T,d[T]);
} }
int main(){
cin>>n>>m;
for(int i=0,a,b,W;i<m;++i)
scanf("%d%d%d",&a,&b,&W),add(a,b,W),add(b,a,W);
cin>>S>>T;
dijk(d,S,T,2);
dijk(D,T,S,0);
for(int i=0;i<n;++i)
inSP[i]=d[i]+D[i]==d[T];
for(int i=2,x,y;i<=ec;++i){
x=to[i^1];
y=to[i];
if(inSP[x]&&inSP[y]&&d[y]==d[x]+w[i])
ban[i]=ban[i^1]=1;
}
dijk(d,S,T,1);
}

最新文章

  1. php常用函数(不定时更新)
  2. android-BaseAdapter自定义控件深刻理解
  3. 《TCP/IP详解 卷一》读书笔记-----TCP连接建立
  4. 自动获取wordpress日志中的第一张图片作为缩略图
  5. Codeforces 716C[数论][构造]
  6. webservice wsdl 生成服务
  7. cas sso单点登录系列8_抛弃Https让Cas以Http协议提供单点登录服务
  8. 一个优秀团队leader应该具备的几点素质
  9. .Net语言 APP开发平台——Smobiler学习日志:SmoOne新增考勤功能
  10. Python爬虫-02:HTTPS请求与响应,以及抓包工具Fiddler的使用
  11. 【python003-变量】
  12. PDCA管理方法论
  13. C++ template —— 模板与继承(八)
  14. ros建立ospf邻居的条件
  15. Innosetup新增Wizard Page
  16. WCF使用net.tcp传输文件
  17. IPv4到IPv6的三种过渡技术
  18. warshall-floyd算法:POJ No 2139 Six Degress of Cowvin Bacon(任意两点最短路径))
  19. 富文本是在modal框中弹出显示的问题
  20. &ldquo;互联网+&rdquo;背景下使用微信公众号增强班主任工作与整合教学资源(泰微课)

热门文章

  1. js技术之截取substring(int,int)
  2. ctfhub 双写绕过 文件头检查
  3. C++类中隐藏的六个默认函数
  4. C/C++头文件以及避免头文件包含造成的重定义方法
  5. 用AltiumDesigner画完PCB生成gerber(光绘)文件详细说明
  6. 基于腾讯开源的msec来进行php开发模块
  7. 在TypeScript项目中进行BDD测试
  8. c++字符串替换
  9. CCS基础样式表
  10. 【C++】二叉树的遍历(前中后)- 迭代法