[算法笔记]图

一:图的遍历

1)深度优先遍历(DFS)

每次沿着路径到不能再前进的时候才退回到最近的岔道口
每一次DFS过程可以遍历一个连通分量,遍历所有联通分量即遍历图

邻接矩阵存储

const int maxv=10000;
const int INF=1000000000;
bool vis[maxv]={false};
int n,G[maxv][maxv];
void DFS(int u,int depth){
    vis[u]=true;
    for(int v=0;v<maxv;v++){
        if(vis[i]==false&&G[u][v]!=INF){
            DFS(v,depth+1);
        }
    }
}
void DFSTrave(){
    for(int u=0;u<maxv;u++){
        if(vis[u]==false){
            DFS(u,1);
        }
    }
}

邻接表存储

vector<int> Adj[maxv];
int n;
void DFS(int u,int depth){
    vis[u]=true;
    for(int i=0;i<Adj[u].size();i++){
        int v=Adj[u][i];
        if(vis[v]==false){
            DFS(v,depth+1);
        }
    }
}
void DFSTrave(){
    for(int u=0;u<maxv;u++){
        if(vis[u]==false){
            DFS(u,1);
        }
    }
}

2)广度优先遍历(BFS)

使用一个队列,通过反复取出队首顶点,将该顶点可到达的未曾加入过队列的顶点全部入队,直到队列为空时遍历结束
同DFS,遍历所有联通分量,即遍历完图

邻接矩阵存储

int n,G[maxv][maxv];
bool inq[maxv]={false};//标记有没有进过队列
void BFS(int u){
    queue<int> q;
    q.push(u);
    inq[u]=true;
    while(!q.empty()){
        int u=q.front();
        q.pop();
        for(int v=0;v<n;v++){
            if(inq[v]==false&&G[u][v]!=INF){
                q.push(v);
                inq[v]=true;
            }
        }
    }
}

邻接表存储

vector(int) Adj[maxv];
int n;
bool inq[maxv]={false};
void BFS(int u){
    queue<int> q;
    q.push(u);
    inq[u]=true;
    while(!q.empty()){
        int u=q.fornt();
        q.pop();
        for(int i=0;i<Adj[u].size();i++){
            int v=Adj[u][i];
            if(inq[v]!=false){
                q.push(v);
                inq[v]==ture;
            }
            
        }
    }
}

void BFSTrave(){
    for(int u=0;u<n;u++){
        if(inq[u]==false){
            BFS(u);
        }
    }
}

如果需要配合层号的传递关系,则在结构体中定义层号,在队列循环中处理层数

struct Node{
    int v;
    int layer;
}
vector<Node> Adj[maxv];
void BFS(int s){
    queue<Node> q;
    Node start;
    start.v=s;
    start.layer=0;
    q.push(start);
    inq[start.v]=true;
    while(!q.empty()){
        Node topNode=q.front();
        q.pop();
        int u=topNode.v;
        for(int i=0;i<Adj[u].size();i++){
            Node next=Adj[u][i];
            next.layer=topNode.layer+1;
            if(inq[next.v]==false){
                q.push(next);
                inq[next.v]=true;
            }
        }
    }
}

二:最短路径

1)Dijkstra算法:单源最短路径

给定图G(V,E)和起点s,求顶点s到其他顶点的最短距离**

起点到终点的最短距离是这类问题第一标尺,题目中一般存在第二标尺(如在同时存在距离相同的最短路径,选出点权最大的/边权最小的等等)

Dijkstra+DFS方法
基本思想:用临时数组保存下来所有的最短路径,对这些最短路径统一计算点权,边权等条件,即第二甚至第三标尺,得到最终结果
1.使用Dijkstra算法记录所有最短路径,每个节点会存在多个前驱节点,定义vector数组pre,保存每个节点的前驱节点
2.从最后一个节点开始倒序DFS遍历生成树(每个节点的前驱节点可能有多个,遍历的过程可能会形成一颗递归树),每次到达叶子节点就是一条最短路径,对这条路径计算需要的条件,如果得到更优的解则覆盖

基本代码的模板(非常重要)
在DFS处递归到跟节点根据题目要求计算第二标尺即可,其余地方都相同

