1. 程式人生 > 實用技巧 >資料結構-圖程式設計知識詳細總結C++(圖建立、圖遍歷、最短路徑、拓撲排序、關鍵路徑)

資料結構-圖程式設計知識詳細總結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++){
        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 v = 0; v < G[u].size(); v++){
            int v = G[u][v].v;
            if(vis[v] == false && d[u] + G[u][v] < d[v]){
                d[v] = d[u] + G[u][v];
            }
        }
    }
}
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][j] != 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(indegree[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\n");
		return;
	}
	int ans = -1;
	for(int i = 1; i <= n; i++){
		if(ve[i] > ans) ans = ve[i];
	}
	printf("%d\n", 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\n", 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;
}