现有一个 3x3 规格的 Android 智能手机锁屏程序和两个正整数 m 和 n ,请计算出使用最少m 个键和最多 n个键可以解锁该屏幕的所有有效模式总数。 其中有效模式是指: 1、每个模式必须连接至少m个键和最多n个键; 2、所有的键都必须是不同的; 3、如果在模式中连接两个连续键的行通过任何其他键,则其他键必须在模式中选择,不允许跳过非选择键(如图); 4、顺序相关,单键有效(这里可能跟部分手机不同)。 输入:m,n 代表允许解锁的最少m个键和最多n个键 输出:满足m和n个键数的所有有效模式的总数
正常解题思路是把九个点按如下编号进行计算
这种编号虽然比较直观,但是缺失去了九个点的对称性。
那么如何给9个点编号,以示区分又体现出对称性呢?可以用坐标方式,将中间的点标记为0,0原点:
但这样会给编程带来更多的复杂性,这时候我们可以引入平衡三进制,将坐标改为如下的数字:
再进一步转换成十进制数字:
现在9个点就变成这个矩阵了,从题目中看到,我们需要计算两点之间是否存在一个点,用这个矩阵中的数字就很好算了,两个数字相加=0说明穿过中间的点。如果两个点相乘的绝对值=8说明是2、4组合(包括-2、-4)必经过(1、3、-1、-3),直接通过相加除以2即能得到穿过的点,比如4和2,相加除以2得到3,即4和2必然穿过3,以此类推。
最后通过类似寻路算法来遍历所有的路径,这里有一个技巧,就是不需要遍历所有的起点,起点在角落的四个点-2、4、-4、2可以形成相同的形状(通过旋转)所以只需要计算任意一个角的路径数X4即可,1、3、-1、-3同理也是X4。最后,从0点出发,0-1作为起点X4,0-4作为起点X4即可。
实现代码如下:
/**
* 实现方案
* @param m int整型 最少m个键
* @param n int整型 最多n个键
* @return int整型
*/
const dots = {};//根据坐标速查map
//构造坐标系,0,0为原点
[-1, 0, 1].forEach((i, index, cor) => (cor.forEach(j => dots['' + (i * 3 + j)] = (i * 3 + j))))
function solution(m, n) {
//存储长度对应对组合数
const counts = [0].concat(new Array(n).fill(0))
//移动到下一个节点 path:已有路径,start:本次起点,_remain:剩余可用节点
function moveNext(path, start, _remain) {
const { ['' + start]: _, ...remain } = _remain
//拼入路径
path = [...path, start]
const len = path.length + 1
//计算出可到达的节点
const reach = Object.keys(remain).map(k => remain[k]).filter(i => (Math.abs(i * start) == 8 || i + start == 0) ? path.includes((i + start) / 2) : true)
//由于中心对称性,每一种方案数量X4
counts[len] += reach.length * 4
if (len < n) reach.forEach(i => moveNext(path, i, remain))
}
if (n > 0) {
//得到排除中心点的集合
const { ['0']: dot00, ...dotsWithout00 } = dots
//计算任意顶点和边节点的集合数,以及起点为中心点,第二节点为顶点或者边节点的集合数
moveNext([], 1, dots)
moveNext([], 4, dots)
moveNext([0], 1, dotsWithout00)
moveNext([0], 4, dotsWithout00)
counts[1] = 9
if (n > 1) counts[2] += 8//弥补起点为中心点路径长度为2的8种方案数
}
console.log(counts)
return counts.reduce((acc, c, i) => i >= m ? acc + (c || 0) : 0)
}
solution(3, 3)
module.exports = {
solution: solution
};