1720. 解码异或后的数组
题目描述
未知 整数数组 arr 由 n 个非负整数组成。
经编码后变为长度为 n - 1 的另一个整数数组 encoded ,其中 encoded[i] = arr[i] XOR arr[i + 1] 。例如,arr = [1,0,2,1] 经编码后得到 encoded = [1,2,3] 。
给你编码后的数组 encoded 和原数组 arr 的第一个元素 first(arr[0])。
请解码返回原数组 arr 。可以证明答案存在并且是唯一的。
题目分析
简单题,主要考察异或的两个特性。
- 恒等律:p XOR 0 = p
- 归零律:p XOR p = 0
推及此题,
根据题目描述可知 encoded[i] = result[i] ^ result[i + 1]
可得 encoded[i] ^ result[i] = result[i] ^ result[i + 1] ^ result[i] = result[i + 1];
参考代码
/**
* @param {number[]} encoded
* @param {number} first
* @return {number[]}
*/
var decode = function(encoded, first) {
let result = [first];
for (let i = 0; i < encoded.length; i++) {
result.push(result[i] ^ encoded[i]);
}
return result;
};
1721. 交换链表中的节点
题目描述
给你链表的头节点
head和一个整数k。交换 链表正数第
k个节点和倒数第k个节点的值后,返回链表的头节点(链表 从 1 开始索引)。
题目分析
是一道链表的题目,我曾经在 juejin.cn/post/684490… 里写过对链表的题目,如何快速的在本地调试,需要的可以参考下。
题目比较容易理解,就是交换链表中指定的两个节点。把这两个节点分别用 A 和 B 代表,简单的画了下图来说明交换节点的过程。
但是有几个特殊情况需要特殊处理:
-
A 为首节点
这里需要介绍一个小技巧,就是在处理链表类题目时,为链表额外添加一个虚拟节点作为首节点,这样可以省去处理一些边界情况,例如表头为空等,此题中加了空节点就不再需要单独处理 A 为首节点的情况。
let dummy = new ListNode(); dummy.next = head; // solve code... return dummy.next; -
A 和 B 为相同节点
这种情况比较简单,不需要做任何处理,可以直接返回。
-
A 和 B 相邻
其实也比较好处理,但不可以像上图中一样处理,否则会造成循环链表。
其他的情况都可以按图中的交互方式处理即可。
参考代码
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var swapNodes = function(head, k) {
let dummy = new ListNode(); // 额外添加一个空表头 这样可以省去判断 k=1 的特殊情况
dummy.next = head;
// 遍历链表 得到链表的长度
let current = dummy;
let length = 0;
while (current = current.next) {
length++;
}
// 判断 a 和 b 的位置 保证 a 在 b 前面
let a = Math.min(k, length - k + 1);
let b = Math.max(k, length - k + 1);
// 第一种情况 两个节点相同 不需要交换
if (a === b) return dummy.next;
// 遍历链表 节点 a 和 b 及其前面的节点
current = dummy;
let a_prev_node, b_prev_node, a_node, b_node;
for (let i = 0; i <= length; i++) {
if (i === a - 1) {
a_prev_node = current;
}
if (i === a) {
a_node = current;
}
if (i === b - 1) {
b_prev_node = current;
}
if (i === b) {
b_node = current;
break; // 找到第二个就可以退出了
}
current = current.next;
}
// 第二种情况 两个节点相邻
if (a + 1 === b) {
a_prev_node.next = b_node;
a_node.next = b_node.next;
b_node.next = a_node;
return dummy.next;
}
// 第三种情况 两个节点不相邻
a_prev_node.next = b_node;
b_prev_node.next = a_node;
let temp = a_node.next;
a_node.next = b_node.next;
b_node.next = temp;
return dummy.next;
};
1722. 执行交换操作后的最小汉明距离
题目描述
给你两个整数数组 source 和 target ,长度都是 n 。还有一个数组 allowedSwaps ,其中每个 allowedSwaps[i] = [ai, bi] 表示你可以交换数组 source 中下标为 ai 和 bi(下标从 0 开始)的两个元素。注意,你可以按 任意 顺序 多次 交换一对特定下标指向的元素。
相同长度的两个数组 source 和 target 间的 汉明距离 是元素不同的下标数量。形式上,其值等于满足 source[i] != target[i] (下标从 0 开始)的下标 i(0 <= i <= n-1)的数量。
在对数组 source 执行 任意 数量的交换操作后,返回 source 和 target 间的 最小汉明距离 。
题目分析
由于题目规则可以交换任意多次和任意顺序交换,也就是说,如果 a 和 b 可以交换,a 和 c 可以交换,那么 a, b, c 的位置是可以任意交换的。想起了什么?并查集!
关于并查集,不了解的可以去搜一下,代码简单但非常精妙。简单来说就是,如果 a 和 b 是连接的,那么把 a 的父节点设置为 b,这样相互连接的节点都被连到一个树上,判断两个节点是否连接,只需要判断两个节点的根节点是否相同。
然后,根据 allowedSwaps 把 source 和 target 数组被分为几个块,我们分别判断每个块中相同的数字的个数,就是进行交换后可以达到相同的数字个数,总数减去这个数字就是答案,即最小汉明距离。
参考代码
/**
* @param {number[]} source
* @param {number[]} target
* @param {number[][]} allowedSwaps
* @return {number}
*/
var minimumHammingDistance = function(source, target, allowedSwaps) {
let n = source.length;
let fa = new Array(n).fill().map((v, index) => index);
let findRoot = (x) => x === fa[x] ? x : x = findRoot(fa[x]);
for (let [a, b] of allowedSwaps) {
let roota = findRoot(a);
let rootb = findRoot(b);
if (roota !== rootb) {
fa[roota] = rootb;
}
}
let sets = new Array(n).fill().map(() => []);
for (let i = 0; i < n; i++) {
sets[findRoot(i)].push(i);
}
let count = 0;
for (let i = 0; i < n; i++) {
if (sets[i].length) {
let sourcevals = [];
let targetvals = [];
for (let index of sets[i]) {
sourcevals.push(source[index]);
targetvals.push(target[index]);
}
// 判断 source 和 target 有多少个数字是相同的
sourcevals.sort((a, b) => a - b);
targetvals.sort((a, b) => a - b);
for (let i = 0, j = 0, l = sourcevals.length; i < l && j < l;) {
if (sourcevals[i] === targetvals[j]) {
count++;
i++, j++;
} else if (sourcevals[i] < targetvals[j]) {
i++;
} else {
j++;
}
}
}
}
return n - count;
};
1723. 完成所有工作的最短时间
题目描述
给你一个整数数组
jobs,其中jobs[i]是完成第i项工作要花费的时间。请你将这些工作分配给
k位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。工人的工作时间是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的最大工作时间得以最小化 。返回分配方案中尽可能最小的最大工作时间 。
题目分析
一向讨厌看题解做题,花了三个多小时苦思冥想,才终于水过了。QAQ
其实看到题目第一反应是二分,看了数据范围又觉得是状压DP,最后还是靠搜索+剪枝过的,说起来,剪枝好的话,可以大大减少解题时间,不过一般还挺难想的,比如这道题,就还是挺巧妙的。
代码中有一个重要的剪枝条件 if (personTime[i] === 0) break;
可以理解为,如果当前工人的工时为 0 就不继续递归。这个题中,每个工人其实是一样的,没有特殊性,也就是说,对于一个 job 分配给 a 或分配给 b 是一样的,那么在搜索的时候,当前工人的工时为 0 也就是说我要略过当前工人,而去把该工作分配给下一个人,那么这样的遍历其实是多余的,所以可以直接略过。(代码参考了 题解:DFS+剪枝 40ms)
参考代码
/**
* @param {number[]} jobs
* @param {number} k
* @return {number}
*/
var minimumTimeRequired = function(jobs, k) {
let ans = jobs.reduce((prev, curr) => prev + curr, 0);
let n = jobs.length;
let personTime = new Array(k).fill(0);
/**
* index 当前处理的 jobs 下标,从 0 开始,处理到 n 则证明全部处理结束
*/
function dfs(index) {
if (index === n) {
ans = Math.min(ans, Math.max(...personTime));
return;
}
// 尝试把 jobs[index] 分配给每个人
for (let i = 0; i < k; i++) {
// 如果时间已经大于之前求得的最小值 证明不是最优解 不需要考虑
if (personTime[i] + jobs[index] >= ans) {
continue;
}
personTime[i] += jobs[index];
dfs(index + 1);
personTime[i] -= jobs[index];
if (personTime[i] === 0) break;
}
}
dfs(0);
return ans;
};