图
- 判断两点是否连通、求连通分量的个数:并查集
- 判断图中是否存在环:拓扑排序
- 判断图中是否存在负环:Bellman-Ford
- 差分约束系统:Bellman-Ford
- 单源最短路径:Dijkstra(无负权边)、Bellman-Ford / SPFA(有负权边)
- 所有结点对最短路径:Floyd
- 传递闭包(所有点的连通性):Floyd
DFS
步骤
- 建立一个数组标记是否被访问
- 从数组的第一个元素开始,访问当前节点,并标记已访问
- 遍历当前节点的每一个邻点(未被标记访问的),并依次进行深度优先搜索
时间复杂度
- 邻接矩阵:查找每个顶点的邻接点所需时间为O(n²)
- 邻接表:查找邻接点的时间为O(e)(e为边数),最终时间复杂度为O(n+e)。
剪枝
- 可行性剪枝:剪去不可能出现的情况
给定n个整数,要求选出K个数,使得选出来的K个数的和为sum。求有多少组解。
void dfs(int i, int cnt, int s) {
if (cnt > k)
return;
if (s > sum)
return;
if (i == n) {
if (cnt == k && s == sum)
ans++;
return;
}
dfs(i + 1, cnt, s); //不选该数
dfs(i + 1, cnt + 1, s + a[i]); //选择该数
}
- 最优性剪枝:若当前情况已经劣于最优解,则不再对当前节点进行后续搜索
用一个二维的字符数组来表示前面画出的迷宫:
S**.
....
***T
其中字符S 表示起点,字符T表示终点,字符∗表示墙壁,字符.表示平地。你需要从S出发走到T,每次只能向上下左右相邻的位置移动,不能走出地图,也不能穿过墙壁,每个点只能通过一次。求从起点到终点的最短路径长度。
int n, m;
string maze[110]; //地图
bool vis[110][110]; //标记是否访问过
int dir[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}}; //方向矩阵
int ans = 100000000;
//判断该点是否在地图内
bool in(int x, int y) {
return 0 <= x && x < n && 0 <= y && y < m;
}
//dfs
void dfs(int x, int y, int step) {
if (step >= ans) {
return;
}
if (maze[x][y] == 'T') {
ans = step;
return;
}
vis[x][y] = 1;
//四个方向进行搜索
for (int i = 0; i < 4; ++i) {
int tx = x + dir[i][0];
int ty = y + dir[i][1];
if (in(tx, ty) && maze[tx][ty] != '*' && !vis[tx][ty]) {
dfs(tx, ty, step + 1);
}
}
vis[x][y] = 0;
}
- 重复性剪枝
给定n个整数,要求选出K个数,使得选出来的K个数的和为sum。求有多少组解。
考虑所有的情况,对于每一种情况进行dfs,找到符合要求的则ans++;考虑选取数字的顺序,可能选出重复的几个数字,如(2,3,4)、(3,2,4)这种,为避免这种重复性的搜索,我们则需要剪枝:传入一个参数pos,标记选取数字的顺序。
int a[40];
int n, k, sum, ans;
bool xuan[40];
//重复性剪枝
void dfs(int cnt, int s, int pos) {
//可行性剪枝
if (cnt > k || s > sum) {
return;
}
if (cnt == k && s == sum) {
ans++;
}
for (int i = pos; i < n; ++i) {
if (!xuan[i]) {
xuan[i] = 1;
dfs(cnt + 1, s + a[i], i + 1);
xuan[i] = 0;
}
}
}
- 奇偶性剪枝
有一个n * m大小的迷宫。其中字符’S’表示起点,字符’D’表示出口,字符’X’表示墙壁,字符’.'表示平地。你需要从’S’走到‘D’,每次只能向上下左右相邻的位置移动,并且不能走出地图,也不能走进墙壁。每次移动消耗1时间,走过路都会塌陷,因此不能走回头路或原地不动。现在已知出口的大门会在T时间打开,判断在0时间从起点出发能否逃离迷宫。(数据范围n,m <= 10, T <= 50)
![](http://www.pianshen.com/images/233/94b59cdb1713a5c3d31ac000c8294729.png)
将n * m的网格染成黑白色。发现:走奇数会改变颜色,走偶数步则颜色不变。如果起点和终点的颜色一样,而T是奇数的话,就不可能离开迷宫。同理,如果终点和起点颜色不一样,而T是偶数的话,那么也不可能逃离迷宫。遇到这两种情况时,就不用进行DFS了。
int sx, sy, ex, ey; //sx,sy表示起点坐标,ex,ey表示终点坐标
//奇偶性剪枝
if ((sx + sy + ex + ey + T) % 2 != 0) {
cout << "NO" << endl;
} else {
ok = false;
dfs(sx, sy, 0);
if (ok) {
cout << "YES" << endl;
} else {
cout << "NO" << endl;
}
}
BFS
步骤
- 建立一个队列
- 访问当前节点并标记,入队
- 循环,若队不为空,用一个索引存储队头元素,再将队头pop出去
- 找到这个索引的所有邻点,并依次访问、入队
拓扑排序
在一个有向无环图中,对所有的节点进行排序,要求没有一个节点指向它前面的节点。
时间复杂度为O(n+e)
- 统计所有节点的入度,对于入度为0的节点就可以分离出来,然后把这个节点指向的节点的入度减1。
- 重复步骤1,直到所有的节点都被分离出来。
- 如果最后不存在入度为0的节点,那就说明有环,不存在拓扑排序。
queue<int>q;
for(int i=0;i<n;i++) // n: 节点的总数
if(in[i]==0) q.push(i); // 将入度为0的点入队列
vector<int>ans; // ans 为拓扑序列
while(!q.empty()) {
int p=q.top();
q.pop(); // 选一个入度为0的点,出队列
ans.push_back(p);
for(int i=0;i<edge[p].size();i++) {
int y=edge[p][i];
in[y]--;
if(in[y]==0)
q.push(y);
}
}
if(ans.size()==n) {
for(int i=0;i<ans.size();i++)
printf( "%d ",ans[i] );
printf("\n");
}
else printf("No Answer!\n"); // ans 中的长度与n不相等,就说明无拓扑序列
若要求字典序,可使用priority_queue
强连通分量
在有向图G中,如果两点互相可达,则称这两个点强连通,如果G中任意两点互相可达,则称G是强连通图。
-
一个有向图是强连通的,当且仅当G中有一个回路,它至少包含每个节点一次。
-
非强连通有向图的极大强连通子图,称为强连通分量(SCC即Strongly Connected Componenet)。
最小生成树
Prim算法(以点为基础,选出最小权边)
时间复杂度为:邻接矩阵 - O(v);邻接表 - O(elog2v)
- 输入:一个加权连通图,其中顶点集合为V,边集合为E;
- 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
- 重复下列操作,直到Vnew = V:
- 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条具有相同权值的边,则可任意选取其一);
- 将v加入集合Vnew中,将<u, v>边加入集合Enew中;
![](https://i-blog.csdnimg.cn/blog_migrate/8a93eba4be23466eaf61bfedfcdbaec3.png)
Kruskal算法(从所有边中选出每次选出最小权边)
时间复杂度为O(|E|log|E|)
- 设一个有n个顶点的连通网络为G(V,E),最初先构造一个只有n个顶点,没有边的非连通图T={V,空},图中每个顶点自成一格连通分量。
- 在E中选择一条具有最小权植的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到T中;否则,即这条边的两个顶点落到同一连通分量上,则将此边舍去(此后永不选用这条边),重新选择一条权植最小的边。
- 如此重复下去,直到所有顶点在同一连通分量上为止。
单源最短路径
Bellman-Ford / SPFA 算法
解决一般情况下的单源最短路径问题,可以处理负权边并判断负环是否存在。
Bellman-Ford与SPFA算法的时间复杂度均为O(VE)。
- 初始化:将除起点s外所有顶点的距离数组置无穷大 d[v] = INF, d[s] = 0
- 迭代:遍历图中的每条边,对边的两个顶点分别进行一次松弛操作,直到没有点能被再次松弛
- 判断负环:如果迭代超过V-1次,则存在负环
bool bellman_ford(int s, int n) {
// 最多松弛n-1次
for (int i = 1; i <= n-1; i++) {
// 对于每一条边
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= n; k++) {
// 松弛
if (d[k] > d[j] + G[j][k]) {
d[k] = d[j] + G[j][k];
}
}
}
}
for (int j = 1; j <= n; j++) {
for (int k = 1; k <= n; k++) {
// 如果还能松弛,则说明存在负环
if (d[k] > d[j] + G[j][k]) {
return false;
}
}
}
return true;
}
bool SPFA(int s, int N) {
queue <int> q;
d[s] = 0;
int cnt = 0;
q.push(s);
q.push(cnt);
vis[s] = 1;
while (!q.empty()) {
int x = q.front(); q.pop();
cnt = q.front(); q.pop();
vis[x] = 0;
if (cnt > N) return false;
for (int i = 0; i < N; i++) {
if (d[i] > d[x] + G[x][i]) {
d[i] = d[x] + G[x][i];
if (!vis[i]) {
q.push(i);
q.push(cnt+1);
vis[i] = 1;
}
}
}
}
return true;
}
差分约束系统和最短路径
对于一组不等式:
全都是两个未知数的差小于等于某个常数(大于等于也可以,因为左右乘 −1 就可以化成小于等于的形式),这样的不等式组称作差分约束系统。
这个不等式组要么无解,要么就有无限组解。因为如果存在一组解 {x1,x2,…,xn}的话,那么对于任何一个常数 k,有 {x1+k,x2+k,…,xn+k}也肯定是一组解,因为任何两个数加上一个数以后,它们之间的关系(差)是不变的,这个差分约束系统中的所有不等式都不会被破坏。
差分约束系统的解法用到了单源最短路径问题中的三角形不等式。即对有向图中任意一条边 <u,v>都有:dis[v] ≤ dis[u]+len[u][v],其中 dis[u]和 dis[v] 是从源点分别到点 u 和点 v 的最短路径的长度,len[u][v] 是边 <u,v>的长度值。即:如果存在顶点 u 到顶点 v 的有向边,那么从源点到顶点 v 的最短路径长度 小于等于从源点到顶点 u 的最短路径长度加上边 <u,v> 的长度值。
构图,再使用Bellman-Ford算法求解,如下:
使用Bellman-Ford算法求出的一组最短路径权重即为差分约束系统的一组可行解。
Dijkstra算法
不能处理存在负权边。
时间复杂度为O(n2)。
- 声明一个数组distance来保存a到各个顶点的最短距离。初始化,到a的距离为0,到a的邻点的距离为边长,到其他点的距离为无穷大
- 声明一个集合(store)保存已经找到了最短路径的顶点的集合,初始化将a放入集合中
- 声明一个变量index保存当前位置
- 对所有点循环,遍历当前点的所有邻点,若这个点不在store中,则移动(改变index)到这个点,并将这个点加入集合中
int dis[n+1];
void Dijkstra(int v) {
bool vis[n+1] = {0};
vis[v] = 1;
int index = v;
// 主循环,每次求得v到某顶点i的最短路径,并将i添加到集合中
for (int i = 1; i <= n; ++i) {
if (i == index) continue;
int min = INF;
// 找到v到i的最短边(直接相连)
for (int j = 1; j <= n; ++j) {
if (!vis[j]) { // j不在集合中
if (dis[j] < min) {
index = j;
min = dis[j];
}
}
}
vis[index] = 1;
for (int j = 1; j <= n; ++j) { // 松弛更新最短路径和距离
if (!vis[j] && min + adj[index][j] < dis[j]) {
dis[j] = min + adj[index][j];
}
}
}
}
使用堆优化,时间复杂度可以降到O**((m+n)logn)**
struct node{
int p, w;
node(int a, int b):p(a), w(b){}
bool operator< (const node& b) const
{
return w > b.w;
}
};
vector<node> g[N];
priority_queue<node> sup;
void dijkstra(int start) {
memset(dis, 0x3f, sizeof(dis));
dis[start] = 0;
pre[start] = start;
sup.push(node(start, 0));
while (!sup.empty()) {
node front = sup.top();
sup.pop(); int tempv = front.p;
if (visit[tempv]) continue;
visit[tempv] = true;
for (int i = 0; i < g[tempv].size(); i++) {
int p = g[tempv][i].p;
if (!visit[p] && dis[tempv]+g[tempv][i].w < dis[p]) {
dis[p] = dis[tempv]+g[tempv][i].w;
pre[p] = tempv;
sup.push(node(p, dis[p]));
}
}
}
}
所有结点对的最短路径问题
Floyd算法
时间复杂度为:O(n3);空间复杂度:O(n2)
可处理负权边,但不能处理负环。
int map[maxN][maxN] = adj;
int path[maxN][maxN]; // 保存中转点
void floyd() {
memset(path, -1, sizeof(path));
for(int k = 1; k <= n; k++) {
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
if(map[i][k] + map[k][j] < map[i][j]) {
map[i][j] = map[i][k] + map[k][j];
path[i][j] = k; // 记录路径
}
}
}
}
}
有向图的传递闭包(判断连通性)
定义:对于n个顶点有向图来说,如果第i个顶点到第j个顶点之间存在一条有效的有向路径,那么T(i, j) = 1,否则T(i, j) = 0。例如:
// 传递闭包问题
int ans[maxN][maxN];
void floyd() {
// 初始化
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (i == j or adj[i][j] != 0) {
ans[i][j] = 1;
} else {
ans[i][j] = 0;
}
}
}
// Floyd算法,遍历所有点
for(int k = 1; k <= n; k++) {
for(int i = 1; i <= n; i++) {
for(int j = 1; j <= n; j++) {
if (ans[i][j] != 0) {
ans[i][j] = 1;
} else if (ans[i][k]!=0 && ans[k][j]!=0) {
ans[i][j] = 1;
}
}
}
}
}