图论——txf
倘若考研需要像写算法题目那样,写出图论的代码,那无疑图论是最难级别的。 -----Williams Tian
1. 重点表述
①线形表可以空表,树可以空树,但是图不能一个顶点也没有(允许一条边也没有).
②注意“无向图” 和 “有向图” 中一些表述的不同
顶点个数n,边为m
1. 有向图,无向图都可以说的
完全图、简单完全图:(图中任意两点,可以直接直接直接到达)
(无向图)n个点最多有 (n(n-1))/2 条边;
(有向图)n个点最多有 (n(n-1)) 条边;
思考:无向图9个点,至少需要多少条边才能确保其实一个连通图?
2. 连通图、连通分量一般用于无向图中,对于有向图的话,多一个“强”字
**连通图:**任意两点都可以到达,注意是到达,不是直接到达
极大连通子图:又被称为连通分量!
**极小连通子图:**注意与极大连通子图区分!(并不一定包含图的所有顶点,同极大连通子图)
生成树:包含图的所有顶点,并且连接这些点使之是一个连通图 所需要的边最少
概念解析题:
G’是生成树,仔细想想生成树包含子图的所有边吗?而连通分量包含子图所有边,故I错
然后根据生成树的概念可知2 3 对 此题选D
③ 关于图的表示
1.邻接矩阵:遍历–>普遍选O(n^2)时间复杂度
2.邻接表:遍历–>普遍选O(n+e)时间复杂度
3.邻接多重表(无向图),十字链表(有向图)--------记忆:室友(“室”字链表-----“友”向图)
④图的遍历
bfs:邻接矩阵时间复杂度O(n^2),空间复杂度O(n);邻接表时间O(n+e),空间O(n)
dfs:邻接矩阵时间复杂度O(n^2),空间复杂度O(n);邻接表时间O(n+e),空间O(n)
判断回路:确切的两个方法 1.拓扑排序 2.dfs遍历; 有争议的方法:关键路径
2. 代码论述
对于考研题目来说,不一定要运行能够跑通啊,只是大致完整的思路,(比较规范的伪代码)
①图的表示
邻接矩阵:
typedef struct {
VertType vert[MAXN]; //表示顶点的信息
EdgeType edge[MAXN][MAXN]; //二维数组
int n, m; //顶点数,边数
}MGraph;
邻接表
typedef struct ArcNode{ //边表结点
int index; //指向的点所在的位置
struct ArcNode *next;
}ArcNode;
typedef struct VNode{ //顶点表信息
VertType data; //顶点信息
ArcNode *first; //顶点表的第一条弧
}VNode;
typedef struct {
Vnode vertTable[MAXN]; //邻接表
int n, m; //顶点,边数
}ALGraph; //邻接表所表示的图
可能看着会有些复杂,我画一个图就理解了;(边无权值)
思考:如果边有权值,该怎么表示?
邻接表,邻接矩阵二者互相转化
//邻接表转为邻接矩阵
void table_TO_matrix(ALGraph *G, int matrix[N][N]) {
for ( int i = 0; i < G->n; ++i ) { //遍历顶点表
ArcNode *p = G->vertTable[i].first; //p就定位到第一个边结点了
while ( p != null ) {
matrix[i][p->index] = 1;
p = p->next;
}
}
}
//邻接矩阵转为邻接表(稍微有些复杂哦)
void matrix_TO_table(int MGraph *G, VNode vertTable[N]) {
//将顶点信息 先复制下来
for ( int i = 0; i < G->n; ++i ) {
vertTable[i] = G->vert[i];
}
//组织边的关系
for ( int i = 0; i < G->n; ++i ) {
ArcNode *p;
bool sign = false;
for ( int j = 0; j < G->n; ++j ) {
if ( G->edge[i][j] == 1 ) {
ArcNode *t = new ArcNode;
t->index = j;
t->next = null;
if ( !sign) {
vertTable[i].first = t;
p = t;
}
else{
p->next = t;
p =t;
}
sign = true;
}
}
}
}
② 图的遍历
bfs:
//需要借助一个队列
//1.基于邻接矩阵的bfs
bool visted[MAXN] = {false};
void visit( node ); //表示访问此点
void Bfs(Graph *G, int start) {
Queue q = InitQueue(); //初始化一个队列
q.push(start);
while ( !q.empty() ) {
top = q.top(); //获取队头
q.pop(); //删除队首元素
visit(top); //访问此元素
visited[top] = true;
for ( int i = 0; i < G->n; ++i ) {
if ( G->edge[top][i] == 1 && !visited[i] ) {
q.push(i);
}
}
}
}
//2.基于邻接表的bfs
bool visted[MAXN] = {false};
void Bfs(Graph *G, int start) {
Queue q = InitQueue(); //初始化一个队列
q.push(start);
while ( !q.empty() ) {
top = q.top(); //获取队头
q.pop(); //删除队首元素
visit(top); //访问此元素
visited[top] = true;
ArcNode *p = G->vertTable[top].first;
while ( p != null ) {
if ( !visited[p->index] ) q.push(p->index);
p = p->next;
}
}
}
dfs
//1.基于邻接矩阵的dfs,递归
bool visited[MAXN] = {false};
void Dfs( Graph *G, int start ) {
visit(start);
visited[start] = true;
for ( int i = 0; i < G->n; ++i ) {
if ( G[start][i] == 1 && !visited[i] ) {
Dfs(G, i);
}
}
}
//2.基于邻接表的dfs,递归
bool visited[MAXN] = {false};
void Dfs( Graph *G, int start ) {
visit(start);
visited[start] = true;
ArcNode *p = G->vertTable[start].first;
while ( p != null ) {
if ( !visited[p->index] ) {
Dfs(G, p->index);
}
p = p->next;
}
}
//3.基于邻接表的dfs,非递归,需要一个栈了
bool visited[MAXN] = {false};
void Dfs( Graph *G, int start ) {
Stack s = InitStack(); //初始化一个栈
s.push(start);
visited[start] = true;
while ( !s.empty() ) {
top = s.top(); //获取栈顶元素
s.pop(); //弹出栈顶元素
visit(top); //访问此元素
ArcNode *p = G->vertTable[top].first;
while ( p != null ) {
if ( !visited[p->index] ) {
s.push(p->index);
visited[p->index] = true;
}
}
}
}
//1 3方法遍历顺序有区别!!请思考一下。 但是都是深度优先的模式
③生成树算法
1.prim算法
算法模型:
void Prim( G, T ) { //G表示图,T表示生成树
T = NULL; //空树
T.put(start); // 将start(任意一节点)放入生成树中
while ( T.number != G.n ) { //T的顶点个数 不等于 G图的顶点个数
Vert t = Find_Min_Go_T();//在G中非生成树的点中,寻找距离生成树最近的点
T.put(t); //将此点放入生成树中
UpDateDis(t); //用此点更新其余点到生成树的最小距离
}
}
伪代码(邻接矩阵表示)
/*
edge[i][j] = k //表示 i到j 这条边距离是 k
*/
const int INF = 0x3f3f3f3f; //表示无穷大
void Prim( Graph G ) {
int TreeTotal = 0; //生成树的顶点个数
bool tree[G.n] = {false};
int dis[G.n] = { 0 }; //dis[i]表示点i到生成树的距离
tree[0] = true; //从0顶点开始,此点加入生成树
TreeTotal++;
for ( int i = 0; i < G.n; ++i ) {
dis[i] = G.edge[0][i];
}
while ( TreeTotal < G.n ) {
//找距离生成树最近的点
int mins = INF, t = -1;
for ( int i = 0; i < G.n; ++i ) {
//如果没有在生成树里面
if ( !tree[i] && mins > dis[i] ) {
mins = dis[i];
t = i;
}
}
//以上步骤找到了最近的点t
if ( t != -1 ) {
TreeTotal++; //放入生成树中,此时这条边其实也找出来了,也可以想个办法把边也加入生成树
tree[t] = true;
//用此点更新其他点到生成树的距离
for ( int i = 0; i < G.n; ++i ) {
//t到i有边
if ( !tree[i] && G.edge[t][i] ) {
if ( dis[i] > G.edge[t][i] ) dis[i] = G.edge[t][i];
}
}
}
}
//结束了
}
此种算法是O(n^2), 时间复杂度,可以优化,但是我这里再讲就超纲了。
2.kruskal算法
算法模型:
void Kruskal( G, T ) {
int numS = G.n; //刚开始连通分量为n
while ( numS > 1 ) {
//从 边的集合 中按照权值大小从小到大依次寻找
edge = findMinEdge(); //找到了最小的边
if ( 边的起点,终点 不在一个连通分量中 ) {
//将边的起点,终点并入到一个集合中
Merge(start, end);
numS--;
}
}
//结束,最后只有一个连通分量了
}
伪代码(邻接矩阵)
涉及到了快速排序,并查集
typedef struct {
int start, end, v; //此条边表示start -> end 距离为v
}Edge;
//传进来一个边构成的数组,就是图了
void Kruskal ( Edge edge[], int n ) {
//将edge数组中的元素,按v从小到大排序
//思考一下,为什么?
QuickSort( edge , a.v < b.v );
/*
初始化并查集数组,f[i]表示i的父亲是f[i];
例如f[1] = 5, f[5] = 2, f[3] = 2;
可以得知1 2 3 5为同一个集合里面, 他们的"根"都是2
*/
int f[n];
for ( int i = 0; i < n; ++i ) f[i] = i;
//此数组表示第i条边是否选入生成树
bool select[n] = {false};
int numS = n; //初始连通分量为n,因为此时生成树个数为0
while ( numS > 1 ) {
int t;
for ( t = 0; i < n; ++i ) {
if ( !select[i] ) break; //选出了边
}
int start = edge[t].start;
int end = edge[t].end;
int fs = findRoot(start); //表示找start的 “根”!!
int fe = findRoot(end);
if ( fs != fe ) {
f[fe] = fs; //将它们的最高级祖先合并,那么start,end自然就合并了
numS--;
select[t] = true;
}
}
//结束了
}
//此函数需要理解,建议结合上面的例子 f[1] = 5, f[5] = 2, f[3] = 2;
int findRoot(int f[], int x ) {
if ( f[x] == x ) return x;
return f[x] = findRoot(f[x]);
}
思考:分析一下此算法的时间复杂度?
对比Prim算法,请自行理解 哪个算法适合稠密图,那个适合稀疏图。对于稠密图,稀疏图的严格区分,无绝对标准,只有一个
相对的判别: 边数E,顶点数V ==> E < VlogV稀疏
④最短路径算法
1.dijkstra算法 (单源最短路径)
算法模型略了,有点类似于prim
每次都从dis数组中找未选中的最近的点,用它来更新dis数组
代码:
//求start到各点的距离, INF表示无穷大
void Dijkstra(Graph G, int start) {
int dis[G.n] = {0}; //此数组表示strat点到i点的最近距离
bool sign[G.n] = {false}; //i点选中了没有
//初始化dis数组
for ( int i = 0; i < G.n; ++i ) {
if ( i == start ) dis[i] = 0;
else dis[i] = INF;
}
sign[start] = true;
for ( int i = 0; i < G.n; ++i ) {
//strat到i可以直接到达
if ( G.edge[start][i] != INF ) dis[i] = G.edge[start][i];
}
//=======================================================
//这中间才是核心部分
for ( int i = 0; i < G.n; ++i ) {
//从dis数组中选出距离strat最近的点,并且此点未被选中
int mins = INF, t = -1;
for ( int j = 0; j < G.n; ++j ) {
if ( !sign[j] && mins > dis[j] ) {
mins = dis[j]; t = j;
}
}
if ( t == -1 ) break; //找了一轮没找到,说明点选中完了
sign[t] = true; //选中t
//把t作为中转点,看可不可以strat->j 通过t中转变得更小 start -> t -> j
for ( int j = 0; j < G.n; ++j ) {
if ( dis[j] > dis[t] + G.edge[t][j] ) {
dis[j] = dis[t] + G.edge[t][j];
}
}
}
//=======================================================
//结束
for ( int i = 0; i < G.n; ++i ) {
cout << start << "-->" << i << " = ";
if ( dis[i] == INF ) cout << "∞" << endl;
else cout << dis[i] << endl;
}
cout << endl;
}
测试数据图片(参考自别人的)
可以运行的测试代码
#include <iostream>
#include <cstdio>
#define MAX 100
#define INF 0x3f3f3f3f
using namespace std;
/* 测试数据
10 13
0 1 10
0 2 15
1 3 20
1 4 5
2 5 8
2 6 6
3 7 7
4 5 10
4 7 3
4 8 4
5 6 9
5 8 3
6 8 12
*/
int path[105];//打印路径用的
typedef struct {
int edge[MAX][MAX];//邻接矩阵,记录的是两点之间的距离,也就是权值
int n,m;//边数和顶点数
}Graph;
void Dijkstra(Graph G, int start);
void printPath( int u, int x, int dis[]); //打印从u到x的路径
//主函数
int main()
{
Graph g;
int n, m;
cin >> n >> m;
for ( int i = 0; i < n; ++i ) {
for ( int j = 0; j < n; ++j ) {
g.edge[i][j] = INF;
}
}
for ( int i = 0; i < m; ++i ) {
int s, e, v;
cin >> s >> e >> v;
g.edge[s][e] = v;
g.edge[e][s] = v;
}
g.n = n; g.m = m;
Dijkstra(g, 0);
return 0;
}
//求start到各点的距离, INF表示无穷大
void Dijkstra(Graph G, int start) {
int dis[G.n] = {0}; //此数组表示strat点到i点的最近距离
bool sign[G.n] = {false}; //i点选中了没有
//初始化dis数组
for ( int i = 0; i < G.n; ++i ) {
if ( i == start ) dis[i] = 0;
else dis[i] = INF;
}
sign[start] = true;
for ( int i = 0; i < G.n; ++i ) {
//strat到i可以直接到达
if ( G.edge[start][i] != INF ) {
dis[i] = G.edge[start][i];
path[i] = start;
}
}
for ( int i = 0; i < G.n; ++i ) {
//从dis数组中选出距离strat最近的点,并且此点未被选中
int mins = INF, t = -1;
for ( int j = 0; j < G.n; ++j ) {
if ( !sign[j] && mins > dis[j] ) {
mins = dis[j]; t = j;
}
}
if ( t == -1 ) break; //找了一轮没找到,说明点选中完了
sign[t] = true; //选中t
//把t作为中转点,看可不可以strat->j 通过t中转变得更小 start -> t -> j
for ( int j = 0; j < G.n; ++j ) {
if ( dis[j] > dis[t] + G.edge[t][j] ) {
dis[j] = dis[t] + G.edge[t][j];
path[j] = t;
}
}
}
//结束
for ( int i = 0; i < G.n; ++i ) {
cout << start << "-->" << i << " = ";
if ( dis[i] == INF ) cout << "∞" << endl;
else cout << dis[i] << endl;
}
cout << endl;
for ( int i = 0; i < G.n; ++i ) {
printPath(start, i, dis);
}
}
void printPath( int u, int x, int dis[]) {
int a[MAX], cou = 0, ex = x;
if( u == x )
printf("%d --> %d = 0", u, x);
else if( dis[x] == INF )
printf("%d --> %d = ∞", u, x);//没有路径
else
{
while( x != u) {
a[cou++] = x;
x = path[x];
}
a[cou] = x;
for(int i = cou; i > 0; i--)
printf("%d-->",a[i]);
printf("%d", a[0]);
}
cout << endl;
}
总结:dijkstra三步法
1.初始化数据,dis数组,sign数组
2.从起点更新一下dis数组
3.for循环核心内容
2.Floyd算法 (多源最短路径)
算法模型;三重循环
for ( int k = 0; k < n; ++k ) {
for ( int i = 0; i < n; ++i ) {
for ( int j = 0; j < n; ++j ) {
/*
i->j的距离 为s1
i->k->j的距离为(i通过k中转,然后去到j) s2
如果s1 > s2, 那么就把短的赋值给edge[i][j]
*/
if ( G.edge[i][j] > G.edge[i][k] + G.edge[k][j] ) {
G.edge[i][j] = G.edge[i][k] + G.edge[k][j]
}
}
}
}
注意事项:最外层循环必须是中转点
无了