图的遍历

图的遍历方法一般有两种:深度优先搜索(DFS)和广度优先搜索(BFS)

采用深度优先搜索(DFS)遍历图

沿着一条路径直到无法继续前进,才退回到路径上离当前顶点最近的还存在未访问分支顶点的岔道口,并前往访问那些未访问分支顶点,直到遍历完整个图。
两个概念:
连通分量:在无向图中,如果两个顶点之间可以相互到达(可以是通过一定路径间接到达),那么就称这两个顶点连通。如果G(V,E)的任意两个顶点都连通,则称图G为连通图;否则,称图G为非连通图,且称其中的极大连通子图为连通分量。
强连通分量:在有向图中,如果两个顶点可以各自通过一条有向路径到达另一个顶点,就称这两个顶点强连通。如果图G(V,E)的任意两个顶点都强连通,则称图G为强连通图;否则,称图G为非强连通图,且称其中的极大强连通子图为强连通分量。
把连通分量和强连通分量均称为连通块
如果要遍历整个图,就需要对所有连通块分别进行遍历。
基本思路:将经历过的顶点设置为已访问,在下次递归碰到这个顶点时就不再去处理,直到整个图的顶点都被标记为已访问。
如已知给定的图时一个连通图,则只需要一次DFS就能完成遍历
伪代码:

DFS(u) {    //访问顶点u 
    vis[u] = true;  //设置u已被访问 
    for(从u出发能到达的所有顶点v)  //枚举从u出发可以到达的所有顶点v
        if vis[v] == false      //如果v未被访问 
            DFS(v);     //递归访问v 
}

DFSTrave(G) {   //遍历图G 
    for(G的所有顶点u)    //对G的所有顶点u
        if vis[u] == false  //如果u未被访问
            DFS(u);     //访问u所在的连通块 
} 

实现邻接矩阵和邻接表前需要先定义MAXV为最大顶点数、INF为一个很大的数字

const int MAXV = 1000;    //最大顶点数
const int INF = 1000000000;  //设INF为一个很大的数

邻接矩阵版本

int n, G[MAXV][MAXV];       //n为顶点数,MAXV为最大顶点数
bool vis[MAXV] = {false};   //如果顶点i已被访问,则vis[i] == true。初值为false

void DFS(int u, int depth) {    //u为当前访问的顶点标号,depth为深度 
    vis[u] = true;      //设置u已被访问
    //如果需要对u进行一些操作,可以在这里进行
    //下面对所有从u出发能到达的分支顶点就像枚举
    for(int v = 0; v < n; v++) {
        if(vis[v] == false && G[u][v] != INF) {     //如果v未被访问,且u可到达v 
            DFS(v, depth + 1);      //访问v, 深度加一 
        }
    } 
} 

void DFSTrave() (
    for(int u = 0; u < n; u++) {        //对每个顶点u 
        if(vis[u] == false) {   //如果u未被访问    
            DFS(u, 1);      //访问u和u所在的连通块,1表示初始为第一层 
        }
    }
) 

邻接表版本

vector Adj[MAXN];  //图G的邻接表
int n;      //n为顶点数,MAXV为最大顶点数
bool vis[MAXV] = {false};   //如果顶点i已被访问,则vis[i] == true。初值为false
 
void DFS(int u, int depth) {    //u为当前访问的顶点标号,depth为深度 
    vis[u] = true;  //设置u已被访问 
    //如果需要对u进行一些操作,可以在这里进行
    for(int i = 0; i < Adj[u].size(); i++) {
        int v = Adj[u][i];
        if(vis[v] == false){
            DFS(v, depth + 1);  //访问v, 深度加一 
        }
    } 
} 

void DFSTrave() (
    for(int u = 0; u < n; u++) {        //对每个顶点u 
        if(vis[u] == false) {   //如果u未被访问    
            DFS(u, 1);      //访问u和u所在的连通块,1表示初始为第一层 
        }
    }
) 

实例:
给出若干人之间的通话长度(视为无向边),这些通话将他们分为若干组。每个组的总边权设为该组内的所有通话的长度之和,而每个人的点权设为该人参与的通话长度之和。现在给定一个阀值K,且只要一个组的总边权超过K,并满足成员人数超过2,则将该组视为”犯罪团伙(Gang)“,而该组内点权最大的人视为头目。要求输出”犯罪团伙“的个数,并按头目姓名字典序从小到大的顺序输出每个”犯罪团伙“的头目姓名和成员人数。
思路:

  1. 解决姓名和编号的对应关系。可使用map直接建立字符串与整型的映射关系,也可使用字符串hash的方法将字符串转换为整型。编号与姓名的对应关系则可以直接使用string数字进行定义,或者使用map也是可以的
  2. 需要获得每个人的点权,即与之相关的通话记录的时长之和,可以在读入时进行处理。该步在求与某个点相连的边的边权之和
  3. 图的遍历。使用DFS遍历每个连通块,目的是获取每个连通块的头目(即连通块内点权最大的结点)、成员个数、总边权
  4. 通过步骤3可以获得连通块的总边权totalValue。如果totalValue大于给定的阀值K,且成员人数大于2,则说明该连通块是一个团伙,将该团伙的信息存储下来
    可以定义map,来建立团伙头目的姓名与成员人数的映射关系。由于map中元素自动按键从小到大排序。

