資料結構-圖程式設計知識詳細總結C++(圖建立、圖遍歷、最短路徑、拓撲排序、關鍵路徑)
阿新 • • 發佈:2020-07-17
一、圖的儲存
- 鄰接矩陣,適用於節點數不超過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;
}