基础图论知识总结

来源:互联网 发布:战舰世界凤凰数据 编辑:程序博客网 时间:2024/05/20 11:48

1. 最短路

何为最短路?

给定两个顶点,在以这两个点为起点和终点的路径中,边的权值和最小的路径即为最短路

何为单源最短路?何为两点之间的最短路?

固定一个起点,求它到其他所有点的最短路的问题,终点也固定的问题叫做两点之间的最短路问题

BellmanFord 算法

记从起点S 出发到顶点i 的最短路径为d[i] ,则存在下述等式

d[i]=min{d[j]+cost(i,j)|e=(i,j)E} 

其中cost(i,j) 表示路径e(i,j) 的花费

对于给定的初始化起点S ,我们设初值d[S]=0,d[i]=INF ,然后不断更新d 的值即可求出最终的答案

时间复杂度为O(|V|×|E|) 

const int INF = 0x3f3f3f3f;const int MAXN = 1e3 + 5;struct edge{    int from, to, cost;}edge es[MAXN];//边int d[MAXN];//最短距离int V, E;//V是顶点数,E是边数//求解顶点s到所有点的最短距离void shortest_path(int s){    memset(d, 0x3f, sizeof(d));    d[s] = 0;//初始位置最短距离即为0    while(true){        bool update = false;        for(int i = 0;i < E;i ++){            edge e = es[i];            if(d[e.from] != INF && d[e.to] > d[e.from] + e.cost){                d[e.to] = d[e.from] + e.cost;                update = true;            }        }        if(!update) break;    }}

如果要检查负环,则要对d[i] 初始化为0 ,可以检查出所有的负环

时间复杂度为O(|V|×|E|) 

//返回true则存在负环bool find_negative_loop(){    memset(d, 0, sizeof(d));    for(int i = 0;i < V;i ++){        for(int j = 0;j < E;j ++){            edge e = es[j];            if(d[e.to] > d[e.from] + e.cost){                d[e.to] = d[e.from] + e.cost;                //如果第n次仍然更新了,则存在负环                if(i == V - 1) return true;            }        }    }    return false;}

Dijkstra 算法

基于BellmanFord 算法进行优化,做如下修改:

  1. 找到最短距离已经确定的顶点,从它出发更新相邻顶点的最短距离

  2. 此后不需要关系1 中”最短距离已经确定的顶点”

用邻接矩阵实现的Dijkstra 算法的复杂度是O(|V| 2 ) ,如下是使用优先队列进行优化的代码
时间复杂度为O(|E|×log|V|) 

const int MAXN = 1e3 + 5;const int INF = 0x3f3f3f3f;struct edge{    int to, cost;}typedef pair<int, int >PII;//first是最短距离,second是顶点的编号int V;vector<edge>G[MAXN];int d[MAXN];void dijkstra(int s){    //通过制定greater<PII>参数,堆按照first从小到大的顺序取出值    priority_queue<PII, vector<PII>, greater<PII> >que;    memset(d, 0x3f, sizeof(d));    d[s] = 0;    que.push(PII(0, s));    while(!que.empty()){        PII e = que.top();        que.pop();        int v = e.second;        if(d[v] < e.first) continue;//如果取出的不是最短距离则继续取        for(int i = 0;i < G[v].size();i ++){            edge e = G[v][i];            if(d[e.to] > d[v] + e.cost){                d[e.to] = d[v] + e.cost;                que.push(PII(d[e.to], e.to));            }        }    }}

如果存在负边,则还需使用BellmanFord 算法

FloydWarshall 算法

作用:任意两点之间的最短路问题

列出状态转移方程:

d[i][j]=min(d[i][j],d[i][k]+d[k][j]) 

可以处理负数即负边,而判断图中是否有负环,只需检查是否存在d[i][i] 是负数的顶点i 就可以了

int d[MAXN][MAXN];//d[v][u]表示边e=(u,v)的权值(不存在时舍为INF, 不过d[i][i]=0)int V;//顶点数void warshall_floyd(){    for(int k = 0;k < V;k ++){        for(int i = 0;i < V;i ++){            for(int j = 0;j < V;j ++){                d[i][j] = min(d[i][j], d[i][k] + d[k][j]);            }        }    }}

路径还原

在求解最短距离时,满足d[j]=d[k]+cost[k][j] 的顶点k ,就是最短路上顶点j 的前驱结点,因此通过不断赵旭前驱节点就可以恢复最短路,时间复杂度为O(|E|) 

此外,如果用prev[j] 来记录最短路上顶点j 的前驱,那么就可以在O(|V|) 的时间内完成最短路的恢复。在d[j] d[j]=d[k]+cost[k][j] 更新时,修改prev[j]=k ,就可以求得prev 数组,在计算从S 出发到j 的最短路时,通过prev[j] 就可以知道顶点j 的前驱,因此不断把j 替换成prev[j] 知道j=s 为止,

代码:

vector<int> get_path(int t){    vector<int> path;    for(; t != -1; t = prev[t]){        path.push_back(t);    }    reverse(path.begin(), path.end());    return path;}

