树&图总结
树
定义
树(tree)是包含n(n>=1)个结点,(n-1)条边的有穷集,其中:
(1)每个元素称为结点(node);
(2)有一个特定的结点被称为根结点或树根(root)。
(3)除根结点之外的其余数据元素被分为m(m≥0)个互不相交的集合T1,T2,……Tm-1,其中每一个集合Ti(1<=i<=m)本身也是一棵树,被称作原树的子树(subtree)。
树也可以这样定义:树是由根结点和若干颗子树构成的。树是由一个集合以及在该集合上定义的一种关系构成的。集合中的元素称为树的结点,所定义的关系称为父子关系。父子关系在树的结点之间建立了一个层次结构。在这种层次结构中有一个结点具有特殊的地位,这个结点称为该树的根结点,或称为树根。
我们可以形式地给出树的递归定义如下:
单个结点是一棵树,树根就是该结点本身。
设T1,T2,..,Tk是树,它们的根结点分别为n1,n2,..,nk。用一个新结点n作为n1,n2,..,nk的父亲,则得到一棵新树,结点n就是新树的根。我们称n1,n2,..,nk为一组兄弟结点,它们都是结点n的子结点。我们还称T1,T2,..,Tk为结点n的子树。
空集合也是树,称为空树。空树中没有结点。
结点的度:一个结点含有的子结点的个数称为该结点的度;
叶结点或终端结点:度为0的结点称为叶结点;
非终端结点或分支结点:度不为0的结点;
双亲结点或父结点:若一个结点含有子结点,则这个结点称为其子结点的父结点;
孩子结点或子结点:一个结点含有的子树的根结点称为该结点的子结点;
兄弟结点:具有相同父结点的结点互称为兄弟结点;
树的度:一棵树中,最大的结点的度称为树的度;
结点的层次:从根开始定义起,根为第1层,根的子结点为第2层,以此类推;
树的高度或深度:树中结点的最大层次;
堂兄弟结点:双亲在同一层的结点互为堂兄弟;
结点的祖先:从根到该结点所经分支上的所有结点;
子孙:以某结点为根的子树中任一结点都称为该结点的子孙。
森林:由m(m>=0)棵互不相交的树的集合称为森林;
存储方式及模板
动态数组(常用)
cin>>n;
for(int i=1;i<=n;i++){
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
邻接矩阵(常用于传递闭包)
cin>>n;
for(int i=1;i<=n;i++){
cin>>u>>v;
g[u][v]=true;
}
例题
#10032. 二叉树的序遍历
思路
使用dfs遍历整棵树,再将dfs序输出(前序则最先输出,中序于中间输出,后序则最后输出,符合根左右、左根右和左右根的顺序)
Code
#include<bits/stdc++.h>
using namespace std;
int n;
int l,r;
vector<int>v[30];
//第一次写代码的时候函数调用错了,故一定要注意细节qwq
void dfs(int x){//前序遍历
if(x==0)return;
cout<<x<<" ";
dfs(v[x][0]);
dfs(v[x][1]);
}
void dfs1(int x){//中序遍历
if(x==0)return;
dfs1(v[x][0]);
cout<<x<<" ";
dfs1(v[x][1]);
}
void dfs2(int x){//后序遍历
if(x==0)return;
dfs2(v[x][0]);
dfs2(v[x][1]);
cout<<x<<" ";
}
int main(){
cin>>n;
for(int i=1;i<=n;i++){
cin>>l>>r;
v[i].push_back(l);
v[i].push_back(r);
}
dfs(1);//从根结点开始遍历(题目中说了根结点一定为1)
cout<<endl;
dfs1(1);
cout<<endl;
dfs2(1);
return 0;
}
#10033. 二叉树的叶节点和
思路
由于叶结点的特点是,除了根结点外,度数必为1,则可编写代码
Code
#include<bits/stdc++.h>
using namespace std;
int n;
int l,r;
int a[10000010];
int ans;
int main(){
cin>>n;
if(n==1){
cout<<1;
return 0;
}
for(int i=1;i<=n;i++){
cin>>l>>r;
if(l!=0){
a[l]++;
a[i]++;
}
if(r!=0){
a[r]++;
a[i]++;
}
}
//之前这里就因把根结点算了进去,所以这里要注意qwq
for(int i=2;i<=n;i++){
if(a[i]==1){
ans+=i;
}
}
cout<<ans;
return 0;
}
#210756. 树上两点距离
思路
可以使用dfs遍历全树,如到了给定的结束点,则保存深度并回溯
Code
#include<bits/stdc++.h>
using namespace std;
int n;
int x,y;
int a,b,ans;
vector<int>nodes[1010];
void dfs(int x,int from,int dep){
if(x==b){
ans=dep;
return;
}
int len=nodes[x].size();
for(int i=0;i<len;i++){
int v=nodes[x][i];
if(v!=from){
dfs(v,x,dep+1);
}
}
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
cin>>x>>y;
nodes[x].push_back(y);
nodes[y].push_back(x);
}
cin>>a>>b;
dfs(a,0,0);
cout<<ans;
return 0;
}
#210777. 树的深度
思路
直接使用dfs遍历全树,每进入一次dfs,就进行ans=max(ans,dep),最后输出ans即可
Code
#include<bits/stdc++.h>
using namespace std;
int n;
int x,y;
int a[1010],b,ans;
vector<int>nodes[1010];
void dfs(int x,int from,int dep){
ans=max(ans,dep);
int len=nodes[x].size();
for(int i=0;i<len;i++){
int v=nodes[x][i];
if(v!=from){
dfs(v,x,dep+1);
}
}
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
cin>>x>>y;
nodes[x].push_back(y);
nodes[y].push_back(x);
}
dfs(1,0,1);
cout<<ans;
return 0;
}
#210776. 每个点的子树大小
思路
使用dfs遍历树,如果符合遍历条件,则使用f数组记录每个结点的子树的结点数(f数组初始化为1)
Code
#include<bits/stdc++.h>
using namespace std;
int n;
int x,y;
vector<int>nodes[1010];
int f[1010];
void dfs(int x,int from){
int len=nodes[x].size();
for(int i=0;i<len;i++){
int v1=nodes[x][i];
if(v1!=from){
dfs(v1,x);
f[x]+=f[v1];
}
}
}
int main(){
cin>>n;
for(int i=1;i<n;i++){
cin>>x>>y;
nodes[x].push_back(y);
nodes[y].push_back(x);
}
for(int i=1;i<=n;i++){
f[i]=1;
}
dfs(1,0);
for(int i=1;i<=n;i++){
cout<<f[i]<<" ";
}
return 0;
}
图
定义
图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。
存储方式及模板
动态数组(常用)
cin>>n;
for(int i=1;i<=n;i++){
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
邻接矩阵(常用于传递闭包)
cin>>n;
for(int i=1;i<=n;i++){
cin>>u>>v;
g[u][v]=true;
}
由上,我们可以发现图的存储方式与树的存储方式一致,我们称之为树图同存
例题
#1612. 【Usaco2008 Jan】 Cow Contest奶牛的比赛
思路
一道使用传递闭包解决的典型题目
Code
#include<bits/stdc++.h>
using namespace std;
int n,m,a,b,ans;
bool f[110][110];
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>a>>b;
f[a][b]=true;
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j]|=f[i][k]&f[k][j];
}
}
}
for(int i=1;i<=n;i++){
int x=0;
int y=0;
for(int j=1;j<=n;j++){
if(f[j][i])x++;
if(f[i][j])y++;
}
if(x+y==n-1)ans++;
}
cout<<ans;
return 0;
}
#10068. 珍珠
思路
使用传递闭包,最后统计如果某一颗珍珠的重量关系不在中间,则计数器++,最后输出计数器即可
Code
#include<bits/stdc++.h>
using namespace std;
const int N=110;
int g[N][N],cnt;
int main(){
int n,m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
g[x][y]=true;
}
for(int k=1;k<=n;k++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
g[i][j]|=g[i][k]&g[k][j];
}
}
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
g[0][j]+=g[i][j];
g[i][0]+=g[i][j];
}
}
for(int i=1;i<=n;i++){
if(g[0][i]>n/2||g[i][0]>n/2){
cnt++;
}
}
cout<<cnt;
return 0;
}
#10292. 刻录光盘
思路
通过传递闭包将子树合并到一起,最后输出根结点个数
Code
#include<bits/stdc++.h>
using namespace std;
int u,v,ans,n,a[1010];
bool f[1010][1010];
int main() {
cin>>n;
for(int i=1;i<=n;i++){
int j=0;
do{
cin>>j;
if(j==0)break;
f[i][j]=true;
}while(j!=0);
}
for(int k=1;k<=n;k++){//传递闭包
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
f[i][j]|=f[i][k]&f[k][j];
}
}
}
for(int i=1;i<=n;i++)a[i]=i;
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
if(f[i][j]){
a[j]=a[i];
}
}
}
for(int i=1;i<=n;i++){
if(a[i]==i){
ans++;
}
}
cout<<ans;
return 0;
}
#10126. 图的连通块
思路
使用dfs遍历全图,如未被遍历过,则计数器++,并使用dfs继续遍历,与二维数组使用dfs求连通块十分相似
Code
#include<bits/stdc++.h>
using namespace std;
int u,v,ans;
bool vis[10010];
vector<int>g[10010];
void dfs(int x){
vis[x]=true;
int len=g[x].size();
for(int i=0;i<len;i++){
int v=g[x][i];
if(!vis[v]){
dfs(v);
}
}
}
int main() {
int m,n,count,num;
cin>>n>>m;
for (int i = 1; i <= m; i++) {
cin>>u>>v;
g[u].push_back(v);
g[v].push_back(u);
}
for(int i=1;i<=n;i++){
if(!vis[i]){
ans++;
dfs(i);
}
}
cout<<ans;
return 0;
}