搜索与图论算法
搜索与图论
深度优先搜索
想象对一棵树进行搜索,深搜会尽可能往深了搜索,不撞南墙不回头,在回溯的时候会搜索该节点其他可能的路径。深搜使用的数据结构一般是栈Stack,于是一般用递归实现,使用的空间是$O(h)$,路径不具有最短性。
DFS最重要的两个部分:回溯和剪枝。用DFS做题时,首先要搞清楚用什么顺序来搜索。
eg: 八皇后问题
char g[N][N];
bool col[N], dg[N], udg[N];
// dg表示正对角线。udg表示斜对角线
void dfs(int u)
{
if (u == n)
{
for (int i = 0; i < n; i ++) puts(g[i]);
puts("");
return;
}
for (int i = 0; i < n; i ++)
{
if (!col[i] && !dg(u + i) && !udg(n - u + i))
{
g[u][i] = 'Q';
col[i] = dg[u + i] = udg[n - u + i] = true;
dfs( u + 1);
col[i] = dg[u + i] = udg[n - u + i] = false;
g[u][i] = '.';
}
}
}
宽度优先搜索
常用的深搜代码用迭代完成,使用的数据结构一般是队列Queue,使用的空间是$O(2^h)$,但是可以宽搜可以搜索到最短路径。
eg:走迷宫
int g[N][N]; // 存图
int d[N][N]; // 存每一个点到起点的距离
PII q[N * N];
int bfs()
{
int hh = 0, tt = 0;
q[0] = {0, 0};
// 首先把距离初始化为-1,表示没有走过
memset(d, -1, sizeof d);
d[0][0] = 0;
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1}; // 表示行走方向
// while队列不空,取出来队头
while (hh <= tt)
{
auto t = q[hh ++];
for (int i = 0; i < 4; i ++)
{
int x = t.first + dx[i], y = t.second + dy[i];
if (x >= 0 && x < n && y >= 0 && y < m && g[x][y] == 0 && d[x][y] == -1)
{
d[x][y] = d[t.first][t.second] + 1;
q[++ tt] = {x, y};
}
}
}
return d[n - 1][m - 1];
}
图
图分为有向图和无向图,有向图是指只能从一个点走向另一个点a->b
,而无向图是两个点互相可以到达a->b, b->a
,无向图可以看作成特殊的有向图,在建图的时候多建一条边即可。
有向图的存储一般分为邻接矩阵和邻接表。邻接矩阵的空间是$n^2$的,一般存储稠密图。邻接表其实就是单链表,每个点都是一个单链表,存储该点可以到达的其他点,一般存储稀疏图。
eg:邻接表建图
int h[N], e[M], ne[M], idx;
// h存的是n个链表的链表头,e存的是所有的节点的值,ne是每个节点的next指针是多少
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
拓扑序列
有向图求拓扑序是图的宽搜非常经典的应用。图的拓扑序列是针对有向图来说的,无向图是没有拓扑序列的。
拓扑序列:若一个由图中所有点构成的序列 A 满足:对于图中的每条边 (x,y),x 在 A 中都出现在 y 之前,则称 A 是该图的一个拓扑序列。
有环的图无法展开有序序列。有向无环图也被称为拓扑图。
有向图的每个点有两个概念:入度和出度。指向该点的边是入度,指向别的点的边称为出度。因此求拓扑序列可以把入度为0的点放在最前面。
int h[N], e[N], ne[N], idx;
int d[N], q[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
bool topsort()
{
int hh = 0, tt = -1;
for (int i = 1; i <= n; i ++ )
if (!d[i])
q[++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++];
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
d[j] -- ;
if (d[j] == 0) q[++ tt] = j;
}
}
return tt == n - 1;
}
最短路问题
一般最短路问题分为两类:单源最短路和多源汇最短路。
单源最短路指的是求一个点到其他所有点的最短距离。多源汇最短路指的是可能有多轮询问,每次询问的起点和终点都是不确定的,问这两个点之间的最短距离。
其中单源最短路也可以分为两大类:
- 所有边权都是正数
- 朴素Dijkstra算法$O(n^2)$,适合稠密图,因为时间复杂度和边没有关系,和点数有关
- 堆优化版的Dijkstra算法$O(mlogn)$,适合稀疏图,m和n为一个级别
- 存在负权边
- Bellman-Ford算法$O(mn)$
- SPFA算法$O(m)$,最坏为$O(mn)$
而多源汇最短路只有Floyd算法$O(n^3)$
朴素Dijkstra算法
步骤:
- 先初始化距离,只有起点的距离是确定的为0,其他的所有点的距离都是不确定的,设为正无穷
- 进行迭代,每次迭代找到不在集合s中的距离最近的点t,将t加入集合s,然后用t更新其他点的距离(集合s存的是当前已确定最短距离的点)
int g[N][N]; // 稠密图用邻接矩阵来存
int dist[N]; // 每个点到起点的最短距离
bool st[N]; // 用来表示当前点是否已经确定最短距离
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < n; i ++)
{
int t = -1; // 找到当前距离最小的点t
for (int j = 1; j <= n; j ++)
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j; // 当前t不是最短的距离,那么更新t
st[t] = true; // 将t加到集合里去
for (int j = 1; j <= n; j ++)
dist[j] = min(dist[j], dist[t] + g[t][j]); // 用t更新到其他点的距离
}
if (dist[n] == 0x3f3f3f3f) return -1; // 两点不连通
return dist[n];
}
堆优化版Dijkstra算法
朴素版寻找最小距离的点需要循环n次,而用数据结构堆来存储可以优化至O(1)
int h[N], w[N], e[N], ne[N]; // 邻接表,w数组是权重
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
int dijkstra()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1}); // Pair存储的是{距离,该点编号},因为要按照距离排序
while (heap.size())
{
auto t = heap.top();
heap.pop();
int ver = t.second, distance = t.first;
if (st[ver]) continue;
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])
{
int j = e[i];
if (dist[j] > distance + w[i])
{
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
Bellman-Ford算法
两层循环,首先迭代n次,每次循环所有边,然后更新该点距离。如果有父权回路,那么不存在最短距离。
// 求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。
int dist[N], backup[N];
struct Edge
{
int a, b, w;
} edges[M];
void bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i ++)
{
memcpy(backup, dist, sizeof dist); // 备份是为了防止串联
for (int j = 0; j < m; j ++)
{
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
dist[b] = min(dist[b], backup[a] + w);
}
}
if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
else printf("%d", dist[n]);
}
SPFA算法
只要没有负环,就可以用
int h[N], w[N], e[N], ne[N];
int dist[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++;
}
void spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[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 (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
if (dist[n] == 0x3f3f3f3f) puts("impossible");
else printf("%d ", dist[n]);
}
Floyd算法
用邻接矩阵存储图,三层循环更新点的最短距离
int d[N][N];
void floyd()
{
for (int k = 1; k <= n; k ++)
for (int i = 1; i <= n; i ++)
for (int j = 1; j <= n; j ++)
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
}
最小生成树
最小生成树的问题对应的都是无向图
Prim算法
找到集合外距离最近的点,用该点更新其他点到集合的距离
int g[N][N], dist[N];
bool st[N];
int prim()
{
memset(dist, 0x3f, sizeof dist);
int res = 0; // 最小生成树的所有边权重之和
for (int i = 0; i < n; i ++)
{
int t = -1;
for (int j = 1; j <= n; j ++) // 找到集合外距离最近的点
if (!st[j] && (t == -1 || dist[t] > dist[j]))
t = j;
if (i && dist[t] == INF) return INF; // 如果不是第一个点并且没有边连向集合
if (i) res += dist[t];
st[t] = true;
for (int j = 1; j <= n; j ++) dist[j] = min(dist[j], g[t][j]);
}
return res;
}
Kruskal算法
首先将所有边按权重从小大排序,枚举每条边,如果这两条边不连通,则加入到集合中
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 200010;
int n, m;
int p[N];
struct Edge
{
int a, b, w;
}edges[N];
int find(int x) // 并查集
{
if (p[x] != x) p[x] = find(p[x]);
return p[x];
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 0; i < m; i ++)
{
int a, b, w;
scanf("%d%d%d", &a, &b, &w);
edges[i] = {a, b, w};
}
sort(edges, edges + m, [=] (Edge x, Edge y) {return x.w < y.w;}); // 重载小于号
for (int i = 1; i <= n; i ++) p[i] = i;
int res = 0, cnt = 0;
for (int i = 0; i < m; i ++)
{
int a = edges[i].a, b = edges[i].b, w = edges[i].w;
a = find(a), b = find(b);
if (a != b)
{
p[a] = b;
res += w;
cnt ++;
}
}
if (cnt < n - 1) puts("impossible");
else printf("%d\n", res);
return 0;
}
二分图
二分图当且仅当图中不含奇数环(奇数环表示该环的边数量是不是奇数)
染色法判定二分图
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010, M = 200010;
int n, m;
int h[N], e[M], ne[M], idx;
int color[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
bool dfs(int u, int c)
{
color[u] = c;
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (!color[j])
{
if (!dfs(j, 3 - c)) return false;
}
else if (color[j] == c) return false;
}
return true;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m --)
{
int a, b;
scanf("%d%d", &a, &b);
add(a,b), add(b, a);
}
bool flag = true;
for (int i = 1; i <= n; i ++)
if (!color[i])
{
if (!dfs(i, 1))
{
flag = false;
break;
}
}
if (flag) puts("Yes");
else puts("No");
return 0;
}
匈牙利算法
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 510, M = 100010;
int n1, n2, m;
int h[N], e[M], ne[M], idx;
int match[N];
bool st[N];
void add(int a, int b)
{
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
int main()
{
scanf("%d%d%d", &n1, &n2, &m);
memset(h, -1, sizeof h);
while (m --)
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);
}
int res = 0;
for (int i = 1; i <= n1; i ++)
{
memset(st, false, sizeof st);
if (find(i)) res ++;
}
printf("%d", res);
return 0;
}