0x21.搜索 - 树与图的遍历、拓扑排序

目录

  • 一、树与图的深度优先遍历及树的一些性质
    • 1.树与图的深度优先遍历
    • 2.时间戳
    • 3.树的DFS序(树链剖分前驱知识)
    • 4.树的深度
    • 5.树的重心与 s i z e size size
    • 6.图的连通块划分
  • 二、树与图的广度优先搜索
  • 三、拓扑排序
    • AcWing 164. 可达性统计

声明:
本系列博客是《算法竞赛进阶指南》+《算法竞赛入门经典》+《挑战程序设计竞赛》的学习笔记,主要是因为我三本都买了 按照《算法竞赛进阶指南》的目录顺序学习,包含书中的少部分重要知识点、例题解题报告及我个人的学习心得和对该算法的补充拓展,仅用于学习交流和复习,无任何商业用途。博客中部分内容来源于书本和网络(我尽量减少书中引用),由我个人整理总结(习题和代码可全都是我自己敲哒)部分内容由我个人编写而成,如果想要有更好的学习体验或者希望学习到更全面的知识,请于京东搜索购买正版图书:《算法竞赛进阶指南》——作者李煜东,强烈安利,好书不火系列,谢谢配合。


下方链接为学习笔记目录链接(中转站)

学习笔记目录链接


ACM-ICPC在线模板


注:以下图的建立都是使用链式前向星建图。

int head[N],ver[N],nex[N],edge[N],tot;

void add(int u,int v,int val){
     //链式前向星建图
    ver[++tot] = v;
    edge[tot] = val;
    nex[tot] = head[u];
    head[u] = tot;
}

一、树与图的深度优先遍历及树的一些性质

1.树与图的深度优先遍历

深度优先遍历,就是在每个点x上面的的多条分支时,任意选择一条边走下去,执行递归,直到回溯到点x后再走其他的边

int vis[N];//标记每一个点的状态

void dfs(int u){
     
    vis[u] = 1;
    for(int i = head[u];i;i = nex[i]){
     
        int v = ver[i];
        if(vis[v])
            continue;
        dfs(v);
    }
}

2.时间戳

按照上述的深度优先遍历的过程,以每一个结点第一次被访问的顺序,依次赋值1~N的整数标记,该标记就被称为时间戳。
标记了每一个结点的访问顺序。

3.树的DFS序(树链剖分前驱知识)

一般来说,我们在对树的进行深度优先时,对于每个节点,在刚进入递归时和回溯前各记录一次该点的编号,最后会产生一个长度为 2 N 2N 2N的序列,就成为该树的 D F S DFS DFS序。

int a[N],cnt;
int dfs(int u){
     
     a[++cnt] = u;//用a数组存DFS序
     vis[u] = 1;
     for(int i = head[u]; i;i = nex[i]){
     
        int v = ver[i];
        if(vis[v])
            continue;
        dfs(v);
     }
     a[++cnt] = u;
}

D F S DFS DFS序的特点时:每个节点的 x x x的编号在序列中恰好出现两次。设这两次出现的位置时 L [ x ] , R [ x ] L[x],R[x] L[x],R[x],那么闭区间 [ L [ x ] , R [ x ] ] [L[x],R[x]] [L[x],R[x]]就是以 x x x为根的子树的 D F S DFS DFS序。
dfs序可以把一棵树区间化,即可以求出每个节点的管辖区间。
对于一棵树的dfs序而言,同一棵子树所对应的一定是dfs序中连续的一段。
在这里插入图片描述
在这里插入图片描述
放一个博客。
dfs序的七个基本问题

4.树的深度

树中各个节点的深度是一种自顶向下的统计信息

起初,我们已知根节点深度是 0 0 0.若节点x的深度为 d [ x ] d[x] d[x],则它的子结点 y y y 的深度就是 d [ y ] = d [ x ] + 1 d[y]=d[x]+1 d[y]=d[x]+1


