树与图的存储及遍历(分别①采用DFS深度优先遍历;②采用BFS宽度优先遍历)

树与图的存储

  • 树是一种特殊的图,只是在树中没有环(树是无环连通图),与图的存储方式相同. ==>因此只考虑图的存储即可 ==> 图分为有向图和无向图 ==> 对于无向图中的边a—b,存储两条有向边a->b和b->a,即无向图就是特殊有向图 ==> 因此只考虑有向图的存储即可(如下)

有向图的存储:

  • ① 邻接矩阵 ==> 不便利不推荐使用
    • 思路:g[a][b] 存储边a->b
    • 空间复杂度:O(n²)
  • ② √ 邻接表 ==> 用的最多
    • 即之前学习的单链表:为每个节点开一个单链表
    • 每个单链表中节点存的顺序没有要求
邻接表的存储(经典存储方式):
// 对于每个点k,开一个单链表,存储k所有可以走到的点。h[k]存储这个单链表的头结点
int h[N], e[N], ne[N], idx;

// 添加一条边a->b
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

// 初始化
idx = 0;
memset(h, -1, sizeof h);

树与图的遍历

  • 对于图的遍历有两个思路:深度和宽度优先遍历
    • 两者时间复杂度都为O(n+m) (n表示点数,m表示边数)

①树与图的遍历:采用DFS(深度优先遍历)

  • DFS相似于树的先序遍历
  • 模板
int dfs(int u)
{
    st[u] = true; // 表示点u已经被遍历过

    for (int i = h[u]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j]) dfs(j);
    }
}

例题:846. 树的重心 - AcWing题库

  • 本题是无向无环树
思路
  • 通过对每个节点DFS,计算得出以根节点u为重心的最大子连通块的节点总数,并采用res记录起来,同时不断更新ans ==> DFS不直接返回答案,而是在每次有节点走投无路时,更新迭代一次答案
图解
  • 🔺🔺🔺代码分析图解
    请添加图片描述
  • h[]分析与无向图的建立图解:
    请添加图片描述
代码分析
  • 分析点①for (int idx = h[u]; idx != -1; idx = ne[idx])整个for循环的用途:遍历u所有的相连结点(包括子结点+父节点)。
    • 遍历子节点:依次循环遍历每个子连通块
      • 作用a.不断比较更新res==>保留以u为重心删后,u以下部分中,最大的子连通块数
      • 作用b.每遍历一块子连通块就把节点数不断往sum上叠加==>最终记录包括u节点在内,往下整个连通块的总数
    • 遍历到父节点:走不通了,故完整返回以u节点为根的整个连通块的信息
      • 所有子节点走完后,它也往上面走,但由于上面来时状态已更新为true,相当于走了也白走(堵住了)
  • 分析点②为什么 res = max(res, n - sum);
    • u上半部分也属于,以u为重心删除后u的一个连通块。故下半部分最大的连通块仍需与上半部分一整个连通块,max比较下,res才能最终确定以u为重心删除后,最大的连通块数。
      • 等号右侧的res:是经历了for循环后,以u为重心删除后,u下半部分里,所有子连通块中最大的连通块数。
      • n - sum:根节点上面一坨的连通块的节点总数(如图解①的n-sum,不包括根节点)
    • max(res,n-sum)对于首次遍历的节点来说是没必要的;但对后续的子节点dfs时,由于已经过滤掉其父节点了,for循环中就不能获取到以父节点为根的子树,这种情况就需要max(n-sum,res)来保证
  • 分析点③为什么返回sum?
    • 因为sum表示的是以当前节点u为根的连通块节点总数。
      • 若当前节点没被遍历过,就会一直递归遍历子节点,直到叶子节点时返回。递归走到头后,通过返回sum,依次回溯叠加sum(sum都是从下一个节点返回上来的),从而累加得到节点总数(包括根节点)
    • sum的返回并不是用作main函数中结果的输出,主要是用于dfs内部自身要不断往深处走,不断调用来接收返回叠加的sum;所以也可见分析点⑤处,它并没有用某个变量来接收,只是执行,在每次执行中用全局变量ans记录最终结果
  • 分析点④每次一个节点走投无路时,就会得出其最大的连通块(即res),并且与历史中最小的最大连通块比较(即ans),最后更新最小的最大连通块在ans中
    • ans是通过“每个重心删除后的最大连通块res”和“历史ans”的min比较后,直接赋值,作为全局变量被改变,不必返回。
  • 分析点⑤为什么dfs()可以选任意数字?
    • 任意选定节点 u<=n
    • 因为深度优先遍历从任何节点出发都会遍历到整个连通图
