一、问题描述
存在一棵无向连通树,树中有编号从 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的父节点,会存在以下三种情况:
- 删除的两条边在同一颗子树内,且 y1是 x2的祖先节点(或重合)。如下图所示,这三个连通块的异或和分别是xor[y2]、xor[y1]⊕xor[y2] 和 xor[0]⊕xor[y1](⊕ 表示异或运算)。
- 删除的两条边在同一颗子树内,且 y2是 x1的祖先节点(或重合)。同上,这三个连通块的异或和分别为 xor[y1]、xor[y2]⊕xor[y1] 和 xor[0]⊕xor[y2]。
- 删除的两条边分别属于两颗不相交的子树。如下图所示,这三个连通块的异或和分别为 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]