《算法笔记》图

图的存储方式(邻接矩阵和邻接表)
邻接矩阵:邻接矩阵是一个方阵,行数 = 列数 = 节点元素个数;可以较方便的存储节点间的权值,
但是耗费空间,1000 * 1000 以上就不要用了;
邻接表:每一个元素都有一个数组,数组存与他相连的节点id,如果存权值的话,需要放结构体,可以用 vector 实现;
图的遍历(BFS和DFS)
图的遍历实际上是多次BFS或DFS的过程
#include <iostream>
#include<vector>
#include<string>
#include<algorithm>
#include<map>
using namespace std;
const int maxn = 2010;
const int INF = 1000000000;

map<int, string> intToString;//编号到姓名
map<string, int>stringToInt;//姓名到编号
map<string, int>Gang; //头目和人数

int G[maxn][maxn] = {0}, weight[maxn] = {0};//邻接矩阵,每个人的权重
int n, k, numPeople = 0;//n是边的个数, k是下限,numPeople是总人数
bool vis[maxn] = {false};//标记是否被访问

//遍历每一个点,能进入DFS的点必然是没有被访问过的
void DFS(int id, int& head, int& sumNum, int& sumWeight){
     vis[id] = true;//标记为已读
     sumNum ++;
     //如果当前节点的权值大于当前head权值,则改变head
     if(weight[id] > weight[head]){
          head = id;
     }
     for(int i = 0; i < numPeople; i ++){
          if(G[id][i] > 0){
               sumWeight += G[id][i];//总权重加和
               G[id][i] = G[i][id] = 0;//由于权重只能加一次,加一次后要设为0,以防有环导致重复加权
               //如果未被访问
               if(vis[i] == false){
                    DFS(i, head, sumNum, sumWeight);
               }

          }
     }
}
//遍历整张图的连通块
void DFSTrave(){
     for(int i = 0; i < numPeople; i ++){
          if(vis[i] == false){
               //sumNum代表这个块有多少人,sumWeight代表这个块的权值,head代表这个块的头头
               int sumNum = 0, sumWeight = 0, head = i;
               DFS(i, head, sumNum, sumWeight);
               //如果这个块的人数大于2,并且总权重大于阈值,插入结果集
               if(sumNum > 2 && sumWeight > k){
                    string name = intToString[head];
                    Gang.insert(make_pair(name, sumNum));
               }
          }

     }
}
int change(string str){
     if(stringToInt.find(str) != stringToInt.end()){
          return stringToInt[str];//返回编号
     }
     else{
          stringToInt[str] = numPeople;//str对应编号
          intToString[numPeople] = str;//编号对应str
          return numPeople++;//总人数++
     }
}
int main(){
    cin >> n >> k;
    for(int i = 0; i < n; i++){
          string str1, str2;
          int w;
          cin >> str1 >> str2 >> w;
          int id1 = change(str1);
          int id2 = change(str2);
          weight[id1] += w;
          weight[id2] += w;
          G[id1][id2] += w;
          G[id2][id1] += w;
    }
    DFSTrave();//遍历整张图
     cout << Gang.size() << endl;
     for(auto it = Gang.begin(); it != Gang.end(); it++){
          cout << it->first << " " << it->second <<endl;
     }
    system("pause");
    return 0;
}

对于上面那个题,也可以用BFS来做,因为这个队伍的人数和头目都是唯一全局的,所以可以引用传值~~~~
在用BFS,DFS遍历图时有几个问题需要注意:
1、每次传入的参数应该是不包括将要判断这个端点的结果,也就是一个开区间,比如,各个节点的权值一定是要处理这个节点时才会加上,而不会先加上;而这个节点的边权值实际上在处理这个节点前已经加上了;
2、图的遍历如果有环的存在,会有一个重复加边权值的问题;为了解决这个问题,可以在每次加上一个边权值后,就将这个边删去,这样就不会有重复的问题了;
3、做BFS和DFS时,要搞清楚是每一次遍历都会产生一个结果,最后比较最优解还是全局只有一个结果,如果是前者的话需要每次遍历时传参,如果是后者的话那么只需要在全局维护一个变量就行;比如这道题每个队的人数,就是一个全局的,BFS写不需要每次传引用,但是DFS由于自身调用自己的原因,还是需要传值的~~~~
4、一个节点只要能够进入DFS或BFS那么他一定是未被访问过的,所以每个点都要预先判断一下是否被遍历过;