1.1 多源多汇最短路

何为多源多汇最短路?

即起点有多个,终点也有多个的最短路

相关知识点连接

对于多源多汇最短路,大家也比较熟悉了,FloydWarshall 算法即是经典的多源多汇最短路,求任意两点之间的最短路。

原理分析

而现在要讲的是针对小数目的求解多源多汇最短路的方法-构造超级源点和超级终点(专业用语称为汇点)

针对每一个起点,我们构造一个超级源点,这个超级源点连接每一个需要进行求解的起点,边的权值为0 ,同理可以构造一个超级汇点.

一般超级源点为0 ,超级汇点为点数n+1 ,这个划定是针对顶点为1,2,3...n .

接下直接求超级起点0 开始到超级汇点n+1 的最短路即可


2. 最小生成树

何为最小生成树?

给定一个无向图,如果它的某个子图中任意两个顶点都互相连通并且是一棵树,那么这个数叫做生成树,如果边上有权值,那么使得权值和最小的生成树叫做最小生成树[MST,MinimumSpanningTree] 

Prim 算法

Prim 算法和Dijkstra 算法非常相似,都是从某一个顶点出发,不断增添边的算法

原理解析

我们假设有一颗只包含一个顶点V 的树T ,然后贪心地选取T 和其它顶点之间相连的最小权值的边,并把它加到T 中,不断进行这个操作,就可以得到一颗生成树了。

首先,我们设任意两顶点之间的最小距离为mincost[v] ,那么更新这个数组的状态转移方程为

mincost[v]={min(mincost[v],cost(u,v))|e(u,v)E} 

其中cost(u,v) 为顶点u 与顶点v 之间的权值

简单一点的理解思路:

首先是从一个无向图中随便找一个顶点当作一个起始点,原因很简单,便是对于一个最小生成树而言,他一定包括所有的点,那么以任意一个点为起点对最终的结果是没有什么影响的。
如此,我们接下来就是如何去构造出一个最小生成树出来了。

一个最简单明了的思路,也是与后续即将讲到的kruskal 算法有点相似,都是选择最小的边即选择不是生成树中顶点但与当前已经构成生成树相连接的最小的边。

如果单纯的使用Prim 算法的话,它的时间复杂度O(|V| 2 ) ,我们可以在这里效仿Dijkstra 算法用优先队列进行优化的操作在此处处理,对Prim 算法进行优化,将mincost数组用优先队列进行优化,时间复杂度就变得非常可观了

没有优化的Prim 算法:

时间复杂度:O(|V| 2 ) 

using namespace std;const int MAXN = 1e3 + 5;/***************************/int Cost[MAXN][MAXN];//已经在主函数中读入数据(读入前必须进行初始化设置)int mincost[MAXN];//即为当前我已经选择了的点离其他点的最小距离bool vis[MAXN];//是否已经加入了我选择的顶点中int V;//顶点的个数int prim(){    memset(mincost, 0x3f, sizeof(mincost));    memset(vis, false, sizeof(vis));    mincost[0] = 0;//随便找一个点作为起点,此处可以是mincost[2] = 0,mincost[4] = 0,当然最好是mincost[0] = 0,原因大家思考    int res = 0;//用于计算最终的最小生成树的权值和    while(true){        int v = -1;//用于存储我要将哪个顶点加入到我的最小生成树中来        for(int u = 0; u < V;u ++){//这个循环的目的是为了得到离我现在已经构成的生成树的最小距离的顶点            if(!vis[u] && (v == -1 || mincost[u] < mincost[v])) v = u;        }        if(v == -1) {//没有找到,证明已经得到最小生成树            break;        }        res += mincost[v];        vis[v] = true;        for(int u = 0;u < V;u ++){//更新当前没有加入最小生成树的点离我架构的最小生成树的最小距离,不断更新            mincost[u] = min(mincost[u], Cost[u][v]);        }    }    return res;}

用优先队列进行优化的Prim 算法

时间复杂度:O(|E|log|V|) 

