题目链接:753. 破解保险箱
很日常,没看题解之前依然没有发现这是道图论的题目(每日一题从官方题解开始=。=)。第一次在OJ练习上做到图论的题,之前做图论的题都是辛辛苦苦写了好多结构体然后按点按边做好邻接表,看了答案才知道图的存储方式也不只是那么单一的一种形式,彻底颠覆了我对编程、算法的认知角度。
先读一下题(个人表示读了好久才明白题意,已经读懂题意的直接跳过下两段):保险箱的密码有n位,每一位只能从0~k-1中选(相当于一个n位的k进制数),然后你需要输入一个序列(不限制位数),保险箱会用这个序列的最后n位去“尝试”,如果不是正确密码就向右滑动一位,再用最后n位去尝试直到打开箱子。答案要求的就是保证能打开箱子的最短的那个序列。
举个小例子:n = 2, k = 2, 那么密码可能是 00, 01, 10, 11 四种排列,00 + 01 + 10 + 11这个序列可以保证打开箱子但它不是最短的序列,最短的序列是 00110(不唯一)。
既然用图论的知识去做,就要确定结点和边分别是什么。密码是n位,都可以由n-1位k进制数表示的序列再拼接上一位0~k-1的数构成一个可能成为密码的组合。就相当于我们可以把n-1位当成一个结点,0~k-1个数字当成这个结点扩展出来的边,那么这样,所有n-1位k进制数就构成了所有结点,我用一个n = 3,k = 2的例子给大家看(感谢力站大佬的图):
解释一下这个图是怎么走的:从00开始,选择边0构成000组合,保存,取后n-1即2位成为下一个到达的结点,在这里是还是00;下一步,边0被选过了,走边1,构成001组合,保存,取后两位01成为下一个到达的结点,把所有的边都走完回到出发点(欧拉回路),我们也就完成对所有可能的密码组合进行了一次遍历,答案实际上就是我们所有遍历过的边再拼接上初始结点00形成的序列。
用眼睛看我们很容易知道从一个点出发他有哪条边还没走,那条边走过了。但是如果像刚才一样,从00出发,每到达一个新结点都优先选择0那条边去走,四步之后将回到原点且无路可走,交给计算机去处理可能直接认为回路形成,走进了一个“死胡同”。所以,在编码的时候要注意这一点,具体的解决方式我总结了两种:
- 当走入死胡同时,强行跳出死胡同另选一条边去走,实际上我们可以对所有结点的所有边进行遍历,这实际上就是官方题解给出的做法。
- 利用本身特性,每一次寻找下一个出边的时候,按照此结点还未遍历过的边的数值降序遍历,即按边的数字从大到小的优先级去遍历,大数字号的边先遍历,大家试着走一下,按照这样的方式是不会进入死胡同的。
第一种思路的代码:
class Solution {
public:
int k; //记录参数k
int node; //所有结点的个数即k^(n-1)
vector<int> visit; //在遍历图的过程中记录被访问过的状态
string res; //答案串
void dfs(int n) { //参数n表示当前结点
for(int i = 0; i < k; i++) { //共0~k-1条边
int cur = n * k + i; //n*k+i表示将结点与边拼接
if(!visit[cur]) { //检查是否已遍历过该答案状态
visit[cur] = 1;
dfs(cur % node); //从该状态继续遍历搜索
res.push_back(i + '0'); //答案串加上本位所选择的边
}
}
}
string crackSafe(int n, int k) {
this->k = k;
this->node = pow(k, n-1);
visit = vector<int>(node * k);
dfs(0);
res += string(n - 1, '0'); //把初始结点拼接上
return res;
}
};
第二种方式省去了递归遍历图的过程,代码也更为简洁一些,作者码力有限在这里我就不加以展示了,大家可以在评论区贴出自己的代码,也可以提出更好的简易,我们共同进步!