//BFS
void BFS(int id, int& head, int& sumNum, int& sumWeight){
     queue<int> Q;
     Q.push(id);
     vis[id] = true;
     while(!Q.empty()){
          int idx = Q.front();
          Q.pop();
          sumNum++;
          if(weight[id] > weight[head]) head = id;
          for(int i = 0; i < numPeople; i ++){
               if(G[idx][i] > 0){
                    sumWeight += G[idx][i];
                    G[idx][i] = G[i][idx] = 0;
                    if(vis[i] == false){
                         Q.push(i);
                         vis[i] = true;//只要入过队,就表明已经遍历过了,不能放到外面
                    }
               }
          }
     }
}
Dijkstra算法(单源最短距离)
理论

迪特斯特拉算法用于求图中从一个点出发,到达图中其余点的最短距离,思路是:
1、对每个节点依次进行处理一遍,所以肯定要循环n次,
用一个vis存储节点是否被遍历过,d[]代表从起始点出发到该节点的最短距离,初始为INF,代表不可达;
2、在每次循环分两步进行
(1)找到未遍历节点中距离出发点距离最短的点,如果找到进行步骤2;如果未找到说明出发点与剩余节点不相连,退出即可;
(2)在找到中间节点后,由于这个中间结点的存在,从起始点到剩余节点的最短距离可能会发生改变,所以需要更新;

邻接矩阵版
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<map>
#include<queue>
using namespace std;
const int maxn = 1000;//最大的节点数
const int INF = 1e9;//表示不可达

int G[maxn][maxn], n = 0;//图和节点个数
bool vis[maxn] = {false};//存储是否被遍历过
int d[maxn];//存储起点到其余节点的距离,刚开始都是INF,即不可达

void Dijkstra(int s){
     fill(d, d + maxn, INF);
     d[s] = 0;//开始点到开始点的距离设为1
     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;
          printf("%d->%d距离为%d\n", s, u, d[u]);
          //开始更新剩余点到开始节点的距离
          for(int j = 0; j < n; j++){
               //如果与中间节点u相连的节点没有被访问过,并且经过u到达开始点的距离小于当前存储的距离
               if(vis[j] == false && G[u][j] < INF && d[u] + G[u][j] < d[j]){
                    d[j] = d[u] + G[u][j];
               }
          }
     }
}

int main(){
     int k, l, r, w, s;
     scanf("%d%d", &n, &k);
     //读入图
     for(int i = 0; i < k; i++){
          scanf("%d%d%d", &l, &r, &w);
          G[l][r] = w;
          G[r][l] = w;
     }
     scanf("%d", &s);
     Dijkstra(s);
     system("pause");
     return 0;
}

邻接表版
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<map>
#include<queue>
using namespace std;
const int maxn = 1000;//最大的节点数
const int INF = 1e9;//表示不可达

int n = 0;//节点个数
bool vis[maxn] = {false};//存储是否被遍历过
int d[maxn];//存储起点到其余节点的距离,刚开始都是INF,即不可达
//结构体定义
struct node{
     int idx;
     int weight;
     node(int idx1, int weight1){
          this->idx = idx1;
          this->weight = weight1;
     }
};
vector<vector<node>> G;

void Dijkstra(int s){
     fill(d, d + maxn, INF);
     d[s] = 0;//开始点到开始点的距离设为1
     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;
          printf("%d->%d距离为%d\n", s, u, d[u]);
          //开始更新剩余点到开始节点的距离
          for(int j = 0; j < G[u].size(); j++){
               //如果与中间节点u相连的节点没有被访问过,并且经过u到达开始点的距离小于当前存储的距离
               //邻接表与邻接矩阵唯一的不同就是想要知道与当前点相连点的id需要去结构体寻找
               int v = G[u][j].idx;
               if(vis[v] == false && d[u] + G[u][j].weight < d[v]){
                    d[v] = d[u] + G[u][j].weight;
               }
          }
     }
}

int main(){
     int k, l, r, w, s;
     scanf("%d%d", &n, &k);
     G.resize(n);
     //读入图
     for(int i = 0; i < k; i++){
          scanf("%d%d%d", &l, &r, &w);
          node temp = node(r, w);
          G[l].push_back(temp);
          temp.idx = l;
          G[r].push_back(temp);
     }
     scanf("%d", &s);
     Dijkstra(s);
     system("pause");
     return 0;
}

输出路径

如果要输出路径,只需要增加一个pre数组,用于存储最短路径上每个节点的前驱节点,在更新节点到初始点的距离时更新这个节点的前驱节点,然后递归打印即可;