注意点:

  1. 由于通话记录的条数最多有1000条,意味着不同的人可能有2000人,因此数组大小必须在2000以上
  2. 可以使用使用结构体来存放头目姓名与成员人数,但会增加一定代码量
struct Gang {
    string head;    //团伙头目
    int numMember;  //成员数量
}arrayGang[maxn];
int numGang = 0;    //团伙个数
bool cmp(Gang a, Gang b) {
    return a.head < b.head;     //按头目姓名的字典序从小到大排序 
}
  1. 由于每个结点在访问后不应再次被访问,但是图中可能有环,即遍历过程中发生一条边连接已访问结点 的情况。此时为了边权不被漏加,需要先累加边权,再去考虑结点递归访问的问题
  2. 本题也可用并查集解决
//输入1
8 59
AAA BBB 10
BBB AAA 20
AAA CCC 40
DDD EEE 5
EEE DDD 70
FFF GGG 30
GGG HHH 20
HHH FFF 10
//输出1
2
AAA 3
GGG 3
//输入2
8 70
AAA BBB 10
BBB AAA 20
AAA CCC 40
DDD EEE 5
EEE DDD 70
FFF GGG 30
GGG HHH 20
HHH FFF 10
//输出2
0
#include
#include
#include
using namespace std;
const int maxn = 2010;  //总人数
const int INF = 1000000000; //无穷大

map intToString;   //编号->姓名
map stringToInt;   //姓名->编号
map Gang;          //head->人数
int G[maxn][maxn] = {0}, weight[maxn] = {0};    //邻接矩阵G,点权weight
int n, k, numPerson = 0;    //边数n、下限k、总人数numPerson
bool vis[maxn] = {false};   //标记是否被访问

//DFS函数访问单个连通块,nowVisit为当前访问的编号
//head为头目,numMember为成员编号,totalValue为连通块的总边权/
void DFS(int nowVisit, int& head, int& numMember, int& totalValue) {
    numMember++;    //成员人数+1
    vis[nowVisit] = true;   //标记已被访问
    if(weight[nowVisit] > weight[head]) {
        head = nowVisit;    //更新头目 
    } 
    for(int i = 0; i < numPerson; i++) {
        if(G[nowVisit][i] > 0) {    //如果nowVisit能到达i 
            totalValue += G[nowVisit][i];   //连通块的总边权增加该边权
            G[nowVisit][i] = G[i][nowVisit] = 0;    //删除这条边,防止回头
            if(vis[i] == false) {
                DFS(i, head, numMember, totalValue);
            } 
        }
    }
} 

//DFSTrave函数遍历真个图,获得每个连通块的信息
void DFSTrave() {
    for(int i = 0; i < numPerson; i++) {
        if(vis[i] == false) {
            int head = 1, numMember = 0, totalValue = 0;
            DFS(i, head, numMember, totalValue);
            if(numMember > 2 && totalValue > k) {
                Gang[intToString[head]] = numMember;
            }
        }
    }
} 

//change函数返回姓名str对应的编号
int change(string str) {
    if(stringToInt.find(str) != stringToInt.end()) {  //如果str已经出现过, 
        return stringToInt[str];            //返回编号 
    } else {
        stringToInt[str] = numPerson;   //str的编号为numPerson
        intToString[numPerson] = str;   //numPerson对应str
        return numPerson++; 
    }
} 

int main() {
    int w;
    string str1, str2;
    cin >> n >> k;
    for(int i = 0; i < n ; i++) {
        cin >> str1 >> str2 >> w;
        int id1 = change(str1);
        int id2 = change(str2);
        weight[id1] += w;
        weight[id2] += w;
        G[id1][id2] += w;
        G[id2][id1] += w;
    } 
    DFSTrave();
    cout << Gang.size() << endl;
    map::iterator it;
    for(it = Gang.begin(); it != Gang.end(); it++) {
        cout << it->first << " " << it->second << endl;
    }
    return 0;
}

采用广度优先搜索(BFS)遍历图

通过反复取出队首顶点,将该顶点可到达的未曾加入过队列(而不是未访问)的顶点全部入队,直到队列为空时遍历结束。
伪代码:

