zoukankan      html  css  js  c++  java
  • 数据结构-图编程知识详细总结C++(图创建、图遍历、最短路径、拓扑排序、关键路径)

    一、图的存储

    • 邻接矩阵,适用于节点数不超过1000个的情况:
    const int MAXV = 1000;
    int n;
    const int INF = 1000000000;
    int G[MAXV][MAXV];
    bool vis[MAXV] = {false};
    
    • 邻接表
    const int MAXV;
    int n;
    struct node{
        int id, v;
    }
    vector<int> Adj[MAXV];
    vector<node> Adj[MAXV];
    bool vis[MAXV] = {false};
    

    二、基本遍历(DFS、BFS)

    1、DFS

    • 邻接矩阵版:主要是G邻接矩阵的初始化,使用fill函数, fill(G[0], G[0] + MAXV * MAXV, INF);
    • 以及在判断的时候加上如果两点间距离为INF,表示不连通
    fill(G[0], G[0] + MAXV*MAXV, INF);
    void DFS(int u){
        vis[u] = true;
        for(int i = 0; i < n; i++){
            if(vis[i] == false && G[u][i] != INF){
                DFS(i);
            }
        }
    }
    void DFSTrave(){
        for(int u = 0; u < n; u++){
            if(vis[u] == false){
                DFS(u);
            }
        }
    }
    
    • 邻接表版:如果是使用DFS的邻接表,一般是尽量不含有其他参数的,不然尽量使用BFS。
    vector<int> Adj[MAXV];
    void DFS(int u){
        vis[u] = true;
        for(int v = 0; v < Adj[u].size(); v++){
            if(vis[Adj[u][v]] == false){
                DFS(Adj[u][v]);
            }
        }
    }
    void DFSTrave(){
        for(int v = 0; v < n; v++){
            if(vis[v] == false){
                DFS(v);
            }
        }
    }
    

    2、BFS

    • 邻接表版
    struct node{
        int id, v;
    }
    bool vis[MAXV] = {false};
    vector<node> Adj[MAXV];
    void BFS(int u){
        queue<int> q;
        q.push(u);
        vis[u] = true;
        while(!q.empty()){
            int now = q.front();
            //进行操作
            for(int v = 0; v < Adj[now].size(); v++){
                int id = Adj[u][v].id;
                if(vis[id] == false){
                    q.push(id);
                    vis[id] = true;
                }
            }
        }
    }
    

    一些需要注意的点

    • 如果是那种深度有限的遍历,统计结果最好使用BFS,因为DFS可能出现结果重复,以及缺点的情况。
    • 还有就是统计连通块的数量,也就是需要添加的路径使得整个区域连接起来,就是连通块的数量减一;

    三、最短路径

    1. Dijkstra算法(迪杰斯特拉算法)

    • 用于解决单源最短路径问题(无负权图)。
    • 邻接矩阵版本
    const int MAXN;
    const int INF = 1000000000;
    int G[MAXN][MAXN];
    int d[MAXN];
    bool vis[MAXN] = {false};
    int n;
    void Dijkstra(int s){
        fill(d, d + MAXN, INF);
        d[s] = 0;
        for(int i = 0; i < n; i++){
            int u = -1, MIN = INF;
            for(int j = 0; j < n; j++){
                if(d[j] < MIN && vis[j] == false){
                    u = j;
                    MIN = d[j];
                }
            }
            if(u == -1) return;
            vis[u] = true;
            for(int j = 0; j < n; j++){
                if(vis[j] == false && G[u][j] != INF && d[u] + G[u][j] < d[j]){
                    d[j] = d[u] + G[u][j];
                }
            }
        }
    }
    
    • 邻接表版本
    struct node{
        int v, dis;
    };
    vector<node> G[MAXV];
    int n;
    int d[MAXV];
    bool vis[MAXV] = {false};
    
    void Dijkstra(int s){
        fill(d, d + MAXV, INF);
        d[s] = 0;
        for(int i = 0; i < n; i++){
            int u = -1, MIN = INF;
            for(int j = 0; j < n; j++){
                if(d[j] < MIN && vis[j] == false){
                    u = j;
                    MIN = d[j];
                }
            }
            if(u == -1) return;
            vis[u] = true;
            for(int i = 0; i < G[u].size(); i++){
                int v = G[u][i].v;
                if(vis[v] == false && d[u] + G[u][i].dis < d[v]){
                    d[v] = d[u] + G[u][i].dis;
                }
            }
        }
    }
    
    2) 对于一些情况的改进,如加上点权和边权
    • 加新的边权的权值
    • 城市点的花费,新增判断,首先是以第一标准为判断条件,如果出现第一判断条件相等,那么再使用第二条件进行判断;记得初始化起点的权值为当前的权值
    • 最短路径的条数:使用一个path数组进行存储,如果出现路径小于,就用上面的路径覆盖掉当前路径,如果出现最短路径相等的情况,用那就加上当前的路径。要记得初始化起点的路径为1
    • 初始化起点路径为0
    • 逻辑没问题,一般是判断的时候 == 写成了 =
    3) Dijkstra+DFS
    • 对于Dijkstra部分的任务是统计出路径最短的条数即可;
    • 注意判断的时候双等号;
    • 以及添加的pre数组,存储前驱结点;
    • G在初始化的时候是fill(G[0], G[0]+maxn*maxn, INF);
    void Dijkstra(int s){
    	fill(d, d+maxn, INF);
    	d[s] = 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){
    				u = j;
    				MIN = d[j];
    			}
    		}
    		if(u == -1) return;
    		vis[u] = true;
    		for(int v = 0; v < n; v++){
    			if(vis[v] == false && G[u][v] != INF){
    				if(d[u] + G[u][v] < d[v]){
    					d[v] = d[u] + G[u][v];
    					pre[v].clear();
    					pre[v].push_back(u);
    				}else if(d[u] + G[u][v] == d[v]){
    					pre[v].push_back(u);
    				}
    			}
    		}
    	}
    }
    
    • 对于DFS部分,是用Dijkstra统计出的路径pre统计第二判定条件下选择最优路径;
    • 递归边界是,遍历到结点等于起点st;
    • 递归条件是对于当前结点接下来可以到的结点;
    • 还有就是回溯,一个是起点要自己临时添加进入tempPath,在返回前还要弹出;遍历完当前节点可以到的结点后也要弹出结点;
    void DFS(int v){
    	if(v == s){
    		tempPath.push_back(v);
    		int value = 0;
    		for(int i = tempPath.size() - 1; i > 0; i--){
    			int id = tempPath[i], next = tempPath[i - 1];
    			value += Cost[id][next];
    		}
    		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();
    }
    

    2. Bellman-Ford算法和SPFA算法

    • 用于解决单源最短路径的问题,用于可能出现负环的情况,也就是边中存在负的权值;
    • 主要思想也就是判断n-1循环,每次判断其中每一条边,最后一次判断如果,还是会有出现距离变短,说明会出现负环的情况;
    • 使用邻接表更加方便
    • 跟Dijkstra区别是统计最短路径的条数;如果出现更短的路径还是一样直接覆盖;如果出现一样的路径,那么直接需要创建set进行存储,记住前驱是使用set进行存储的
    set<int> pre[maxn];
    if(d[u] + dis < d[v]){
        d[v] = d[u] + dis;
        w[v] = w[u] + weight[u];
        num[v] = num[u];
        pre[v].clear();
        pre[v].insert(u);
    }else if(d[u] + dis == d[v]){
        if(w[u] + weight[v] > w[v]){
            w[v] = w[u] + weight[v];
        }
        pre[v].insert(u);
        num[v] = 0;
        set<int>::iterator it;
        for(auto it = pre[v].begin(); it != pre[v].end(); it++){
            num[v] += num[*it];
        }
    }
    
    struct node{
        int v, dis;
    };
    vector<node> G[maxn];
    int n;
    int d[maxn];
    bool Bellman(int s){
        fill(d, d+maxn, INF);
        d[s] = 0;
        for(int i = 0; i < n - 1; i++){
            for(int u = 0; u < n; u++){
                for(int j = 0; j < G[u].size(); j++){
                    int v = G[u][j].v;
                    int dis = G[u][j].dis;
                    if(d[u] + dis < d[v]){
                        d[v] = d[u] + dis;
                    }
                }
            }
        }
        
        for(int u = 0; u < n; u++){
            for(int j = 0; j < G[u].size(); j++){
                int v = G[u][j].v;
                int dis = G[u][j].dis;
                if(d[u] + dis < d[v]){
                    return false;
                }
            }
        }
        return true;
    }
    

    3. Floyd算法(弗洛伊德算法)

    • 用于解决全源最短路径问题
    • 使用邻接矩阵解决问题, 结点限制在200个以内;
    • 枚举顶点k数,然后以该结点作为中介点,能否使得i, j顶点之间的距离变短;
    int dis[maxn][maxn];
    int n;
    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[k][j] != INF && dis[i][k] + dis[k][j] < dis[i][j]){
                        dis[i][j] = dis[i][k] + dis[k][j];
                    }
                }
            }
        }
    }
    

    四、最小生成树

    • 给定一个无向图G,然后求其中一棵生成树T,使得这棵树拥有图的所有结点;
    • 且所有边来自图中的边,使得整棵树的边权之和最小;

    1. prim算法(普里姆算法)

    • 用于稠密图,边多
    • 跟Dijkstra只有一点不同,即距离数组d的含义,在这里含义是到集合S的距离,即已经生成的树,而在Dijkstra是两结点的距离;
    • 同时多一个变量ans,用于存储最小生成树的所有边权之和
    const int INF = 100000000;
    const int maxn;
    int n, G[maxn][maxn];
    int d[maxn];
    bool vis[maxn] = {false};
    //以s为根结点生成的最小生成树
    int prim(int s){
        fill(d, d+maxn, INF);
        d[s] = 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){
                    u = j;
                    MIN = dis[j];
                }
            }
        }
        if(u == -1) return -1;
        ans += d[u];
        for(int v = 0; v < n; v++){
            if(vis[v] == false && G[u][v] != INF && G[u][v] < d[v]){
                d[v] = G[u][v];
            }
        }
        return ans;
    }
    

    2. kruskal(克鲁斯卡尔算法)

    • 用于稀疏图,边少
    • 算法思想是需要判断两个端点是否在不同的连通块中,因此两个端点的编号一定是需要的,边权也是需要的;
    • 所有边按从小到大进行排序;
    • 一个是判断两个顶点是否在不同的连通块中;
    • 如何将测试边加入连通块;
    struct edge{
        int u, v;
        int cost;
    }E[maxn];
    
    bool cmp(edge a, edge b){
        return a.cost < b.cost;
    }
    
    int father[N];
    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;
    }
    
    //n是顶点个数,m为图的边数
    int kruskal(int n, int m){
        int ans = 0, num_edge = 0;
        for(int i = 1; i <= n; i++){//假设顶点范围位1-n
            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;
    }
    

    五、拓扑排序

    • 明确两个东西,一个是有向无环图和拓扑排序之间的关系;对于一个有向无环图,可以生成拓扑序列;同时,也可以根据所给序列判定其是否为该有向无环图的拓扑序列;核心是每个结点的入度
    • 在一个就是怎么样判定有向无环图和拓扑排序;
    • 首先是必不可少存储图的邻接表和存储每个结点的入度的数组
    • 明确一点当访问当该点时,一定这时入度已经是0,否则该序列不是拓扑序列;
    • 在判定图是否为有向无环图时,首先将入度为0的结点加入队列中,然后遍历访问到该结点时,将该结点能够到达的左右结点的入度减一,如果出现入度为0,则加入队列中,然后统计结点变为0的数量num,如果等于图的结点数量,说明为有向无环图;
    • 如果有向无环图,想要从编号从小开始生成拓扑序列,使用priority_queue, 优先队列是默认从大到小排序,访问队首元素使用top()函数
    • 其优先级设置刚好跟sort相反;
    //表示从小到大
    priority_queue<int, vector<int>, greater<int>> q;
    
    //从大到小
    priority_queue<int, vector<int>, less<int>> q;
    
    结构体
    struct cmp{
        bool operator() (fruit f1, fruit f2){
            return f1.price > f2.price;
        }
    }
    //表示从小到大
    priority_queue<fruit, vector<fruit>, cmp> q;
    
    vector<int> G[maxn];
    int indegree[maxn];
    vector<int> tin(indegree, indegree+n+1);//将入度直接赋值到tin向量中,用于判定序列是否为拓扑序列
    
    bool toplogicalSort(){
        queue<int> q;
        for(int i = 0; i < n; i++){
            if(indegree[i] == 0){
                q.push(i);
            }
        }
        int num = 0;
        while(!q.empty()){
            int top = q.front();
            //printf("%d", top);
            q.pop();
            for(int j = 0; j < G[top].size(); j++){
                indegree[G[top][j]]--;
                if(indegree[G[top][j]] == 0){
                    q.push(G[top][j]);
                }
            }
            G[top].clear();
            num++;
        }
        if(num == n) return true;
        else return false;
    }
    

    六、关键路径

    • 明确两个点,结点代表事件,他有最早开始时间ve和最迟开始时间vl;而边代表活动,它也有最早开始时间e和最迟开始时间l
    • 怎么求事件的最早和最迟开始时间:最早开始时间直接等于前驱事件最早开始时间ve加上其到达的活动持续时间的最大值;最晚开始时间,根据栈,进行逆序拓扑序列,然后等于后驱事件最迟开始时间vl和其活动持续时间的最小值;
    const int maxn = 105;
    struct node{
    	int v, w;
    }; 
    vector<node> G[maxn];
    int indegree[maxn] = {0};
    stack<int> topOrder;
    int ve[maxn], vl[maxn];
    int n , m;
    bool topSort(){
    	queue<int> q;
    	for(int i = 1; i <= n; i++){
    		if(indegree[i] == 0){
    			q.push(i);
    		} 
    	}
    	while(!q.empty()){
    		int now = q.front();
    		topOrder.push(now);
    		q.pop();
    		for(int i = 0; i < G[now].size(); i++){
    			int v = G[now][i].v, w = G[now][i].w;
    			indegree[v]--;
    			if(indegree[v] == 0){
    				q.push(v);
    			}
    			if(ve[now] + w > ve[v]){
    				ve[v] = ve[now] + w;
    			}
    		}
    	}
    	if(topOrder.size() == n) return true;
    	else return false;
    }
    
    • 他们之间的关系,活动的最早开始时间和前驱事件的最早开始时间ve是一样的,而最迟开始时间为后驱事件的最迟开始时间vl减去活动的持续时间;
    void cPath(){
    	fill(ve, ve+maxn, 0);
    	if(topSort() == false){
    		printf("0
    ");
    		return;
    	}
    	int ans = -1;
    	for(int i = 1; i <= n; i++){
    		if(ve[i] > ans) ans = ve[i];
    	}
    	printf("%d
    ", ans);
    	fill(vl, vl+maxn, ans);
    	while(!topOrder.empty()){
    		int u = topOrder.top();
    		topOrder.pop();
    		for(int i = 0; i < G[u].size(); i++){
    			int v = G[u][i].v, w = G[u][i].w;
    			if(vl[v] -  w < vl[u]){
    				vl[u] = vl[v] - w;
    			}
    		}
    	}
    	for(int u = 1; u <= n; u++){
    		for(int i = G[u].size()-1; i >= 0 ; i--){
    			int v = G[u][i].v, w = G[u][i].w;
    			int e = ve[u], l = vl[v] - w;
    			if(e == l){
    				printf("%d->%d
    ", u, v);
    			}
    		}
    	}
    	return;
    }
    int main(){
    	cin >> n >> m;
    	int a, b, w;
    	for(int i = 0; i < m; i++){
    		scanf("%d%d%d", &a, &b, &w);
    		indegree[b]++;
    		G[a].push_back(node{b, w});
    	}
    	cPath();
    	return 0;
    }
    
  • 相关阅读:
    【原】CSS实现背景透明,文字不透明,兼容所有浏览器
    【原】我是超级收银员,你敢来挑战吗
    【原】iphone6来了,我该做点什么(兼容iphone6的方法)
    【原】移动web页面兼容处理的思考
    【原】移动web动画设计的一点心得——css3实现跑步
    【原】移动web页面使用字体的思考
    【原】HTML5 新增的结构元素——能用并不代表对了
    更为简单的Ctrl+S自动刷新浏览器工具-LinrF5
    博客3周年
    【原】移动web页面支持弹性滚动的3个方案
  • 原文地址:https://www.cnblogs.com/tsruixi/p/13328098.html
Copyright © 2011-2022 走看看