#include<cstdio>
#include<string>
#include<vector>
#include<algorithm>
using namespace std;
const int MAXV = 510;
const int INF = 1000000000;
int n, m, st, ed, G[MAXV][MAXV], cost[MAXV][MAXV];
int d[MAXV], minCost = INF;
bool vis[MAXV] = { false };
vector<int> pre[MAXV];//前驱
vector<int> tempPath, path;//临时路径,最优路径
void Dijkstra(int s) {
	//s为起点的单源最短路径
	fill(d, d + MAXV, INF);
	d[s] = 0;//起点s到自身距离为0
	for (int i = 0; i < n; i++) {
		//每次循环寻找与s最近的 未访问过的节点
		//与s的距离会在下面的循环中更新
		//执行n次,找n个节点 
		//第一次选择的顶点是起点
		//如果中途找不见顶点,则图不连通
		int u = -1, MIN = INF;
		for (int j = 0; j < n; j++) {
			if (vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		if (u == -1)return;//剩下的顶点与起点不连通
		vis[u] = true;//将u置为已访问
		for (int v = 0; v < n; v++) {
			//如果v未访问 且u能到达v
			//更新最短路径,经过该点能到达所有点的前驱
			//更新所有通过u能到达的顶点v距离s的最短距离
			if (vis[v] == false && G[u][v] != INF) {
				//找见了以u为中介到达v的最短路径
				if (d[u] + G[u][v] < d[v]) {//如果经过u到达v比直接到达v更近
					d[v] = d[u] + G[u][v];
					pre[v].clear();
					pre[v].push_back(u);//清空前驱,u是v的前驱
				}
				//找到不止一条最短路径,则到达v不止一个前驱,将u加入到v的前驱
				else if (d[u] + G[u][v] == d[v])pre[v].push_back(u);
			}
		}
	}
}
void DFS(int v) {//寻找以v为起点的路径
	if (v == st) {//递归边界:v是路径起点st的时候
		tempPath.push_back(v);//最后一个起点需要手动加入,计算完权值之后再退出
		//计算第二标尺
		int value;
		计算路径上的value值
		if(value优于optValue){
			optValue=value;
			path=tempPath;
		}
		tempPath.pop_back();
		return;
	}
	//递归式
	tempPath.push_back(v);
	for (int i = 0; i < pre[v].size(); i++) {
		DFS(pre[v][i]);//倒序遍历该节点的所有递归节点
	}
	tempPath.pop_back();
}

第二标尺的计算

//边权
int value=0;
for(int i=tempPath.size()-1;i>0;i--){
    int id=tempPath[i],idNext=tempPath[i-1];
    value+=v[id][idNext];
}
//点权
int valur=0;
for(int i=tempPath()-1;i>=0;i--){
    int id=tempPath[i];
    value+=W[id];
}

相关例题 PAT-A 1003,A1018,A1030,A1072,A1087

2)Floyd算法:全源最短路径

给定图G(V,E),求任意两点uv之间的最短路径长度,时间复杂度O(n^3)。
O(n^3)的时间复杂度决定了顶点数n限制在200之内,因此更适合使用邻接矩阵来实现Floyd算法。

算法思想:如果存在一个顶点k,使得以k作为中介时顶点i和顶点j的当前最短距离缩短,则使用顶点k作为顶点i和顶点j的中介点,即当
dis[i][j]+dis[k][j]<dis[i][j]时,令dis[i][j]=dis[i][j]+dis[j][k]`

#include<cstdio>
#include<algorithm>
using namespace std;
const int inf = 0x3fffffff;
const int maxv = 200;
int n, m;
int dis[maxv][maxv];
void floyd(){
	for (int k = 0; k < n; k++) {
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				if (dis[i][k] != inf && dis[j][k] != inf && dis[i][k] + dis[k][j] < dis[i][j])
					dis[i][j] = dis[i][j] + dis[k][j];
			}
		}
	}
}
int main() {
	int u, v, w;
	fill(dis[0], dis[0] + maxv * maxv, inf);
	(void)scanf("%d %d", &n, &m);
	for (int i = 0; i < n; i++) {
		dis[i][i] = 0;
	}
	for (int i = 0; i < m; i++) {
		(void)scanf("%d %d %d", &u, &v, &w);
		dis[u][v] = w;//有向图
	}
	floyd();
	for (int i = 0; i < n; i++) {
		for (int j = 0; j < n; j++) {
			printf("%d ", dis[i][j]);
		}
		printf("\n");
	}
	return 0;
}

input:

6 8
0 1 1
0 3 4
0 4 4
1 3 2 
2 5 1 
3 2 2
3 4 3
4 5 3

output:

0 1 1073741823 4 4 1073741823
1073741823 0 1073741823 2 1073741823 1073741823
1073741823 1073741823 0 1073741823 1073741823 1
1073741823 1073741823 2 0 3 1073741823
1073741823 1073741823 1073741823 1073741823 0 3
1073741823 1073741823 1073741823 1073741823 1073741823 0

三.最小生成树

最小生成树:在一个给定的无向图中求一颗树T,使得这颗树拥有图G中所有的顶点,且所有边都是来自图G中的边,并且满足整棵树的边权之和最小。
最小生成树三个性质:
1)边数等于顶点数减1,且树内一定不会有环。
2)对给定的图G(V,E),其最小生成树可以不唯一,但其边权之和一定唯一。
3)由于最小生成树在无向图上生成,所以其根节点可以是图上任意一个根节点,如果题目本身设计最小生成树的输出,为了保证唯一,题目一般会给出根节点。
最小生成树算法本质上是贪心策略。

1)prim算法

算法思想:
对图G(V,E)设置集合S,存放已经被访问的节点,然后每次从集合V-S中选择与集合D的最短距离最小的一个顶点,(记为u),访问并加入S,再令u为中介点,优化所有从u能到达的顶点v与集合S之间的最短距离。以上操作执行n次,直到集合S包含所有顶点。

过程:
1)对于集合S,使用一个bool类型的数组vis[]表示顶点是否已经访问,其中vis[i]==true表示顶点Vi已经访问,false表示还没有访问。
2)令int型数组d[]来存放顶点Vi与集合s的最短距离,初始时除了起点s的d[s]赋值为0,其余赋值为inf。
Prim与Dijkstra算法思想几乎相同,只有在数组d[]的含义上有所区别,dijkstra中的d[]表示起点s到达顶点Vi的最短距离,prim中的d[]表示顶点Vi与集合S的最短距离。

伪代码:

Prim(G,d[]){
	初始化;
	for(循环n次){
		u=使d[u]最小的还未访问的顶点编号;
		记u已经被访问;
		for(从u能发到达的所有顶点v){//更新从u出发的所有顶点v到u的距离
			if(v未被访问&&以u为中介使得v与集合S的最短距离d[v]更优){
				将G[u][v]赋值给v与集合S的最短距离d[v];
			}
		}
	}
}

算法实现:(默认使用0号顶点作为起点)

//邻接矩阵
#include<cstdio>
#include<algorithm>
using namespace std;
const int inf = 0x3fffffff;
const int maxv = 1000;
int n, G[maxv][maxv];
int d[maxv];
bool vis[maxv] = { false };
int prim() {
	fill(d, d + maxv, inf);
	d[0] = 0;//0号顶点到集合S的距离为0,其余为inf
	int ans = 0;//最小生成树的边权之和
	for (int i = 0; i < n; i++) {//找见一个距离s最近的点u
		int u = -1, MIN = inf;
		for (int j = 0; j < n; i++) {
			if (vis[j] == false && d[j] < MIN) {
				u = j;
				MIN = d[j];
			}
		}
		if (u == -1)return -1;//没有找见说明不连通
		vis[u] = true;//将u加入集合
		ans += d[u];
		for (int v = 0; v < n; v++) {//更新从u出发所有能到达的顶点v距离集合s的距离
		//取v直接到达s的的距离d[v]和v通过u到达s的距离G[u][v]中的极小值
			if (vis[i] == false && G[u][v] != inf && G[u][v] < d[v]) {
				d[v] = G[u][v];//将G[u][v]赋值给d[v];
			}
		}
	}
	return ans;//返回边权之和
}

//邻接表
#include<cstdio>
#include<algorithm>
#include<vector>
using namespace std;
const int inf = 0x3fffffff;
const int maxv = 1000;
struct Node {
	int v, dis;//顶点编号,边权
};
vector<Node> adj[maxv];
int n, d[maxv];
bool vis[maxv] = { false };
int Prim() {
	fill(d, d + maxv, inf);
	d[0] = 0;
	int ans = 0;
	for (int i = 0; i < n; i++) {
		int u = -1, MIN = inf;
		for (int j = 0; j < n; j++) {
			if (vis[j] == false && d[j] < MIN) {
				MIN = d[j];
				u = j;
			}
		}
		if (u == -1)return;
		vis[u] == true;
		ans += d[u];
		for (int j = 0; j < adj[u].size(); j++) {//只有这里与邻接矩阵有区别
			int v = adj[u][j].v;
			if (vis[v] == false && adj[u][j].dis < d[v]) {
				d[v] = adj[u][j].dis;
			}
		}
	}
	return ans;	
}

2)Kruskal算法

算法思想:
采用边贪心策略
1.在初始状态是隐去所有边,图中每个定点自成一个联通块。
2.对所有边按权值大小进行排序。
3.按边权从小到大测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块,则把这条测试边加入当前的最小生成树中,否则舍弃。
4.执行步骤2,直到最小生成树中的总边数等于总顶点数减一,或是测试完所有的边时结束,而当结束时如果最小生成树的边数小于顶点数减一,则说明图不连通。

伪代码:

int Kruskal(){
	令最小生成树的边权之和为ans,最小生成树的当前边数为Num_Edge;
	将所有边都按边权从小到大排序;
	for(从小到大枚举所有边){
		if(当前测试便的两个端点在不同的联通块中){
			将该测试边加入最小生成树中;
			ans+=测试边的边权;
			最小生成树当前边数Num_Edge加一;
			当边数Num_Edge等于顶点数减一的时候结束循环;
		}
	}
	return ans;
}

算法实现:
判断测试边的两个端点是否在不同的联通块中和将测试边加入到最小生成树中可以使用并查集。

代码实现(邻接矩阵):

#include<cstdio>
#include<algorithm>
using namespace std;
const int maxv = 110;
const int maxe = 10010;
struct edge {//边的定义
	int u, v;
	int cost;
}E[maxv];
bool cmp(edge a, edge b) {
	return a.cost < b.cost;
}
int father[maxv];
int findFather(int x) {
	int a = x;
	while (x != father[x])x = father[x];
	while (a != father[a]) {//路径压缩
		int z = a;
		a = father[a];
		father[z] = x;
	}
	return x;
}
int kruskal(int n, int m) {
	int ans = 0, Num_Edge = 0;
	for (int i = 0; i < n; i++) {
		father[i] = i;//并查集初始化
	}
	sort(E, E + m, cmp);
	for (int i = 0; i < m; i++) {
		int faU = findFather(E[i].u);
		int faV = findFather(E[i].v);
		if (faU != faV) {//如果两个点不在一个联通块中
			father[faU] = faV;//合并成一个
			ans += E[i].cost;
			Num_Edge++;
			if (Num_Edge == n - 1)break;//边数等于顶点数减一时结束算法
		}
	}
	if (Num_Edge != n - 1)return -1;
	else return ans;
}
int main() {
	int n, m;
	(void)scanf("%d %d", &n, &m);
	for (int i = 0; i < m; i++) {
		(void)scanf("%d %d %d", &E[i].u, &E[i].v, &E[i].cost);
	}
	int ans = kruskal(n, m);
	printf("%d\n", ans);
	return 0;
}
//input
6 10
0 1 4
0 4 1
0 5 2
1 2 1
1 5 3
2 3 6
2 5 5
3 4 5
3 5 4
4 5 3
//output
11

四:拓扑排序

1:有向无环图

如果一个有向图的任意顶点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图(DAG)

2:拓扑排序

拓扑排序是将有向无环图的所有顶点排列成一个线性序列,使得对图中任意两个顶点uv,如果存在边u->v,那么u一定在v前面,这个序列被称为拓扑序列。

求解拓扑排序方法:
1)定义一个队列Q,并把所有入度为0的节点加入队列。
2)取队首节点,输出。然后删去所有从它的出发边,并令这些边到达的顶点的入度减一,直到某个顶点的入度减为0.将其加入队列。
3)反复进行操作2,直到队列为空。如果队列为空时入过队的节点数目恰好为N,说明拓扑排序成功,图G为有向无环图,否则拓扑排序失败,图G中有环。

可以使用邻接表实现拓扑排序,显然,由于需要记录节点的入度,因此需要额外建立一个数组inDegree[maxv],并在程序一开始读入图时就记录好每个节点的入度。

代码实现:

#include<cstdio>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
const int maxv = 1010;
vector<int> G[maxv];
int n, m, inDegree[maxv];
bool topologicalSort() {
	int num = 0;
	queue<int> q;
	for (int i = 0; i < n; i++) {
		if (inDegree[i] == 0) q.push(i);//将所有入度为0的点入队
	}
	while (!q.empty()) {
		int u = q.front();
		num++;//加入拓扑序列的顶点+1
		printf("%d", u);//可以输出拓扑排序的顶点
		q.pop();
		for (int i = 0; i < G[u].size(); i++) {
			int v = G[u][i];
			inDegree[v]--;
			if (inDegree[v] == 0)q.push(v);
		}
		G[u].clear();//删掉顶点u的所有出边
	}
	if (num == n)return true;
	else return false;
}

五:关键路径(动态规划)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值