BFS(u) {    //遍历u所在的连通块 
    queue q; //定义队列q
    将u入队;
    inq[u] = true;  //设置u已入过队
    while(q非空) {    //只要队列非空 
        取出q的队首元素u进行访问
        for(从u出发可达的所有顶点v) { //枚举从u能直接到达的顶点v 
            if(inq[v] == false) {   //如果v未曾加入过队列 
                将v入队;
                inq[v] = true;  //设置v已加入过队列 
            }
        } 
    } 
} 

BFSTrave(G) {   //遍历图G 
    for(G的所有顶点u)        //枚举G的所有顶点 
        if(inq[u] == false) {   //如果u未曾入过队列 
            BFS(u);     //遍历u所在的连通块 
        } 
} 

邻接矩阵版本

bool inq[MAXV] = {false};   //记录顶点是否入过队列 
 
void BFS(int u) {
    queue q;
    q.push(u);
    inq[u] = true;
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        for(int v = 0; v < n; v++) {
            if(inq[v] == false && G[u][v] != INF){
                q.push(v);
                inq[v] = true;
            }
        }
    }
}

void BFSTrave() {
    for(int u = 0; u < n; u++) {
        if(inq[u] == false) {
            BFS(u);
        }
    }
}

邻接表版本

vector Adj[MAXN];
int n;
bool inq[MAXV] = {false};

void BFS(int u) {
    queue q;
    q.push(u);
    inq[u] = true;
    while(!q.empty()) {
        int u = q.front();
        q.pop();
        for(int i = 0; i < Adj[u].size();i++) {
            int v = Adj[u][i];
            if(inq[v] == false) {
                q.push(v);
                inq[v] = true;
            }
        }
    }
} 

void BFSTrave() {
    for(int u = 0; u < n; u++) {
        if(inq[u] == false) {
            BFS(u);
        }
    }
}

如果需要存放顶点层号,需要定义结构体Node,存放顶点的编号和层号

struct Node {
    int v;
    int layer;
};

vector Adj[N];

void BFS(int s) {
    queue q;
    Node start;
    start.v = s;
    start.layer = 0;
    q.push(start);
    inq[start.v] = true;
    inq(!q.empty()) {
        Node topNode = q.front();
        q.pop();
        int u = topNode.v;
        for(int i = 0; i < Adj[u].size(); i++) {
            Node next = Adj[u][i];
            next.layer = topNode.layer + 1;
            if(inq[next.v] == false) {
                q.push(next);
                inq[next.v] = true;`
            }
        }
    } 
}

实例:
在微博中,每个用户都可能被若干个其他用户关注。而当该用户发布一条信息时,他的关注者就可以看到这条信息并选择是否转发它,且转发的信息也可以被转发者的关注者再次转发,但同一用户最多只能转发该信息一次(信息的最初发布者不会转发该信息)。现在给出N个用户的关注情况(即他们各自关注了哪些用户)以及一个转发层数上限L,并给出最初发布消息的用户编号,求在转发层上限内消息最多会被多少用户转发

//输入
7 3
3 2 3 4
0
2 5 6
2 3 1
2 3 4
1 4
1 5
2 2 6
//输出
4
5
#include
#include
#include
#include
using namespace std;
const int MAXV = 1010;

struct Node {
    int id;
    int layer;
};

vector Adj[MAXV]; //邻接表
bool inq[MAXV] = {false};

int BFS(int s, int L) {
    int numForward = 0;
    queue q;
    Node start;
    start.id = s;
    start.layer = 0;
    q.push(start);
    inq[start.id] = true;
    while(!q.empty()) {
        Node topNode = q.front();
        q.pop();
        int u = topNode.id;
        for(int i = 0; i < Adj[u].size(); i++) {
            Node next = Adj[u][i];
            next.layer = topNode.layer + 1;
            if(inq[next.id] == false && next.layer <= L) {
                q.push(next);
                inq[next.id] = true;
                numForward++;
            }
        }
    }
    return numForward;
} 

int main() {
    Node user;
    int n, L, numFollow, idFollow;
    scanf("%d%d", &n, &L);
    for(int i = 1; i <= n; i++) {
        user.id = i;
        scanf("%d", &numFollow);
        for(int j = 0; j < numFollow; j++) {
            scanf("%d", &idFollow);
            Adj[idFollow].push_back(user);
        }
    }
    int numQuery, s;
    scanf("%d", &numQuery); //查询个数
    for(int i = 0; i < numQuery; i++) {
        memset(inq, false, sizeof(inq));    //inq数组初始化 
        scanf("%d", &s);                //起始结点编号 
        int numForward = BFS(s, L);         //BFS,返回转发数 
        printf("%d\n", numForward);         //输出转发数 
    } 
    return 0;
}

你可能感兴趣的:(图的遍历)