int dep[N];
void dfs(int u){
     
	vis[u] = 1;
	for(int i = head[u];i;i = nex[i]){
     
		int v = ver[i];
		if(vis[v])
			continue;
		dep[v] = dep[u]+1;//父结点 u 到子结点 v  递推 
		dfs(v);
	}
}

5.树的重心与 s i z e size size

树的重心是自底向上统计的
树的重心也叫树的质心。对于一棵树n个节点的无根树,找到一个点,使得把树变成以该点为根的有根树时,最大子树的结点数最小。

【树形DP】树的重心详解+多组例题详解


int vis[N];
int Size[N];
int ans = INF;
int id;
void dfs(int u){
     
    vis[u] = 1;
    Size[u] = 1;//子树的大小
    int max_part = 0;
    for(int i = head[u];i;i = nex[i]){
     
        int v = ver[i];
        if(vis[v])
            continue;
        dfs(v);
        Size[u] += Size[v];
        max_part = max(max_part,Size[v]);//比较儿子的size因为这里是假设以u为重心
    }
    max_part = max(max_part,n-Size[u]);//n为整棵树的结点数
    if(max_part<ans){
     //更新
        ans = max_part;//记录重心对应的max_part的值
        id = u;//记录重心位置
    }
}

6.图的连通块划分

若在一个无向图中的一个子图中任意两个点之间都存在一条路径(可以相互到达),并且这个子图是“极大的”(不能在扩展),则称该子图是原图的一个联通块

如下代码所示,cnt是联通块的个数,v记录的是每一个点属于哪一个联通块
经过连通块划分,可以将森林划分出每一颗树,或者将图划分为各个连通块。

int cnt;
void dfs(int u){
     
    vis[u] = cnt;//这里存的是第几颗树或者是第几块连通图
    for(int i = head[u];i;i = nex[i]){
     
        int v = ver[i];
        if(vis[v])
            continue;
        dfs(v);
    }
}
int main()
{
     
    for(int i = 1;i<=n;++i){
     
        if(!vis[i])//如果是颗新树就往里面搜
            ++cnt,dfs(i);
    }
}

二、树与图的广度优先搜索

树与图的广度优先遍历,顺便求d数组(树结点的深度/图结点的层次)。

void bfs(){
     
    memset(d,0,sizeof d);
    queue<int>q;
    q.push(1);
    d[1] = 1;
    while(q.size()){
     
        int u = q.front();
        q.pop();
        for(int i = head[u];i;i = nex[i]){
     
            int v = ver[i];
            if(d[v])continue;
            d[v] = d[u]+1;
            q.push(v);
        }
    }
}

广度优先遍历是一种按照层次顺序访问的方法。
它具有两个重要的性质:

  1. 在访问完所有的第i层结点后,才会访问第i+1层结点。
  2. 任意时刻,队列中只会有两个层次的结点,满足“两段性”和“单调性”。

三、拓扑排序

给定一张有向无环图,若一个序列A满足图中的任意一条边(x,y)x都在y的前面呢么序列A就是图的拓扑排序

求拓扑序的过程非常简单我们只需要不断将入度为0的点加入序列中即可
(入度:有向图中以结点x为终点的有向边的条数)
(出度:有向图中以结点x为起点的有向边的条数)
(无向图中的度:以x为端点的无向边的条数)

  1. 建立空拓扑序列A
  2. 预处理出所有入度为deg[i],起初把入度为0的点入队
  3. 取出对头节点x,并把x放入序列A中
  4. 对于从x出发的每条边(x,y),把deg[y]减1,若deg[y] = 0 ,把y加入队列中
  5. 重复3,4直到队列为空,此时A即为所求

拓扑排序可以判定有向图中是否存在环。若拓扑排序以后的A序列的长度小于图中点的长度,说明有的点没有被遍历,进而说明存在环。

