leetcode hard模式專殺之420. Strong Password Checker
先上程式碼:
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(即使此時不再是三連星了),原因是如果刪光可能會引發字元型別數的變化。總之按照上面這個原則,先對子串做一個預處理(其實只是邏輯上的一個預處理,不需要真的去刪字串中的字元什麼的),預處理完畢之後,把預處理的步驟,預處理後可能仍然沒有刪夠位的個數,加上【缺少的型別數,剩餘字串中破壞三連星需要的最少步數】中的大者,就是結果。
大家有興趣可以想象是否有更簡潔的方法,以上我的方法雖然很直觀,但是很不簡潔,以至於我自己描述都感覺略困難。