你有一个带有四个圆形拨轮的转盘锁。每一个拨轮都有 10 个数字: '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' 。每一个拨轮能够自由旋转:例如把 '9' 变为 '0','0' 变为 '9' 。每次旋转都只能旋转一个拨轮的一位数字。javascript
锁的初始数字为 '0000' ,一个表明四个拨轮的数字的字符串。java
列表 deadends 包含了一组死亡数字,一旦拨轮的数字和列表里的任何一个元素相同,这个锁将会被永久锁定,没法再被旋转。node
字符串 target 表明能够解锁的数字,你须要给出最小的旋转次数,若是不管如何不能解锁,返回 -1。算法
示例 1:测试
输入:deadends = ["0201","0101","0102","1212","2002"], target = "0202" 输出:6 解释: 可能的移动序列为 "0000" -> "1000" -> "1100" -> "1200" -> "1201" -> "1202" -> "0202"。 注意 "0000" -> "0001" -> "0002" -> "0102" -> "0202" 这样的序列是不能解锁的, 由于当拨动到 "0102" 时这个锁就会被锁定。
示例 2:优化
输入: deadends = ["8888"], target = "0009" 输出:1 解释: 把最后一位反向旋转一次便可 "0000" -> "0009"。
示例 3:spa
输入: deadends = ["8887","8889","8878","8898","8788","8988","7888","9888"], target = "8888" 输出:-1 解释: 没法旋转到目标数字且不被锁定。
示例 4:code
输入: deadends = ["0000"], target = "8888" 输出:-1
提示:blog
来源:力扣(LeetCode)连接ip
这道题颇有意思,有个相似于咱们皮箱的那种密码锁,能够经过上下转动转出任意的密码,不过这里有个限制,就是不能经过一些死亡数字,算是给咱们增长了一些难度,不然的话就只看密码的数字是大仍是小了,例如是 3 的话就从0->1->2->3
,是 8 的话就0->9->8
。
咱们能够把0 0 0 0
看作一个原点,而后这 1 个点能够变化出 8 种不一样的结果,以下图所示:
而后这 8 个点能够继续变化:
请注意观察上图,其中有 8 种组合又回到了 0 0 0 0
,还有蓝色部分都是具备重合项的。
那么咱们的思路来了,能够利用这种变化,从0 0 0 0
变化为 8 个点,再继续由先和 8 个点继续变化。。。直到咱们找到了目标密码,每变化一次须要旋转一次。
var openLock = function (deadends, target) { // 存储全部的原点,最初是 0000 let nodes = new Set(); nodes.add('0000'); // 匹配过的点,好比 0000 这种,对于匹配过的点不会加入到原点集合里面去 const history = new Set(); // 初始化旋转次数 let step = 0; // 向上旋转,例如从0->1 const plus = function (nums, i) { let array = nums.split(''); if (array[i] === '9') { array[i] = '0'; } else { array[i] = Number(array[i]) + 1; } return array.join(''); }; // 向下旋转,例如从0->9 const miuns = function (nums, i) { let array = nums.split(''); if (array[i] === '0') { array[i] = '9'; } else { array[i] = Number(array[i]) - 1; } return array.join(''); }; // 原点没有目标密码 while (!nodes.has(target)) { // 新增的原点集合 const newNodes = new Set(); // 当前原点集合 for (const nums of nodes) { // 遇到不通的路就跳过 if (deadends.includes(nums)) { continue; } // 遍历数字,分别作向上和向下旋转 for (let i = 0; i < nums.length; i++) { // 旋转后的结果,把向上和向下旋转后的原点都存储起来 let result = plus(nums, i); // 排除已选择的原点 if (!history.has(result) && !newNodes.has(result)) { newNodes.add(result); } result = miuns(nums, i); if (!history.has(result) && !newNodes.has(result)) { newNodes.add(result); } } // 已检查过的原点 history.add(nums); } step++; // 新生成的原点集合,下一轮将对这些原点进行旋转 nodes = newNodes; // 这里很关键,最后可能收敛没了 if (nodes.size === 0) { return -1; } } return step; };
通过测试结果以下:
结果正确,但好像运行时间有点长,哪里能够优化呢?
咱们目前的思路是由一个原点慢慢扩散到终点,也就是目标密码。就像是向水面扔了一颗石子,激起了一圈的涟漪,而后这圈涟漪最终碰到了咱们的目标,那么若是我同时在目标处扔一个石子,让两个涟漪互相靠近,这样是否是会快不少呢?直觉告诉我确定会快不少,并且,涟漪不须要扩散得很大就能够发现目标,咱们来试一下
var openLock = function (deadends, target) { // 存储全部的原点,最初是 0000 let nodes = new Set(); nodes.add('0000'); // 目标原点 let targetNodes = new Set(); targetNodes.add(target); // 匹配过的点,好比 0000 这种,对于匹配过的点不会加入到原点集合里面去 const history = new Set(); // 初始化旋转次数 let step = 0; // 向上旋转,例如从0->1 const plus = function (nums, i) { let array = nums.split(''); if (array[i] === '9') { array[i] = '0'; } else { array[i] = Number(array[i]) + 1; } return array.join(''); }; // 向下旋转,例如从0->9 const miuns = function (nums, i) { let array = nums.split(''); if (array[i] === '0') { array[i] = '9'; } else { array[i] = Number(array[i]) - 1; } return array.join(''); }; // 原点没有目标密码 while (nodes.size > 0 && targetNodes.size > 0) { // 新增的原点集合 const newNodes = new Set(); // 当前原点集合 for (const nums of nodes) { // 遇到不通的路就跳过 if (deadends.includes(nums)) { continue; } // 相遇 if (targetNodes.has(nums)) { return step; } // 遍历数字,分别作向上和向下旋转 for (let i = 0; i < nums.length; i++) { // 旋转后的结果,把向上和向下旋转后的原点都存储起来 let result = plus(nums, i); // 排除已选择的原点 if (!history.has(result) && !newNodes.has(result)) { newNodes.add(result); } result = miuns(nums, i); if (!history.has(result) && !newNodes.has(result)) { newNodes.add(result); } } // 已检查过的原点 history.add(nums); } step++; // 交换集合,下一轮对targetNodes进行检查 nodes = targetNodes; // 新生成的原点集合,下下一轮将对这些原点进行旋转 targetNodes = newNodes; } // 没有结果 return -1; };
代码稍加改动的结果:
借用斯温(DOTA 英雄)的一句名言:『这下牛 b 了』,运行时间和所占内存都上了一个台阶
最近在看算法方面的内容,碰到有趣的就分享一下,会持续分享