Graph

图定义和相关术语

抽象看,图由顶点(Vertex)和边(Edge)组成,可记作G(V,E)。连接边的两个顶点一定要是V中的,可以存在独立的顶点。

图分为有向图无向图

顶点的是指和该顶点相连的边的条数,对于有向图,顶点的出边条数称为出度,入边条数成为入度。

顶点和边都可以有一定属性,而量化的属性称为权值,顶点的权值称为点权,边的权值成为边权

图的存储

一般图的存储方式有两种,邻接矩阵和邻接表。

邻接矩阵

设图G(V,E)的顶点标号为0,1,...N-1,那么可以令二维数组G[N][N]的两维分别表示图的顶点标号,G[i][j]的值表示边权(或有无边)。
这种存储方式比较耗内存。

邻接表

设图G(V,E)的顶点标号为0,1,...N-1,把每个顶点的出边放在一个列表中,那么N个顶点就有N个列表,而每个列表的元素可以是一个struct,里面包含边的重点编号和边权。

图的遍历

DFS

沿着一条路径直到无法继续前进,才退回到路径上离当前顶点最近的还存在未访问分支顶点的岔道口,并前往访问那些未访问分支顶点,直到遍历完整个图。

BFS

使用BFS遍历图的基本思想是建立一个队列,并把初始顶点加入队列,此后每次都取出队首顶点进行访问,并把从该顶点出发可以达到的为曾加入过队列(而不是未访问)的顶点全部加入队列,直至队列为空。

#include<cstdio>
#include<vector>
#include<queue>
using namespace std;

const int N=6;

struct Node {
    int v;   // 边的终点编号
    int w;   // 边权
    Node(int _v, int _w) : v(_v), w(_w) {}   // 构造函数
};

vector<Node> Adj[N];
bool vis[N]={false};   // 顶点是否被访问过

void DFS(int u, int depth) {   // u为当前访问顶点,depth为深度
    printf("%d ", u);
    vis[u]=true;
    for(int i=0; i<Adj[u].size(); i++) {
        Node temp = Adj[u][i];
        if(vis[temp.v]==false) {
            DFS(temp.v, depth+1);
        }
    }
}

void GTravelDFS() {
    for(int i=0; i<N; i++) {
        if(vis[i] == false) {
            DFS(i, 1);
        }
    }
}

void BFS(int u) {
    queue<int> q;
    q.push(u);
    vis[u]=true;
    while(!q.empty()) {
        int top = q.front();
        printf("%d ", top);
        q.pop();
        for(int i=0; i<Adj[u].size(); i++) {
            Node temp = Adj[u][i];
            if(vis[temp.v] == false) {
                q.push(temp.v);
                vis[temp.v]=true;
            }
        }
    }
}

void GTravelBFS() {
    for(int i=0; i<N; i++) {
        if(vis[i] == false) {
            BFS(i);
        }
    }
}

int main() {
    // 顶点1 》顶点3,边权为2
    Adj[0].push_back(Node(1, 2));
    Adj[0].push_back(Node(2, 1));
    Adj[1].push_back(Node(3, 2));
    Adj[1].push_back(Node(4, 2));
    Adj[2].push_back(Node(1, 2));
    Adj[2].push_back(Node(4, 1));
    Adj[4].push_back(Node(3, 1));
    Adj[4].push_back(Node(5, 2));
    Adj[5].push_back(Node(3, 1));
    //GTravelDFS();
    GTravelBFS();
    return 0;
}

最短路径

给定图G(V,E),求一条起点到终点的路径,使得这条路径上经过的所有边的边权之和最小,即最短路径。

解决最短路径的常用算法有Dijkstra算法、Bellman-Ford算法、SPFA算法和Floyd算法。

Dijkstra算法

Dijkstra算法解决的是单源最短路径问题,即给定图G(V,E)和起点s(起点又称为源点),求从起点s到达其他顶点的最短距离。其算法策略如下:

设置集合M存放已被访问的顶点,然后执行n次下面的两个步骤(n为顶点个数):

  1. 每次从未被访问的顶点中选择与起点s的最短距离最小的一个顶点(记为u),访问并加入集合M
  2. 之后,令顶点u为中介点,优化起点s与所有从u能达到的顶点之间的最短距离。
