【算法】06 图

  1. 判断两点是否连通、求连通分量的个数:并查集
  2. 判断图中是否存在环:拓扑排序
  3. 判断图中是否存在负环:Bellman-Ford
  4. 差分约束系统:Bellman-Ford
  5. 单源最短路径:Dijkstra(无负权边)、Bellman-Ford / SPFA(有负权边)
  6. 所有结点对最短路径:Floyd
  7. 传递闭包(所有点的连通性):Floyd

DFS

步骤
  1. 建立一个数组标记是否被访问
  2. 从数组的第一个元素开始,访问当前节点,并标记已访问
  3. 遍历当前节点的每一个邻点(未被标记访问的),并依次进行深度优先搜索
时间复杂度
  1. 邻接矩阵:查找每个顶点的邻接点所需时间为O(n²)
  2. 邻接表:查找邻接点的时间为O(e)(e为边数),最终时间复杂度为O(n+e)
剪枝
  1. 可行性剪枝:剪去不可能出现的情况

给定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]); //选择该数
}
  1. 最优性剪枝:若当前情况已经劣于最优解,则不再对当前节点进行后续搜索

用一个二维的字符数组来表示前面画出的迷宫:
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;
}
  1. 重复性剪枝

给定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;
		}
	}
}
  1. 奇偶性剪枝

有一个n * m大小的迷宫。其中字符’S’表示起点,字符’D’表示出口,字符’X’表示墙壁,字符’.'表示平地。你需要从’S’走到‘D’,每次只能向上下左右相邻的位置移动,并且不能走出地图,也不能走进墙壁。每次移动消耗1时间,走过路都会塌陷,因此不能走回头路或原地不动。现在已知出口的大门会在T时间打开,判断在0时间从起点出发能否逃离迷宫。(数据范围n,m <= 10, T <= 50)

将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

步骤

  1. 建立一个队列
  2. 访问当前节点并标记,入队
  3. 循环,若队不为空,用一个索引存储队头元素,再将队头pop出去
  4. 找到这个索引的所有邻点,并依次访问、入队

拓扑排序

在一个有向无环图中,对所有的节点进行排序,要求没有一个节点指向它前面的节点。

时间复杂度为O(n+e)

  1. 统计所有节点的入度,对于入度为0的节点就可以分离出来,然后把这个节点指向的节点的入度减1
  2. 重复步骤1,直到所有的节点都被分离出来。
  3. 如果最后不存在入度为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是强连通图。

  1. 一个有向图是强连通的,当且仅当G中有一个回路,它至少包含每个节点一次。

  2. 非强连通有向图的极大强连通子图,称为强连通分量(SCC即Strongly Connected Componenet)。

最小生成树

Prim算法(以点为基础,选出最小权边)

时间复杂度为:邻接矩阵 - O(v);邻接表 - O(elog2v)

  1. 输入:一个加权连通图,其中顶点集合为V,边集合为E;
  2. 初始化:Vnew = {x},其中x为集合V中的任一节点(起始点),Enew = {},为空;
  3. 重复下列操作,直到Vnew = V:
    1. 在集合E中选取权值最小的边<u, v>,其中u为集合Vnew中的元素,而v不在Vnew集合当中,并且v∈V(如果存在有多条具有相同权值的边,则可任意选取其一);
    2. 将v加入集合Vnew中,将<u, v>边加入集合Enew中;
Kruskal算法(从所有边中选出每次选出最小权边)

时间复杂度为O(|E|log|E|)

  1. 设一个有n个顶点的连通网络为G(V,E),最初先构造一个只有n个顶点,没有边的非连通图T={V,空},图中每个顶点自成一格连通分量。
  2. 在E中选择一条具有最小权植的边时,若该边的两个顶点落在不同的连通分量上,则将此边加入到T中;否则,即这条边的两个顶点落到同一连通分量上,则将此边舍去(此后永不选用这条边),重新选择一条权植最小的边。
  3. 如此重复下去,直到所有顶点在同一连通分量上为止。

单源最短路径

Bellman-Ford / SPFA 算法

解决一般情况下的单源最短路径问题,可以处理负权边并判断负环是否存在
Bellman-Ford与SPFA算法的时间复杂度均为O(VE)

  1. 初始化:将除起点s外所有顶点的距离数组置无穷大 d[v] = INF, d[s] = 0
  2. 迭代:遍历图中的每条边,对边的两个顶点分别进行一次松弛操作,直到没有点能被再次松弛
  3. 判断负环:如果迭代超过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)

  1. 声明一个数组distance来保存a到各个顶点的最短距离。初始化,到a的距离为0,到a的邻点的距离为边长,到其他点的距离为无穷大
  2. 声明一个集合(store)保存已经找到了最短路径的顶点的集合,初始化将a放入集合中
  3. 声明一个变量index保存当前位置
  4. 对所有点循环,遍历当前点的所有邻点,若这个点不在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;
                }
            }
        }
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值