题目一
题意
有一叠扑克牌,每张牌介于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),得不偿失,个人认为没必要排序,但是排序可以节省桶的空间,算是时间换空间吧。
由于本人没有真实参加面试,以上代码均没通过官方检测,不保证完全正确,仅供参考,有问题欢迎指出。