//开始更新剩余点到开始节点的距离
     for(int j = 0; j < G[u].size(); j++){
          //如果与中间节点u相连的节点没有被访问过,并且经过u到达开始点的距离小于当前存储的距离
          //邻接表与邻接矩阵唯一的不同就是想要知道与当前点相连点的id需要去结构体寻找
          int v = G[u][j].idx;
          if(vis[v] == false && d[u] + G[u][j].weight < d[v]){
               d[v] = d[u] + G[u][j].weight;
               pre[v] = u;
          }
     }
 //递归输出从出发点到这个点的路径
void print(int idx){
     if(pre[idx] == -1) {
          printf("%d\n", idx);
          return;
     }
     printf("%d <-", idx);
     print(pre[idx]);
}
存在多条最短路径,增加额外条件
void DijkDis(int head){
     //初始化
     fill(d, d + maxn, inf);
     fill(t, t + maxn, inf);
     fill(pre, pre + maxn, -1);
     fill(vis, vis + maxn, false);
     d[head] = 0, t[head] = 0;
     //循环n次
     for(int i = 0; i < n; i ++){
          int u = -1, minn = inf;
          //找到最近的点
          for(int j = 0; j < n; j ++){
               if(vis[j] == false && d[j] < minn){
                    minn = d[j], u = j;
               }
          }
          if(u == -1) break;
          //标记已看过
          vis[u] = true;
          //更新最短距离
          for(int v = 0; v < n; v ++){
               //遍历与该点连着的点
               if(dis[u][v] != inf && vis[v] == false){
                    //如果出现最短路径
                    if(dis[u][v] + d[u] < d[v]){
                         d[v] = dis[u][v] + d[u];
                         t[v] = t[u] + times[u][v];
                         pre[v] = u;
                    }
                    //如果出现相同的最短路径
                    else if(dis[u][v] + d[u] == d[v]){
                         if(t[u] + times[u][v] < t[v]){
                              t[v] = t[u] + times[u][v];
                              d[v] = dis[u][v] + d[u];
                              pre[v] = u;
                         }
                    }
               }

          }
     }
     return;
}
Dijktras + DFS
理论

在使用Dijktras 算法时,因为可能存在多条最短路径,此时需要用第二标尺或第三标尺来衡量最短路,上面的做法是额外用数组存储第二或第三标尺的信息,但是有一个问题,就是这种做法只能为每个点存储一个最优解,也就是当找到一个相同的最优路径时,必须对第二或第三标尺也进行修改,使其保持最优,但这种做法实在繁琐。
为了避免这个问题,我们可以结合Dijktras和DFS来做,先用Dijktras算法找到所有最短路径上每个节点的前驱,由于可能存在多条最短路径,所以每个点的前驱是不唯一的,这样pre就是一个二维数组,每个节点占一行;再用DFS遍历所有可能的结果,每找到一个解,就跟当前全局最优解进行比较,最后找到整个问题的最优解。

代码
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<map>
#include<queue>
using namespace std;
const int maxn = 510;
const int inf = 1e9;
//G存储距离,cost存储花费, d[]存储到初始节点的距离,minCost全局最小的花费
int G[maxn][maxn], cost[maxn][maxn];
int n, m, st, ed, d[maxn], minCost = inf;
bool vis[maxn] = {false};
//pre数组存储最短路径上每个节点的前驱
vector<vector<int>> pre(maxn);
vector<int> tempPath, path;

