图论——树有关知识
树的遍历顺序有关
前序,中序,后序概念自行百度。
知道几种顺序来确定二叉树形态
首先先明确一点至少要知道两种顺序以上才能确定一个树,因为如果只知道前序或者后序就只能确定根节点,而不能确定根节点的左右子树。但如果知道中序,那就不能确定根节点。
然后如果知道两种顺序的话,必须要知道中序,因为另外一种顺序可以确定根节点,中序来确定根节点的左右子树,然后再根据前序后者后序确定根节点的左右儿子。
具体确定代码实现(前序+中序)
#include<bits/stdc++.h>
using namespace std;
const int N=5e3+50;
string s,t;
int n;
void solve(int l,int r,int ml,int mr)
{
if(l>r)
return;
if(l==r)
{
cout<<s[l];
return;
}
int pos;
for(int i=ml;i<=mr;i++)
if(t[i]==s[l])
pos=i;
solve(l+1,l-ml+pos,ml,pos-1);solve(l-ml+pos+1,r,pos+1,r);
cout<<s[l];
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>s>>t;
n=s.length();
s=" "+s;t=" "+t;
solve(1,n,1,n);
return 0;
}
结合代码还是很好理解的。
二叉树计数
但还有一种题型,就是已知前序和后序,求有多少种二叉树满足这种遍历顺序。
我们能确定根节点,但是不能确定根节点有一个儿子还是两个儿子。如果一个点有一个儿子,那么就有两种情况,儿子朝左朝右都可以。比如看前序遍历的第二个数,再在后序中找到,如果在后序的下一个位置为根,那么证明根节点只有一个儿子。否则就有两个儿子。
具体代码实现如下。
#include<bits/stdc++.h>
using namespace std;
const int N=5050;
long long ans;
int n,m;
char a[N],b[N];
int main(){
scanf("%s %s",a,b);
n=strlen(a);
for(int i=0;i<n-1;i++)
for(int j=1;j<n;j++)
if(a[i]==b[j]&&a[i+1]==b[j-1])ans++;
printf("%lld\n",(1ll<<ans));
return 0;
}
多叉树转二叉树
记住口诀 "左儿子,右兄弟"。
意义:为什么要把多叉树转为二叉树?因为普遍情况下二叉树性质比多叉树的性质好。
\(Code\):
#include<bits/stdc++.h>
using namespace std;
const int N=150;
map<char,char> son,L,R;
map<char,bool> mp,F,xx;
bool hav[N];
char root;int n;
void pre(char u)
{
cout<<u;
if(hav[u])pre(L[u]);
if(mp[u])pre(R[u]);
return;
}
void mid(char u)
{
if(hav[u])mid(L[u]);
cout<<u;
if(mp[u])mid(R[u]);
return;
}
void su(char u)
{
if(hav[u])su(L[u]);
if(mp[u])su(R[u]);
cout<<u;
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++)
{
char u;cin>>u;xx[u]=true;
char x;
while(1)
{
cin>>x;
if(x=='0')break;
F[x]=true;
if(!hav[u])L[u]=x;
else R[son[u]]=x,mp[son[u]]=true;
son[u]=x;hav[u]=true;
}
}
for(char i='A';i<='Z';i++)
{
if(xx[i]&&!F[i])
root=i;
}
pre(root);cout<<endl;mid(root);cout<<endl;su(root);
return 0;
}
树上差分
通常用来求两点之间的距离与经过的点数。
求两点间的距离:
树上贪心
树的重心
定义&性质&证明
定义: 以它为根的所有子树中最大的子树大小最小。则称这个点为树的重心。
性质 \(1\):一个点是重心,当且仅当以该点为根的最大子树的大小不超过整棵树的大小一半(向下取整,及 $\left \lfloor \frac{n}{2} \right \rfloor $)
证明:
令 \(MSS(u)\) 表示节点 \(u\) 的最大子树大小。\(size_u(v)\) 表示以 \(u\) 为根时,包含节点 \(v\) 的子树大小。
先证明其充分性:
如果 \(u\) 为重心,则与 \(u\) 直接相连的节点 \(v\) 的大小 \(size_u(v) \le \left \lfloor \frac{n}{2} \right \rfloor\)。考虑反证法,假设节点 \(u\) 为重心,而存在一个 \(u\) 的子树使得 $size_u(v) > \left \lfloor \frac{n}{2} \right \rfloor $,则 \(size_v(u) = n - size_u(v) < \left \lfloor \frac{n}{2} \right \rfloor < size_u(v) = MSS(u)\)。对于 \(w \ne u\) 又有 \(size_v(w) < size_u(v) = MSS(u)\) (因为 \(w\) 如果是在以 节点\(v\) 为根,节点 \(u\) 为子树中,\(size_v(u) < \left \lfloor \frac{n}{2} \right \rfloor < size_u(v) = MSS(u)\),而有如果 \(w\) 在 \(v\) 的其他子树中的话,那么 \(size_v(w)\) < \(size_u(w) = size_u(v) = MSS(u)\)),所以 \(MSS(v) < MSS(u)\),与重心定义矛盾,所以 \(u\) 不是重心,得证。
再证明其必要性:
如果一个节点 \(u\) 的 \(MSS(u) \le \left \lfloor \frac{n}{2} \right \rfloor\),则 \(u\) 为重心。\(size_v(u) = n -size_u(v) \ge \left \lfloor \frac{n}{2} \right \rfloor \ge MSS(u)\)。即 \(MSS(v) \ge MSS(u)\)。对于与 \(u\) 不相邻的节点 \(w\),都有 \(size_w(v) > MSS(u)\)。故得证。
性质 \(2\):树至多有两个重心。如果树有两个重心,那么它们相邻。此时树一定有偶数个节点,且可以被划分为两个大小相等的分支,每个分支各自包含一个重心。
注: 一个偶数节点的树可以有两个重心,也可以只有一个重心,而一个奇数节点的树只可能有一个重心,不可能有两个重心。
证明:
根据上一个证明必要性时,\(size_v(u) = n -size_u(v) \ge \left \lfloor \frac{n}{2} \right \rfloor \ge MSS(u)\) 得到,当 \(n\) 为偶数时且 \(size_v(u) = \frac{n}{2} = size_u(v) = MSS(u)\)。故得证。
性质 \(3\):树中所有点到某个点的距离和中,到重心的距离和是最小的;如果有两个重心,那么到它们的距离和一样。反过来,距离和最小的点一定是重心。
证明:
假设所有点到节点 \(u\) 的距离最小,且有一个与节点 \(u\) 相邻的节点 \(v\) 使得 \(size_u(v) > \frac{n}{2}\),那么根据调整法所有点到节点 \(v\) 的距离为 \(dis(u) - size_u(v) + (n - size_u(v)) = dis(u) + n - 2\times size_u(v)\)。因为 \(size_u(v) > \frac{n}{2}\),所以 \(2\times size_u(v) > n\),所以 \(n - 2\times size_u(v) < 0\),即 \(dis(u) > dis(v)\),此时与所有点到节点 \(u\) 的距离最小矛盾,所以只要一个节点的 \(u\) 的 \(MSS(u) > \frac{n}{2}\),那 \(dis(u)\) 一定不是最小值。而如果 \(MSS(u) \le \frac{n}{2}\),那么与 \(u\) 直接相邻的节点 \(v\) 的 \(size_u(v) \le \frac{n}{2}\)。即不管往哪里调整,都会使 \(dis\) 值增加。所以再结合根据性质 \(1\),即可证毕。
性质 \(4\):往树上增加或减少一个叶子,如果原节点数是奇数,那么重心可能增加一个,原重心仍是重心;如果原节点数是偶数,重心可能减少一个,另一个重心仍是重心。
性质 \(5\):把两棵树通过一条边相连得到一棵新的树,则新的重心在较大的一棵树一侧的连接点与原重心之间的简单路径上。如果两棵树大小一样,则重心就是两个连接点。
如何求重心
方法一(不能求出最大子树的大小):统计节点 \(u\) 的子树的大小和除了 \(u\) 子树的剩余部分的大小看是否都小于 \(\frac{n}{2}\),如果都小于,那么根据性质1这个节点就是重心,否则不是。
方法二:根据重心的定义,是得最大的子树最小的节点即为重心,所以另 \(res=max{size_u(v),n-size_u}\)。最小的几个 \(res\) 即为重心,且 \(res\) 表示最大子树最小的值。
代码如下:(采用方法二)
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+50;
int head[N],cnt,n,sz[N],ans=1e9;
vector<int> G;
struct edge{
int to,nxt;
}e[N*2];
void add(int u,int v)
{
e[++cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
return;
}
void dfs(int u,int fa)
{
sz[u]=1;int res=0;
for(int i=head[u];i;i=e[i].nxt)
{
int v=e[i].to;
if(v==fa)
continue;
dfs(v,u);
res=max(res,sz[v]);
sz[u]+=sz[v];
}
res=max(res,n-sz[u]);
if(res<ans)
{
ans=res;
G.clear();
G.push_back(u);
}
else if(res==ans)
G.push_back(u);
return;
}
int main()
{
scanf("%d",&n);
for(int i=1,u,v;i<=n-1;i++)
{
scanf("%d %d",&u,&v);
add(u,v);add(v,u);
}
dfs(0,0);
printf("%d %d\n",ans,(int)G.size());
for(int i=0;i<(int)G.size();i++)
printf("%d ",G[i]);
return 0;
}