无向图
术语表
图:由一组顶点个一组能够将两个顶点相连的边组成。
自环:一条连接一个顶点和其自身的边。
平行边:连接同一对顶点的两条边称为平行边。
多重图:含有平行边。
简单图:没有平行边和自环。
相邻:两个顶点通过一条边相连,并称这条边依附这两个顶点。
顶点的度数:依附于他的边的数量。
子图:一幅图的所有边的一个子集(以及所依附的所有顶点)组成的图。
路径:由边顺序连接的一系列顶点。
简单路径:一条没有重复顶点的路径。
环:一条至少含有一条边且起点和终点相同的路径。
简单环:除起点外不含重复顶点和边的环。
路径或环的长度:包含的边数。
文中环和路径指简单环和简单路径,一般环和路径指有可重复的顶点。
连通图:如果任意一个顶点都存在一条路径到达另一个任意顶点,我们称这幅图是连通图。
极大连通子图:一幅非连通的图由若干连通部分组成,他们都是极大连通子图。
无环图:一种没有环的图。
树:一幅无环连通子图。
森林:换不相连的树。
连通图的生成树:他的一幅子图,含有图中所有的顶点且是一棵树。
图的生成树森林:他的所有连通子图的生成树的集合。
当一幅含有V个结点的图G满足下列5个条件之一时,他就是一棵树:
1)G有V-1条边且不含有环;
2)G有V-1条边且是连通的:
3)G是连通的,但删除任意一条边都会使他不在连通;
4)G是无环图,但添加任意一条边都会产生一条环;
5)G中的任意一对顶点之间仅存在一条简单路径。
图的密度:已经链接的顶点对占所有可能被连接的顶点对的比例,稀疏图中,被链接的顶点对很少;而稠密图中很多。如果一幅图中不同的边的数量在顶点总数v的一个小的常数倍以内为稀疏图,否则为稠密图。
二分图:一种能够将所有结点分为两部分的图,每条边所连接的两个顶点都分别属于不同的部分。
图的表示方式:
要求:
1)它必须为可能在应用中碰到的各种类型的图预留出足够的空间。
2)图的实例方法一定要快——他们是开发处理图的各种用例的基础。
1.邻接矩阵:V*V的布尔矩阵,满足不了空间。
2.边的数组:满足不了第二个条件。
3.邻接表数组:一个以顶点为索引的列表数组,其中的每个元素都是和该顶点相邻的列表数组。(类似于拉链式的散列表)
本章解决的无向图问题:
问题 | 解决方法 |
无向图的创建 | Graph |
单点连通性 | DepthFirstSearch |
单点路径 | DepthFirstPaths |
单点最短路径 | BreadthFirstPaths |
连通性 | CC |
检测环 | Cycle |
双色问题(二分) | TwoColor |
实现:
1.无向图的创建
具体方法是构建一个邻接表存储所有结点,每个结点指向一条链表存储结点相连的所有边。
代码:
#ifndef GRAPH_H
#define GRAPH_H
#include<iostream>
#include<vector>
#include"Bag.h"
using std::cout;
using std::endl;
using std::cin;
using std::vector;
class Graph
{
private:
int V = 0;//顶点数
int E = 0;//边数
vector<Bag<int>> adj;//邻接表
public:
Graph(int V):V(V),E(0),adj(V){}//构造结点
Graph(int V, int E) :V(V), adj(V)//构造整个图
{
int v, w;
for (int i = 0; i < E; i++)
{
cout << "输入第" << i << "边的两个结点:" << endl;
cin >> v >> w;
addEdge(v, w);//添加边
}
}
int getV(){ return V; }
int getE(){ return E; }
void addEdge(int v, int w)//连接v,w
{
adj[v].add(w);//将连接的结点倒入自己的链表
adj[w].add(v);
E++;
}
Bag<int> getadj(int v) { return adj[v]; }
};
#endif
#include<string>
#include<iostream>
#include<vector>
#include"Graph.h"
#include"Bag.h"
using namespace std;
int main()
{
cout << "输入顶点数和边数:" << endl;
int n,m;
cin >> n>>m;
Graph G(n,m);
for (int v = 0; v < G.getV(); v++)
{
cout << v << ": ";
G.getadj(v).trave();
cout << endl;
}
system("pause");
return 0;
}
2.单点连通性(深度优先搜索)
搜索和一个结点连通的所以结点,具体步骤:
1)设置一个bool数组,记录结点是否已经经过,也就是是否连通;
2)遍历结点的链表,被查看的链表元素没有经过则记录元素,然后递归遍历被查看的元素的链表;
3)有记录则查看链表下一个元素。
深度优先搜索的意思是在图中优先搜索一条路径,走到头之后(也就是该结点连接的所以链表元素都被记录),返回上一路口,走另一条路。
代码:
#ifndef DEPTHFIRSTSEARCH_H
#define DEPTHFIRSTSEARCH_H
#include<vector>
#include"Graph.h"
using std::vector;
class DepthFirstSearch//深度优先搜索
{
private:
vector<bool> marked;//记录顶点是否和起点S连通
int count=0;//与起点S连通的顶点总数
public:
DepthFirstSearch(Graph G, int s) :marked(G.getV())
{
dfs(G, s);
}
void dfs(Graph G, int v)
{
marked[v] = true;
count++;
for (const auto & w: G.getadj(v))
if (!marked[w])dfs(G, w);
}
bool getmarked(int w){ return marked[w]; }
int getcount(){ return count; }
};
#endif
#include<string>
#include<iostream>
#include<vector>
#include"Graph.h"
#include"Bag.h"
#include"DepthFirstSearch.h"
using namespace std;
int main()
{
cout << "输入顶点数和边数:" << endl;
int n,m;
cin >> n>>m;
Graph G(n,m);
DepthFirstSearch search(G, 0);
for (int v = 0; v < G.getV(); v++)
if (search.getmarked(v))
cout << v << " ";
cout << endl;
system("pause");
return 0;
}
3.寻找路径(基于深度优先搜索)
在深度优先搜索的基础上加一个数组,用来存储每个结点来时的路径(前置结点),然后用栈逆向输出即可。
代码:
#ifndef DEPTHFIRSTPATHS_H
#define DEPTHFIRSTPATHS_H
#include<vector>
#include"Graph.h"
#include"stack.h"
using std::vector;
class DepthFirstPaths
{
private:
vector<bool> marked;//记录结点是否经过
vector<int> edgeTo;//记录结点的上一结点
int s=0;//起点
public:
DepthFirstPaths(Graph G, int s) :
marked(G.getV()), edgeTo(G.getV()), s(s)
{
dfs(G, s);
}
void dfs(Graph G, int v)
{
marked[v] = true;//已经过v
for (int w : G.getadj(v))//遍历v的链表
if (!marked[w])//W没有经过
{
edgeTo[w] = v;//记录此结点的上一路径
dfs(G, w);//查询w的链表元素
}
}
bool hasPathTo(int v){ return marked[v]; }//是否存在s到v的路径
stack<int> pathTo(int v)//输出s到v的路径
{
stack<int> path;
if (!hasPathTo(v))return path;
for (int i = v; i != s; i = edgeTo[i])
path.push(i);
path.push(s);
return path;
}
};
#endif
测试用例:
#include<string>
#include<iostream>
#include<vector>
#include"Graph.h"
#include"Bag.h"
#include"DepthFirstPaths.h"
using namespace std;
int main()//DepthFirstPaths.h
{
cout << "输入顶点数和边数:" << endl;
int n, m,s;
cin >> n >> m;
Graph G(n, m);
cout << "输入查询顶点:" << endl;
cin >> s;
DepthFirstPaths Paths(G, s);
for (int v = 0; v < G.getV(); v++)
{
cout << s << " to " << v << ": ";
if (Paths.hasPathTo(v))
for (int x : Paths.pathTo(v))
if (x == s)cout << x;
else cout << "-" << x;
cout << endl;
}
system("pause");
return 0;
}
4.单点最短路径(基于广度优先搜索)
广度优先搜索与深度优先搜索的区别在于,我们先将与结点相连的结点搜索完,之后在查询下一层级,也就是说,我们先搜索结点的链表,结束之后,搜索结点链表首元素的链表,次元素的链表,依次下去直到全部元素所有元素都查完。所以这里我们就需要用一个队列记录搜索过的结点,以便返回使用,具体步骤:
1)设置队列记录首元素;
2)首元素出队,检查元素链表,经过元素全部入队,并记录;
3)对不为空,下一元素出队,继续进行检查,记录;
4)为空,结束。
代码:
#ifndef BREADTHFIRSTPATHS_H
#define BREADTHFIRSTPATHS_H
#include<vector>
#include"Graph.h"
#include"queue.h"
#include"stack.h"
using std::vector;
class BreadthFirstPaths
{
private:
vector<bool> marked;//记录是否已经过
vector<int> edgeTo;//记录上一路径
int s=0;//起点
public:
BreadthFirstPaths(Graph G, int s) :
marked(G.getV()), edgeTo(G.getV()), s(s)
{
bfs(G, s);
}
void bfs(Graph G, int s)
{
queue<int> que;
marked[s] = true;//标记起点
que.enqueue(s);
while (!que.Empty())
{
int v = que.dequeue();//取出下一个结点
for (int w : G.getadj(v))//遍历结点链表
if (!marked[w])//链表元素未被标记
{
edgeTo[w] = v;//记录他上一路径
marked[w] = true;//标记元素
que.enqueue(w);//入队
}
}
}
bool hasPathTo(int v){ return marked[v]; }//是否存在s->v的路径
stack<int> pathTo(int v)//输出s->v的路径
{
stack<int> path;
if (!hasPathTo(v))return path;
for (int i = v; i != s; i = edgeTo[i])
path.push(i);
path.push(s);
return path;
}
};
#endif
测试用例:
#include<string>
#include<iostream>
#include<vector>
#include"Graph.h"
#include"Bag.h"
#include"BreadthFirstPaths.h"
using namespace std;
int main()//BreadthFirstPaths
{
cout << "输入顶点数和边数:" << endl;
int n, m, s;
cin >> n >> m;
Graph G(n, m);
cout << "输入查询顶点:" << endl;
cin >> s;
BreadthFirstPaths paths(G, s);
for (int v = 0; v < G.getV(); v++)
{
cout << s << " to " << v << ": ";
if (paths.hasPathTo(v))
for (int x : paths.pathTo(v))
if (x == s)cout << x;
else cout << "-" << x;
cout << endl;
}
system("pause");
return 0;
}
5.连通分量(基于深度优先搜索)
深度优先搜索的下一个直接应用就是找出一幅图的所有连通分量。
连通分量:即图中,一个最大连通子图就是一个连通分量。
查找图的所有连通分量,只需要深度优先搜索所有的结点,用一个数组记录是相同连通分量的结点打上同样的标记即可。
代码:
#ifndef CC_H
#define CC_H
#include<vector>
#include"Graph.h"
#include"Bag.h"
using std::vector;
class CC
{
private:
vector<bool> marked;//结点是否被记录
vector<int> id;//记录结点在那一连通分量
int count;//连通分量的数目
public:
CC(Graph G) :marked(G.getV()), id(G.getV()), count(0)
{
for (int s=0 ; s < G.getV();s++)
if (!marked[s])
{
dfs(G, s);
count++;
}
}
void dfs(Graph G, int v)
{
marked[v] = true;//标记v
id[v] = count;//记录v输入那一连通分量
for (int w : G.getadj(v))
if (!marked[w])
dfs(G, w);
}
bool connected(int v, int w)//判断v,w是否连通
{
return id[v] == id[w];
}
int getid(int v){ return id[v]; }//输入v在那一连通分量
int getcount(){ return count; }//连通分量数目
vector<Bag<int>> componentsTo(Graph G)
{
int m = getcount();
vector<Bag<int>> components(m);
for (int v = 0; v < G.getV(); v++)
components[getid(v)].add(v);
return components;
}
};
#endif
测试用例:
#include<string>
#include<iostream>
#include<vector>
#include"Graph.h"
#include"Bag.h"
#include"CC.h"
using namespace std;
int main()//cc
{
cout << "输入顶点数和边数:" << endl;
int n, m, s;
cin >> n >> m;
Graph G(n, m);
CC cc(G);
m=cc.getcount();
vector<Bag<int>> components = cc.componentsTo(G);
for (int i = 0; i < m; i++)
{
cout << i << " : ";
for (auto v : components[i])
cout << v << " ";
cout << endl;
}
system("pause");
return 0;
}
5.无环图(基于深度优先搜索)
检测图是不是无环图。
在深度优先探索中当探索子元素的链表时,发现一个已经标记的结点且不是结点本身时,说明图是有环图。
代码:
#ifndef CYCLE_H
#define CYCLE_H
#include<vector>
using std::vector;
#include"Graph.h"
class Cycle
{
private:
vector<bool> marked;
bool hasCycle;//无环图
public:
Cycle(Graph G) :marked(G.getV())
{
for (int s = 0; s < G.getV(); s++)
if (!marked[s])dfs(G, s, s);
}
void dfs(Graph G, int v, int u)//正在探测的元素u的链表
{
marked[v] = true;
for (int w : G.getadj(v))
if (!marked[w])dfs(G, w, v);
else if (w != u)hasCycle = true;//探索到一个已标记的结点,且不是自己
}
bool hasCycleTo(){ return hasCycle; }
};
#endif
测试用例:
#include<string>
#include<iostream>
#include<vector>
#include"Graph.h"
#include"Bag.h"
#include"Cycle.h"
using namespace std;
int main()
{
cout << "输入顶点数和边数:" << endl;
int n, m, s;
cin >> n >> m;
Graph G(n, m);
Cycle cy(G);
if (cy.hasCycleTo())cout << "有环图" << endl;
else cout << "无环图" << endl;
system("pause");
}
6.二分图(双色问题)
将一幅图用两种颜色一分为二,也就是每一个条边的两个端点的颜色都不同。
在深度优先探索中,用两种颜色依次标记连通的结点,如果一个结点链表中有一个被标记元素且颜色与自己相同,则不是二分图。
代码:
#ifndef TWOCOLOR_H
#define TWOCOLOR_H
#include<vector>
using std::vector;
#include"Graph.h"
class TwoColor
{
private:
vector<bool> marked;
vector<bool> color;//结点的颜色
bool isTwoColor = true;//双色图(默认是)
public:
TwoColor(Graph G) :marked(G.getV()), color(G.getV())
{
for (int s = 0; s < G.getV(); s++)
if (!marked[s])dfs(G, s);
}
void dfs(Graph G, int v)
{
marked[v] = true;
for (int w : G.getadj(v))
if (!marked[w])
{
color[w] = !color[v];
dfs(G, w);
}
else if (color[w] == color[v])isTwoColor = false;//子链接中一个元素被标记,且颜色与自己相同
}
bool isBipartite()
{
return isTwoColor;
}
};
#endif
测试用例:
#include<string>
#include<iostream>
#include<vector>
#include"Graph.h"
#include"Bag.h"
#include"TwoColor.h"
using namespace std;
int main()//TwoColor
{
cout << "输入顶点数和边数:" << endl;
int n, m, s;
cin >> n >> m;
Graph G(n, m);
TwoColor cy(G);
if (cy.isBipartite())cout << "双色图" << endl;
else cout << "不是双色图" << endl;
system("pause");
}
符号图
在典型应用中,图都是通过文件或者网页定义的,使用的是字符串而非整数来指代顶点。为了适应应用,我们定义了拥有以下性质的输入格式:
1)顶点名为字符串
2)用指定的分隔符来隔开顶点名
3)每一行都表示一组边的集合,每一条边都连接着这一晃的第一个名称表示的顶点和另一个顶点
4)结点总数V和边总数E,除了输入时都是隐式定义的
很显然符号图需要用到符号表,下面是带模板的符号表:
#ifndef ST_H
#define ST_H
#include<string>
#include<vector>
#include"queue.h"
using std::string;
using std::vector;
template<typename key, typename Item>
class ST
{
class Node
{
friend class ST;
key key;
Item data;
Node* next;
public:
template<typename key, typename Item>
Node(key key, Item item, Node* next) :
key(key), data(item), next(next){}
};
private:
Node* first = NULL;
public:
void put(key key, Item item);//插入
Item get(key key);//顺序查找
void delete_1(key key);//删除
bool Empty(){ return first ? 0 : 1; }
int size();//长度
queue<key> keys();//键的集合
bool contains(key key){ return get(key) != NULL; }//键是否存在
};
template<typename key, typename Item>
void ST<key, Item>::put(key key, Item item)//头插法
{
for (Node* x = first; x; x = x->next)
if (key == x->key)
{
x->data = item;
return;
}
first = new Node(key, item, first);
}
template<typename key, typename Item>
Item ST<key, Item>::get(key key)
{
for (Node* x = first; x; x = x->next)
if (key == x->key)return x->data;
return NULL;
}
template<typename key, typename Item>
void ST<key, Item>::delete_1(key key)
{
if (first->key == key)
{
Node* x = first;
first = x->next;
delete x;
return;
}
for (Node* x = first; x->next; x = x->next)
if (key == x->next->key)
{
Node* t = x->next;
x->next = t->next;
delete(t);
return;
}
}
template<typename key, typename Item>
int ST<key, Item>::size()
{
int n = 0;
for (Node* x = first; x; x = x->next)
n++;
return n;
}
template<typename key, typename Item>
queue<key> ST<key,Item>::keys()
{
queue<key> a;
for (Node* x = first; x; x = x->next)
a.enqueue(x->key);
return a;
}
#endif
符号图代码:
#ifndef SYMBOLGRAPH_H
#define SYMBOLGRAPH_H
#include<iostream>
using std::cout;
using std::endl;
using std::cin;
#include<string>
using std::string;
#include<vector>
using std::vector;
#include"Graph.h"
#include"ST.h"
class SymbolGraph
{
private:
ST<string, int> st;//符号名->索引
vector<string> keys;//索引->符号名
Graph G;//图
string sy = "-";//间隔符号
public:
SymbolGraph(int V, int E) :G(V)
{
string sp;//读取字符串
for(int i=0;i<V;i++)//第一遍,构建索引
{
cout << "输入第"<<i<<"个符号名:" << endl;
cin >> sp;
if (!st.contains(sp))//符号名唯一
{
st.put(sp, i);//添加符号名->索引
keys.push_back(sp);//添加索引->符号名
}
else i--;
}
string sp1,sp2;
for (int i = 0; i < E; i++)//第二遍,构造图
{
cout << "输入第" << i << "条边的两个顶点:" << endl;
cin >> sp1 >> sp2;
G.addEdge(st.get(sp1), st.get(sp2));
}
}
bool contains(string s){ return st.contains(s); }//检索符号名是否存在
int index(string s){ return st.get(s); }//检索符号名是第几个结点
string name(int v){ return keys[v]; }//检索第v个索引的名称
Graph getG(){ return G; }//输出图
string Sy(){ return sy; }//输出分隔符
};
#endif
测试用例:
#include<string>
#include<iostream>
#include<vector>
#include"Graph.h"
#include"BreadthFirstPaths.h"
#include"SymbolGraph.h"
using namespace std;
int main()//SymbolGraph
{
cout << "输入图的结点数和边数:" << endl;
int V, E;
cin >> V >> E;
SymbolGraph sym(V, E);
Graph G = sym.getG();
cout << "输入要查找的结点的符号名:" << endl;
string source;
cin >> source;
int s = sym.index(source);
BreadthFirstPaths bfs(G,s);
cout << "输入你要到达的结点名:" << endl;
string sink;
cin >> sink;
if (sym.contains(sink))
{
int t = sym.index(sink);
cout << source << "->"<<sink << "的最短路径为:" << endl;
if (bfs.hasPathTo(t))
for (int v : bfs.pathTo(t))
if (v == s)cout << sym.name(v);
else cout << sym.Sy() + sym.name(v);
else cout << "Not connected" << endl;
}
else cout << "Not in datebase" ;
cout << endl;
system("pause");
}