【算法】最近公共祖先(hihoCoder #1062)

  • 题目描述
    小Ho最近发现了一个神奇的网站!虽然还不够像58同城那样神奇,但这个网站仍然让小Ho乐在其中,但这是为什么呢?
    “为什么呢?”小Hi如是问道,在他的观察中小Ho已经沉迷这个网站一周之久了,甚至连他心爱的树玩具都弃置一边。
    “嘿嘿,小Hi,你快过来看!”小Ho招呼道。
    “你看,在这个对话框里输入我的名字,在另一个对话框里,输入你的名字,再点这个查询按钮,就可以查出来……什么!我们居然有同一个祖祖祖祖祖爷爷?”
    “诶,真是诶……这个网站有点厉害啊。”小Hi不由感叹道。
    “是啊,这是什么算法啊,这么厉害!”小Ho也附和道。
    “别2,我说的是他能弄到这些数据很厉害,而人类的繁殖树这种层数比较浅的树对这类算法的要求可是简单的不得了,你都能写出来呢!”小Hi道。
    “啊?我也能写出来?可是……该从哪开始呢?”小Ho困惑了。
    小Ho要面临的问题是这样的,假设现在他知道了N个人的信息——他们的父亲是谁,他需要对于小Hi的每一次提问——两个人的名字,告诉小Hi这两个人的是否存在同一个祖先,如果存在,那么他们的所有共同祖先中辈分最低的一个是谁?
  • 输入
    每个测试点(输入文件)有且仅有一组测试数据。
    每组测试数据的第1行为一个整数N,意义如前文所述。
    每组测试数据的第2~N+1行,每行分别描述一对父子关系,其中第i+1行为两个由大小写字母组成的字符串Father_i, Son_i,分别表示父亲的名字和儿子的名字。
    每组测试数据的第N+2行为一个整数M,表示小Hi总共询问的次数。
    每组测试数据的第N+3~N+M+2行,每行分别描述一个询问,其中第N+i+2行为两个由大小写字母组成的字符串Name1_i, Name2_i,分别表示小Hi询问中的两个名字。
    对于100%的数据,满足N<=10^2,M<=10^2, 且数据中所有涉及的人物中不存在两个名字相同的人(即姓名唯一的确定了一个人)。
  • 输出
    对于每组测试数据,对于每个小Hi的询问,输出一行,表示查询的结果:如果根据已知信息,可以判定询问中的两个人存在共同的祖先,则输出他们的所有共同祖先中辈分最低的一个人的名字,否则输出-1。
  • 样例输入
    11
    JiaYan JiaDaihua
    JiaDaihua JiaFu
    JiaDaihua JiaJing
    JiaJing JiaZhen
    JiaZhen JiaRong
    JiaYuan JiaDaishan
    JiaDaishan JiaShe
    JiaDaishan JiaZheng
    JiaShe JiaLian
    JiaZheng JiaZhu
    JiaZheng JiaBaoyu
    3
    JiaBaoyu JiaLian
    JiaBaoyu JiaZheng
    JiaBaoyu LinDaiyu
  • 样例输出
    JiaDaishan
    JiaZheng
    -1

  • 算法思路
    本文提出的算法思路很简单,主要在于如何构建这个关系(家族)树,要考虑关系中是可以存在多棵关系树,因此本文提出了用List存放多棵关系树,接下来则是如何构建关系树,而且要便于搜索两个人的最近公共祖先。
    关系树结构如下:

class Relation{
    public int childNo=0;//孩子数
    public String name;//名字
    public Relation[] child = new Relation[100];//孩子节点
    public Set<String> hasAllnode = new HashSet<>();//包括自己和自己分支下的所有节点
    public Relation(){}
    public Relation(String name) {
        this.name = name;
    }

}

首先,包括该节点的名字name,拥有孩子的数量(也就是说这是一棵多叉树)childNo,孩子节点数组child,最重要的是存储包括自己节点以及自己孩子,以及孩子的孩子的节点,也就是当前节点及其子树的所有节点的集合hasAllnode,这样在搜索两人是否具有公共祖先时,只要一棵一棵的搜索家族树的头节点的hasAllnode集合中是否同时包含两个人,如果只含一个,那要搜索的两个人肯定不是一个家族的,则直接输出-1,如果同属于某一家族树,则在该棵关系(家族)树种搜索,如果所有关系树中都不存在这两个人,毫无疑问也是输出-1。
然后针对在一棵关系树中存在,则遍历该树,此时会发生以下三种情况:
1、树中遍历的节点与要搜索的两个人中一个相同,则这个就是他们的最近公共祖先,返回输出
2、树中遍历的节点hasAllnode集合包含搜索的两人,但是搜索的两人分属于该节点的不同孩子数中,则当前节点即他们的最近公共祖先
3、搜索的两人同属于当前节点的某一孩子的hasAllnode集合,则继续在该孩子书进行搜索,直到出现情况1、2中的一种。

  • 具体算法实现
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Scanner;
import java.util.Set;
//关系结构
class Relation{
    public int childNo=0;//孩子数
    public String name;//名字
    public Relation[] child = new Relation[100];//孩子节点
    public Set<String> hasAllnode = new HashSet<>();//包括自己和自己分支下的所有节点
    public Relation(){}
    public Relation(String name) {
        this.name = name;
    }

}
public class Main{
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        List<String[]> nodes = new ArrayList<String[]>();
        int N = sc.nextInt();

        while(N-->0){
            String names[]  = new String[2];
            names[0] = sc.next();
            names[1] = sc.next();
            int index=0;
            //将具有关系的节点放在一块,即让属于同一家族树的节点放在一块
            for(;index<nodes.size();index++){
                String node[] = nodes.get(index);
                if(node[0].equals(names[0])||node[0].equals(names[1])||node[1].equals(names[0])||node[1].equals(names[1])){
                    nodes.add(index+1, names);
                    break;
                }
            }
            if(index>=nodes.size()){
                nodes.add(names);
            }
        }
        //存储家族树,最终会构建出多棵家族树
        List<Relation> relations= new ArrayList<>();
        for(String[] names:nodes){
            //对于第一棵家族树
            if(relations.size()<=0){
                Relation root = new Relation(names[0]);
                root.hasAllnode.add(names[0]);
                root.hasAllnode.add(names[1]);
                root.child[root.childNo] = new Relation(names[1]); //子节点name
                root.child[root.childNo].hasAllnode.add(names[1]);
                root.childNo++;//子节点数
                relations.add(root);
            }else{
                boolean flag=false;
                //在构建的家族树中寻找属于自己的家族树
                for(int index=0;index<relations.size();index++){
                    Relation root=relations.get(index);
                    if(root.hasAllnode.contains(names[0])||root.hasAllnode.contains(names[1])){
                        relations.set(index, CreateRelationTree(root,names[0],names[1],root));
                        flag=true;

                    }
                }
                //如果没有找到属于自己家族树,则说明是一棵新的家族树
                if(!flag){
                    Relation root = new Relation(names[0]);
                    root.hasAllnode.add(names[0]);
                    root.hasAllnode.add(names[1]);
                    root.child[root.childNo] = new Relation(names[1]); //子节点name
                    root.child[root.childNo].hasAllnode.add(names[1]);
                    root.childNo++;//子节点数
                    relations.add(root);
                }

            }

        }
        //打印按前序遍历打印家族树
        for(int index=0;index<relations.size();index++){
            Relation root=relations.get(index);
            System.out.println("这是"+(index+1)+"棵关系树:");
            printRelation(root);
        }
        int M = sc.nextInt();//测试组数
        System.out.println("寻找的最近公共祖先结果如下:");
        while(M-->0){
            String names[]  = new String[2];
            names[0] = sc.next();
            names[1] = sc.next();
            //如果输入名字相同,则直接输出
            if(names[0].equals(names[1])){
                System.out.println(names[0]);
                continue;
            }
            boolean flag = false;
            for(Relation root:relations){//在家族树中寻找两人属于同一家族树,然后深入搜索最近祖先,如果不属于同一家族树,则无共同祖先
                if(root.hasAllnode.contains(names[0])&&root.hasAllnode.contains(names[1])){
                    flag=true;
                    System.out.println(getRecentlyAncestor(root,names[0],names[1]));
                }
            }
            if(!flag){
                System.out.println(-1);
            }

        }
    }
   //创建家族树
    private static Relation CreateRelationTree(Relation root, String parent, String child,Relation rootnode) {
        if(root!=null&&root.name!=null&&!root.name.equals("")){
            //如果加入的节点是头节点,则应该插在当前节点前面,形成新的家族树头结点返回
            if(root.name.equals(child)){

                Relation newroot = new Relation(parent);
                newroot.hasAllnode.addAll(root.hasAllnode);
                newroot.child[newroot.childNo] = root;
                newroot.childNo++;
                root = newroot;
                root.hasAllnode.add(parent);

                return root;
            }else{
                root.hasAllnode.add(child);
            }
            if(root.name.equals(parent)){//找到父亲节点,将孩子节点插入家族树中
                root.child[root.childNo] = new Relation(child); //子节点name
                root.child[root.childNo].hasAllnode.add(child);
                root.childNo++;
                return rootnode;
            }else{//没有找到父亲节点,继续深入寻找
                for(int index=0;index<root.childNo;index++){
                    if(root.child[index].hasAllnode.contains(parent)||root.child[index].hasAllnode.contains(child)){
                       return  CreateRelationTree(root.child[index], parent,child,rootnode);
                    }

                }

            }
        }
        return rootnode;
    }
    //按前序遍历打印家族树
    public static void printRelation(Relation root){
        if(root==null||root.name==null||root.name.equals("")){
            return;
        }
        System.out.println("当前节点值:"+root.name+",所有孩子节点:"+root.hasAllnode);
        for(int index=0;index<root.childNo;index++){
            printRelation(root.child[index]);
        }
    }
    //寻找最近祖先
    public static String getRecentlyAncestor(Relation root,String name1,String name2){
        //最近祖先是其中需要找的人中的一个
        if(root.name.equals(name1)||root.name.equals(name2)){
            if(root.name.equals(name1)){
                return name1;
            }else{
                return name2;
            }
        }
        for(int index=0;index<root.childNo;index++){
            //要找的人在该节点开始分叉,则当前节点即为最近祖先
            if((root.child[index].hasAllnode.contains(name1)&&!root.child[index].hasAllnode.contains(name2))||
                    (!root.child[index].hasAllnode.contains(name1)&&root.child[index].hasAllnode.contains(name2))){
                return root.name;
            }else if(root.child[index].hasAllnode.contains(name1)&&root.child[index].hasAllnode.contains(name2)){
                return getRecentlyAncestor(root.child[index],name1,name2);
            }
        }
        return "-1";
    }
}
  • 结果输出

    11
    JiaYan JiaDaihua
    JiaDaihua JiaFu
    JiaDaihua JiaJing
    JiaJing JiaZhen
    JiaZhen JiaRong
    JiaYuan JiaDaishan
    JiaDaishan JiaShe
    JiaDaishan JiaZheng
    JiaShe JiaLian
    JiaZheng JiaZhu
    JiaZheng JiaBaoyu
    3
    JiaBaoyu JiaLian
    JiaBaoyu JiaZheng
    JiaBaoyu LinDaiyu
    这是1棵关系树:
    当前节点值:JiaYan,所有孩子节点:[JiaJing, JiaRong, JiaDaihua, JiaFu, JiaZhen, JiaYan]
    当前节点值:JiaDaihua,所有孩子节点:[JiaJing, JiaRong, JiaDaihua, JiaFu, JiaZhen]
    当前节点值:JiaJing,所有孩子节点:[JiaJing, JiaRong, JiaZhen]
    当前节点值:JiaZhen,所有孩子节点:[JiaRong, JiaZhen]
    当前节点值:JiaRong,所有孩子节点:[JiaRong]
    当前节点值:JiaFu,所有孩子节点:[JiaFu]
    这是2棵关系树:
    当前节点值:JiaYuan,所有孩子节点:[JiaLian, JiaShe, JiaYuan, JiaZhu, JiaDaishan, JiaZheng, JiaBaoyu]
    当前节点值:JiaDaishan,所有孩子节点:[JiaLian, JiaShe, JiaZhu, JiaDaishan, JiaZheng, JiaBaoyu]
    当前节点值:JiaZheng,所有孩子节点:[JiaZhu, JiaZheng, JiaBaoyu]
    当前节点值:JiaBaoyu,所有孩子节点:[JiaBaoyu]
    当前节点值:JiaZhu,所有孩子节点:[JiaZhu]
    当前节点值:JiaShe,所有孩子节点:[JiaLian, JiaShe]
    当前节点值:JiaLian,所有孩子节点:[JiaLian]
    寻找的最近公共祖先结果如下:
    JiaDaishan
    JiaZheng
    -1
    
  • 注意
    如果测试数据两人名字是一样,尽管都没在家族树中出现,则最近公共祖先是他们自己,而不是输出-1
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值