您的位置:首页 > Web前端

753-Cracking the Safe

2018-03-26 21:24 323 查看
Description:

There is a box protected by a password. The password is n digits, where each letter can be one of the first k digits 0, 1, …, k-1.

You can keep inputting the password, the password will automatically be matched against the last n digits entered.

For example, assuming the password is “345”, I can open it when I type “012345”, but I enter a total of 6 digits.

Please return any string of minimum length that is guaranteed to open the box after the entire string is inputted.

Example 1:

Input: n = 1, k = 2
Output: "01"
Note: "10" will be accepted too.


Example 2:

Input: n = 2, k = 2
Output: "00110"
Note: "01100", "10011", "11001" will be accepted too.


Note:

n will be in the range [1, 4].

k will be in the range [1, 10].

k^n will be at most 4096.

问题描述:

密码为n位数字,每位数字的取值范围为[0,k - 1].你可以持续输入数字,密码会自动匹配输入的后n位.

例如:

如果密码为”345”,那么我输入6位数字”012345”算作开锁。

给定参数n(代表密码位数),k(代表每位的取值范围),请返回确保能够开锁的具有最短长度的字符串。

这题实际上是图论中很著名的一个概念,欧拉路径(Eulerian path)

来看看维基上面关于欧拉路径的定义:

In graph theory, an Eulerian trail (or Eulerian path) is a trail in a finite graph which visits every edge exactly once.

图论中,欧拉路径就是在有限图中的一条访问每条边且只访问一次的路径。

再来看看欧拉路径成立的条件:

Euler proved that a necessary condition for the existence of Eulerian circuits is that all vertices in the graph have an even degree, and stated without proof that connected graphs with all vertices of even degree have an Eulerian circuit.

条件如下:

一个连通图,所有顶点中有0个或者两个顶点的度数为奇数

分为两种情况:

1.所有顶点度数为偶数的连通图,那么所有欧拉路径都是欧拉环路(即路径由起点开始,起点结束)

2.如果有两个顶点度数为奇数,那么所有欧拉路径由其中一个顶点开始,另一个顶点结束,也就是不是欧拉环,被称作半欧拉路径( semi-Eulerian)

扯得有点远了。回到题目

为了自己的反思,还是先上自己的反例:

/*
*方便记录反思用,这种解法是错误的
*我的想法是先dfs找出所有可能的密码,然后在后续的迭代处理中进行连接
*其实稍微试一下就知道这种解法不对(至少我这样写不对)
*例如n = 2, k = 2的情况,我会先找出['00', '01', '10', '11']这四种密码
*然后用00拼接01得到001,再拼接10,得到0010,这时候11拼接不上,我有点想当然了,直接从剩下的
*元素中随意找一个插进去,于是结果是001011,并不是最短的字符串
*/
class Solution {
public String crackSafe(int n, int k) {
List<String> psequence = new ArrayList();

dfs(n, 0, new StringBuilder(), k, psequence);

StringBuilder res = new StringBuilder();
String start = psequence.get(0);
res.append(start);
psequence.remove(0);

while(psequence.size() != 0){
boolean flag = false;
for(int i = 0;i < psequence.size();i++){
String candidate = psequence.get(i);
if(candidate.startsWith(start.substring(1, start.length()))){
start = candidate;
res.append(c
c363
andidate.charAt(candidate.length() - 1));
psequence.remove(i);
flag = true;
}
}
if(!flag){
String candidate = psequence.get(0);
start = candidate;
psequence.remove(0);
res.append(candidate.charAt(candidate.length() - 1));
}
}

return res.toString();
}
public void dfs(int n, int depth, StringBuilder strb, int k, List<String> res){
if(depth == n){
res.add(strb.toString());
return;
}

for(int i = 0;i < k;i++){
strb.append(String.valueOf(i));
dfs(n, depth + 1, strb, k, res);
strb.deleteCharAt(strb.length() - 1);
}
}
}


下面是正确解法。

解法1(dfs):

其实思路大体与我一样,只是我处理连接不上的情况太草率了,想当然。

/*
*思想是回溯法
*还是以n = 2,k = 2举例
*以res = 00作为起始,截断res的倒数n - 1位,即0,向后依次尝试添加[0, k - 1](会有一个set
*记录添加过的元素),如果可以添加,那么向下递归。否则,"撤销"之前那一步,返回上层调用函数
*,尝试添加另外的字符
*整个流程是:00->001->0010->001(注意这一步,相当于回退了)->0011->00110
*/
class Solution {
public String crackSafe(int n, int k) {
StringBuilder res = new StringBuilder();
Set<String> visited = new HashSet();
int total = (int)Math.pow(k, n);

for(int i = 0;i < n;i++)    res.append("0");
visited.add(res.toString());

dfs(res, visited, total, n, k);

return res.toString();
}
public boolean dfs(StringBuilder res, Set<String> visited, int total, int n, int k){
if(visited.size() == total) return true;

for(int i = 0;i < k;i++){
String next = res.substring(res.length() - n + 1) + i;
if(!visited.contains(next)){
visited.add(next);
res.append(i);
if(dfs(res, visited, total, n, k))      return true;
else{
visited.remove(next);
res.deleteCharAt(res.length() - 1);
}
}
}

return false;
}
}


dfs这种解法花了20ms,还有一种更刺激的解法,只需要5ms左右

解法2:

class Solution {
public String crackSafe(int n, int k) {
int M = (int) Math.pow(k, n-1);
int[] P = new int[M * k];
for (int i = 0; i < k; ++i)
for (int q = 0; q < M; ++q)
P[i*M + q] = q*k + i;

StringBuilder ans = new StringBuilder();
for (int i = 0; i < M*k; ++i) {
int j = i;
while (P[j] >= 0) {
ans.append(String.valueOf(j / M));
int v = P[j];
P[j] = -1;
j = v;
}
}

for (int i = 0; i < n-1; ++i)
ans.append("0");
return new String(ans);
}
}


想看懂这个解法,得先知道什么是lyndon words

先来看看维基的定义:

In mathematics, in the areas of combinatorics and computer science, a Lyndon word is a nonempty string that is strictly smaller in lexicographic order than all of its rotations.

按我的理解,lyndon words就是它的所有排列中字典序最小的那个,比如’01’,如果每一位的范围为[0,1],那么你不可能找到比它字典序更小的其他的排列

为了更好地理解lyndon words是什么,看看下面一组数就明白了:

0, 1, 01, 001, 011, 0001, 0011, 0111, 00001, 00011, 00101, 00111, 01011, 01111, …

那么lyndon words与我们的解法有什么关系?

还是以n = 2,k = 2为例,它的长度可以被2整除的lyndonwords按字典序排列如下:

0, 01, 1.将这三个lyndonwords连接,得到’0011’

与我们想要的答案’00110’是不是很接近?

实际上,算法主要包含两个步骤:

1.使用 Inverse Burrows-Wheeler Transform产生lyndonwords

2.组合lydonwords

3.往尾部填充n - 1个’0’

于是关键就是这个算法,Inverse Burrows-Wheeler Transform。如果实在有兴趣的话可以看看这个链接:

http://www.macs.hw.ac.uk/~markl/Higgins.pdf

也可以看看这个链接:

https://leetcode.com/problems/cracking-the-safe/solution/

最后说一下感想,我其实并没看Inverse Burrows-Wheeler Transform,我只是知道它包含了哪些步骤,目的是什么,应该怎么做。这就跟编一个程序,绞尽脑汁突然发现需要数论的知识,看了公式却一脸懵逼一样。。。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: