这里是题目描述:LeetCode-1483.树节点的第 K 个祖先
本题有两种解法,分别是 使用倍增的动态规划 和 层序遍历+前序遍历+二分查找,这两种方法各有特点,下面我们对这两种解法进行介绍
方法1 倍增动态规划
如果使用动态规划来解本题,按照常规想法应该建立并维护的动规表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官网的题解:官网题解
预备知识
- 一个节点的所有祖先节点(从根节点到该节点的路径上经过的所有点))的dfs序(从根节点已dfs访问的顺序)都在该节点前。
例如:
dfs序为 0, 1, 3, 4, 2, 5, 6。每个节点的祖先节点都在它前面。 - 可以通过对树进行层次遍历,确定每层有哪些节点。
分析
有了上面两个预备知识,我们可以想到:
假设要找节点A
的第k
个节点,节点A
在L
层,及只需要找到第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)