考研图论算法

图论——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]
            }
		}    
	}   
}

注意事项:最外层循环必须是中转点

无了

3. 关键路径

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值