所有可能的密码总数 k^n,如果把这些密码都拼起来,长度 n*k^n
如果每个密码与前一个密码共用 n-1 位,那么长度缩减为 k^n + (n-1)
上述字符串也被称为 De Bruijn sequence。
所以问题转变为,如果得到这个序列。很容易想到转化为图的问题来做,而且可以转化不同的问题。
-> Hamilton Path
如果把所有可能的密码分别作为节点,通过共用 n-1 位转换得到的节点间添加有向边 (因此每个节点有 k 条边),如 X000 -1-> 0001 。原问题就转化为了寻找 Hamilton Path 的问题。但是判断以及寻找 Hamilton Path 是 NP-Complete 问题,因此只能暴力回溯去做。最后的答案就是 初始选择的节点+转换时添加位。
由于本题的图的对称性以及别的较好的性质,Hamilton Path 是一定存在的,而且甚至不需要回溯就能找到路径。但是由于主题思路还是回溯,回溯部分代码还是添加上使得思路更加清晰。
class Solution { public: string res; string crackSafe(int n, int k) { int size=pow(k,n); // number of all possible passwords res.resize(n,'0'); unordered_set<string> visited{res}; if (dfs(string(n,'0'),size,k,visited)) return res; return ""; } bool dfs(string node, int size, int k, unordered_set<string> &visited){ if (visited.size()==size) return true; // n-1 digits of last password string suffix=node.substr(1); for (char ch='0';ch<'0'+k;++ch){ string newNode=suffix+ch; if (!visited.count(newNode)){ visited.insert(newNode); res.push_back(ch); if (dfs(newNode,size,k,visited)) return true; res.pop_back(); visited.erase(newNode); } } return false; } };
-> Euler Circuit
每个密码的最后 n-1 位作为节点,通过共用这 n-1 位转换得到的节点间添加有向边,每个节点同样有 k 条边,如 000 -1-> 001 。原问题就转化为寻找 Euler Path 的问题,因为每个节点加上出去边上的那一位就是一种密码,要得到所有密码,必须访问每条边一次。要最短的字符串,就是 Euler Circuit 的问题。Eular Circuit 是可以在多项式时间内有解的。
Hierholzer's Algorithm 可以 O(V+E) 内找到 Euler Circuit,详见以下链接。由于链接里是把节点一次print出来,所以更加繁琐一点。https://www.geeksforgeeks.org/hierholzers-algorithm-directed-graph/
对于本题,我们只需要任意一个点加上所有路径上的字符即可。所有路径可以通过后序遍历轻松得到 (和上述链接做法本质一样)。其实连reverse都不用,因为是无向图。
class Solution { public: string res; string crackSafe(int n, int k) { unordered_set<string> visited; res = ""; dfs(string(n-1,'0'),k,visited); reverse(res.begin(),res.end()); return string(n-1,'0')+res; } void dfs(string node, int k, unordered_set<string> &visited){ for (char ch='0';ch<'0'+k;++ch){ string newNode=node+ch; if (!visited.count(newNode)){ visited.insert(newNode); dfs(newNode.substr(1),k,visited); res.push_back(ch); } } } };
时间复杂度 O(k*k^n),因为每次找边的时候,是for循环枚举的。如果用链表或者别的方式存储,时间复杂度为 O(k^n)。纯粹是for循环更好些才这么做的。
Reference: