树的那些事儿

连通、不含圈、恰好有n-1条边和n个顶点——这就是树
而这3个条件中的任意两点都可以对出另一点
本文将深入浅出介绍各种树的相关

无根树转有根树

其实所谓无根树,指代的就是无圈连通图。我们知道树其实就是图的特例,只要这个图满足无圈连通,那么其实本质上就是树,只是没有一个根罢了。

我们姑且来看看一个最简单的例子。

  A
/  \
B   C
  /   \
  D    E

毫无疑问,这是一个典型的无根树。

  • 对于A来说,它有两个邻点B和C;
  • 对于B来说,只有一个邻点A(是它的父节点)
  • 对于C来说,有三个点D和E,还有A(父节点)
  • 对于D和E,都只有一个C(父节点)
    我们看到了一些端倪,每一个点(除了根节点),都只有一个父节点。那我们只需要在遍历某一个点的邻点时,把除了父节点之外的都和其连接就好。我们可以尝试使用递归来解决。
//假设已知G[][]图
//u当前点的编号
//p[i] i号点的父节点
void dfs(int u,int fa){
    //遍历这个点的所有邻点
    for(int i=0;i<G[u].size();i++){
        int v=G[u][i];
        if(v!=fa) {
            p[v]=u;
            dfs(v,u);}
    }
}

后来得到的p[v]就可以表示出整个树!
另外这里还有一个小细节,因为root是没有父节点的,所以我们可以将其父节点设为-1来表示,即p[root]=-1。调用时的形式应该是dfs(root,-1);

表达式树

这个问题看起来要比上一个麻烦一些,顾名思义,表达式树是处理表达式的常用工具。
a*(b+c)-d

      -
     / \
    *   d
   /  \
   a   +
      /  \
      b   c

我们这里提供一种思路,也是递归。
首先找出最后一个运算符,然后以这个运算符为根,将其左侧表达式和右侧表达式为左子树和右子树,然后分别对左子树和右子树进行递归(他们两者都是独立的表达式树)

//x表示的是当前表达式的开始,y表示结束位置+1 如*s=a+b,则x=0,y=3
int build_tree(char*s ,int x,int y){
    if(y-x==1){
        //只有一个单独的字符,建立结点
        return;
    }
    for(int i=x;x<y;i++){
        //寻找最后一个+-号
        //寻找最后一个*\号
    }
    //找到这个操作符(a*b+c就是+)
    //左子树
    //右子树
}

确定了思路,我们就来具体做一下吧!

const int maxn=10000;
int lch[maxn],right[maxn];//结点i的左子树和右子树
char op[maxn];//结点i的字符
int nc;//用来记录目前已经有的结点号

这个结构是ACM比赛中较为常用的,你如果不习惯,完全可以用一个完整的结构体来合并这三个数组。

int build_tree(char* s,int x,int y){
    int u;
    int c1=-1/*表示最后一个+-号的位置*/,c2=-1/*最后一个乘除号的位置*/,p/*当前是否走出了括号*/;
    if(y-x==1){
        u=nc++;
        lch[u]=rch[u]=0;op[u]=s[x];
        return;
    }
    for(int i=x;i<y;i++){
        switch(s[i]){
            //括号中的运算符先不考虑
            case '(':p++;break;
            case ')':p--;break;
            case '+':case '-':if(!p)c1=i;break;
            case '*':case '/':if(!p)c2=i;break;
        }
        if(c1<0)c1=c2;//如果没有+-,那么就以乘除为优先级最高;
        if(c1<0) return build_tree(s,x+1,y-1);//说明整个式子被括号了,重新找
        u=nc++;
        lch[u]=build_tree(s,x,c1);
        lch[u]=build_tree(s,c1+1,y);
        op[u]=s[c1];
        return u;
    }

}

最小生成树

最小生成树是指总权值最小。至于什么是生成树,其实就是选取一个图中的所有点构成一棵树。
Kurskal算法
这个算法的思路是,将边的权值从小到大排列,依次遍历这些边。对于一条边(u,v),如果u、v分属于两个不同的”集合“,那么就将这条变加入到MST中(这两个集合随即合并为一个集合)。

是不是看起来很简单?怎样证明其正确呢?
我们来反证一下,如果说:不加入这条边e=>能得到一个最优解T=>那么T+e必然成环(因为T已经连通了各个点)=>我们必然可以用e替换掉T中的一个边e2(且e< e2),使图依然连通。

(共有m条边)
把所有边排序
每一个点都是一个集合(连通分量)
for(int i=0;i<m;i++)
    if(e[i].u和e[i].v不在一个连通分量中)
        加入这条边;
        合并e[i].u和e[i].v的集合;

如何查询他们是不是在一个连通分量中呢?我们可以用BFS或者DFS来判定当前这两个点时候已经连通,但是这样比较麻烦。我们有更好的工具。

并查集
把连通分量看作一个集合,包含了1,2,3,4,5,6的图有三个连通分量{1,2,3}{4,5}{6},那么这三个分量都是一棵树。

这里还有一个小细节,就是如何寻找最后的父节点呢?其实这个问题非常简单。在开始时初始化p[i](还记得这一点么,这个数组就足以说明树中各个点的连结)为i,然后对任意一个点使用这个函数

int find(int x){return x=p[x]?x:find(x)}
//p[i] i结点的父节点
//e[i] i号边

for(int i=0;i<n;i++) p[i]=i;
sort(e.begin(),e.end());
for(int i=0;i<m;i++){
    int x=find(e.u);//找到e.u的父节点的父节点的。。。
    int y=find(e.v);
    if(x!=y){p[x]=y};

}

版权声明:本文为博主原创文章,转载请标明出处。

posted @ 2015-09-10 20:10  Fridge  阅读(157)  评论(0编辑  收藏  举报