基础图论知识总结

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;
vectorG[MAXN];
int d[MAXN];

void dijkstra(int s){

    //通过制定greater参数,堆按照first从小到大的顺序取出值
    priority_queuevector, greater >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;
vectorG[MAXN];
int mincost[MAXN];
bool vis[MAXN];


int Prim(void){
    priority_queuevector, greater >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_queueque;
    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;
}

知识点未完,敬请期待!

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

你可能感兴趣的:(基础图论知识总结)