#include<cstdio>
#include<vector>
using namespace std;

const int MAXV=100;   // 最大顶点数
const int INF = 1000000;   // 设INF为一个很大的数
int n, m, st;  // 顶点数, 边数,起点

struct Node {
    int v, dis;   // v为边的终点顶点,dis为边权
};

int d[MAXV];   // 起点达到各顶点的最短距离
bool vis[MAXV] = {false};   // 标记数组,false表未访问

vector<Node> Adj[MAXV];
vector<int> pre[MAXV];   // pre[v]表示全部的从起点到顶点v的最短路径上v的前一个顶点的数组


void Dijkstra(int s) {   // s为起点
    fill(d, d+MAXV, INF);   // 将整个数组元素赋值INF
    d[s] = 0;   // 起点到自身的距离是0
    pre[s].push_back(s);
    for(int i=0; i<n; i++) {
        int u=-1, MIN=INF;   // u为未被访问的顶点中选择与起点s的最短距离最小的一个顶点
        for(int j=0; j<n; j++) {
            if(vis[j]==false && d[j]<MIN) {
                u = j;
                MIN = d[j];
            }
        }
        if(u==-1) return;   // 剩下的顶点和u不连通
        vis[u]=true;   // 标记已访问
        for(int j=0; j<Adj[u].size(); j++) {
            int v = Adj[u][j].v;
            int dis = Adj[u][j].dis;
            if(vis[v]==false && d[u]+dis < d[v]) {   // 优化起点s与所有从u能达到的顶点之间的最短距离
                d[v] = d[u] + dis;
                pre[v].clear();
                pre[v].push_back(u);
            } else if(vis[v]==false && d[u]+dis==d[v]) {
                pre[v].push_back(u);
            }
        }
    }
}

int optvalue;   // 第二标尺最优值
vector<int> path, tempPath;   // 最优路径,临时路径
int W[MAXV];   // 点权

void DFS(int v) {   // v为当前访问顶点
    if(v == st) {
        tempPath.push_back(v);
        int value=0;
        for(int i=tempPath.size()-1; i>=0; i--) {
            int id = tempPath[i];
            value += W[id];
        }
        if(value>optvalue) {
            optvalue = value;
            path = tempPath;
        }
        tempPath.pop_back();
        return;
    }
    tempPath.push_back(v);
    for(int i=0; i<pre[v].size(); i++) {
        DFS(pre[v][i]);
    }
    tempPath.pop_back();
}

int main() {
    scanf("%d%d%d", &n, &m, &st);
    for(int i=0; i<n; i++) {
        scanf("%d", &W[i]);
    }

    for(int i=0; i<m; i++) {
        int l, r, dis;
        Node temp1, temp2;
        scanf("%d%d%d", &l, &r, &dis);
        temp1.v = r, temp1.dis = dis;
        Adj[l].push_back(temp1);
        temp2.v = l, temp2.dis = dis;
        Adj[r].push_back(temp2);
    }

    Dijkstra(st);
    for(int i=0; i<n; i++) {
        //printf("%d:", d[i]);
        printf("%d: ",i);
        for(vector<int>::iterator it=pre[i].begin(); it != pre[i].end(); it++) {
            printf("%d ", *it);
        }
        printf(" \n");
    }

    DFS(5);
    for(int i=path.size()-1; i>=0; i--) {
        printf("%d ", path[i]);
    }
    return 0;
}

说明:

  • Dijkstra算法只能应对边权都是非负数的情况,如果边权出现负数,那么很可能会出错,这是最好使用SPFA算法。
  • 如果是无向边,只需要把无向边当成指向相反的有向边即可。对邻接表来说,只需在u的邻接表Adj[u]末尾添加上v,并在v的邻接表末尾添加上u。

Bellman-Ford算法和SPFA算法

Bellman-Ford算法也是解决单源最短路径问题,能处理边权是负数的情况

Floyd算法

Floyd算法用来解决全源最短路径问题,即给定图G(V,E),求任意两点u,v之间的最短路径长度,时间复杂度是O(n^3)。

如果存在顶点k,使得以k作为中介点时顶点i和顶点j的当前最短距离缩短,则使用顶点k作为顶点i和顶点k的中介点,即当dis[i][k]+dis[k][j] < dis[i][j]时,令dis[i][j]=dis[i][k]+dis[k][j]

