目录
图的存储方式(邻接矩阵和邻接表)
邻接矩阵:邻接矩阵是一个方阵,行数 = 列数 = 节点元素个数;可以较方便的存储节点间的权值,
但是耗费空间,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;
}