題目一
題意
有一疊撲克牌,每張牌介於1和10之間
有四種出牌方法:
- 單出一張
- 出兩張相同的牌(對子)
- 出五張順子(如12345)
- 出三連對子(如112233)
給10個數,表示1-10每種牌有幾張,問最少要多少次能出完
思路
暴力+回溯,從最小的牌開始出,分別判斷四種情況能不能出,若能出,則去除掉出的牌,變成問題模型相同,規模更小的子問題求解。
card數組長度為10,card[i]表示牌號為"i+1"的牌的數量。
//打牌 public int Poker(int[] cards){ return subPoker(cards,0); } private int subPoker(int[] cards, int k){ int ans = Integer.MAX_VALUE; if (k >= cards.length) { return 0; } //當前牌出完,出下一張 else if (cards[k] == 0){ return subPoker(cards,k+1); } //出連對 if (k <= cards.length - 3 && cards[k] >= 2 && cards[k+1] >= 2 && cards[k+2] >=2){ cards[k] -= 2; cards[k+1] -= 2; cards[k+2] -= 2; ans = Math.min(1+subPoker(cards,k),ans); cards[k] += 2; cards[k+1] += 2; cards[k+2] += 2; } //出順子 if (k <= cards.length - 5 && cards[k] >= 1 && cards[k+1] >= 1 && cards[k+2] >=1 && cards[k+3] >= 1 && cards[k+4] >= 1){ cards[k] -= 1; cards[k+1] -= 1; cards[k+2] -= 1; cards[k+3] -= 1; cards[k+4] -= 1; ans = Math.min(1+subPoker(cards,k),ans); cards[k] += 1; cards[k+1] += 1; cards[k+2] += 1; cards[k+3] += 1; cards[k+4] += 1; } //出對子 if (cards[k] >= 2){ cards[k] -= 2; ans = Math.min(1+subPoker(cards,k),ans); cards[k] += 2; } //出單牌 if (cards[k] >= 1){ cards[k] -= 1; ans = Math.min(1+subPoker(cards,k),ans); cards[k] += 1; } return ans; }
這種方法是暴力求解遍歷所有情況,估算時間復雜度應該是4^n(n為總牌數),考慮可否剪枝,在本題中,剪枝可以從能出xx牌型,則不可能出xx牌型出發,根據牌型優先級考慮剪枝。
首先根據相關性考慮
- 情況1(能出對子則不出單牌),顯然不合理,打過牌都知道[2,1,1,1,1]。
- 情況2(能出順子則不出單牌),反例[1,2,2,2,1],若出順子,則需要4手才能把牌打完,出單牌+連對+單牌只需要3手,不合理。
- 情況3(能出連對則不出單牌),反例[3,2,2,2,2],若出連對,則需要4手才能把牌打完,出單排+順子+順子只需要3手,不合理。
- 情況4(能出連對則不出對子),反例[4,2,2,2,2],若出連對,則需要4手才能把牌打完,出對子+順子+順子只需要3手,不合理。
總體可以看出,牌型之間的優先級關聯較弱,而從改變出牌順序(不從最小的牌開始出,從最多的牌開始考慮),則會增加狀態轉移情況(考慮順子和連對要往哪個方向),也不行。
根據本題題型來看,真實情況下牌數應該不會太多(結合實際場景),所以暫時想到的方法如上,后續有優化再更新編輯此處。
思路二
動態規划,可以看出上述思路解決的子問題重復度是非常非常非常高的,因此可以考慮用動態規划來實現。
邊界狀態集合不難找,但是此題狀態轉換太多,而且狀態空間及其龐大,所以要定義很大的dp數組來存狀態,當n變得很大的時候,內存占用會過多,但是動態規划本身就是空間換時間的一種算法。
題目二
題意
首先定義上升字符串,s[i] >= s[i-1],比如aaa,abc是,acb不是
給n個上升字符串,選擇任意個拼起來,問能拼出來的最長上升字符串長度。
思路
動態規划,創建一個長度為26的dp[]數組,dp[i]表示以字符'a'+i結尾的最長上升字符串長度。
用一個桶,將所有字符串依據字符串末尾字符分成26份裝入桶中。
對dp[i]的求法是,從第i個桶中拿出所有字符串s,設s的字符串長度為l,開頭字符為c,則遍歷0~c-'a'的dp數組,加上l,則構成一種情況。
對於開頭字符和結尾字符相同的字符串,需要特殊處理一下,詳見代碼
//上升字符串最大連接,返回最大連接長度 public int maxLengthConcat(String[] str){ int ans = 0; int[] dp = new int[26]; int[] add = new int[26]; List<ArrayList<String>> l = new ArrayList<ArrayList<String>>(); for (int i = 0; i < 26; i++) { l.add(new ArrayList<String>()); } //用桶的思想,將以(int)x結尾的字符串裝到相應的桶里 for (int i = 0; i < str.length; i++) { //字符結尾 int j = str[i].charAt(str[i].length()-1) - 'a'; //特殊情況,以x開頭並以x結尾,不裝入,將長度加到add數組,可以視為 //所有以x結尾的字符串的長度,默認+add[x] if (str[i].charAt(0) - 'a' == j) { add[j] += str[i].length(); } else { l.get(j).add(str[i]); } } //初始化以'a'為結尾的最長長度 for (int i = 0; i < l.get(0).size(); i++) { dp[0] = Math.max(l.get(0).get(i).length(),dp[0]); } //從'a'開始更新 for (int i = 0; i < 26; i++) { if (l.get(i).size() == 0 && add[i] > 0){ for (int j = 0; j < i; j++) { dp[i] = Math.max(dp[i],dp[j]); } }
//遍歷以'a'+i 為結尾的字符串 for (int j = 0; j < l.get(i).size(); j++) { String s = l.get(i).get(j); int len = s.length(); int c = s.charAt(0) - 'a'; for (int k = 0; k <= c; k++) { dp[i] = Math.max(dp[i], dp[k] + len); } } dp[i] += add[i]; } for (int i = 0; i < 26; i++) { ans = ans > dp[i] ? ans : dp[i]; } return ans; }
雖然裝在不同的桶里,但實際上每個字符串遍歷一次,每次遍歷需要對前面的dp數組進行遍歷,而dp數組的長度固定為26,所以該方法時間復雜度為O(n)。
PS:看到牛客網有評論說這樣會超時,但是從題目上看無論如何都想不到跟二分的關系,那么就基本不可能是O(logn),所以個人認為O(n)已經是最優解法,有想到優化再更新。
PS2:有其他評論說可以按照字符串的末尾字符給字符串排序,但是排序的時間復雜度就超出了O(n),得不償失,個人認為沒必要排序,但是排序可以節省桶的空間,算是時間換空間吧。
由於本人沒有真實參加面試,以上代碼均沒通過官方檢測,不保證完全正確,僅供參考,有問題歡迎指出。