JavaScript算法05-从树中删除边的最小分数

一、问题描述

存在一棵无向连通树,树中有编号从 0 到 n - 1 的 n 个节点, 以及 n - 1 条边。给你一个下标从0开始的整数数组nums,长度为n,其中nums[i]表示第i个节点的值。另给你一个二维整数数组edges,长度为n-1,其中edges[i]=[ai, bi]表示树中存在一条位于节点ai和bi之间的边。
删除树中两条 不同 的边以形成三个连通组件。对于一种删除边方案,定义如下步骤以计算其分数:

  • 分别获取三个组件 每个 组件中所有节点值的异或值。
  • 最大 异或值和 最小 异或值的 差值 就是这一种删除边方案的分数。
    例如,三个组件的节点值分别是:[4,5,7]、[1,9] 和 [3,3,3] 。三个异或值分别是 4 ^ 5 ^ 7 = 6、1 ^ 9 = 8 和 3 ^ 3 ^ 3 = 3 。最大异或值是 8 ,最小异或值是 3 ,分数是 8 - 3 = 5 。返回在给定树上执行任意删除边方案可能的 最小 分数。
    示例1:
    在这里插入图片描述
    输入:nums = [1,5,5,4,11], edges = [[0,1],[1,2],[1,3],[3,4]]
    输出:9
    解释:上图展示了一种删除边方案。
  • 第 1 个组件的节点是 [1,3,4] ,值是 [5,4,11] 。异或值是 5 ^ 4 ^ 11 = 10 。
  • 第 2 个组件的节点是 [0] ,值是 [1] 。异或值是 1 = 1 。
  • 第 3 个组件的节点是 [2] ,值是 [5] 。异或值是 5 = 5 。
    分数是最大异或值和最小异或值的差值,10 - 1 = 9 。

二、解题方法

时间戳解题

1、何为时间戳
在DFS一棵树的过程中,维护一个全局的时间戳clock,每访问一个新的节点,就将clock加一。同时,记录进入节点x时的时间戳in[x],和离开(递归结束)这个节点时的时间戳out[x]。
2、时间戳的性质
根据DFS的性质,当我们递归以x为根的子树时,设y是x的子孙节点,我们必须先递归完以y为根的子树,然后才能递归完以x为根的子树。
从时间戳上看,如果y是x的子孙节点,那么区间[in[y],ouy[y]]必然被区间[in[x],out[x]]所包含。反之,如果区间 [in[y],out[y]]被区间 [in[x],out[x]],out[x]] 所包含,那么 y 必然是 x 的子孙节点(换句话说 x是 y 的祖先节点)。因此我们可以通过
in[x]<in[y]<=out[y]<=out[x]
来判断x是否为y的祖先节点,由于in[y]<=out[y]恒成立,上式可以简化为
in[x]<in[y]≤out[x]

在本题中,由于需要求出子树的异或和,不妨以0为根,DFS这棵树,在求出时间戳的同时,求出每颗以x为根的子树的异或和xor[x]。
由于n比较小,我们可以用O(n^2)的时间枚举要删除的两条边x1​-y1​ 和 x2-y2,并假设x是y的父节点,会存在以下三种情况:

  1. 删除的两条边在同一颗子树内,且 y1是 x2的祖先节点(或重合)。如下图所示,这三个连通块的异或和分别是xor[y2​]、xor[y1]⊕xor[y2] 和 xor[0]⊕xor[y1](⊕ 表示异或运算)。
    在这里插入图片描述
  2. 删除的两条边在同一颗子树内,且 y2是 x1的祖先节点(或重合)。同上,这三个连通块的异或和分别为 xor[y1]、xor[y2]⊕xor[y1] 和 xor[0]⊕xor[y2]。
  3. 删除的两条边分别属于两颗不相交的子树。如下图所示,这三个连通块的异或和分别为 xor[y1]、xor[y2]和 xor[0]⊕xor[y1]⊕xor[y2]。
    在这里插入图片描述因此关键之处在于判断这两条边的关系,这可以用上文提到的时间戳的性质 O(1)地判断出来。
    代码:
/**
 * @param {number[]} nums
 * @param {number[][]} edges
 * @return {number}
 */