细节思考
  • 思考①:题意中“返回最小的最大的连通子图”的意思
    • 最大:指删除某结点后,剩下的许多连通块里最多点数的
    • 最小:每个结点都对应有删除它之后的连通块的最大数,在所有节点的连通块的最大数中再取最小值
  • 思考②:h[a]是找到数字a里记录的下标
    • 能和ne[]搭配使用找到所有与a相连的节点:如a的子节点有b、c、d,父节点是e,则a——b, a——c, a——d,a——e(为无向),则 h[a]->d->c->b->e->-1(每个单链表记录的是各自的子节点)
  • 思考③:为什么本题中DFS不需要回溯?
    • dfs有回溯和不回溯两种;
    • 每次需要更新答案的dfs(比如全排列从123更新到321)就需要回溯
    • 本题是返回节点个数,每个节点只会被遍历一次,故不需要回溯
具体代码
#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 1e5 + 10; // 树最大的节点树是10^5
const int M = 2 * N;    // 以有向图的格式存储无向图,所以每个节点至多对应2n-2条边

//也可用vector存,但vector模拟不如数组快
int h[N];    // 邻接表存储树:有n个节点,就需要n个链表的头节点(idx)
int e[M];    // 存储节点自身的值
int ne[M];   // 存储列表的next值(idx)
int idx;     // 单链表指针
int n;       // 题目所给的输入,n个节点
int ans = N; // 记录重心的最大子树中最小的那个
 
bool st[N];  //记录1~n个数是否被访问过,访问过则标记为true

//邻接表(几乎所有树和图)都是这么存的
void add(int a, int b) //表a→b
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

//返回以u为根的子树里的节点总数(包括u节点)
int dfs(int u) {
    int res = 0;  //存储:删掉重心后,最大的连通块节点数
    st[u] = true; //标记是否访问过;
    int sum = 1;  //存储:以u为根的树节点总数(包括u)

    //采用深度优先遍历:访问u的每个子节点
    for (int idx = h[u]; idx != -1; idx = ne[idx]) //分析点①
    {
        int data = e[idx];//data只会为1~n中某个数
        if (!st[data]) {  
            int s = dfs(data);  // s记录u节点下某子连通块的节点数
            res = max(res, s);  // 对比u节点的另一个子连通块,res记录两者最大的
            sum += s; //每走完一块子连通块,含u的主连通块sum总数加一块
        }
    }

	//每一个子节点都走投无路时(即所有与它相连的点,st均为true,都不能走时),才会进行如下步骤
    res = max(res, n - sum); // 分析点②
    ans = min(res, ans); //分析点④
    return sum;//分析点③
}

int main() {
    memset(h, -1, sizeof h); //初始化h数组,-1表示尾节点
    cin >> n; //表示树的结点数

    // 本题是无环树:故对于有n个节点的树,必定是n-1条边
    for (int i = 0; i < n - 1; i++) {
        int a, b;//无向边需要加入两条边
        cin >> a >> b;
        add(a, b), add(b, a); //无向图
    }

    dfs(1); //分析点⑤

    cout << ans << endl;

    return 0;
}

②树与图的遍历:采用BFS(宽度优先遍历)

  • BFS相似于树的层序遍历
  • 模板
queue<int> q;
st[1] = true; // 表示1号点已经被遍历过
q.push(1);

while (q.size())
{
    int t = q.front();
    q.pop();

    for (int i = h[t]; i != -1; i = ne[i])
    {
        int j = e[i];
        if (!st[j])
        {
            st[j] = true; // 表示点j已经被遍历过
            q.push(j);
        }
    }
}

例题:847. 图中点的层次 - AcWing题库

写法① :采用数组模拟队列实现
  • 速度慢,400ms左右;
  • 此写法适用于没有提供queue,需要自己实现queue的语言
  • 写法②代码部分
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

const int N = 100010;

int h[N],ne[N], e[N], idx;//邻接表存储图
int q[N],dist[N];//q[N]数组模拟队列;dist[N]存储距离
int n, m;

