zoukankan      html  css  js  c++  java
  • 两道经典面试算法题2020-3-20(打牌,最长上升字符串拼接)

    题目一

    题意

    有一叠扑克牌,每张牌介于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),得不偿失,个人认为没必要排序,但是排序可以节省桶的空间,算是时间换空间吧。

    由于本人没有真实参加面试,以上代码均没通过官方检测,不保证完全正确,仅供参考,有问题欢迎指出。

       

  • 相关阅读:
    204. Count Primes (Integer)
    203. Remove Linked List Elements (List)
    202. Happy Number (INT)
    201. Bitwise AND of Numbers Range (Bit)
    200. Number of Islands (Graph)
    199. Binary Tree Right Side View (Tree, Stack)
    198. House Robber(Array; DP)
    191. Number of 1 Bits (Int; Bit)
    190. Reverse Bits (Int; Bit)
    189. Rotate Array(Array)
  • 原文地址:https://www.cnblogs.com/liusandao/p/12531116.html
Copyright © 2011-2022 走看看