表示方法:
给定N个点的树或图,其节点编号为1-N,无向图中的边看作成对出现的双向边,树看作一张具有n-1条边的无向图,他们的边都存储在一个邻接表中,邻接表以head数组为表头,使用ver数组存储边的终点,edge数组存储边的权值,使用next数组模拟链表指针。
//加入有向边(x,y)权值为z
void add(int x,int y,int z)
{
ver[++tot]=y;edge[tot]=z;
next[tot]=head[x];head[x]=tot;//在表头处插入
}
//访问从x出发的所有边
for(int i=head[x];i;i=next[i])
{
int y=ver[i],z=edge[i];//有向边(x,y)权值为z
}
图的深度优先遍历:
dfs(1):访问每条边和点恰好一次(无向图正反向各访问一次)O(n+m)
void dfs(int x)
{
vis[x]=1;
for(int i=head[x];i;i=next[i])
{
int y=ver[i];
if(v[y]) continue;
dfs(y);
}
}
时间戳dfn
按照以上优先遍历过程,以每个节点第一次被访问的顺序,依次给N个节点1~N的整数标记,该标记称为时间戳。
树的dfs序:
进行深度优先遍历时,对于每个节点,在刚进入递归后以及即将回溯前各记录一次该点的编号,最后产生长度为2N的节点序列就称为DFS序。
特点:每个节点编号在序列中恰好出现两次。设这两次出现位置为L[x],R[x],闭区间【L[x],R[x]】就是以x为根的子树的DFS序。
可以通过DFS序把子树统计转化为序列上的区间统计。
void dfs(int x)
{
a[++m]=x;
v[x]=1;
for(int i=head[x];i;i=next[i])
{
int y=ver[i];
if(v[y]) continue;
dfs(y);
}
a[++m]=x;
}
树的深度:(自顶向下的统计信息)
void dfs(int x)
{
v[x]=1;
for(int i=head[x];i;i=next[i])
{
int y=ver[x];
if(v[y]) continue;
d[y]=d[x]+1;
dfs(y);
}
}
树的大小:(自底向上的统计信息)
以每个节点x为根的子树大小size[x],对于叶子节点,已知以其为根的子树大小为1,非叶子节点x,有k个子节点y1~yk,则以x为根的子树大小:size[x]=size[y1]+size[y2]+·····+size[yk]+1
树的中心
对于一个节点x,删除x可能会得到多个子树
设max_part(x)表示在删除节点x后产生的子树中,最大的一棵的大小,使max_part函数取到最小值的节点p称为整棵树的重心。
void dfs(int x)
{
v[x]=1;size[x]=1;
int max_part=0;//删除x后分成的最大子树的大小
for(int i=head[x];i;i=next[i])
{
int y=ver[i];
if(v[y]) continue;
dfs(y);
size[x]+=size[y];
max_part=max(max_part,size[y]);
}
max_part=max(max_part,n-size[x]);//n是整个树的节点数
if(max_part<ans)
{
ans=max_part;pos=x;//全局变量ans记录重心对应的max_part,pos记录重心
}
}
图的连通块划分:
void dfs(int x)
{
v[x]=cnt;
for(int i=head[i];i;i=next[i])
{
int y=ver[i];
if(v[y]) continue;
dfs(y);
}
}
for(int i=1;i<=n;++i)
{
if(!v[i])
{
cnt++;dfs(i);
}
}
广度优先遍历(并求树的深度)
void bfs()
{
memset(d,0,sizeof(d));
queue<int>q;
q.push(1);d[1]=1;
while(q.size()>0)
{
int x=q.front();q.pop();
for(int i=head[x];i;i=next[i])
{
int y=ver[i];
if(d[y]) continue;
d[y]=d[x]+1;
q.push(y);
}
}
}
广度优先遍历的重要性质:
- 在访问完所有的第i层节点后,才会开始访问第i+1层节点
- 任意时刻,队列中至多有两个层次的节点,若其中一部分属于第i层,则另一部分属于第i+1层,且所有第i层节点排在第i+1层节点之前,即广度优先遍历队列中元素关于层次满足“两段性”和“单调性”
拓扑排序:
给定一张有向无环图(DAG),若一个由图中所有点构成的序列A满足:对于图中的每条边(x,y),x在A中都出现在y之前,则称A是该有向无环图顶点的一个拓扑序、求解拓扑序的过程即是拓扑排序。
DAG的拓扑序不唯一。
非DAG无拓扑序。
拓扑排序过程:
- 建立空的拓扑序列A
- 预处理出所有点的入度deg[i],起初把所有入度为0的点入队
- 取出队头节点x,把x加入拓扑序列A的末尾
- 对于从x出发的每条边(x,y),deg[y]–,若deg[y]=0,则把y入队
- 重复第3~4步直到队列为空,此时A即为所求。
拓扑排序可以判定有向图中是否存在环,对有向图执行拓扑排序操作,完成后检查序列A的长度,若A序列长度小于图中点的数量,说明存在环。
void add(int x,int y)
{
ver[++tot]=y;next[tot]=head[x];head[x]=tot;
deg[y]++;
}
void topsort()
{
queue<int>q;
for(int i=1;i<=n;++i)
{
if(deg[i]==0) q.push(i);
}
while(q.size())
{
int x=q.front();q.pop();
a[++cnt]=x;
for(int i=head[x];i;i=next[i])
{
int y=ver[i];
if(--deg[y]==0) q.push(y);
}
}
}
#include<bits/stdc++.h>
using namespace std;
const int N=3e4+10;
int ver[N],ne[N],head[N],tot,n,m;
int deg[N],a[N],cnt;
bitset<N> f[N];
void add(int x,int y)
{
ver[++tot]=y;ne[tot]=head[x];head[x]=tot;
deg[y]++;
}
void topsort()
{
queue<int>q;
for(int i=1;i<=n;++i)
{
if(deg[i]==0) q.push(i);
}cnt=0;
while(q.size())
{
int x=q.front();q.pop();
a[++cnt]=x;
for(int i=head[x];i;i=ne[i])
{
int y=ver[i];deg[y]--;
if(deg[y]==0) q.push(y);
}
}
}
int main()
{
scanf("%d%d",&n,&m);
memset(head,-1,sizeof(head));
memset(deg,0,sizeof(deg));
for(int i=0;i<m;++i)
{
int x,y;scanf("%d%d",&x,&y);
add(x,y);
}
topsort();
for(int i=cnt;i>=1;--i)
{
int x=a[i];
f[x][x]=1;
for(int j=head[x];j;j=ne[j])
{
int y=ver[j];
f[x]|=f[y];
}
}
for(int i=1;i<=n;++i)
{
printf("%d\n",f[i].count());
}
}