void add(int a, int b)//邻接表存储图
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}

int bfs()
{
    int hh = 0,tt = 0;
    q[0]=1;
    
    memset(dist, -1, sizeof dist );//-1初始都没有走到过
    
    dist[1] = 0;//从1号节点开始,距离为0

    while(hh<=tt)//一直往后搜索
    {
        int qh = q[hh++];//队头出队,找该点能到的点(qh记录队头的值,即当前节点位于上一层的父节点值)

		//遍历队头下一层所有与其距离为1的点(idx为节点索引)
		// ne[i]上的点都是与idx节点距离为1的点
		// 若队头节点下没有子节点(即下一层没得走),就不进入for循环,而继续while循环,继续取队头节点
        for(int idx = h[qh]; idx != -1; idx = ne[idx])
        {
            int data = e[idx];//通过索引i得到t能到的节点编号
            if(dist[data]==-1)//如果data没有遍历过
            {
                // 因为路径长度都是1,故直接在上一步的基础上加上1即可
                //1号节点与当前节点的距离=1号节点到t号节点(data上一个节点)的距离+1
                dist[data] = dist[qh] + 1;
                q[++tt]=data;//节点入队
            }
        }
    }
    return dist[n];// 返回的d[n]即是节点1到节点n的距离
}

int main()
{
    memset(h, -1, sizeof h);//初始化,所有节点没有后继,结尾默认-1
    
    cin >> n >> m;//n:要找到数;m:共进行插入的次数(也对应有向边的条数)
    
    for(int i = 0; i < m; i++)//读入所有边
    {
        int a, b;
        cin >> a >> b;
        add(a, b);//加入邻接表:有向图只用加入单向(不必add(b,a)了)
    }
    
    cout << bfs() << endl;
    return 0;
}
写法② √:直接采用C++库中队列
  • 优势:好理解;速度快(大概130ms左右)
  • 思路:通过邻接表的方式存储图,而后采用BFS宽度优先遍历,就能保证最终找到的路径走的距离最短
  • 思考:BFS在该题代码部分如何实现?
    • 邻接表中每个节点对应的单链存储的是:所有距离当前头结点为1的各节点(如1下一层就是2、3、4;3下一层就是5)
  • 图解
    • 有向图的模拟生成及h[]存储图示:请添加图片描述
      • 队列q的存储图示:请添加图片描述
  • 写法②代码部分
#include <cstring> //memset头文件
#include <iostream>
#include <queue>   
using namespace std;

const int N = 100010;

int n, m;
int h[N], e[N], ne[N], idx;//邻接表存储图:h存idx,e存值,ne存idx
int dist[N];// 保存1号点到各个点的距离

//邻接表存储图
void add(int a, int b)
{
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++ ;
}

int bfs()
{
    //初始化dist[]数组,表所有点都未走到过
    memset(dist, -1, sizeof dist);

    //创建队列q,存每层遍历的节点
    //-->排好队,走完当前层后,再根据入队顺序,走下一层
    queue<int> q;
    //1距离1为0(同时dist[1]≠-1也表示走到过1了)
    dist[1] = 0;
    //每新走到一个节点,就将其加入队列(此处走到1,就把1放入队列)
    q.push(1);

    //只要队列q里还有值就不断走
    while (q.size())
    {
        //拿出队头(现将队头值赋给hh,再从队列中弹出队头)
        int hh = q.front();
        q.pop();
        // 循环遍历所有与队头hh相距为1的节点
        for (int idx = h[hh]; idx != -1; idx = ne[idx]) // ne[idx]上的点都是与i节点距离为1的点
        {
            int data = e[idx];    // 得对应数值
            if (dist[data] == -1) // 若data没有被遍历过
            {
                dist[data] = dist[hh] + 1; // 因为路径长度都是1,故直接在上一步的基础上+1即可
                q.push(data); // 每新走到一个节点data,就将其加入队列
            }
        }
    }
    return dist[n]; // 返回的dist[n]即是节点1到节点n的距离
}

int main()
{
    scanf("%d%d", &n, &m);
    memset(h, -1, sizeof h);

    for (int i = 0; i < m; i ++ )
    {
        int a, b;
        scanf("%d%d", &a, &b);
        add(a, b);
    }

    cout << bfs() << endl;

    return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值