题干:
A password is considered strong if below conditions are all met:
- It has at least 6 characters and at most 20 characters.
- It must contain at least one lowercase letter, at least one uppercase letter, and at least one digit.
- It must NOT contain three repeating characters in a row ("...aaa..." is weak, but "...aa...a..." is strong, assuming other conditions are met).
Write a function strongPasswordChecker(s), that takes a string s as input, and return the MINIMUM change required to make s a strong password. If s is already strong, return 0.
Insertion, deletion or replace of any one character are all considered as one change.
先上代码:
class Solution {
static class SizeComparator implements Comparator<Integer> {
@Override
public int compare(Integer s1, Integer s2) {
if(s1<s2 && s1==1){
return 1;
}
if(s1>s2 && s2==1){
return -1;
}
if (s1%3 < s2%3){
return -1;
}
else if (s1%3 > s2%3){
return 1;
}else{
if(s1<s2){
return 1;
}else if(s1>s2){
return -1;
}else{
return 0;
}
}
}
}
private static class Stats{
public int lengap;
public int preremove;
public List<Integer> consecLengthList;
public Stats(int lengap, int preremove, List<Integer> consecLengthList) {
this.lengap = lengap;
this.preremove = preremove;
this.consecLengthList = consecLengthList;
}
}
private static Stats uniDecrease(int lengap, List<Integer> consecLengthList){
PriorityQueue<Integer> pq = new PriorityQueue(5, new SizeComparator());
pq.addAll(consecLengthList);
if(pq.size()<1){
return new Stats(lengap, 0, consecLengthList);
}
int step =0;
for(int i = 0;i<lengap;i++){
int tmp = pq.peek();
if(tmp>1){
pq.poll();
pq.offer(tmp-1);
step++;
}else {
break;
}
}
lengap=lengap-step;
consecLengthList = new ArrayList(pq);
return new Stats(lengap, step, consecLengthList);
}
private static int checkStep(String weakPass, boolean containsLower, boolean containsUpper, boolean containsNum, List<Integer> consecLengthList){
if(weakPass.length()<6){
int cnt = 0;
if(containsLower){
cnt++;
}
if(containsUpper){
cnt++;
}
if(containsNum){
cnt++;
}
if(weakPass.length()+3-cnt>=6){
return 3-cnt;
}else{
return 6-weakPass.length();
}
}else if(weakPass.length()>20){
// too long password
int wordlength = weakPass.length();
int cnt = 0;
if(containsLower){
cnt++;
}
if(containsUpper){
cnt++;
}
if(containsNum){
cnt++;
}
int lengap = wordlength-20;
Stats stats = uniDecrease(lengap, consecLengthList);
lengap = stats.lengap;
consecLengthList = stats.consecLengthList;
int typegap = 3-cnt;
int sum = 0;
for(Integer val: consecLengthList){
sum+=val/3;
}
return stats.preremove+lengap+ Math.max(typegap,sum);
}else{
// length is good
int cnt = 0;
if(containsLower){
cnt++;
}
if(containsUpper){
cnt++;
}
if(containsNum){
cnt++;
}
int step = 0;
for(Integer consec3: consecLengthList){
step+=consec3/3;
}
if(step>=3-cnt){
return step;
}else{
return 3-cnt;
}
}
}
public static int strongPasswordChecker(String s) {
boolean containsLower = false;
boolean containsUpper = false;
boolean containsNum = false;
boolean threeconsec = false;
char prev = ' ';
int cnt = 1;
List<Integer> consecLengthList = new ArrayList<>();
for(int i=0;i<s.length();i++){
if(s.charAt(i)>='a' && s.charAt(i)<='z'){
containsLower = true;
}
if(s.charAt(i)>='A' && s.charAt(i)<='Z'){
containsUpper = true;
}
if(s.charAt(i)>='0' && s.charAt(i)<='9'){
containsNum = true;
}
if(prev!=s.charAt(i)){
if(cnt>2){
consecLengthList.add(cnt);
}
cnt = 1;
}else{
cnt++;
if(cnt>2){
threeconsec = true;
}
}
prev = s.charAt(i);
}
if(cnt>2){
consecLengthList.add(cnt);
}
if(s.length()<6 || s.length()>20 || !containsLower || !containsUpper || !containsNum || threeconsec){
// illegal
return checkStep(s, containsLower, containsUpper, containsNum, consecLengthList);
}else{
// legal
return 0;
}
}
}
依图科技面试碰到的题目,这公司竟然在面试时出hard模式的题目,说明还是挺有水准的。现场说了个大概思路,写了一部分代码,但是显然是不能编译通过的,后来自己在leetcode上通过了OJ,才发现这题的难度确实挺大,非常不适合用手写,讲真谁能用一张A4纸直接手写出完整能编译的代码,那真是天才。
我上面的代码看起来很长,很不简洁,不过好在能通过Leetcode OJ,尽管应该不是最优的,但毕竟是自己思路的结晶,略感欣慰。
废话不多说了,说思路:
首先这题分成两部分,第一部分是判定合法与否,第二部分是在判定不合法的情况下,寻找最小编辑距离。
第一部分是很简单的了,根据题干条件一次扫描即可判断是否合法,但第二部分则比较复杂。首先,不合法密码分为几种情况:过长,过短,不包含小写字母,不包含大写字母,不包含数字,连续同样字符超过3个等各种情况(可以是其中的任意组合),如何分类讨论决定了代码实现的策略。比如我的初步思路是分3大类讨论,第一类处理长度低于6的,第二类处理长度在合理范围内但内容不符合的,第三类处理长度大于20的。后来证明这三大类的分类法是可行的。
题目比较仁慈的一点是只要求编辑多少次,不需要求编辑的详细过程,否则就真的给跪了。
好,来看第一大类:长度小于6的,这类相对来说比较简单,可以这样想,还差多少位就先补多少位,例如只有4位的密码,那么补2位长度即可达到要求,而且在这个过程中,如果原密码有字符种类的缺陷(例如缺数字,缺大写字母等),还可以在补位的过程中同时补回来,以达到“使用最少次数”的要求,但能否完全补回来呢?未必,例如原密码中缺了两类,但是你只有一位可补(即长度为5时),那么此时必然还需要再加一次编辑操作才可达到,所以总结下来该类情况下的最小编辑距离为【需要补位的数,还缺少的字符类型的数目】中的大者。这里不用担心“三连星”的问题,例如aaaB2,这样密码,我只要在aaa中间的位置插入一个其他什么字符,就一定可以破坏“三连星”,例如编程acaaB2, 长度6以下最多只能出现一组三连星,所以三连星问题在本类情况几乎是不需要考量的一个点。
再来看第二类: 长度符合要求(即在6~20之间的密码),这类密码的编辑不需要增删,只需要修改即可,修改又分两种,一种是增加缺失的字符种类(如果缺数字,就挑一个合适的位置改成数字,不用担心这个动作会把其他种类给淹没了,根据鸽巢原理,不会的,你总能挑到一个合适的位置坐这种字符变换),另一种是破坏三连星,例如aaa只要把中间的a换成其他什么字符就可以了,那么问题来了,增加字符种类很容易,这跟第一大类问题的处理方法一样,但对三连星问题最少需要做多少次替换能解决呢?答案是如果三连星的长度是x,那么x/3次变换就可以,这个可以做一个很简单的证明,图上稍微画画也不难知道。只不过,三连星可能分布在好几个子串中,例如accccbeeeeefk12,这里cccc, eeeee都是三连星(超过3的我这里也这样统称了),那么我们要把这样的序列找出来,例如上面这个串的三连星序列就可以【4,5】,即代表每个三连星子串的长度。在第一遍扫描字符串整体时,我们应该把这个序列统计出来,然后在本类中做统计就很容易了。修改三连星的同时,也可以增加缺失的字符种类,所以最终本类的结果跟第一类类似,也是【缺失字符类型数,破坏三连星动作数】中的大者。
第三类,长度大于20的,这是本题中最复杂的分类,难点的核心所在,原因在于,大于20,你必须先删除一些字符以使得长度变成20,而坑爹的是,不同的删除方法还会导致不同的结果.例如有这么个字符串 D3GDGDKKHJJHGNMabcdaaa,长度22,必须删掉两个字符才能回归合法长度,但大家注意到最后有个三连星aaa,如果我删2个字符在这其中,则同时破坏了一个三连星,如果我删的是开头的两个字符D3,则这个三连星后面肯定还是要至少做一次变换处理,那么最终的步数必然不同。那么问题来了,怎样的步数才是最少的呢?通过画图,我发现了一个方法,我们把三连星子串的长度列出来,假设这样:【3,4,5,6,7】,好现在告诉你你必须要删除k个字符才能达到20的合法长度,那么怎么删最合理呢?粗略来说,可以先从三连星相关的字符开始删起,因为删了三连星就可以在减少字符的同时破坏三连星的结构,相当于一举多得,节省步骤,但是这样还不够精细,例如上面的【3,4,5,6,7】这五组三连星,从哪个开始删起呢?有没有讲究呢?有!而且很重要,假如现在密码长度21,那么你只有1位可删,那么从“3”这个长度的三连星中删除一个字符就比从“5”这个里面删要好,为什么呢?因为3删除后就成了2,就不再构成三连星了,而长度为5的三连星,根据前面说的,要破坏它至少要改5/3也就是1位,而你删了其中一位后成了4/3依然是1位,所以这种删法显然没有前一种删法好,那么如何高效定位该删哪个三连星呢?我想到一种数据结构,优先级队列,否则,如果你用数组或者链表类型的普通结构,将不得不每次遍历整个数据结构,而优先级队列本身可以在你更新其结构后,自动调整结构,始终把你最需要的那个元素放在可以O(1)时间可以获得的位置。所以我这里要定义一个“优先级”的概念,就是给定这个三连星子串长度列表【3,4,5,6,7】这样的东东,为了尽快地把有限的删字符额度同时作用到降低三连星的除法的结果上,我会倾向于让模3的余数越小的,优先级越高,在模数相等的情况下,数字越大的优先级越高,所以上面这个例子就变成【6,3,7,4,5】,那么做删位时,我从长度为6的子串开始删,删完后优先级队列自动更新为【3,7,4,5,5】,以此类推,当然注意一个终止条件,就是我不希望把任意一组三连星子串删光,而是至少留下1(即使此时不再是三连星了),原因是如果删光可能会引发字符类型数的变化。总之按照上面这个原则,先对子串做一个预处理(其实只是逻辑上的一个预处理,不需要真的去删字符串中的字符什么的),预处理完毕之后,把预处理的步骤,预处理后可能仍然没有删够位的个数,加上【缺少的类型数,剩余字符串中破坏三连星需要的最少步数】中的大者,就是结果。
大家有兴趣可以想象是否有更简洁的方法,以上我的方法虽然很直观,但是很不简洁,以至于我自己描述都感觉略困难。