LeetCode-1483.树节点的第 K 个祖先 倍增动规、层序遍历+前序遍历+二分查找

这里是题目描述:LeetCode-1483.树节点的第 K 个祖先

本题有两种解法,分别是 使用倍增的动态规划层序遍历+前序遍历+二分查找,这两种方法各有特点,下面我们对这两种解法进行介绍

方法1 倍增动态规划

本解法参考了LeetCode官网的题解:
题解一
题解二

如果使用动态规划来解本题,按照常规想法应该建立并维护的动规表dp是什么样呢?常规想法dp[i][j]存储的是编号为i的节点的第j个祖先。但是从题干中可知,节点总数n和第k个祖先的取值范围是1<= k<=n<= 5*10^4,如果按常规方法建立动规表,那么就需要建立一个尺寸为n*n的二维动规表,整个算法的时间和空间开销将有可能达到25*10^8,一定会超出时间限制。

为了降低时间和空间开销,我们使用倍增法进行优化,将动规表进行变化:dp[i][j]存储的是编号为i的节点的第2^j个祖先,也就是dp[i][0]存储节点i的第1个祖先(2^0=1)dp[i][1]存储节点i的第2个祖先(2^1=2)dp[i][2]存储节点i的第4个祖先(2^2=4)dp[i][j]dp[i][j+1]之间满足倍增关系。倍增动规表的状态转移方程如下:

dp[i][j]=parent[i]                                    //j=0
dp[i][j]=-1                                           //j>0,dp[i][j-1]==-1
dp[i][j]=dp[dp[i][j-1]][j-1]                          //j>0,dp[i][j-1]!=-1

采用倍增动态规划后,需要维护的动规表尺寸减小为n*logn,建立并向动规表中填值的时间开销为n*logn时间和空间开销大大减少

建立好倍增动规表后,就可以利用动规表进行祖先查询。动规表记录了每个节点的第 1,2,4,8,16...个祖先节点。那么对于每次询问,最多只会向上跳log(n)次。如下图所示,每个点最多会建立logn条向上的边。以寻找结点5的第5个祖先为例,可以转化为寻找结点1的第1个祖先,再转化为寻找结点0个第0个祖先,即结点0
在这里插入图片描述
而上述寻找过程中在代码上的实现,用到了位运算的技巧,详见题解代码

倍增动规题解代码:

class TreeAncestor {

    int[][] dp; //dp[i][j]存储节点i的第2^j个祖先
    //倍增法动态规划
    public TreeAncestor(int n, int[] parent) {
        dp=new int[n][(int)(Math.log(n)/Math.log(2))+1];
        for(int i=0;i<n;i++)
        {
            dp[i][0]=parent[i];
        }
        for(int j=1;j<dp[0].length;j++)
        {
            for(int i=0;i<n;i++)
            {
                if(dp[i][j-1]==-1)
                {
                    dp[i][j]=-1;
                }
                else
                {
                    dp[i][j]=dp[dp[i][j-1]][j-1];
                }
            }
        }
    }

    //根据倍增法建立的dp表来求一个节点的第k个祖先
    public int getKthAncestor(int node, int k) {
        int bit=0; //记录位
        while(k!=0)
        {
            if(bit>=dp[node].length)
            {
                return -1;
            }
            if((k&1)!=0) //当前二进制位不为0
            {
                node=dp[node][bit];
            }
            if(node==-1)
            {
                return -1;
            }
            bit++;
            k=k>>>1;
        }
        return node;
    }
}

时间复杂度:建表阶段O(nlogn),查询节点每次查询O(logn)
空间复杂度:O(nlogn)

方法2 层序遍历+前序遍历+二分查找

本解法参考了LeetCode官网的题解:官网题解

预备知识

  1. 一个节点的所有祖先节点(从根节点到该节点的路径上经过的所有点))的dfs序(从根节点已dfs访问的顺序)都在该节点前。
    例如:
    在这里插入图片描述
    dfs序为 0, 1, 3, 4, 2, 5, 6。每个节点的祖先节点都在它前面。
  2. 可以通过对树进行层次遍历,确定每层有哪些节点。

分析
有了上面两个预备知识,我们可以想到:
假设要找节点A的第k个节点,节点AL层,及只需要找到第L-k的所有节点中A的祖先节点
又由于A的祖先节点在每一层只有一个,且dfs序必然在节点A前面,所以可以通过二分查找在L-k层中找比A小且距离A最近的元素即为答案
另外,这个方法需要先根据parent数组还原出对饮的数结构,然后再进行下面的层序遍历、先序遍历、二分查找等操作

题解代码:

class TreeAncestor {

    TreeNode root;
    TreeNode[] treeNodeList;
    HashMap<Integer,ArrayList<Integer>> degreeMap;

    //构建树结构+层序遍历+先序遍历+二分查找
    public TreeAncestor(int n, int[] parent) {
        //构建树结构
        treeNodeList=new TreeNode[n];
        for(int i=0;i<treeNodeList.length;i++)
        {
            treeNodeList[i]=new TreeNode(i);
        }
        root=treeNodeList[0];
        for(int i=0;i<parent.length;i++)
        {
            if(parent[i]==-1)
            {
                root=treeNodeList[i];
            }
            else
            {
                treeNodeList[parent[i]].child.add(treeNodeList[i]);
            }
        }
        //层序遍历
        degreeMap=new HashMap<>();
        Queue<TreeNode> queue=new LinkedList<>();
        queue.offer(root);
        int d=0;
        while(!queue.isEmpty())
        {
            Queue<TreeNode> tempQueue=new LinkedList<>();
            ArrayList<Integer> list=new ArrayList<>();
            while(!queue.isEmpty())
            {
                TreeNode curNode=queue.remove();
                curNode.degree=d;
                list.add(curNode.value);
                for(TreeNode node:curNode.child)
                {
                    tempQueue.offer(node);
                }
            }
            queue.addAll(tempQueue);
            degreeMap.put(d,list);
            d+=1;
        }
        //前序遍历
        preOrderTravel(root,0);
    }
    //前序遍历,在此过程中记下每个节点的在前序遍历序列中的位置
    int preOrderTravel(TreeNode node,int m)
    {
        if(node==null)
        {
            return m;
        }
        m+=1;
        node.posInPreOrd=m;
        for(TreeNode c:node.child)
        {
            m=preOrderTravel(c,m);
        }
        return m;
    }
    public int getKthAncestor(int node, int k) {
        TreeNode treeNode=treeNodeList[node];
        if(treeNode.degree-k<0)
        {
            return -1;
        }
        ArrayList<Integer> list=degreeMap.get(treeNode.degree-k);
        int p=treeNode.posInPreOrd;
        int l=0,r=list.size()-1;
        while(l<=r)
        {
            int m=(l+r)/2;
            if(treeNodeList[list.get(m)].posInPreOrd>p)
            {
                r=m-1;
            }
            else
            {
                l=m+1;
            }
        }
        return treeNodeList[list.get(r)].value;
    }
}
class TreeNode //树节点
{
    int value;
    int posInPreOrd; //该节点在dfs序列中的位置
    int degree; //该节点在层序遍历中的层级
    ArrayList<TreeNode> child;
    TreeNode(int value)
    {
        this.value=value;
        child=new ArrayList<>();
    }
}

时间复杂度:层序遍历和前序遍历均为O(n),二分查找阶段每次O(logn)
空间复杂度:O(n)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值