图论——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;}}//以上步骤找到了最近的点tif ( 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; //刚开始连通分量为nwhile ( 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,因为此时生成树个数为0while ( 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 -> jfor ( 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 -> jfor ( 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的距离 为s1i->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]}} }
}
注意事项:最外层循环必须是中转点
无了