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),得不偿失,个人认为没必要排序,但是排序可以节省桶的空间,算是时间换空间吧。

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

       

  • 相关阅读:
    C# 数据操作系列
    C# 数据操作系列
    C# 基础知识系列- 17 小工具优化
    C# 基础知识系列- 17 实战篇 编写一个小工具(1)
    计算机网络知识概述
    微信公众号开发:消息处理
    微信公众号开发:服务器配置
    C#调用接口注意要点
    npm安装和Vue运行
    实战spring自定义属性(schema)
  • 原文地址:https://www.cnblogs.com/liusandao/p/12531116.html
Copyright © 2011-2022 走看看