var minimumScore = function(nums, edges) {
    const n=nums.length;
    let clock=0; //时间戳
    const g=new Array(n).fill(0).map(()=>[]); //存储父节点,这里我理解为把每一个元素转换为一个列表,方便存储节点,因为一个节点可能有多个子节点。

    for(const[x,y] of edges){++
        g[x].push(y);
        g[y].push(x);
    }

    const xor=new Array(n);
    const inn=new Array(n);
    const out=new Array(n);

    const dfs=(x,fa)=>{
        inn[x]=++clock;
        xor[x]=nums[x];
        for(const y of g[x]){
            if(y!==fa){
                dfs(y,x);
                xor[x]^=xor[y];
            }
        }
        out[x]=clock;
    }

    dfs(0,-1);  //从根节点出发开始递归

    //返回x是否为y的父节点
    const isParent=(x,y)=>{
        //这里的<=是为了兼容两条边连续的情况,即x1-y1,x2-y2。其中y1和xx1是同一个节点
        return inn[x]<=inn[y]&&inn[y]<=out[x];
    }
    //预处理,当temp[0]不是temp[1]的父节点时,交换节点位置
    for(const temp of edges){
        const [x,y]=temp;
        if(!isParent(x,y)){
            [temp[0],temp[1]]=[temp[1],temp[0]];
        }
    }
    let ans=Infinity;
    //双循环遍历边
    for(let i=0;i<edges.length;i++){
        const [x1,y1]=edges[i];
        for(let j=0;j<i;j++){
            const [x2,y2]=edges[j];
            let x,y,z;
            //分类讨论
            if(isParent(y1,x2)){  //y1是x2的祖先节点
                x=xor[y2];
                y=xor[y1]^xor[y2];
                z=xor[0]^xor[y1]
            }else if(isParent(y2,x1)){  //y2是x1的祖先节点
                x=xor[y1];
                y=xor[y2]^xor[y1];
                z=xor[0]^xor[y2];
            }else{ //删除的两条边分别属于两颗不相交的子树
                x=xor[y1];
                y=xor[y2];
                z=xor[0]^xor[y1]^xor[y2];
            }
            ans=Math.min(ans,Math.max(x,y,z)-Math.min(x,y,z))
            if ans==0:return 0;  //提前退出
        }
    }
    return ans;
};

三、知识补充

3.1异或值计算

异或运算:相同为0,不同为1
异或运算就记成无进位相加。
满足交换律,结合律。

0^N = N
N^N = 0

int a = 7; //  00111
int b = 13; // 01101
			   01010 = 8+2 = 10

3.2 js数组中的map方法

作用是遍历整个数组,对里面的每个值做处理再返回一个新的值。
map方法的结构及入参:map的入参需要传递一个函数进去,因为说它是可以遍历数组的,所以传入的这个函数调用的次数由数组的长度决定,长度是3就是3次,是4就是4次。

//index可以不传
[1,2,3].map(function(item,index){
    //这个数组长度是3所以函数调用3次
    //item指的遍历到的对应的数组值 函数调用的三次中 第一次是1 ,然后是2、3
    //index是数组的索引,三次分别是0,1,2
})

上面的例子意思就是map里面的函数运行了3次,分别是function(1,0)、function(2,1)、function(3,2)。
然后你可以做什么呢,可以在函数里面return(return的内容将作为新值代替数组遍历到的旧值item),比如将函数里面的值都变成原来的两倍:

let b=[1,2,3].map(function(item,index){
   return item*2
})
console.log(b)//[2,4,6]

**语法糖:**ES6的一种便捷用法,如箭头函数()=>{},在这里可以使用箭头代替原来的函数:

let b=[1,2,3].map((item,index)=>{
   return item*2
})
console.log(b)//[2,4,6]

用箭头函数有什么好处呢,如果里面不用进行复杂的判断,我们可以将右边的{}改成表达式直接返回,省略return:

let b=[1,2,3].map((item,index)=> item*2 )
console.log(b)//[2,4,6]

因为在我们这里例子里面index索引值没用到所以我们可以不传,当入参只有一个时,前面的括号()都可以省略。

let b=[1,2,3].map( item => item*2 )
console.log(b)//[2,4,6]

还有有时候你可能不想改变所有的值,比如我只想改变小于2的数字,其他的不变,那就进行判断不满足条件的把item原路返回:

let b=[1,2,3].map(item=>{
   if(item<2){
      return item*2
   }else{
      return item
   }
})
console.log(b)//[2,2,3]

改为三目运算。

let b=[1,2,3].map(item=>{
   //返回,当item<2的时候返回item*2否则返回item
   return item<2?item*2:item
})
console.log(b)//[2,2,3]
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值