int head[N],ver[N],nex[N],edge[N],tot;
int n,m;
int deg[N];
int A[N];
void add(int u,int v){
     
    ver[++tot] = v;
    nex[tot] = head[u];
    head[u] = tot;
    deg[v]++;//入度加1
}
int cnt;
void toposort(){
     //拓扑排序
    queue<int>q;
    for(int i = 1;i<=n;++i)
        if(deg[i] == 0)q.push(i);//步骤2,先找所有度为0的点
    while(q.size()){
     
        int u = q.front();
        q.pop();
        A[++cnt] = u;
        for(int i = head[u];i;i = nex[i]){
     
            int v = ver[i];
            if(--deg[v] == 0)
                q.push(v);
        }
    }
}
int main()
{
     
    cin>>n>>m;
    for(int i = 1;i <=m;++i){
     
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    toposort();
    for(int i = 1;i <=cnt;++i){
     
        printf("%d ",A[i]);
    }
    puts("");
    return 0;
}

AcWing 164. 可达性统计

0x21.搜索 - 树与图的遍历、拓扑排序_第1张图片

利用拓扑排序得到的序列满足x一定在y 的前面,也就是说求完拓扑序以后倒序可以保证从所有的叶子节点开始往根走。这里要求每个点的能到达的数量,其实就是从叶子结点往上递推,有点类似最短Hamilton路径,所以用一个二进制数来状态压缩。1代表可以到达。这里可以用bitset来压缩空间个时间,而且bitset还支持count功能直接求得1的个数,这个1的个数就是这个点能到达的点的个数。

#include
#include
#include
#include
#include
#include
#include
#define ls (p<<1)
#define rs (p<<1|1)
#pragma GCC optimize (2)
#pragma G++ optimize (2)
#define over(i,s,t) for(register int i = s;i <= t;++i)
#define lver(i,t,s) for(register int i = t;i >= s;--i)
//#define int __int128
using namespace std;
#undef mid
typedef long long ll;
typedef pair<int,int> PII;

const int N = 30007;
const ll mod = 1e9+7;
const ll INF = 1e15+7;
const double EPS = 1e-10;
const int base = 131;

int n,m;
int ver[N],nex[N],head[N],tot,cnt;
int deg[N],a[N];
bitset<N>f[N];

void add(int u,int v){
     
    ver[++tot] = v;
    nex[tot] = head[u];
    head[u] = tot;
    deg[v]++;
}

void toposort(){
     
    queue<int>q;
    for(int i = 1;i<=n;++i){
     
        if(deg[i] == 0)
            q.push(i);
    }
    while(q.size()){
     
        int u = q.front();
        q.pop();
        a[++cnt] = u;
        for(int i = head[u];i;i = nex[i]){
     
            int v = ver[i];
            if(--deg[v] == 0)
                q.push(v);
        }
    }
}

void calc(){
     
    lver(i,cnt,0){
     //倒序从叶子结点开始
        int x = a[i];
        f[x][x] = 1;//自己肯定要到达自己
        for(int i = head[x];i;i = nex[i]){
     
            int y = ver[i];
            f[x] |= f[y];//求并集
        }
    }
}

int main()
{
     
    scanf("%d%d",&n,&m);
    over(i,1,m){
     
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);
    }
    toposort();
    calc();
    for(int i = 1;i <=n;++i){
     
        printf("%d\n",f[i].count());//输出有多少个1
    }
    return 0;
}

注:如果您通过本文,有(qi)用(guai)的知识增加了,请您点个赞再离开,如果不嫌弃的话,点个关注再走吧,日更博主每天在线答疑 ! 当然,也非常欢迎您能在讨论区指出此文的不足处,作者会及时对文章加以修正 !如果有任何问题,欢迎评论,非常乐意为您解答!( •̀ ω •́ )✧

你可能感兴趣的:(【算法竞赛学习笔记】,#,图论基础,#,拓扑排序)