最小生成树

定义

最小生成树是在一个给定的无向图G(V,E)中求一棵树T,使得这棵树拥有图G中的所有顶点,且所有的边都是来自图G中的边,并且满足整棵树的边权之和最小。

满足性质

  • 最小生成树是树,因此其边数等于顶点数减1,且数内一定不会有环
  • 对给定的图G(V,E),其最小生成树可以不唯一,但其边权之和一定是唯一的。
  • 由于最小生成树是在无向图上生成的,因此其根结点可以是这棵树上的任意一个结点。

求解最小生成树一般有两种算法,即prim算法和kruskal算法。

prim算法

prim算法的基本思想是对图G(V,E)设置集合S,来存放已经被访问的顶点,然后执行n次下面的两个步骤(n为顶点个数)

  1. 每次从集合V-S(即未访问)中选择与集合S最近的一个顶点(记为u),访问u并将其加入集合S,同时把这条离集合S最近的边加入最小生成树中
  2. 令顶点u作为集合S与集合V-S连接的接口,优化从u能到达的未访问顶点与集合S的最短距离
#include<cstdio>
#include<vector>
using namespace std;

const int MAXV=1000;
const int INF=10000000;
struct Node{
    int v, dis;
    Node(int _v, int _dis) : v(_v), dis(_dis) {}   // 构造函数
};
vector<Node> Adj[MAXV];
int n;
int d[MAXV];   // 顶点与集合S的最短距离,此处与dijkstra不同
bool vis[MAXV]={false};

int prim() {   // 默认0号为初始点
    fill(d, d+MAXV, INF);
    d[0]=0;
    int ans=0;   // 存放最小生成树的边权之和
    for(int i=0; i<n; i++) {
        int u=-1, MIN=INF;
        for(int j=0; j<n; j++) {
            if(vis[j]==false && d[j]<MIN) {
                u=j;
                MIN=d[j];
            }
        }
        if(u == -1) return -1;
        vis[u]=true;
        ans += d[u];
        for(int k=0; k<Adj[u].size(); k++) {
            int v=Adj[u][k].v, dis=Adj[u][k].dis;
            if(vis[v]==false && dis<d[v]) {   // 此处与dijkstra不同
                d[v]=dis;
            }
        }
    }
    return ans;
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i=0; i<m; i++) {
        int l, r, dis;
        scanf("%d%d%d", &l, &r, &dis);
        Adj[l].push_back(Node(r, dis));
        Adj[r].push_back(Node(l, dis));
    }
    int ans = prim();
    printf("%d\n", ans);
    for(int i=0; i<n; i++) {
        printf("%d ", d[i]);
    }
    return 0;
}

prim算法的时间复杂度是O(V^2),其中邻接表实现的prim算法可以通过堆优化使时间复杂度降为O(VlogE+E)

kruskal算法

kruskal算法的基本思想:

  1. 对所有边按边权从小到大进行排序
  2. 按边权从小到大测试所有边,如果当前测试边所连接的两个顶点不在同一个连通块中,则把这条测试边加入当前最小生成树中;否则将边舍弃
  3. 执行步骤2,直到最小生成树中的边数等于总顶点数减1或是测试完所有边时结束。而当结束时如果最小生成树的边数小于总顶点数减1,说明该图不连通
#include<cstdio>
#include<algorithm>
using namespace std;

const int MAXV=110;   // 最大顶点数
const int MAXE=10010;   // 最大边数

struct edge{
    int v, u;   // 边的两个端点
    int cost;   // 边权
}E[MAXE];

bool cmp(edge a, edge b) {
    return a.cost < b.cost;
}

// 并查集部分
int father[MAXV];   

int findFather(int x) {
    int a=x;
    while(x != father[x]) {
        x = father[x];
    }
    // 路径压缩
    while(a != father[a]) {
        int z = a;
        a = father[a];
        father[z] = x;
    }
    return x;
}

bool noSame(edge E) {
    int u = findFather(E.u);
    int v = findFather(E.v);
    if(u != v) {
        return true;
    }
    return false;
}

