初涉图论

拓扑排序:

DFS实现

1
2
3
4
5
dfs(int u){
for u 的所有邻点
seq<-u
}
//seq是拓扑排序的逆序

BFS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/*对于一个序列x...y,对于图中的每一条有向边,x都出现y的前面
即所有边都是从前指向后的
只要有环,必然无拓扑序列
有向无环图必然存在拓扑序列
入度为0的点都可以排在前面
做法:
1.把所有入度为0的点入队
2.bfs()
while(!Q>empty()){
t.push(队头)
队头->t
枚举t的所有出边{
删除t->j,d[j]--;
if(d[j]==0) j入队
}
}
*/
bool topsort(){
queue<int> Q;
for(int i=1;i<=n;i++){
if(d[i]==0){
Q.push(i);
}
}
while(!Q.empty()){
int t=Q.front();ds.push_back(Q.front());
Q.pop();
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
d[j]--;
if(d[j]==0) Q.push(j);
}
}
int sz=ds.size();
return sz>=n;
}

最短路算法知识结构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/*
最短路问题{
单源最短路{
1.所有边权都是正数{
1.朴素Dijkstra算法 O(n^2) 适用于稠密图

2.堆优化的Dijkstra O(mlog(n)) 适用于稀疏图
}

2.存在负权边{
1.Bellman-Ford O(nm)

2.SPFA 一般O(m)最坏O(nm)
}
}
多源汇最短路{
Floyed算法
}
}
*/

稠密图:一般边数m=n^2

稀疏图:边数m=n

Dijkstra基于贪心算法

Floyed基于动态规划

Bellman-Ford基于离散数学中的知识


朴素版Dijkstra算法

s={当前已经确定最短距离的点}

1.初始化

1
dis[1]=0,dis[otherwise]=+∞

2.循环迭代(贪婪规则:更新当前还没有确定的点中距离最小的点)

1
2
3
4
for(i:0~n)迭代循环n次
不在s中的距离最近的点->t
t->s //将t加到s集合内
用t来更新其他点的距离(check(dis[x]>dis[t])

3.可确定每一个点到起点的最短距离了

存法:使用邻接矩阵(因为是稠密图)

朴素Dijstra解法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int n,m;
int dis[MAXN],g[MAXN][MAXN];
bool st[MAXN];

int dijkstra(){
memset(dis,0x3f3f3f3f,sizeof(dis));
dis[1]=0;
for(int i=0;i<n;i++){
int t=-1;

for(int j=1;j<=n;j++)
if(!st[j] && (t==-1||dis[t]>dis[j]))
t=j;
if(t==n) break;
st[t]=true;

for(int j=1;j<=n;j++)
dis[j]=min(dis[j],dis[t]+g[t][j]);
}
//判断一下是否是孤立点
if(dis[n]==0x3f3f3f3f) return -1;
return dis[n];
}

如果是稀疏图的话,对照上面的朴素Dijkstra,我们可以在这一步:

2.循环迭代(贪婪规则:更新当前还没有确定的点中距离最小的点)

1
2
3
4
for(i:0~n)迭代循环n次
不在s中的距离最近的点->t ****************O(n^2)
t->s //将t加到s集合内
用t来更新其他点的距离(check(dis[x]>dis[t]) ***O(mlogn)

此处使用堆来进行优化,直接借助于STL中的prority_queue或者手写堆(Python中的set)

bfs的迭代方式

1
2
3
4
5
1.先将(0,1)放入优先队列 //必须是小根堆
2.while!empty:取堆顶元素并弹出,如果此点已经被更新过了则继续迭代。
否则用当前点来更新其他点(遍历邻接表),记住,一定要将此点放入st[]数组来被标记
如果当前点距离大于从最近元素过来的距离,则更新dis[]并把j点放入优先队列;
3.最后结束的时候需要判断是否是孤立点,即是否为连通图

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//使用邻接表存储稀疏图
int n,m,idx;
int h[MAXN],e[MAXN],ne[MAXN],w[MAXN];//h[]存储每个邻接表上的头结点;ne[]存的是每个节点的下一个节点,即next;w存储权重
int dis[MAXN];
bool st[MAXN];
//pair的first存的是距离,second存的是编号
void add(int a,int b,int c){
e[idx]=b,w[idx]=c;
ne[idx]=h[a],h[a]=idx++;
}
int dijkstra(){
memset(dis,0x3f,sizeof(dis));
dis[1]=0;

priority_queue<pii,vector<pii>,greater<pii>> heap; //存储一个小根堆
heap.push({0,1});

while(heap.size()){
auto u = heap.top();heap.pop();

int ver = u.second,distance = u.first; //ver存储点的序号,distance存储距离
if(st[ver])
continue;
st[ver]=true; //这nm千万别忘了啊
for(int i=h[ver];i!=-1;i=ne[i]){
int j=e[i];
if(dis[j]>distance+w[i]){
dis[j]=distance+w[i];
heap.push({dis[j],j});
}
}
}
if(dis[n]==0x3f3f3f3f) return -1;
return dis[n];
}

Bellman_Ford算法 O(n*m)

任意存边方式都可,建议结构体

1
2
3
4
5
6
for(n次){
备份(防止用更新过的点更新其他点)
for 所有从a走到b的边,权重是w{
dis[b]=min(dis[b],dis[a]+w);
}
}

循环完,所有边都满足三角不等式dis[b]<=dis[a]+w;迭代k次表示经过不超过k条边的最短路的距离。如果第n次迭代仍然有边更新,根据抽屉原理,说明有负环。因此,Bellman-Ford算法可以用来找负环。

注意:如果有负权回路,最短路不一定存在

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#include<iostream>
#include<algorithm>
#include<string>
#include<cstring>
using namespace std;
#define MAXN 10010
#define maxn 510

struct Edge{
int a,b,w;
} edge[MAXN];

int dis[MAXN],backup[MAXN];
int n,m,k;

int bellman_ford(){
memset(dis,0x3f,sizeof(dis));
dis[1]=0;
//n次迭代,又名松弛操作
for(int i=1;i<=k;i++){
memcpy(backup,dis,sizeof(dis));
for(int j=1;j<=m;j++){
int a=edge[j].a,b=edge[j].b,w=edge[j].w;
dis[b]=min(dis[b],backup[a]+w);
}
}
if(dis[n]>0x3f3f3f3f/2) return -1;
return dis[n];
}

int main(){
scanf("%d%d%d",&n,&m,&k);
for(int i=1;i<=m;i++){
int a,b,w;
scanf("%d%d%d",&a,&b,&w);
edge[i].a=a,edge[i].b=b,edge[i].w=w;
}
int t=bellman_ford();
if(t==-1) puts("impossible");
else printf("%d\n",t);
return 0;
}

SPFA算法

步骤:

0.用邻接表存储1.队头入队,更新st数组2.BFS思路while队列不空,t<-q.front(),p.pop()3.遍历t的邻接表,更新dis[]数组,即t每一出点的最小距离;4.在每一出点判断,如果不在队列里,则加入队列;5.最后迭代完之后的判断

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include<iostream>
#include<string>
#include<algorithm>
#include<string.h>
#include<queue>
#define MAXN 160010
using namespace std;

typedef pair<int,int> pii;

//使用邻接表存储稀疏图
int n,m,idx;
int h[MAXN],e[MAXN],ne[MAXN],w[MAXN];//h[]存储每个邻接表上的头结点;ne[]存的是每个节点的下一个节点,即next;w存储权重
int dis[MAXN];
bool st[MAXN];
//pair的first存的是距离,second存的是编号
int spfa(){
memset(dis,0x3f,sizeof(dis));
dis[1]=0;

queue<int> Q;
Q.push(1);
st[1]=true;
while(Q.size()){
int t=Q.front();Q.pop();
st[t]=false;

for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];
if(dis[j]>dis[t]+w[i]){
dis[j]=dis[t]+w[i];
if(!st[j]){
Q.push(j);
st[j]=true;
}
}
}
}
if(dis[n]==0x3f3f3f3f) return -1;
return dis[n];
}

