这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战
题目
难度:hard
head First
因为没有思路,所以先从 如何构造开始,而不考虑效率问题。
将题目分解成以下流程:
-
从列表中,取出一颗树
-
从列表剩下的树里,取出root和叶子节点值相等的做替换
- 如果取不到,那么返回
- 如果取到了,那么从接上去的节点之前的节点,再做一次上述的替换操作
- 这里是为了让新接上的部分可以接着往下接其他的树
-
执行完了,看看:
-
是否接上了所有的树?
-
都接上的话,是不是BST?
否则,将整个合并操作回归。
-
-
循环以上流程,就可以得到对应的。
根据流程,拟代码如下:
public static TreeNode canMerge(List<TreeNode> trees) {
Map<Integer,TreeNode> dic = new HashMap<>();
Map<Integer,TreeNode> replace = new HashMap<>();
for (TreeNode treeNode : trees) dic.put(treeNode.val,treeNode);
List<Integer> l = new ArrayList<>(dic.keySet());
for (Integer integer : l) {
TreeNode node = dic.get(integer);
dic.remove(integer);
test(node,dic,replace);
if(dic.isEmpty() && isValid(node,Long.MAX_VALUE,Long.MIN_VALUE)) return node;
rollback(node,dic,replace);
dic.put(integer,node);
}
return null;
}
public static void test(TreeNode root,Map<Integer,TreeNode> dic,Map<Integer,TreeNode> replace){
if(root==null) return;
if(root.left!=null && root.left.left==null && root.left.right==null) {
if (dic.containsKey(root.left.val)) {
TreeNode l = root.left;
root.left = dic.get(root.left.val);
replace.put(l.val, l);
dic.remove(l.val);
test(root, dic, replace);
}
}else{
test(root.left,dic,replace);
}
if(root.right!=null && root.right.left==null && root.right.right==null) {
if (dic.containsKey(root.right.val)) {
TreeNode r = root.right;
root.right = dic.get(root.right.val);
replace.put(r.val, r);
dic.remove(r.val);
test(root, dic, replace);
}
}else{
test(root.right,dic,replace);
}
}
private static void rollback(TreeNode root, Map<Integer, TreeNode> dic, Map<Integer, TreeNode> replace) {
if(root == null) return;
if(root.left!=null){
if(replace.containsKey(root.left.val)){
TreeNode n = root.left;
root.left = replace.get(root.left.val);
replace.remove(root.left.val);
dic.put(root.left.val,n);
}else rollback(root.left, dic, replace);
}
if(root.right!=null){
if(replace.containsKey(root.right.val)){
TreeNode n = root.right;
root.right = replace.get(root.right.val);
replace.remove(root.right.val);
dic.put(root.right.val,n);
}else rollback(root.right, dic, replace);
}
}
public static boolean isValid(TreeNode node,long up,long down){
if(null == node) return true;
if(node.left!=null && (node.left.val>=node.val || node.left.val<=down )) return false;
if(node.right!=null && (node.right.val<=node.val || node.right.val>=up)) return false;
return isValid(node.left,Math.min(node.val,up),down)
&& isValid(node.right,up,Math.max(down,node.val));
}
结果:
错误:[[132],[77,65],[16],[124,112],[181,175],[139,136,150],[175],[65,null,66],[112],[103,26,139],[38,null,62],[66],[183,167,189],[62,null,77],[136,127],[127,124,132],[15,null,16],[189],[26,15,88],[93],[167,103,181],[88,38,93],[150]]
第一考虑:是否回滚不完全?
但做以下考虑:
- 如果没有拼上所有的树,是否需要将整个合并操作完全回滚?
实际上是不需要的,推理如下:
- 题目给定的所有树,都是BST
- 如果某次合并的结果,没有合并所有的树,但是BST,那么其实可以把这个树放回去。
- 因为:
- 根节点的值是唯一的
- 如果可以做合并,那么对于集合中的任意树的任意叶子节点,能替换该节点的树都是唯一的。
- 那么,可以直换某个叶子节点上的树,可以merge上去的树都是确定的(因为上面的推论)。
- 那么,即使在列表中的所有树的所有叶子节点中,有多个叶子节点有相同的值,无论取哪一个节点将树拼上去,事实上对于结果是无所谓的:因为这个子树最后都是算到结果中的,其实我们并不关心拼在哪里,我们只关心最终结果是不是BST(对于这一步,还是需要验证的)。
- 因为:
因此,可以把merge失败但是BST的树,都放回集合中,作为后续的”拼图“。
根据这个思路,修改代码如下:
public static TreeNode canMergeTe(List<TreeNode> trees){
Map<Integer,TreeNode> dic = new HashMap<>();
Map<Integer,TreeNode> replace = new HashMap<>();
for (TreeNode treeNode : trees) dic.put(treeNode.val,treeNode);
List<Integer> rootVal = new ArrayList<>(dic.keySet());
for (Integer integer : rootVal) {
TreeNode tn = dic.get(integer);
if(null == tn) continue;
dic.remove(integer);
test(tn,dic,replace);
if(!isValid(tn,Integer.MAX_VALUE,Integer.MIN_VALUE)){
rollback(tn,dic,replace);
}
dic.put(tn.val,tn);
if(dic.size()==1) return tn;
}
return null;
}
public static void test(TreeNode root,Map<Integer,TreeNode> dic,Map<Integer,TreeNode> replace){
if(root==null) return;
if(root.left!=null && root.left.left==null && root.left.right==null) {
if (dic.containsKey(root.left.val)) {
TreeNode l = root.left;
root.left = dic.get(root.left.val);
replace.put(l.val, l);
dic.remove(l.val);
test(root, dic, replace);
}
}else{
test(root.left,dic,replace);
}
if(root.right!=null && root.right.left==null && root.right.right==null) {
if (dic.containsKey(root.right.val)) {
TreeNode r = root.right;
root.right = dic.get(root.right.val);
replace.put(r.val, r);
dic.remove(r.val);
test(root, dic, replace);
}
}else{
test(root.right,dic,replace);
}
}
private static void rollback(TreeNode root, Map<Integer, TreeNode> dic, Map<Integer, TreeNode> replace) {
if(root == null) return;
if(root.left!=null){
if(replace.containsKey(root.left.val)){
TreeNode n = root.left;
root.left = replace.get(root.left.val);
replace.remove(root.left.val);
dic.put(root.left.val,n);
}else rollback(root.left, dic, replace);
}
if(root.right!=null){
if(replace.containsKey(root.right.val)){
TreeNode n = root.right;
root.right = replace.get(root.right.val);
replace.remove(root.right.val);
dic.put(root.right.val,n);
}else rollback(root.right, dic, replace);
}
}
public static boolean isValid(TreeNode node,long up,long down){
if(null == node) return true;
if(node.left!=null && (node.left.val>=node.val || node.left.val<=down )) return false;
if(node.right!=null && (node.right.val<=node.val || node.right.val>=up)) return false;
return isValid(node.left,Math.min(node.val,up),down)
&& isValid(node.right,up,Math.max(down,node.val));
}
这一次就通过上面的测试用例了,但最终超时了,测试用例太长了就不贴了。
效率提升
思考一下:
- 在哪个节点上述算法可以做提升?
对于每一次二叉树的验证,都是从最上方的根节点往下的,是否存在一种改进策略,可以使得在合并的时候,可以省去一些节点的验证?
其实可以推知:最终合并出的结果的树无论是否是BST,仅有一个。
假设最终有多个结果符合条件:
- 那么由于根节点值互不相同:
- 必然存在多个树的根节点:
- 要么不存在对应的子节点可以将树替换上,这种情况对应的是所有树不能合并到同一个树上。
- 要么所有的根节点都存在相同值的子节点。这种情况下无法满足题目所给的严格BST。
- 因此,结果必然仅有一个。
我们可以将这个结论推广到任意可以拼成一个BST的给定树的子集上,那么对于上述OT的算法可以做出以下的改进:
-
我们不必在任何合并操作时都考虑是否合规一事了,直接合并即可,那么也不用回滚了。
-
并且,为了加快进度并在判断合规中中执行合并操作,那么势必我们要找到唯一一个根节点,随后进行合并,这样子就不用在合并一部分,转到下一次合并的时候,再去重复递归那些已判断合规的节点了。
-
同时注意到:BST可以通过中序遍历判断其严格递增,来判断是否合规。那么,只要我们的合并操作和中序遍历顺序相同且在判断之前就已做了合并操作,合并就可以放入判断之中。
中序遍历检查是否是BST的算法:
static long last = 0; public static boolean isValidBST(TreeNode root){ if(root==null) return true; if(!isValidBST(root.left)) return false; if(root.val<=last) return false; last = root.val; return isValidBST(root.right); }
那么,综合以上这三点,可以得出以下算法:
static long last = Long.MIN_VALUE;
public static TreeNode canMerge(List<TreeNode> trees){
Map<Integer,TreeNode> rootDic = new HashMap<>();
Set<Integer> leavesVal = new HashSet<>();
for (TreeNode tree : trees) {
rootDic.put(tree.val,tree);
//这里对应的是条件中的:输入数据的每个节点可能有子节点但不存在子节点的子节点
if(tree.left!=null) leavesVal.add(tree.left.val);
if(tree.right!=null) leavesVal.add(tree.right.val);
}
for (TreeNode tree : trees) {
//这里是为了找到唯一一个没有同值的叶子节点[1]
if(leavesVal.contains(tree.val)) continue;
//找到了,我们就执行检查+合并的操作,记得要提前将这个节点摘出
//rootDic.remove(tree.val);
last = Long.MIN_VALUE;
//合并完成之后,我们还要检查一下是否所有树都拼好了
return mergeAndCheckTree(tree,rootDic) && rootDic.size()==1?tree:null;
}
return null;
}
public static boolean mergeAndCheckTree(TreeNode root,Map<Integer,TreeNode> dic){
if(root==null) return true;
//判断左边节点,是否是叶子节点
if(root.left!=null && root.left.right==null && root.left.left==null && dic.containsKey(root.left.val)){
root.left = dic.get(root.left.val);
//这里的remove,实际上只和最终的结果有关
dic.remove(root.left.val);
}
//验证左子树
if(!mergeAndCheckTree(root.left,dic)) return false;
//验证本节点
if(root.val<=last) return false;
last = root.val;
//随后对右边,做和左节点相同的操作
if(root.right!=null && root.right.right==null && root.right.left==null && dic.containsKey(root.right.val)){
root.right = dic.get(root.right.val);
//这里的remove,实际上只和最终的结果有关
dic.remove(root.right.val);
}
return mergeAndCheckTree(root.right,dic);
}
执行用时:32 ms, 在所有 Java 提交中击败了54.76%的用户
内存消耗:59 MB, 在所有 Java 提交中击败了100.00%的用户