int kruskal(int n, int m) {   // n顶点数  m边数
    int ans=0, numEdge=0;
    for(int i=0; i<n; i++) {
        father[i]=i;
    }
    sort(E, E+m, cmp);
    for(int i=0; i<m; i++) {
        if(noSame(E[i])) {
            father[E[i].u] = E[i].v;
            ans += E[i].cost;
            numEdge++;
            if(numEdge == n-1) break;
        }
    }
    if(numEdge != n-1) {
        return -1;
    }
    return ans;
}

int main() {
    int n, m;
    scanf("%d%d", &n, &m);
    for(int i=0; i<m; i++) {
        scanf("%d%d%d", &E[i].u, &E[i].v, &E[i].cost);
    }
    int ans = kruskal(n, m);
    printf("%d\n", ans);
    return 0;
}

kruskal算法的时间复杂度主要源于对边进行排序,因此其时间复杂度是O(ElogE),其中E为图的边数。

如果是稠密图(边多),则用prim算法;如果是稀疏图(边少),则用kruskal算法

拓扑排序

有向无环图

如果一个有向图的任意顶点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图(Directed Acyclic Graph-DAG)。

拓扑排序

拓扑排序是将有向无环图G的所有顶点排成一个线性序列,使得对图中的任意两个顶点u、v,如果存在边u->v,那么序列中u一定在v前面。这个序列又被称为拓扑排序。

基本思路如下:

  1. 定义一个队列Q,并把所有入度为0的结点加入队列
  2. 取队列首结点,输出。然后删去所有从它出发的边,并令这些边到达的顶点的入度减1,如果某个顶点的入度减为0,则将其加入队列
  3. 反复进行2操作,直到队列为空。如果队列为空时入过队的结点数目恰好为N,说明拓扑排序成功,图G为有向无环图;否则,拓扑排序失败,图G中有环
#include<cstdio>
#include<queue>
#include<vector>
using namespace std;

const int MAXV=100;

int n, m ,inDegree[MAXV];   // 顶点数,边数,各顶点入度数
vector<int> Adj[MAXV];

void topoLogicalSort() {
    int passNum=0;
    queue<int> q;
    for(int i=0; i<n; i++) {
        if(inDegree[i]==0) {
            q.push(i);
            passNum++;
        }
    }
    while(!q.empty()) {
        int top = q.front();
        printf("%d ", top);
        q.pop();
        for(int i=0; i<Adj[top].size(); i++) {
            int end = Adj[top][i];
            inDegree[end]--;
            if(inDegree[end]==0) {
                q.push(end);
                passNum++;
            }
        }
    }
    if(passNum==n) {
        printf("\nsuccess\n");
    }
}

int main() {
    scanf("%d%d", &n, &m);
    for(int i=0; i<n; i++) {
        scanf("%d", &inDegree[i]);
    }
    for(int i=0; i<m; i++) {
        int st, ed;
        scanf("%d%d", &st, &ed);
        Adj[st].push_back(ed);
    }
    topoLogicalSort();
    return 0;
}

关键路径

AOV网和AOE网

顶点活动(Activity On Vertex,AOV)网是指用顶点表示活动,而用边集表示活动间优先关系的有向图。

边活动(Activity On Edge,AOE)网是指用带权的边集表示活动,而用顶点表示事件的有向图,其中边权表示完成活动需要的时间。

关键路径

关键路径就是AOE网的最长路径,而把关键路径上的活动成为关键活动。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容

  • 1)这本书为什么值得看: Python语言描述,如果学的Python用这本书学数据结构更合适 2016年出版,内容...
    孙怀阔阅读 12,435评论 0 15
  • 1. 图的定义和基本术语 线性结构中,元素仅有线性关系,每个元素只有一个直接前驱和直接后继;树形结构中,数据元素(...
    yinxmm阅读 5,427评论 0 3
  • 图的基本概念 图由结点的有穷集合V和边的集合E组成。图中常常将结点成为顶点,边是顶点的有序偶对。若两个顶点之间存在...
    桔子满地阅读 1,371评论 0 0
  • 线性表是一对一,树是一对多,图是多对多的关系。 图中的数据元素我们称为顶点(相对于树中的结点),顶点集合不能为空,...
    XDgbh阅读 12,556评论 0 0
  • pre - 向前,ante - 也代表向前re-往后,回retro-往后,回inter-中间,来回ex-pro-o...
    朝歌晚舞阅读 361评论 0 0