1A.最小生成树之Kruskal算法
①Kruskal算法能干啥及它的原理和时/空间复杂度
Kruskal算法用于构造最小生成树,它是一个基于贪心思想的算法
图中蓝色的边显示了一棵最小生成树↑
我们建立一个$O(n)$的并查集和一个$O(m)$的结构体,每个结构体存储一对点和点之间的边权,然后按边权从小到大排序,顺次取每一对点。如果两点未被并入一个集合里,就把它们并起来,然后就做完了。
顺便说一句,(由此可见,)Kruskal算法较适用于稀疏图。
时间复杂度:$O(mlog$ $m)$
②Kruskal算法求最小生成树的具体实现
1 #include<iostream> 2 #include<algorithm> 3 using namespace std; 4 struct a{int n1,n2,val;}mst[m]; 5 int n,m,ans,t1,t2; 6 int aset[n]; 7 bool cmp(a x,a y)//按边权从小到大排序 8 { 9 return x.val<y.val; 10 } 11 int find(int x) 12 { 13 return (aset[x]==x)?x:aset[x]=find(aset[x]); 14 } 15 int main() 16 { 17 cin>>n>>m; 18 for(int i=1;i<=m;i++) 19 cin>>mst[i].n1>>mst[i].n2>>mst[i].val; 20 for(int i=1;i<=n;i++) aset[i]=i; 21 sort(mst+1,mst+1+m,cmp);//排序 22 for(int i=1;i<=m;i++) 23 { 24 int t1=find(mst[i].n1),t2=find(mst[i].n2); 25 if(t1!=t2) aset[t1]=t2,ans+=mst[i].val;//合并 26 } 27 cout<<ans; 28 return 0; 29 }
1B.最小生成树之Prim算法(+堆优化)
①Prim算法能干啥及它的原理和时/空间复杂度
Prim当然也是用来构造最小生成树啦,它是一个基于搜索思想的算法。Prim算法先**将随意的一个点作为起始点,然后以类似BFS的方式进行扩展,以新扫到的边更新已选边**。我们需要用**边权建立一个小根堆进行Prim算法**。
由此可见Prim算法**比较适合于稠密图**
时间复杂度:采用邻接表存储,堆优化后可以达到$O(mlog$ $n)$
(正在以Prim算法构造最小生成树↓)
②Prim的具体实现
1 #include<queue> 2 #include<cstdio> 3 #include<cstring> 4 using namespace std; 5 struct a{int nxt,val;}; 6 const int N=10005,M=100005; 7 int p[N],noww[2*M],goal[2*M],val[2*M],dis[N]; 8 int n,m,cnt,t1,t2,t3,c,tot; 9 bool inh[N]; 10 bool operator <(a x,a y) 11 { 12 return x.val>y.val; 13 }//边权小根堆 14 priority_queue<a> qs; 15 void link(int f,int t,int v) 16 { 17 noww[++cnt]=p[f],p[f]=cnt; 18 goal[cnt]=t,val[cnt]=v; 19 } 20 int main () 21 { 22 scanf("%d%d",&n,&m); 23 for(int i=1;i<=m;i++) 24 { 25 scanf("%d%d%d",&t1,&t2,&t3); 26 link(t1,t2,t3),link(t2,t1,t3); 27 } 28 memset(dis,0x3f,sizeof dis); 29 int tn=1;dis[tn]=0,inh[tn]=true;//随便找个初始点 30 for(int i=p[tn];i;i=noww[i]) 31 qs.push((a){goal[i],val[i]}),dis[goal[i]]=val[i];//把边一股脑丢进去 32 while(!qs.empty()) 33 { 34 a tmp=qs.top();qs.pop(); 35 if(!judge[tmp.nxt])//没选过 36 { 37 int tp=tmp.nxt,tv=tmp.val; 38 tot+=tv,c++,judge[tp]=true; 39 for(int i=p[tp];i;i=noww[i])//更新 40 if(dis[goal[i]]>val[i]) 41 dis[goal[i]]=val[i],qs.push((a){goal[i],val[i]}); 42 } 43 } 44 printf("%d",tot); 45 return 0; 46 }
2A.单源最短路径之SPFA
①SPFA能干啥
SPFA是一个由Bellman-Ford算法优化而来的最短路算法,用于求由单点出发,到达其他各点的最短路径长度,适用范围较广,以其为基础还可以判断一张图是否存在负环。
![](https://cdn.luogu.org/upload/pic/16680.png )
从度娘那里抱来的图↑~~(这图好像和SPFA关系不大,只是展示了最短路)~~
②SPFA的原理
通常SPFA以邻接表存图,以一个队列存储等待进行松弛的各个点,队列中起初只有一个起点,利用三角形不等式进行松弛操作,直到队列为空。在这个过程中一个节点可能会入/出队多次,即使存在负权边,也能正常求解。
初始化:$dis[1]=0$,$dis[oth]=INF$,$judge[1]=true$,$judge[oth]=false$,点1入队
③SPFA的时间复杂度
时间复杂度:$O(???)$,准确说来是$O(km)$,k是由边权关系确定的一个常数,与图本身与起点有关,所以说SPFA的时间复杂度为$O(???)$。它也(为什么要说也)**较为适用于稀疏图**,在稠密图和特殊构造的图上,SPFA最惨的话会被卡成$O(nm)$。
既然说起了SPFA的时间复杂度,那就再扯几句。SPFA其实是Shortest Path Fast Algorithm的缩写,但是听说因为玄学的复杂度和玄学复杂度的证明的%^&* ,国际上一般不承认SPFA算法,而是称它为“队列优化的Bellman-Ford算法”(23333)。
④SPFA的具体实现
```cpp
SPFA求单源最短路径
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int dis[n],p[n],noww[m],goal[m],val[m];
int N,M,S,cnt,t1,t2,t3;
bool inq[n];
queue<int> qs;
void link(int f,int t,int v)
{
noww[++cnt]=p[f],p[f]=cnt;
goal[cnt]=t,val[cnt]=v;
}
void setit(int s)
{
memset(dis,0x3f,sizeof dis);
qs.push(S);dis[s]=0;inq[s]=true;
}
void SPFA()
{
while(!qs.empty())
{
int tn=qs.front();
qs.pop(),inq[tn]=false;
for(int i=p[tn];i;i=noww[i])
if(dis[goal[i]]>dis[tn]+val[i])
{
dis[goal[i]]=dis[tn]+val[i];
if(!inq[goal[i]])
inq[goal[i]]=true,qs.push(goal[i]);
}
}
}
int main()
{
scanf("%d%d%d",&N,&M,&S);
for(int i=1;i<=M;i++)
scanf("%d%d%d",&t1,&t2,&t3),link(t1,t2,t3);
setit(S);SPFA();
for(int i=1;i<=N;i++)
printf("%d ",dis[i]);
return 0;
}
```
⑤一些奇怪的东西— —SPFA的“优化”
SPFA有一个相对常见优化,叫做SLF(Small Label First),它基于双端队列(可使用<queue>里的std::deque或者手写)。在每次松弛后,将$dis[temp]$与队首节点的$dis$作比较,若$dis[temp]$较小,将其从队头入队,否则从队尾入队。
还有个叫做LLL(Large Label Last)的优化,原理差不多,看字面意思就能懂吧= =
代码即是将原代码中的
```cpp
if(!inq[goal[i]])
{
inq[goal[i]]=true;
qs.push(goal[i]);
}
```
改为
```cpp
if(!inq[goal[i]])
{
if(!q.empty()&dis[goal[i]]<dis[q.front()])
q.push_front(goal[i]);
else
q.push_back(goal[i]);
inq[goal[i]]=true;
}
```
可是这么明显的优化为什么很少听人说呢?因为这是个假的优化,本来SPFA在构造数据下能被卡成$O(nm)$,用了上述两种优化或许能让你过掉卡SPFA的数据,但是......
这个优化也能被卡,而且会被丧心病狂地卡成$O(2^n)$......
所以没有负权边时老老实实写堆优化下的迪杰斯特拉算法,不要老是想什么骚操作=。=,毕竟
![](https://cdn.luogu.org/upload/pic/26221.png )
(NOI 2018讲解现场,不要在意二重存在的luogu水印=。=)
哦你问什么是堆优化下的迪杰斯特拉算法?往下看就知道了quq
2A*.基于SPFA判负环 以及 基于深度优先思想判负环
--
①原理
$_1$基于SPFA判负环的原理
根据SPFA的原理,显然在图中存在负环时,SPFA会将这个负环上的节点不断循环入队。而一张图若存在最短路则每个点至多入队$n$次(显),所以每个点记录一个入队次数,超过$n$次即存在负环(据lyd学长说记录边的次数会更快),最差情况下复杂度为$O(n^2)$
![](https://cdn.luogu.org/upload/pic/16708.png )
自己画了张~~奇丑无比~~的图,负环用绿色的边标出↑
$_2$基于深度优先思想判负环的原理
基本与DFS相同,我们可以想象出递归时的那个栈。判断时加上一个初始为假的判断标志$flag$,在进行松弛时,如果这个点已经入栈或者$flag$为真,标记$flag$为真,回溯即可。虽然是指数复杂度,但是随机数据下跑的飞快,一般没人卡这个(不过luogu新数据卡了,可真是有心。。。)
另:个人测试下,如果你跑深度优先的话,$dis$初值为$0$每个点跑一次比加超级源点再跑超级源点要快。。。
②具体实现
```cpp
//深度优先
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int p[n],noww[m],goal[m],val[m],dis[n];
int n,m,typ,t1,t2,t3,cnt,T;
bool ins[n];
bool found;
void link(int f,int t,int v)
{
noww[++cnt]=p[f],p[f]=cnt;
goal[cnt]=t,val[cnt]=v;
}
void gtmd()
{
memset(ins,false,sizeof ins);
memset(dis,0,sizeof dis);
memset(p,0,sizeof p);
cnt=0;found=false;
}
void DF_SPFA(int nde)
{
ins[nde]=true;
for(int i=p[nde];i;i=noww[i])
if(dis[goal[i]]>dis[nde]+val[i])
{
if(ins[goal[i]]||found)
{
found=true;
return ;
}
dis[goal[i]]=dis[nde]+val[i];
DF_SPFA(goal[i]);
}
ins[nde]=false;
return ;
}
int main ()
{
scanf("%d",&T);
while(T--)
{
gtmd();
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&t1,&t2,&t3);
link(t1,t2,t3);if(t3>=0) link(t2,t1,t3);
}
for(int i=1;i<=n;i++)
if(!found)
DF_SPFA(i);
else
break;
printf(found?"Ye5\n":"N0\n");
}
return 0;
}
```
```cpp
//SPFA改造
#include<queue>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int dis[n],p[n],noww[n],goal[n],val[m],xnt[n];
int n,m,t1,t2,t3,cnt,T;
bool inq[n];
queue<int> qs;
void link(int f,int t,int v)
{
noww[++cnt]=p[f],p[f]=cnt;
goal[cnt]=t,val[cnt]=v;
}
void gtmd()
{
memset(p,0,sizeof p);
memset(xnt,0,sizeof xnt);
memset(dis,0x3f,sizeof dis);
memset(inq,false,sizeof inq);
while(!qs.empty()) qs.pop();
qs.push(1);cnt=0;
inq[1]=true,dis[1]=0;
}
bool SPFA()
{
while(!qs.empty())
{
int tn=qs.front();inq[tn]=false;qs.pop();
for(int i=p[tn];i;i=noww[i])
{
int tp=goal[i],tv=val[i];
if(dis[tp]>dis[tn]+tv)
{
dis[tp]=dis[tn]+tv;
if(!inq[tp])
{
if(++xnt[tp]>n) return true;
inq[tp]=true,qs.push(tp);
}
}
}
}
return false;
}
int main ()
{
scanf("%d",&T);
while(T--)
{
gtmd();
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d",&t1,&t2,&t3);
link(t1,t2,t3);if(t3>=0) link(t2,t1,t3);
}
printf(SPFA()?"YE5\n":"N0\n");
}
return 0;
}
```
2B.Dijkstra(heap)
--
①Dijkstra能干啥
Dijkstra也是一种求最短路的算法,缺点是不能处理负权边,但是其复杂度稳定,卡不掉
②Dijkstra的原理
![](https://cdn.luogu.org/upload/pic/21235.png )
↑手绘Dijkstra,红点->终点,绿点->起点,蓝点->正在决策的点,灰点->已经决策完的点,白点->尚未决策的点
Dijkstra朴素做法是枚举每个点及和这个点相连的点进行进行扩展,而扩展过的点就不会再扩展,因而无法处理负权边,复杂度为$O(n^2)$,比较差。通常会用一个堆进行优化:每次扩展将目标点和边权加入一个小根堆,在扩展时取堆顶的那个点进行新一轮扩展,复杂度为$(mlog n)$。
③Dijkstra的时间复杂度
时间复杂度:朴素$O(n^2)$ 堆优化下$O(mlogn)$
④Dijkstra的具体实现
```
#include<queue>
#include<cstdio>
using namespace std;
struct SP{int value,node;};
bool operator <(const SP &a,const SP &b) {return a.value>b.value;}//堆优化
int p[n],noww[m],goal[m],val[m],dis[n];
bool inh[n];
priority_queue<SP> qs;
int N,M,S,cnt,t1,t2,t3;
void link(int f,int t,int v)
{
noww[++cnt]=p[f],p[f]=cnt;
goal[cnt]=t;val[cnt]=v;
}
void Dijkstra()
{
while(!qs.empty())
{
SP tmp=qs.top();qs.pop();
int tn=tmp.node; //取点
if(!inh[tn])
{
inh[tn]=true;
for(int i=p[tn];i;i=noww[i])//扩展
{
int tp=goal[i],tv=val[i];
if(dis[tp]>dis[tn]+tv)
{
dis[tp]=dis[tn]+tv;
qs.push((SP){dis[tp],tp});
}
}
}
}
}
int main ()
{
scanf("%d%d%d",&N,&M,&S);
for(int i=1;i<=M;i++)
scanf("%d%d%d",&t1,&t2,&t3),link(t1,t2,t3);
memset(dis,0x3f,sizeof dis);
dis[S]=0;qs.push((SP){0,S});
Dijkstra();
for(int i=1;i<=N;i++)
printf("%d ",dis[i]);
return 0;
}
```