void Dijktras(int s){
     fill(d, d + maxn, inf);
     d[s] = 0;
     for(int  i = 0; i < n; i ++){
          int u = -1, minn = inf;
          for(int j = 0; j < n; j ++ ){
               if(d[j] < minn && vis[j] == false){
                    u = j;
                    minn = d[j];
               }
          }
          if(u == -1) return;
          vis[u] = true;
          for(int v = 0; v < n; v ++){
               if(G[u][v] != inf && vis[v] == false){
                    //如果出现了更短的距离,这个节点的前驱数组需要清空在压入
                    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,找出所有的最短路径
void DFS(int e){
     //如果是起始点,说明已经找到一条最短路径,判断花费是不是最小
     if(e == st){
          //将起始点压入
          tempPath.push_back(st);
          int temp = 0;
          for(int i = tempPath.size() - 1; i > 0; i--){
               temp += cost[tempPath[i]][tempPath[i - 1]];
          }
          if(temp < minCost){
                minCost = temp;
                path = tempPath;
          }
          //将起始点弹出
          tempPath.pop_back();
          return;
     }
     //如果不是起始点,将这个点压入
     tempPath.push_back(e);
     for(int i = 0; i < pre[e].size(); i ++){
          DFS(pre[e][i]);
     }
     //将这个点弹出
     tempPath.pop_back();
}
int main(){
     scanf("%d%d%d%d", &n, &m, &st, &ed);
     fill(G[0], G[0] + maxn * maxn, inf);
     fill(cost[0], cost[0] + maxn * maxn, inf);
     int s, e, dis, c;
     for(int i = 0; i < m; i ++){
          scanf("%d%d%d%d", &s, &e, &dis, &c);
          G[s][e] = G[e][s] = dis;
          cost[s][e] = cost[e][s] = c;
     }
     Dijktras(st);
     DFS(ed);
     //逆序打印最优路径上的节点
     for(int i = path.size() - 1; i >= 0; i --){
          printf("%d ", path[i]);
     }
     printf("%d %d\n", d[ed], minCost);
     system("pause");
     return 0;
}

Floyd算法(全源最短路)
理论

Floyd算法用于求图中任意一个点到其余所有点的最短路径,复杂度O(n3),大概思想是将每个点都依次插入,假设当前插入的点为k,在插入这个点后,就检查一遍图中所有点与点(例如 顶点i和 顶点j)之间的距离会不会因为经过这个点而被优化,被优化的条件是顶点i可以到达k,顶点k可以到达j,并且dis[i][k] + dis[k][j] < dis[i][j];

代码
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
#include<map>
#include<queue>
using namespace std;
const int inf = 1e9;
const int maxn = 200;//最大顶点个数
int dis[maxn][maxn];//dis存储各个顶点间的距离
int n, m; //n 为顶点,m为边数
void Floyed(){
     //对每个节点进行插入尝试
     for(int k = 0; k < n; k ++){
          for(int i = 0; i < n; i ++){
               for(int  j = 0; j < n; j ++){
                    //如果以k为中介点,i与k相连,k与j相连,并且i经过k到达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];
                    }
               }
          }
     }
}
int main(){
     scanf("%d%d", &n, &m);
     int u, v, w;
     fill(dis[0], dis[0] + maxn * maxn, inf);
     //每个点到自身的距离为零
     for(int  i = 0; i < n; i ++){
          dis[i][i] = 0;
     }
     for(int i = 0; i < m; i ++){
          scanf("%d%d%d", &u, &v, &w);
          dis[u][v] = w;
     }
     Floyed();
     for(int i = 0; i < n; i ++){
          for(int j = 0; j < n; j ++){
               printf("%d ", dis[i][j]);
          }
          printf("\n");
     }
     system("pause");
     return 0;
}
prim算法(最小生成树)
理论

prim算法的作用是找到一颗树,使所有的节点都出现在这棵树上,思路与Dijktras的思路大致一样,只是d的含义不一样 ,Dijktras中的d是每个点到初始点的距离,而prim中的d表示每个点到已访问集合的最短距离;
1、对每个节点依次进行处理一遍,所以肯定要循环n次,
用一个vis存储节点是否被遍历过,d[]代表每个点到已访问集合的最短距离,初始只有最开始的树根为零,其余为INF,代表不可达;
2、在每次循环分两步进行
(1)找到未遍历节点中距离已访问节点集合距离最短的点,如果找到进行步骤2;如果未找到说明不能找到一棵树包含所有节点,退出即可;
(2)在找到中间节点后,由于这个中间结点的存在,未访问节点到已访问节点集合的最短距离可能会发生改变,所以需要更新;

邻接矩阵版
using namespace std;
const int maxn = 210;
const int inf = 1e9;
//G存储图,d存储每个点到已访问集合的最短距离
int d[maxn], G[maxn][maxn];
bool vis[maxn] = {false};//记录顶点有没有被访问过
//n 为顶点个数,m为边个数
int n, m;

int prim(){
     int ans = 0; //ans存储最小生成树的权值
     fill(d, d + maxn, inf);
     d[0] = 0;
     for(int i = 0; i < n; i ++){
          int u = -1, minn = inf;
          //先找到未访问节点中距离已访问节点集合最小的节点
          for(int j = 0; j < n; j ++){
               if(vis[j] == false && d[j] < minn){
                    u = j;
                    minn = d[j];
               }
          }
          if(u == -1)return inf;//代表没有这样的树
          vis[u] = true;
          ans += d[u];//在结果中加入距离
          //遍历与当前节点相连的节点
          for(int v = 0; v < n; v ++){
               //如果这个节点为被访问,且从u到v的距离小于[v],则更新距离
               if(vis[v] == false && G[u][v] != inf && G[u][v] < d[v]){
                    d[v] = G[u][v];
               }
          }
     }
     return ans;
}

int main(){
     fill(G[0], G[0] + maxn * maxn, inf);
     int u, v, dis;
     scanf("%d%d", &n, &m);
     for(int i = 0; i < m; i ++){
          scanf("%d%d%d", &u, &v, &dis);
          G[u][v] = dis;
          G[v][u] = dis;
     }
     int ans = prim();
     printf("%d", ans);
     system("pause");
     return 0;
}
邻接表版
#include<iostream>
#include<vector>
using namespace std;
const int maxn = 210;
const int inf = 1e9;
struct node{
     int id, dis;
     node(int id1, int weight1){
          this->id = id1;
          this->dis = weight1;
     }
};
//G存储图,d存储每个点到已访问集合的最短距离
int d[maxn];
vector<vector<node>> G(maxn);
bool vis[maxn] = {false};//记录顶点有没有被访问过
//n 为顶点个数,m为边个数
int n, m;

int prim(){
     int ans = 0; //ans存储最小生成树的权值
     fill(d, d + maxn, inf);
     d[0] = 0;
     for(int i = 0; i < n; i ++){
          int u = -1, minn = inf;
          //先找到未访问节点中距离已访问节点集合最小的节点
          for(int j = 0; j < n; j ++){
               if(vis[j] == false && d[j] < minn){
                    u = j;
                    minn = d[j];
               }
          }
          if(u == -1)return inf;//代表没有这样的树
          vis[u] = true;
          ans += d[u];//在结果中加入距离
          //遍历与当前节点相连的节点
          for(int j = 0; j < G[u].size(); j ++){
               //如果这个节点为被访问,且从u到v的距离小于[v],则更新距离
               int v = G[u][j].id, dis = G[u][j].dis;
               if(vis[v] == false && dis < d[v]){
                    d[v] = dis;
               }
          }
     }
     return ans;
}

int main(){
     int u, v, dis;
     scanf("%d%d", &n, &m);
     for(int i = 0; i < m; i ++){
          scanf("%d%d%d", &u, &v, &dis);
          node temp = node(v,dis);
          G[u].push_back(temp);
          temp.id = u;
          G[v].push_back(temp);
     }
     int ans = prim();
     printf("%d", ans);
     system("pause");
     return 0;
}

Kluskal算法(最小生成树)
理论

Kluskal算法是求最小生成树的另一求法,不同于prim算法,该算法主要以边的权重进行选择,
1、首先将所有边按权值大小从小到大排序,依次处理每条边;
2、选取权值中最小的边,判断这条边的两个端点是否在同一个集合中,如果在同一个集合中,说明这条边无效,不能加入最小生成树中;如果不同,则将这两个集合合并;
3、对于判断两个点是否在同一个集合中,可以用并查集实现;

代码
#include<iostream>
#include<vector>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn = 210;//最大的顶点数
const int maxe = 1e5 + 10;//最大的边数
int n, m;//顶点数和边数
int father[maxn];

struct edge{
     int u, v;
     int cost;
}Edge[maxe];

bool cmp(edge a, edge b){
     return a.cost < b.cost;
}
//并查集
int findFa(int t){
     int u = t;
     while(father[u] != u){
          u = father[u];
     }
     //压缩路径
     while(father[t] != t){
          int z = t;
          t = father[t];
          father[z] = u;
     }
     return u;
}
//kruskal算法
int kruskal(){
     //ans记录结果,edgenum记录当前最小生成树中的边数
     int ans = 0, edgenum = 0;
     //对边权进行排序,从小到大依次选
     sort(Edge, Edge + m, cmp);
     for(int i = 0; i < n; i ++){
          father[i] = i;
     }
     //遍历每条边
     for(int i = 0; i < m; i ++){
          int fau = findFa(Edge[i].u);
          int fav = findFa(Edge[i].v);
          //如何当前边的两个顶点不在同一个集合中
          if(fau != fav){
               ans += Edge[i].cost;
               father[fau] = fav;
               edgenum++;
               if(edgenum == n - 1)break;
          }
     }
     //如果边数不是n - 1,说明不是联通的,输出-1
     if(edgenum != n - 1) return -1;
     else return ans;
}
int main(){
     scanf("%d%d", &n, &m);
     for(int i = 0; i < m; i ++){
          scanf("%d%d%d", &Edge[i].u, &Edge[i].v, &Edge[i].cost);
     }
     int ans = kruskal();
     printf("%d", ans);
     system("pause");
     return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值