const int INF = 0x3f3f3f3f;const int MAXN = 1e3 + 5;struct edge{    int to, from;}typedef pair<int,int>PII;edge es[MAXN];int V;vector<edge>G[MAXN];int mincost[MAXN];bool vis[MAXN];int Prim(void){    priority_queue<PII, vector<PII>, greater<PII> >que;    memset(mincost, 0x3f, sizeof(mincost));    memset(vis, false, sizeof(vis));    mincost[0] = 0;//随便找一个点作为起点,此处可以是mincost[2] = 0,mincost[4] = 0,当然最好是mincost[0] = 0,原因大家思考    int res = 0;//用于计算最终的最小生成树的权值和    que.push(PII(0,0));    while(!que.empty()){        PII p = que.top();        que.pop();        int v = p.second;        vis[v] = true;//记录当前这个顶点已经加入到了生成树中        res += p.first;//将从前驱顶点到这个顶点的距离加入权值和中        for(int i = 0;i < G[v].size();i ++){            edge &e = G[v][i];            if(vis[e.to]) continue;//如果已经加入到了最小生成树则不进行处理            if(mincost[e.to] > e.cost){//如果当前没有加入生成树的顶点是当前的最小距离的话                mincost[e.to] = e.cost;                que.push(PII(e.to, e.cost));            }        }    }    return res;}

3. 树的直径

何为树的直径?

树的直径指的是一颗生成树中最长路

算法原理分析

求解一颗树的直径的方法有若干种,这里介绍一种最常见的,两个BFS即可解决问题

  1. u st 路径上的一点,结论显然成立,否则设搜到的最远点为T d(u,T)>d(u,s) d(u,T)>d(u,t) 则最长路不是st 了,与假设矛盾.

  2. u 不为st 路径上的点,首先明确,假如u 走到了st 路径上的一点,那么接下来的路径肯定都在st 上了,而且终点为s t ,在1 中已经证明过了
    所以现在又有两种情况了:

    1. u 走到了st 路径上的某点,假设为k ,最后肯定走到某个端点,假设是t  ,则路径总长度为d(u,k)+d(k,t) .

    2. u 走到最远点的路径uT st 无交点,则d(uT)>d(u,k)+d(k,t) 显然,如果这个式子成立,则d(u,T)+d(s,k)+d(u,k)>d(s,k)+d(k,t)=d(s,t) 与最长路不是st 矛盾

所以我们只要以任意一顶点为起点,求解出他的最长路的一个端点v ,然后以v 为起点,求解出最长路的另外一个端点u 

代码:[ 代码原题poj1985 ] 

时间复杂度:O(|E|log|V|) 

struct o {    int to, cost, nxt;} E[MAXN << 1];int Head[MAXN], d[MAXN], tot;bool vis[MAXN];void init_edge() {    memset(Head, -1, sizeof(Head));    tot = 0;}void add_edge(int u, int v, int cost) {    E[tot].to = v;    E[tot].cost = cost;    E[tot].nxt = Head[u];    Head[u] = tot ++;}int BFS(int s) {    int sum = 0, dot = -1;    priority_queue<PII>que;    mem(d, -1);    mem(vis, false);    d[s] = 0;    que.push(PII(0, s));    while(!que.empty()) {        PII p = que.top();        que.pop();        int u = p.second;        if(d[u] > p.first || d[u] == -1) continue;        if(vis[u]) continue;        vis[u] = true;        if(p.first > sum) {            sum = p.first;            dot = u;        }        for(int v = Head[u]; ~v; v = E[v].nxt) {            o &e = E[v];            if(d[e.to] < d[u] + e.cost) {                d[e.to] = d[u] + e.cost;                que.push(PII(d[e.to], e.to));            }        }    }    return dot;}int main() {#ifndef ONLINE_JUDGE    //FIN;    //FOUT;#endif    int n, m;    IO_Init();    while(~scanf("%d%d", &n , &m)) {        int x, y, c;        char S[2];        init_edge();        for(int i = 0; i < m; i ++) {            scanf("%d%d%d%s", &x, &y, &c, S);            add_edge(x, y, c);            add_edge(y, x, c);        }        int v = BFS(1);        int u = BFS(v);        printf("%d\n", d[u]);    }    return 0;}

4. 树的重心

何为树的重心

以这个点为根,那么所有的子树(不算整个树自身)的大小都不超过整个树大小的一半.
另一种解释,即一个点的所有子树的最大值最小,这个点即为树的重心.

原理分析

从任一点出发,求解出每一个点下的所有子树的最大值,然后用变量res 保存这个最大值,尽量取最小就可以了.

代码:[ 代码原题poj1655 ] 

代码复杂度:O(|V|) 

int dp[MAXN], son[MAXN];int Head[MAXN], tot, res, n;struct o{    int to, cost, nxt;}E[MAXN << 1];void init_edge(){    mem(Head, -1);    tot = 0;    res = INF;}void add_edge(int u, int v, int cost){    E[tot].to = v;    E[tot].cost = cost;    E[tot].nxt = Head[u];    Head[u] = tot ++;}void dfs(int u, int f){    son[u] = 1;    int nson = 0;    for(int v = Head[u]; ~v; v = E[v].nxt){        o &e = E[v];        if(e.to == f) continue;        dfs(e.to, u);        son[u] += son[e.to];        nson = max(nson, son[e.to]);    }    dp[u] = max(nson,  n - son[u]);    res = min(dp[u], res);}int main() {#ifndef ONLINE_JUDGE    // FIN;    // FOUT;#endif    int T;    int x, y;    IO_Init();    scanf("%d", &T);    while(T --){        init_edge();        scanf("%d", &n);        for(int i = 1;i < n;i ++){            scanf("%d%d", &x, &y);            add_edge(x, y, 1);            add_edge(y, x, 1);        }        dfs(1, 0);        for(int i = 1;i <= n;i ++){            if(dp[i] == res){                printf("%d %d\n", i, res);                break;            }        }    }    return 0;}

知识点未完,敬请期待!

[ 如果有哪些地方看不懂的,记得留言哦 !!] 

1 0