LeetCode 753. Cracking the Safe

所有可能的密码总数 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:

https://leetcode.com/problems/cracking-the-safe/discuss/110265/Having-trouble-understanding-it-Try-this.

 

posted @ 2019-06-12 15:12  約束の空  阅读(444)  评论(0编辑  收藏  举报