DFS
用栈的方式来记录深度优先遍历的路径(或者用递归)
需 配合 剪枝 来优化时间复杂度。
BFS
用队列的方式来实现。
树和图的深度优先遍历
表示一个图的方法
当图比较稠密的时候(边数 = 2倍 节点数),适合用邻接矩阵存
否则 适合用邻接链表存
邻接链表法:
一个图有n个点,m条边
则创建n条链表,每个链表表示一个节点的邻接点是谁
而图的数组来存这些链表头的地址。
邻接矩阵法:
一个图有n个点,m条边
则用一个n*n的矩阵来表示边,如果 i 行 j 列值为1,则节点 i 和 节点 j 之间存在一条边
树和图的宽度优先遍历
拓扑排序问题
拓扑排序问题,指的是,给出一个有向图,对图中的点进行排序,使 不存在 反向的边 即序列后面的点有到前面点 的边。
这也用到了队列。
首先,对原始的图,找到入度为0的点,这种入度为0的点就可以放入拓扑序中了,将这些点放入一个队列中。
然后依次遍历序列中的点,当遍历到一个点的时候,就把 它 出去的边都删掉,这个操作会使某些点的入度减一,就可能产生入度为0的点,如果有,就加入队列,依次循环,直到队列为空,如果最后队列空,且拓扑序的长度等于节点数就存在拓扑序,否则,不存在。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5+10;
int head[N], nxt[N], val[N], ttlen;
int inDeg[N];
int n, m;
void add(int u, int v){
val[ttlen] = v;
nxt[ttlen] = head[u];
head[u] = ttlen;
++inDeg[v];
++ttlen;
}
pair<int*,int> bfs(){
int res[N] , len = 0;
int q[N], hd = 0, tl = 0;
for(int i = 1 ; i <= n ; ++ i){
if(inDeg[i] == 0){
q[tl++] = i;
res[len++] = i;
}
}
while(hd!=tl){
int node = q[hd++];
int link = head[node];
while(link!=-1){
--inDeg[ val[link] ];
if(inDeg[ val[link] ] == 0){
res[len++] = val[link];
q[tl++] = val[link];
}
link = nxt[link];
}
}
return {res,len};
}
int main(){
memset(head, 0xff, sizeof head);
cin >> n >> m;
ttlen = 0;
while(m--){
int u, v; cin >> u >> v;
add(u,v);
}
pair<int*,int> pr = bfs();
if(pr.second !=n) cout<<-1<<endl;
else{
for(int i = 0 ; i < pr.second ; ++ i)
cout<< pr.first[i]<<' ';
cout<<endl;
}
return 0;
}
Dijkstra求单源最短路
单源最短路指的是,从一个点出发,求到所有点的最短路。
边权都 > 0 情况下的最短路
朴素Dijkstra算法 O ( n 2 ) O(n^2) O(n2)(适用稠密图 边数 m m m > 点数 n 2 n^2 n2)
算法一共会经过n层循环,每个循环都能确定一个路径最短的点,n个循环确定n个。
在循环内,首先 findMin,找到一个 未被访问 且 离源点最近的一个点,把它标记为访问过,然后更新它所能到达的节点的路径。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 505, null = 0x3f3f3f3f;
int n, m; // 点数和边数
int matrix[N][N]; // 邻接矩阵
int res[N]; // 节点1~N的最短距离
bool visited[N]; //表示节点是否已经确认最短的
void dijkstra(){
memset(res, 0x3f, sizeof res);
res[1] = 0;
for(int i = 0 ; i < n ; ++ i){
//进行n次循环,每次都能通过findmin 确定一个最短的
//findmin
int tp = 0;
for(int j = 1 ; j <= n ; ++ j){
if(!visited[j]){
if(tp == 0 || res[j] < res[tp]) tp = j;
}
}
visited[tp] = 1;
// 更新 tp 所能达的点
for(int j = 1 ; j <= n ; ++ j){
if(!visited[j] && res[tp] + matrix[tp][j] < res[j])
res[j] = res[tp] + matrix[tp][j];
}
}
}
int main(){
cin >> n >> m;
memset(matrix, 0x3f, sizeof matrix);
for(int i = 0 ; i < m ; ++ i){
int u, v, w;
cin >> u >> v >> w;
if(u == v) continue; // 去点自环
if(w < matrix[u][v]) matrix[u][v] = w; // 重边中,取最小的那个
}
dijkstra();
cout<< (res[n]==null?-1:res[n]) << endl;
return 0;
}
堆优化的Dijkstra算法 O ( m ∗ l o g n ) O(m*logn) O(m∗logn)(适用稀疏图)
堆优化版的Dij算法和朴素版的相比,使用了一个堆来加快findMin的过程。
在循环内部的findMin时,我们使用一个堆,那这里的复杂度就成了O(1),但是在更新一个节点的邻接点距离时,会往堆里面插入数据,这里的复杂度变成了O(logn),因为一共会遍历所有的边一次,所以这里加起来是O(m*logn)
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 2e5, null = 0x3f3f3f3f;
typedef pair<int,int> pii;
int n, m; // 点数和边数
int g[N], nxt[N], len;
pii val[N]; // pii 存的是 节点,距离
int res[N]; // 节点1~N的最短距离
bool visited[N]; //表示节点是否已经确认最短的
void add_edge(int u, int v, int w){
val[++len] = {v,w};
nxt[len] = g[u];
g[u] = len;
}
class mycmp{
public:
bool operator()(pii & a, pii & b){
return a.second > b.second;
}
};
void dijkstra_heap(){
memset(res, 0x3f, sizeof res); // 初始化到所有点的距离为正无穷
res[1] = 0;
priority_queue<pii,vector<pii>,mycmp> pq;
pq.push({1,0});
while(!pq.empty()){
pii tp = pq.top(); pq.pop();
// cout<< tp.first<< ": "<<endl;
if(visited[tp.first]) continue;
visited[tp.first] = 1;
//更新邻接边
int nt = g[tp.first];
while(nt!=-1){
//如果从源点到 确认点tp.first的距离 + 确认点tp.first到邻居nt的距离
// 小于 源点到 邻居nt的距离,则更新到小根堆里面
if(res[val[nt].first] > tp.second + val[nt].second){
res[val[nt].first] = tp.second + val[nt].second;// 同时也更新到res数组
pq.push( {val[nt].first , tp.second + val[nt].second} );
}
nt = nxt[nt];
}
}
}
int main(){
cin >> n >> m;
len = 0;
memset(g, -1, sizeof g);
for(int i = 0 ; i < m ; ++ i){
int u, v, w;
cin >> u >> v >> w;
if(u == v) continue; // 去掉自环
add_edge(u,v,w); // 重边不用管
}
dijkstra_heap();
cout<< (res[n]==null?-1:res[n]) << endl;
return 0;
}
存在负权边情况下的最短路
Bellman-ford 算法 O ( n ∗ m ) O(n*m) O(n∗m)
适用性比较好,能解决负环问题,能解决有限条边的情况。有限条边指的是,从1~n最多经过k条边时的最短路是多少。
其算法思路是,首先对于距离dist矩阵,1号点是更新了的。
然后,经过k次循环进行边更新(k表示的就是最多经过多少条边,那就更新几次)
在边更新过程中,依次遍历所有的边(所以bellman-ford算法可以直接用数组存边,而不需要表示成图),如果边的起始点dist[u]为inf,则不更新,否则,如果dist[u] + weight < dist[v],那就更新dist[v]。注意这里需要用一个数组来存储之前的dist才能实现更新过程。
进行了k次循环之后,如果dist[n] == inf, 则说明没有k步到n的路; 如果k取n-1,则就是从1~n的最短路。
如果图中存在负环,这个结果仍然正确。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e4+10;
int dist[520], predist[520], edge[N][3], len;
int n,m,k;
// bellmanford算法 可以解决 负权、负环、有限条边 的情况
// spfa 虽然在复杂度等更优,但是不能解决负环、有限条边 的情况
void bellmanford(){
dist[1] = 0;
for(int i = 0 ; i < k ; ++ i){1
memcpy(predist, dist, sizeof dist);
for(int j = 0; j < len;++j){
//遍历所有边
int u = edge[j][0], v = edge[j][1], w = edge[j][2];
if(predist[u]!=0x3f3f3f3f && dist[v] > predist[u]+w) dist[v] = predist[u]+w;
}
}
}
int main(){
cin >> n >> m >> k;
len = 0;
memset(dist, 0x3f, sizeof dist);
while(m--){
int u, v, w;
cin>> u >> v >> w;
edge[len][0] = u, edge[len][1] = v, edge[len][2] = w;
++len;
}
bellmanford();
if(dist[n] == 0x3f3f3f3f) cout<< "impossible" << endl;
else cout<< dist[n] << endl;
return 0;
}
spfa算法 一般 O ( m ) O(m) O(m) 最坏 O ( n ∗ m ) O(n*m) O(n∗m)
spfa算法 其实是对bellman-ford算法的优化,bellman-ford算法在每次循环中,都依次更新所有的边。而spfa算法通过一个队列来存储能更新的边,减少时间复杂度。
其思想是,用一个队列q来存储能更新的点,然后依次遍历队列中的点,更新其邻接点的dist,如果邻接点的dist减小了,并且这个邻接点不在可更新队列q中(通过一个bool数组查询),则将这个邻接点也添加到队列中。依次遍历队列,直至q为空。
#include <iostream>
#include <cstring>
using namespace std;
typedef pair<int, int> pii;
const int N = 1e5+10, inf = 0x3f3f3f3f;
//需要遍历一个节点的所有临边,加上数据,这里用链表
int g[N], nxt[N], len, dist[N], n, m;
pii val[N];
int decrease[2*N], head, tail; // 存储距离降低了的点
bool indec[N];
void spfa(){
//spfa 算法 是对bellmanford算法的优化
dist[1] = 0;
decrease[tail++] = 1;
indec[1] = 1;
// 当 降低节点 队列不空的时候
while(head != tail){
// 弹出队头,并且设置节点未访问
int src = decrease[head++];
int nt = g[ src ];
indec[src] = 0;
// 遍历邻接链表,更新每个邻接点的值
while(nt!=-1){
int node = val[nt].first, wei = val[nt].second;
if(dist[src] + wei < dist[node]){
//如果值更小,那就更新dist
dist[ node ] = dist[src] + wei;
//如果节点不在decrease里,就加进去,如果在那就不用加
if(!indec[ node ]){
decrease[tail++] = node;
indec[ node ] = 1;
}
}
nt = nxt[nt];
}
}
}
int main(){
memset(dist, inf, sizeof dist);
memset(g,-1,sizeof g);
head = tail = 0;
len = 0;
cin >> n >> m;
while(m--){
int u, v, w;
cin >> u >> v >> w;
++len;
val[len] = {v,w};
nxt[len] = g[u];
g[u] = len;
}
spfa();
if(dist[n] == inf) cout<<"impossible"<<endl;
else cout<< dist[n] << endl;
return 0;
}
spfa算法判断图中是否有负环
spfa算法判断负环的思路和spfa算法相同,它通过再添加一个pathLen数组来存储1~n节点最短路的长度。如果某次更新使某个节点的最短路的长度>=n了,则说明有负环。
需要注意的是,如果仍然从1开始更新,则找到的是1 ~ i 的路上有没有负环,如果不存在1~k的通路,则找不到k及其之后的负环了。所以找负环算法需要改变一下初始化的思路,首先将所有的点的距离都更新成某一个任意的数,然后将所有的点都添加到可更新队列,再进行更新,当某个dist减小的时候,除了更新对应的dist,还更新一下pathLen数组:pathLen[v] = pathLen[u]+1;
/*
spfa算法找负环,只需要添加一个pathLen[N]数组来统计每个点的最短路长度
如果有点的最短路长度经过更新, >= 节点数了,则说明进入负环了,因为总共才n个点。
*/
#include <bits/stdc++.h> // 万能头文件
using namespace std;
typedef pair<int, int> pii;
const int N = 2100, M = 1e4+10;
pii val[M];
int nxt[M], len, g[N], pathLen[N], dist[N];
int n, m;
queue<int> q;
bool inq[N];
void add(int u, int v, int w){
++len; val[len] = {v,w};
nxt[len] = g[u]; g[u] = len;
}
bool spfa_find_circle(){
dist[1] = 0;
for(int i = 1; i<= n ; ++ i){
q.push(i);
inq[i] = 1;
}
while(!q.empty()){
int src = q.front(); q.pop();
inq[src] = 0;
int nt = g[src];
while(nt!=-1){
int node = val[nt].first, wei = val[nt].second;
if(dist[src] + wei < dist[node]){
dist[node] = dist[src] + wei;
pathLen[node] = pathLen[src] + 1;
if(pathLen[node] >= n) return 1;
if(!inq[node]){
q.push(node);
inq[node] = 1;
}
}
nt = nxt[nt];
}
}
return 0;
}
int main(){
// 优化cin cout
ios::sync_with_stdio(false);
cin.tie(NULL); cout.tie(NULL);
cin >> n >> m;
len = 0;
memset(g, -1, sizeof g);
memset(dist, 0x11, sizeof dist);
while(m--){
int u, v, w; cin >> u >> v >> w;
add(u, v, w);
}
if(spfa_find_circle()) cout << "Yes" << endl;
else cout << "No" << endl;
return 0;
}
多源汇最短路算法
多源最短路指的是,给定出发源和终止源,求出发源到终止源的最短路。
Floyd算法 O ( n 3 ) O(n^3) O(n3)
Floyd算法是一个基于 DP 思想的算法。
其思想是(待补充):
用邻接矩阵matrix[N][N]表示一个图,同时在这个邻接矩阵上进行最短路的更新。
初始化,matrix[i][i]为0,这样可以删掉自环,然后,添加边,如果有i -> j的边weight,则matrix[i][j]为min(weight, matrix[i][j]),这样可以取重边中最小的一条边。
第一层循环,遍历节点 k ,表明的是,其他的u、v经过节点 k 时的最短路
第二层循环 i 和第三层循环 j 遍历整个邻接矩阵,如果matrix[i][k] 或者 matrix[k][j]为inf,则不需要更新了,正无穷再加或者减都是正无穷,如果二者都不是inf,再进行比较,如果matrix[i][k]+matrix[k][j] < matrix[i][j],则表明,从 i 经过 k 到 j 经过的路径要比之前的短,不管之前的经过哪里,反正小,所以更新matrix[i][j] 为 matrix[i][k] + matrix[k][j]
#include <bits/stdc++.h>
using namespace std;
const int N = 210, inf = 0x3f3f3f3f;
int matrix[N][N];
int n, m, q;
void floyd(){
for(int k = 1 ; k <= n ; ++ k){
for(int i = 1 ; i <= n ; ++ i){
for(int j = 1 ; j <= n ; ++ j){
if(matrix[i][k] != inf && matrix[k][j]!=inf){
matrix[i][j] = min(matrix[i][j], matrix[i][k] + matrix[k][j]);
}
}
}
}
}
int main(){
// 优化cin cout
ios::sync_with_stdio(false);
cin.tie(NULL); cout.tie(NULL);
cin >> n >> m >> q;
//init matrix
for(int i = 1 ; i <= n; ++ i){
for(int j = 1 ; j <= n ; ++ j){
if(i == j) matrix[i][j] = 0;
else matrix[i][j] = inf;
}
}
while(m--){
int u, v, w;
cin >> u >> v >> w;
if(u != v)// 避免自环
matrix[u][v] = min(w, matrix[u][v]); // 避免重边,这里选最小的
}
floyd();
while(q--){
int u, v; cin >> u >> v;
if(matrix[u][v] == inf) cout<< "impossible" << endl;
else cout<< matrix[u][v] << endl;
}
return 0;
}
图的最小生成树问题
给定一个可能带有自环、圈的图,给出其最小生成树
prim算法(适用于稠密图, m > n 2 m>n^2 m>n2)
通过邻接矩阵的形式存储一个图。
定义 S 表示已经在生成树中的点,dist为其他未在生成树中的点到S中的最小距离。
和Dijkstra算法差不多,都是进行n次循环,每次循环能加入S中一个节点。
在循环中,首先就是findMin,找到一个里S集合最近的节点,添加到S中,然后更新这个点的邻边(因为只有这个点有出边,会影响其他点到集合的距离,所以只需要更新这个点的邻接点就行)。
#include <bits/stdc++.h>
using namespace std;
const int N = 520;
int adj[N][N], dist[N];//dist记录点到已扩散集合的距离
bool inS[N]; // 记录点是否已经添加到最小生成树集合
int res,n,m;
void add(int u, int v, int w){
if(u == v) return;
adj[u][v] = min(adj[u][v], w);
}
void prim(){
/*
朴素prim算法 采用邻接矩阵的形式存储图,时间复杂度是 O(n^2),适用于稠密图(m>n^2)
还有堆优化版的prim算法,通过堆优化findmin O(mlogn),但是因为有kruskal算法,所以堆优化prin不常用
*/
//for循环n次,每次都能添加进集合一个点
int lenS = 0;
dist[1]=0;
for(int i = 1 ; i <= n ; ++ i){
// findmin 找到距离集合最近的点
int minidx = 0, minval = 0x3f3f3f3f;
for(int i = 1 ; i <= n ; ++ i){
if(!inS[i] && dist[i] < minval){
minval = dist[i], minidx = i;
}
}
if(minidx == 0) continue;
++lenS; inS[minidx]=1;
res+=minval;
for(int i = 1 ; i <= n ; ++ i){
if(!inS[i] && dist[i] > adj[minidx][i]){
dist[i] = adj[minidx][i];
}
}
}
// cout<<lenS<<endl;
if(lenS < n) cout<<"impossible"<<endl;
else cout<< res<<endl;
}
int main(){
//init
res = 0;
memset(adj,0x3f,sizeof adj);
memset(dist,0x3f,sizeof dist);
cin>>n>>m;
while(m--){
int u,v,w; cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
prim();
return 0;
}
Kruskal算法(适用于稀疏图, m < n 2 m<n^2 m<n2)
设计的其他知识点(并查集、快排)
首先是对所有的边进行排序,按照边权升序的方式。
然后从最小权的边开始遍历,如果边的起始、终止节点不在一个集合中,那就可以添加这个边,同时将这两个节点合并在一个集合里。每添加一条边,都进行一次计数,对于一个树来说,边数=节点数-1,那么如果最后添加的次数!=n-1,那就说明不连通,就不存在最小生成树。
#include <iostream>
using namespace std;
const int N = 1e5+10, M = 2*N;
int n, m;
int edges[M][3], fa[N];
int res;
void fastSort(int L, int R){
if(L>=R) return;
int tar = edges[(L+R+1)/2][2];
// cout<<tar<<endl;
int ll = L-1, rr = R+1;
while(ll < rr){
do ++ll; while(edges[ll][2] < tar);
do --rr; while(edges[rr][2] > tar);
if(ll < rr){
for(int i = 0 ; i <= 2 ; ++ i)
swap(edges[ll][i], edges[rr][i]);
}
}
fastSort(L,ll-1);
fastSort(ll, R);
}
int findFa(int a){
if(fa[a] != a) fa[a] = findFa(fa[a]);
return fa[a];
}
int main(){
cin >> n >> m;
int len = 0;
while(m--){
int u, v, w; cin >> u >> v >> w;
edges[len][0] = u, edges[len][1] = v, edges[len][2] = w;
++len;
}
//先对所有的边按照权重升序排序
fastSort(0,len-1);
//并查集,先初始化所有的点为自己的集合
for(int i = 1 ; i <= n ; ++ i){
fa[i] = i;
}
//然后按照顺序,集合合并,如果二者的父不同,则可以添加这个边
res = 0;
int cnt = 0; // cnt表示添加进最小生成树多少条边了
for(int i = 0 ; i < len ; ++ i){
int ffa = findFa(edges[i][0]), ffb = findFa(edges[i][1]);
if( ffa != ffb ){
//更新结果
res+=edges[i][2];
fa[ffb] = ffa;
//维护数量
++cnt;
}
}
//如果最后添加边的数量不是n-1,(树的边数为n-1)那就说明不连通,输出impossible
if(cnt!=n-1) cout<<"impossible"<<endl;
else cout<<res<<endl;
return 0;
}
染色法判定二分图
染色法是基于深度优先遍历的一个算法。算法基于 二分图不含奇圈的定理。
先for循环给每一个节点染 1 色(这里是为了避免非连通无法遍历到的情况),然后去遍历他的子节点,但是染的色逆转一下(如果染色1、2,那可以用3- i 来实现染色的逆转),如果子节点没染色,或者已经染色但恰好是我想染的色,那就成功,回溯,否则,染色冲突。
一旦有染色冲突的,那就说明检测到了奇圈,则这个图不是二分图。
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5+10, M = 2*N;
//用 邻接链表存储图
int g[N], val[M], nxt[M], len;
int n, m;
int color[N];// 记录每个点的着色
bool dfs(int nn, int c){
if(!color[nn]){
//还未着色
color[nn] = c;
for(int i = g[nn]; i!=-1 ; i = nxt[i]){
if(!dfs(val[i],3-c)){
return false;
}
}
return true;
}
else{
return color[nn] == c;
}
}
int main(){
memset(g,-1,sizeof g);
cin >> n >> m;
for(int i = 0 ; i < m ; ++ i){
int u, v; cin >> u >> v;
// 加边 u -> v
val[len] = v;
nxt[len] = g[u];
g[u] = len++;
//加边 v -> u
val[len] = u;
nxt[len] = g[v];
g[v] = len++;
}
/*
这里是为了避免 多个连通分支的情况,
从一个未开始染色的点开始 进行dfs染色
如果中间出现了染色冲突的情况,则染色失败,不是二分图
如果没有出现,则说明 染过色的都是正确的,那我就从下一个没染色的开始,而不管染过色的
*/
for(int node = 1 ; node <=n ; ++node){
if(!color[node] && !dfs(node,1)){
cout<< "No"<<endl;
return 0;
}
}
cout<<"Yes"<<endl;
return 0;
}
匈牙利算法
给定一个二分图,寻找其最大匹配数量(所有边的起始和终止节点都不重合),假定二分图分为左边的节点共 n1 个和右边的节点共 n2 个。然后给定m条边。
匈牙利算法是,选一个类的节点,比如以左边为基础,进行遍历。
当遍历到某一个点的时候,依次遍历它的邻接点,如果邻接点没有和其他点匹配,或者邻接点已经匹配了,但是其他的点能再换点匹配,那就认为这个点可以被匹配。
看邻接点能不能换点匹配的实现方法是递归,见程序
#include <iostream>
#include <cstring>
using namespace std;
const int N = 520, M = 1e5+10;
int n1,n2,m;
// 邻接链表存储图
int g[N], nxt[M], val[M], len;
int match[N], order[N];//match代表已经匹配,order代表预定
bool findGF(int i){
int nt = g[i];
for(; nt != -1; nt = nxt[nt]){
if(!order[val[nt]]){
order[val[nt]] = 1;
if(match[val[nt]] == 0 || findGF(match[val[nt]]) ){
match[val[nt]] = i;
return true;
}
}
}
return false;
}
int main(){
memset(g,-1, sizeof g);
cin >> n1 >> n2 >> m;
for(int i = 0 ; i != m ; ++ i){
int u, v; cin >> u >> v;
// 只添加单向边,将其视为单边 从左到右就可以
val[len] = v;
nxt[len] = g[u];
g[u] = len++;
}
int m = 0;
for(int i = 1 ; i <= n1 ; ++ i){
// 重置order,表示的是为 第 i 个节点findGF时有没有被尝试预定过
memset(order,0,sizeof order);
if(findGF(i)){
++m;
}
}
cout<< m << endl;
return 0;
}