美文网首页java学习之路
leetCode进阶算法题+解析(五十三)

leetCode进阶算法题+解析(五十三)

作者: 唯有努力不欺人丶 | 来源:发表于2020-10-23 15:47 被阅读0次

    国庆+年假一共十几天,我是一点没碰电脑,这次放假忙了好多事,现在要把刷题补回来了。而且最近发现了个b站比较不错的up主,叫狂神,最近在看他的视频,有兴趣的也可以自己看看,用一种讲故事的方式传输思维和知识,推荐一波。
    开始刷题啦(ps:今天发现ac的题目数超过500了,从一开始的简单难度的题都能卡两三天,到现在形成了一定的逻辑思维能力,会了很多小技巧,各种排序,查找方法,简单的dp,回溯,拓扑等,坚持刷题快一年了,感谢自己,只要学不死,就要往死学。)

    一和零

    题目:在计算机界中,我们总是追求用有限的资源获取最大的收益。现在,假设你分别支配着 m 个 0 和 n 个 1。另外,还有一个仅包含 0 和 1 字符串的数组。你的任务是使用给定的 m 个 0 和 n 个 1 ,找到能拼出存在于数组中的字符串的最大数量。每个 0 和 1 至多被使用一次。

    示例 1:
    输入: strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3
    输出: 4
    解释: 总共 4 个字符串可以通过 5 个 0 和 3 个 1 拼出,即 "10","0001","1","0" 。
    示例 2:
    输入: strs = ["10", "0", "1"], m = 1, n = 1
    输出: 2
    解释: 你可以拼出 "10",但之后就没有剩余数字了。更好的选择是拼出 "0" 和 "1" 。
    提示:
    1 <= strs.length <= 600
    1 <= strs[i].length <= 100
    strs[i] 仅由 '0' 和 '1' 组成
    1 <= m, n <= 100

    思路:这个题怎么说呢,我审了好几遍题才理解,感觉是个很明显的最优解的题目,我暂时第一反应是从字符串短的开使拼,莫名其妙的觉得最优解都可以用dp实现。我先去试试看。
    我感觉我的思路还是不错的,写了一个排序,根据1的个数/根据0的个数排序,然后两个数组看哪个能走的结果长,就选择哪个结果。最开始排序是字符串排序,但是真正做了发现转化成二维数组更好,每一个元素用一个数组表示,数组第一个数字是需要的0的个数,第二个数字是需要的1的个数。等ac了再过来分享。
    好吧,上面的思路完美的失败了,果然还是要dp。我去专心研究怎么套模板吧。
    直接贴代码:

    class Solution {
        public int findMaxForm(String[] strs, int m, int n) {
            if(strs.length == 0) return 0;
            //字符串数组转成二维数组
            int[][] arr = new int[strs.length][];
            for(int i = 0;i<arr.length;i++){
                arr[i] = new int[2];
                for(char c:strs[i].toCharArray()) {
                    arr[i][c-'0']++;
                }
            }
            //动态规划,用数组记录中间过程的递归
            int[][][] dp = new int[strs.length+1][m+1][n+1];
            for(int k = 1;k<=strs.length;k++){
                int[] temp = arr[k-1];
                for(int i = 0;i<=m;i++){
                    for(int j = 0;j<=n;j++){
                        if(temp[0]<=i && temp[1]<=j){
                            dp[k][i][j] = Math.max(dp[k-1][i][j],1+dp[k-1][i-temp[0]][j-temp[1]]);
                        }else{
                            dp[k][i][j] = dp[k-1][i][j];
                        }
                    }
                }
            }
            return dp[strs.length][m][n];
        }
    
    }
    

    需要注意下,上面代码的主要逻辑在于三层for循环里面的if-else中。else中很好理解,就是当前的数值已经凑不出来了,那么直接凑出来的最大数就是上一个元素能凑出来的最大数,所以没啥好说的。问题就是当前元素能凑出来,那么减去当前用掉的0和1,然后存入凑成的个数。
    因为这个是个三维数组,我在画草图方便理解的时候把自己都绕懵了。其实这个性能也不咋地,三层for循环,mnl的时间复杂度。这里说是dp还不如说就是记录中间过程的暴力破解。
    看了官方题解中的另一种写法。感觉比我的要明了多了,我直接贴出来:

    class Solution {
        public int findMaxForm(String[] strs, int m, int n) {
            if(strs.length == 0) return 0;
            //字符串数组转成二维数组
            int[][] arr = new int[strs.length][];
            for(int i = 0;i<arr.length;i++){
                arr[i] = new int[2];
                for(char c:strs[i].toCharArray()) {
                    arr[i][c-'0']++;
                }
            }
            //动态规划,用数组记录中间过程的递归
            int[][] dp = new int[m+1][n+1];
            for(int k = 1;k<=strs.length;k++){
                int[] count = arr[k-1];
                for (int zeroes = m; zeroes >= count[0]; zeroes--)
                    for (int ones = n; ones >= count[1]; ones--)
                        dp[zeroes][ones] = Math.max(1 + dp[zeroes - count[0]][ones - count[1]], dp[zeroes][ones]);
    
            }
            return dp[m][n];
        }
    

    这种方式怎么说呢,二维维数组看着更简单了,简单来说就是每一个元素选择/不选择都作为一条线走下去。然后求最优解。我一般遇到这种问题都是按照思路模板套的:

    1. 判断是不是可以用选/不选 来解决这道题。(本题是可以的,每一个字符串选/不选去拆)
    2. 确定是dp。那么状态转移方程是什么?(这个元素选了的当前最优解和不选这个元素的当前最优解。因为虽说是1和0,但是本质是一个串。)
    3. 带入到这个题,因为他本身要考虑0和1两个因素。所以用传统的一维数组是记录不了的,所以这里一定是二维dp确定下来了。
    4. 二维数组怎么记录呢?到了关键是时候了,到这里我一般都是用最笨的方法:带入法来理解这个中题目(我dp不行,所以这里用最笨的方法,其实很多大佬都能看出状态转移方程,我反正不行)。随便写五个元素,然后从第一个开始一点点判断。如下图。


      我的思路

      其实比较low,不夸张的说这个题我推演了三四个小时。确实dp这里稍微难一点就贼费劲。不过好歹是做出来了,这个题就这样了。下一道题了。

    汉明距离总和

    题目:两个整数的 汉明距离 指的是这两个数字的二进制数对应位不同的数量。计算一个数组中,任意两个数之间汉明距离的总和。

    示例:
    输入: 4, 14, 2
    输出: 6
    解释: 在二进制表示中,4表示为0100,14表示为1110,2表示为0010。(这样表示是为了体现后四位之间关系)
    所以答案为:
    HammingDistance(4, 14) + HammingDistance(4, 2) + HammingDistance(14, 2) = 2 + 2 + 2 = 6.
    注意:
    数组中元素的范围为从 0到 10^9。
    数组的长度不超过 10^4。

    思路:这个题的难度,我不知道是不是在超时上,反正我个人感觉单纯的实现不是很难。首先两两比较获得结果。其次双层for循环第一层0-数组长。第二层第一层下一个到最后一一判断相加。我去实现下试试。我预感告诉我会超时。哈哈。

    暴力法超时
    怎么说呢,一点不遗憾,意料之中的结果。毕竟中等难度的题不至于这么简单。剩下的就该是优化了。其实我是有个大胆的想法的。本质上某一位数上比如3个1,4个0。那么是可以算出这位数中不同的个数是多少的。每个1和其余四个0都是四种不同。所以这个位数上不同数目是3*4=12.
    然后所有数字一共就12位。我去试试这种方式能不能ac。
    居然acl,随着时间的执行我这个心都在颤抖,虽然是低空略过,只超过百分之五的人,我先把代码贴上:
    class Solution {
        public int totalHammingDistance(int[] nums) {
            int res = 0;
            //第一个32是位数。每一位分0,1两种情况。所以是二维数组。
            int[][] d = new int[32][2];
            for(int i : nums){
                int j = 0;
                //因为最大是10的九次方,也就是30位
                while(j<30){
                    if((i&1)==0){
                        d[j][0]++;
                    } else{
                        d[j][1]++;
                    }
                    i /= 2;
                    j++;
                }
            }
            for(int[] i : d) {
                res += i[0]*i[1];
            }
            return res;
        }
    
    }
    

    起码说明思路是没问题的。我觉得其实还有优化的空间,比如说这个前置0要赋值为1的这个问题。我再想想。第二版本代码:

    class Solution {
        public int totalHammingDistance(int[] nums) {
            int res = 0;
            int len = nums.length;
            //第一个32是位数。每一位分0,1两种情况。所以是二维数组。
            int[][] d = new int[32][2];
            for(int i : nums){
                int j = 0;
                while(i!=0){
                    if((i&1)==0){
                        d[j][0]++;
                    } else{
                        d[j][1]++;
                    }
                    i /= 2;
                    j++;
                }
            }
            for(int[] i : d) {
                if(i[0]+i[1] == len) {
                    res += i[0]*i[1];
                }else{//走到这说明出现了某位数只有1.0是前置位0所以没计数
                    res += i[1]*(len-i[1]);
                }
            }
            return res;
        }
    }
    

    通过这个变化,我又有了新思路。其实不用统计1的个数0的个数。只统计一个就行了。毕竟不是1就是0.我再去改下。
    最终版代码:

    class Solution {
        public int totalHammingDistance(int[] nums) {
            int res = 0;
            int len = nums.length;
            //计算二进制每一位1的个数。
            int[] d = new int[32];
            for(int i : nums){
                int j = 0;
                while(i!=0){
                    //当结果是1则计数
                    if((i&1)==1) d[j]++;
                    j++;
                    i /= 2;
                }
            }
            for(int i : d) {
                if(i!=0)res += i*(len-i);
            }
            return res;
        }
    }
    

    说一个很悲哀的故事:这么多版本,就没有一个性能是好的。哭了。我去看看性能排行第一的代码吧。


    超过百分之五的用户

    !!!!说实话,除了是用二进制计算,其余的思路是一样一样的,人家是性能第一,我是性能惨不忍睹。。附上代码下一题:

    class Solution {
        public int totalHammingDistance(int[] nums) {
            int res = 0, n = nums.length;
            for (int i = 0; i < 32; i++) {
                int cnt = 0;
                for (int num : nums) {
                    cnt += (num >>> i) & 1;
                }
                res += cnt * (n - cnt);
            }
            return res;
        }
    }
    

    在圆内随机生成点

    题目:给定圆的半径和圆心的 x、y 坐标,写一个在圆中产生均匀随机点的函数 randPoint 。
    说明:
    输入值和输出值都将是浮点数。
    圆的半径和圆心的 x、y 坐标将作为参数传递给类的构造函数。
    圆周上的点也认为是在圆中。
    randPoint 返回一个包含随机点的x坐标和y坐标的大小为2的数组。

    示例 1:
    输入:
    ["Solution","randPoint","randPoint","randPoint"]
    [[1,0,0],[],[],[]]
    输出: [null,[-0.72939,-0.65505],[-0.78502,-0.28626],[-0.83119,-0.19803]]
    示例 2:
    输入:
    ["Solution","randPoint","randPoint","randPoint"]
    [[10,5,-7.5],[],[],[]]
    输出: [null,[11.52438,-8.33273],[2.46992,-16.21705],[11.13430,-12.42337]]
    输入语法说明:
    输入是两个列表:调用成员函数名和调用的参数。Solution 的构造函数有三个参数,圆的半径、圆心的 x 坐标、圆心的 y 坐标。randPoint 没有参数。输入参数是一个列表,即使参数为空,也会输入一个 [] 空列表。

    思路:简单来说,是保证给定的横纵坐标是圆范围内的。其实我觉得这个比较难的是随机圆的范围吧。如果是正方形长方形等是很容易算出边框的。在这区间random就好,但是因为是圆,所以还是挺不好实现的。我去画图理理思路。
    这个题着实是我的知识盲区。毕竟我数学基础一直是渣渣。看了一种题解觉得很适合我(微积分的那个属于看都不想看)。就是半径形成正方形。正方形内取点。如果点在圆上则返回。否则重新取点。我是觉得这种方式我能理解也能做出来,去写代码试试了。
    我直接贴代码:

    class Solution {
        double rad, xc, yc;
        public Solution(double radius, double x_center, double y_center) {
            rad = radius;
            xc = x_center;
            yc = y_center;
        }
    
        public double[] randPoint() {
            double x0 = xc - rad;
            double y0 = yc - rad;
    
            while(true) {
                double xg = x0 + Math.random() * rad * 2;
                double yg = y0 + Math.random() * rad * 2;
                if (Math.sqrt(Math.pow((xg - xc) , 2) + Math.pow((yg - yc), 2)) <= rad)
                    return new double[]{xg, yg};
            }
        }
    }
    
    /**
     * Your Solution object will be instantiated and called as such:
     * Solution obj = new Solution(radius, x_center, y_center);
     * double[] param_1 = obj.randPoint();
     */
    

    怎么说呢,感觉对我这种数学渣很不友好啊。而且get不到考点是什么。反正现在对付实现了(看到题解有的思路)。也不多bb,下一题。

    神奇字符串

    题目:神奇的字符串 S 只包含 '1' 和 '2',并遵守以下规则:字符串 S 是神奇的,因为串联字符 '1' 和 '2' 的连续出现次数会生成字符串 S 本身。字符串 S 的前几个元素如下:S = “1221121221221121122 ......”如果我们将 S 中连续的 1 和 2 进行分组,它将变成:
    1 22 11 2 1 22 1 22 11 2 11 22 ......
    并且每个组中 '1' 或 '2' 的出现次数分别是:
    1 2 2 1 1 2 1 2 2 1 2 2 ......
    你可以看到上面的出现次数就是 S 本身。给定一个整数 N 作为输入,返回神奇字符串 S 中前 N 个数字中的 '1' 的数目。注意:N 不会超过 100,000。

    示例:
    输入:6
    输出:3
    解释:神奇字符串 S 的前 6 个元素是 “12211”,它包含三个 1,因此返回 3。

    思路:怎么说呢,这个串确实是挺神奇的,毕竟我看了好几遍才看懂。我暂时的想法就是前19位是确定的。其实根据这19位是可以继续往下无线顺延的。我去试试代码。
    三次过的。第一次是截取字符串的时候用的n-1.最近学redis学多了,redis的截取就是闭区间。但是java中是含前不含后的,所以这里错了。第二次是我没想到n还能等于0.所以补了下第一句代码。第三次ac,虽然性能差点。

    class Solution {
        public int magicalString(int n) {
            if(n==0) return 0;
            if(n<=3) return 1;
            StringBuffer sb = new StringBuffer("122");
            int idx = 2;//从下标为2的开始遍历
            int flag = 2;//当前是1还是2
            int realIdx = 2;//从下标为2的开始算。
            while(realIdx<n) {
                char c = sb.charAt(idx);         
                if(flag == 1) {
                    //如果上一个是1这个续2
                    sb.append(c=='1'?"2":"22");
                    flag = 2;
                }else {//上一个是2,这个续1
                    sb.append(c=='1'?"1":"11");
                    flag = 1;
                }
                idx++;
                realIdx += (c=='1'?1:2);
            }
            //有可能最后是两个数,sb长度大于n
            String res = sb.toString().substring(0, n).replace("2", "");
            return res.length();
        }
    }
    

    整体来说其实这个题给了前两个数就可以自己往下编了,我这里是把所有的数追加了下,再判断这个字符串的1的个数。这样也是让思路更清晰的。至于性能问题不知道是不是因为我选择stringbuffer这种方式才这样的。可能用队列会更好?我去试一下。

    class Solution {
        public int magicalString(int n) {
            if(n == 0) return 0;
            int res = 1;
            if(n<=3) return res;
            boolean flag = true;
            int num = 3;
            Queue<Integer> queue = new LinkedList<Integer>();
            queue.add(2);
            while(num<n) {
                queue.add(flag==true?1:2);
                if(flag) res++;//说明插入的是1,计数+1
                num++;
                if(queue.poll()==2 && num<n) {//如果num=n了后面的不用计算了
                    queue.add(flag==true?1:2);
                    if(flag) res++;//说明插入的是1,计数+1
                    num++;
                }
                flag = !flag;           
            }
            return res;
        }
    }
    

    说真的这个改动差不多是新做了一次,不过性能没好多少,贴上个截图我去看性能排行第一的代码了:


    提交记录

    看了性能排行第一的代码,只能说可能是越简单的越美好。人家的简单的多了,就是数组操作,但是性能好。贴出来大家一起看看:

    class Solution {
        public int magicalString(int n) {
            if (n == 0) {
                return 0;
            }
            if (n <= 3) {
                return 1;
            }
            boolean[] nums = new boolean[n];
            nums[1] = true;
            int i1 = 1, i2 = 1;
            boolean cur = true;
            while(i2 < n) {
                nums[i2++] = cur;
                if (i2 == n) {
                    break;
                }
                if (nums[i1]) {
                    nums[i2++] = cur;
                }
                i1++;
                cur = !cur;
            }
            int res = 0;
            for (boolean b : nums) {
                if (!b) {
                    res++;
                }
            }
            return res;
        }
    }
    

    好了,下一题吧。

    预测赢家

    题目:给定一个表示分数的非负整数数组。 玩家 1 从数组任意一端拿取一个分数,随后玩家 2 继续从剩余数组任意一端拿取分数,然后玩家 1 拿,…… 。每次一个玩家只能拿取一个分数,分数被拿取之后不再可取。直到没有剩余分数可取时游戏结束。最终获得分数总和最多的玩家获胜。给定一个表示分数的数组,预测玩家1是否会成为赢家。你可以假设每个玩家的玩法都会使他的分数最大化。

    示例 1:
    输入:[1, 5, 2]
    输出:False
    解释:一开始,玩家1可以从1和2中进行选择。
    如果他选择 2(或者 1 ),那么玩家 2 可以从 1(或者 2 )和 5 中进行选择。如果玩家 2 选择了 5 ,那么玩家 1 则只剩下 1(或者 2 )可选。
    所以,玩家 1 的最终分数为 1 + 2 = 3,而玩家 2 为 5 。
    因此,玩家 1 永远不会成为赢家,返回 False 。
    示例 2:
    输入:[1, 5, 233, 7]
    输出:True
    解释:玩家 1 一开始选择 1 。然后玩家 2 必须从 5 和 7 中进行选择。无论玩家 2 选择了哪个,玩家 1 都可以选择 233 。
    最终,玩家 1(234 分)比玩家 2(12 分)获得更多的分数,所以返回 True,表示玩家 1 可以成为赢家。
    提示:
    1 <= 给定的数组长度 <= 20.
    数组里所有分数都为非负数且不会大于 10000000 。
    如果最终两个玩家的分数相等,那么玩家 1 仍为赢家。

    思路:我不知道为啥leetcode里的人这么爱玩游戏,还他妈都是高智商的游戏。太扎心了。反正这个题目总而言之应该可以用暴力法破解吧。其实每个人都选择最大的结果,那么另一个人就是最小的结果,这个是一个四重选择的操作。也可以说是递归操作。思路是有的,我去实现下试试。
    直接贴代码:

    class Solution {
        public boolean PredictTheWinner(int[] nums) {
            int sum = 0;
            for(int i : nums){
                sum += i;
            }
            return dfs1(nums,0,nums.length-1)*2>=sum;
        }
        public int dfs1(int[] nums,int l,int r) {
            //说明只有一个元素了那么下一手的人没得选.所以一个是元素值,一个是0
            if(l==r) return nums[l];
            //剩两个元素,选择大的那个
            if(l==r-1) return Math.max(nums[l],nums[r]);
            //其实这是四个选择。 我可以选左/右,下一个人可以选左/右
            //假如我选择了左,下一个人要么选左2.要么选右。我只能获取较小的那个
            int left = Math.min(dfs1(nums, l+1, r-1), dfs1(nums, l+2, r))+nums[l];
            //假如我选了右,下一个人要么左1,要么右2
            int right = Math.min(dfs1(nums, l, r-2), dfs1(nums, l+1, r-1))+nums[r];
            //聪明人选择结果大的那个
            return Math.max(left, right);
        }
    }
    

    我觉得我备注写的很清楚了,毕竟自己一点点理出来的思路。总体来说没超时我就觉得很惊喜了,但是这种暴力遍历绝对是可以用dp实现的,但是怎么实现我只有正着写出来了反着去看才有可能分析出来,太扎心了。我去试试了。

    class Solution {
        public boolean PredictTheWinner(int[] nums) {
            int length = nums.length;
            int[] dp = new int[length];
            for (int i = 0; i < length; i++) {
                dp[i] = nums[i];
            }
            for (int i = length - 2; i >= 0; i--) {
                for (int j = i + 1; j < length; j++) {
                    dp[j] = Math.max(nums[i] - dp[j], nums[j] - dp[j - 1]);
                }
            }
            return dp[length - 1] >= 0;
        }
    }
    

    直接看的题解,放弃这么复杂的思路了,年纪大了,没追求了,哎。
    这篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注。也祝大家工作顺顺利利!java技术交流群130031711,欢迎各位踊跃加入!

    相关文章

      网友评论

        本文标题:leetCode进阶算法题+解析(五十三)

        本文链接:https://www.haomeiwen.com/subject/kbjnpktx.html