求负环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bool spfa(){    
//memset(dis,0x3f,sizeof(dis));
//dis[1]=0;
queue<int> Q;
for(int i=1;i<=n;i++){
st[i]=true;
Q.push(i);
}
while(Q.size()){
int t=Q.front();Q.pop();
st[t]=false;
for(int i=h[t];i!=-1;i=ne[i]){
int j=e[i];

if(dis[j]>dis[t]+w[i]){
dis[j]=dis[t]+w[i];
cnt[j] = cnt[t]+1;
if(cnt[j]>n) return true;
if(!st[j]){
Q.push(j);
st[j]=true;
}
}
}
}
return false;
}

Floyd算法 O(n^3)

1.邻接矩阵 d [ i , j ] 存图中每个点

1
2
3
4
for(k=1~n)	
for(i=1~n)
for(j=1~n)
d[i][j]=min(d[i][j],d[i][k]+d[k][j];

d[k,i,j]表示从i点经过1~k中间点到达j的最短距离
因此基于动态规划的状态转移方程为:

1
d[k,i,j]=min(d[k,i,j],d[k-1,i,k]+d[k-1,k,j])

从i到j只经过k-1这些点,再从k到j只经过1~k-1这些点,加在一起就是从i~j经过k个点,而k-1可以压缩掉,故状态转移方程为

1
d[i][j]=min(d[i][j],d[i][k]+d[k][j];

Floyd算法用来解决的问题:

​ 1.最短路问题

​ 2.传递闭包

​ 3.找最小环

​ 4.恰好经过k条边的最短路径

传递闭包

在有向图中,能间接到的点也连一条有向边

邻接矩阵的表示方式:已知g(i,j),求d(i,j)

1.初始化 d(i,j)=g(i,j)

2.对d(i,j)做一遍Floyd

1
2
3
4
5
6
g(i,j)=存在i->j?1:0
//Floyd
for k:n
for i:n
for j:n
if(d(i,k)==1&&d(k,j)==1) d(i,j)=1

矛盾的判断方式:d(i,i)=1

能唯一确定的判断方式: i != j 必有 d(i,j) | d(j,i) == 1

顺序不唯一:处于中间阶段

恰好经过k条边的最短路径之改进Floyd
1
2
3
4
Floyd:  d[k,i,j]表示从i到j只经过1-k的最短路径
本思路: d[k,i,j]表示从i到j,恰好经过k条边的最短路径
则状态转移方程为:
d[a+b,i,j]=min(d[a,i,k]+d[b,k,j]) k=1-n

k指的是第a个点,此算法可以处理有负环的情况

使用倍增的思想来拼接,用logn的复杂度逼近k