leetCode进阶算法题+解析(八十一)

五一一眨眼就过去了,感觉还没开始就结束了,哈哈。继续刷题。

最长的斐波那契子序列的长度

题目:如果序列 X_1, X_2, ..., X_n 满足下列条件,就说它是 斐波那契式 的:n >= 3
对于所有 i + 2 <= n,都有 X_i + X_{i+1} = X_{i+2}
给定一个严格递增的正整数数组形成序列,找到 A 中最长的斐波那契式的子序列的长度。如果一个不存在,返回 0 。(回想一下,子序列是从原序列 A 中派生出来的,它从 A 中删掉任意数量的元素(也可以不删),而不改变其余元素的顺序。例如, [3, 5, 8] 是 [3, 4, 5, 6, 7, 8] 的一个子序列)

示例 1:
输入: [1,2,3,4,5,6,7,8]
输出: 5
解释:
最长的斐波那契式子序列为:[1,2,3,5,8] 。
示例 2:
输入: [1,3,7,11,12,14,18]
输出: 3
解释:
最长的斐波那契式子序列有:
[1,11,12],[3,11,14] 以及 [7,11,18] 。
提示:
3 <= A.length <= 1000
1 <= A[0] < A[1] < ... < A[A.length - 1] <= 10^9
(对于以 Java,C,C++,以及 C# 的提交,时间限制被减少了 50%)

思路:这个题就简单的很了。找出每一个元素的当前长度。比较直观的一个dp就可以解决。双层for循环。外层全部,内层是外层前面的元素。找出每个元素当前可凑成的最大子序列长度。因为这个题返回的也不用序列而是长度大大的减少了难度,我去实现了。
第一版实现代码:

class Solution {
    public int lenLongestFibSubseq(int[] arr) {
        List list = new ArrayList<>();
        Set set = new HashSet();
        for(int i : arr) set.add(i);
        int ans = 0;
        for(int i = 0;i2?ans:0;
    }
}

本来想到挺好的dp。但是发现实现起来有点问题。因为斐波那契数列不仅仅与上一个数有关系,与上上一个也有关系。所以上面的思路全都不太对!当然了我觉得dp肯定是能实现的。不过最后写着写着变成了暴力法,然后没超时我都觉得挺惊喜的,哈哈 。不多说了,我去直接看看题解了:
附上一个很好理解的代码:

class Solution {

    /**
     * dp[i][j] 以i,j为最后两个数的数列长度
     * 遍历求j结尾的各个数列长度
     * 因为数组严格递增
     * 双指针求j前面和位j的组合
     *
     * time complexity : N^2
     * space complexity : N^2
     *
     * @param arr
     * @return
     */
    public int lenLongestFibSubseq(int[] arr) {
        int length = arr.length;

        int[][] dp = new int[length][length];
        int result = 0;

        for (int i = 2; i < length; i++) {
            int target = arr[i];
            int left = 0;
            int right = i - 1;

            while (left < right) {
                if (arr[left] + arr[right] < target) {
                    left++;
                } else if (arr[left] + arr[right] > target) {
                    right--;
                } else {
                    dp[right][i] = dp[left][right] + 1;
                    result = Integer.max(result,dp[right][i]);
                    left++;
                }
            }
        }
        return result > 0 ? result + 2 : 0;
    }

}

一个二维数据来表示状态。然后用二分法来找出两数和存不存在,存在则长度+1.而保证所求结果正确的前提的因为数组是严格递增的。我们可以保证得到的结果就是可能的最大值。这个写法和我一开始的思路类似,只不过我自己没写出来,主要是暴力法过了就没动力写了,哈哈。下一题下一题。

爱吃香蕉的珂珂

题目:珂珂喜欢吃香蕉。这里有 N 堆香蕉,第 i 堆中有 piles[i] 根香蕉。警卫已经离开了,将在 H 小时后回来。珂珂可以决定她吃香蕉的速度 K (单位:根/小时)。每个小时,她将会选择一堆香蕉,从中吃掉 K 根。如果这堆香蕉少于 K 根,她将吃掉这堆的所有香蕉,然后这一小时内不会再吃更多的香蕉。 珂珂喜欢慢慢吃,但仍然想在警卫回来前吃掉所有的香蕉。返回她可以在 H 小时内吃掉所有香蕉的最小速度 K(K 为整数)。

示例 1:
输入: piles = [3,6,7,11], H = 8
输出: 4
示例 2:
输入: piles = [30,11,23,4,20], H = 5
输出: 30
示例 3:
输入: piles = [30,11,23,4,20], H = 6
输出: 23
提示:
1 <= piles.length <= 10^4
piles.length <= H <= 10^9
1 <= piles[i] <= 10^9

思路:这个题我的想法就是首先第一步排序。其次h和总堆数的关系。如果遇到正好相等,那么则取最大堆数的值。否则的话,因为题目的标签是二分。我的想法是递增。因为这个数据范围很简单:最小值就是这堆香蕉总数/h的值。最大值就是单堆最大值。然后二分去缩圈。最终确定最小值就行了。我去代码实现下
第一版本代码:

class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int max = -1;
        int min = -1;
        long sum = 0;
        for(int i :piles){
            max = Math.max(i,max);
            sum += i;
        }
        if(h == piles.length) return max;
        if(h>=sum) return 1;
        min =(int)(sum/h);
        while(max>min){
            int mid = (max-min)/2+min;
            if(isOk(mid,piles,h)){//当前mid是可以满足的。往小试试
                max = mid;
            }else{//当前mid不满足条件,所以往大试试
                min = mid+1;
            }
        }
        return  min;
    }
    public boolean isOk(int cur,int[] piles,int h){
        int d = 0;
        for(int i:piles){
            d += i/cur+(i%cur==0?0:1);
        }
        return d<=h;
    }
}

我发现,一个坑我可以不断往里跳。这个代码不难,我两次错误都是数据溢出。一开始sum我用的int,结果溢出了。结果错误。然后把sum改完了直接提交发现min计算的时候long不能直接变成int,又报错了。但是总体上思路没啥变化。性能一般,然后优化的点暂时没想到,感觉应该是isOk中的判断可以优化,但是我没啥思路。我去看看别人的代码吧。

class Solution {
    public int minEatingSpeed(int[] piles, int h) {
        int max = 1000000000;
        int min = 1;
        while(max>min){
            int mid = (max-min)/2+min;
            if(isOk(mid,piles,h)){//当前mid是可以满足的。往小试试
                max = mid;
            }else{//当前mid不满足条件,所以往大试试
                min = mid+1;
            }
        }
        return  min;
    }
    public boolean isOk(int cur,int[] piles,int h){
        int d = 0;
        for(int i:piles){
            d += (i-1)/cur + 1;
        }
        return d<=h;
    }
}

我现在的直觉好准!!!!果然优化点就是在那个三目运算。如上代码只改了计算,就性能贼好了。(i-1)/cur +1.这个方式让我们很好的实现了向上取余。然后项目立刻超过百分之九十八的人,总而言之这个题就这样了,主要还是二分。下一题。

石子游戏

题目:亚历克斯和李用几堆石子在做游戏。偶数堆石子排成一行,每堆都有正整数颗石子 piles[i] 。游戏以谁手中的石子最多来决出胜负。石子的总数是奇数,所以没有平局。亚历克斯和李轮流进行,亚历克斯先开始。 每回合,玩家从行的开始或结束处取走整堆石头。 这种情况一直持续到没有更多的石子堆为止,此时手中石子最多的玩家获胜。假设亚历克斯和李都发挥出最佳水平,当亚历克斯赢得比赛时返回 true ,当李赢得比赛时返回 false 。

示例:
输入:[5,3,4,5]
输出:true
解释:
亚历克斯先开始,只能拿前 5 颗或后 5 颗石子 。
假设他取了前 5 颗,这一行就变成了 [3,4,5] 。
如果李拿走前 3 颗,那么剩下的是 [4,5],亚历克斯拿走后 5 颗赢得 10 分。
如果李拿走后 5 颗,那么剩下的是 [3,4],亚历克斯拿走后 4 颗赢得 9 分。
这表明,取前 5 颗石子对亚历克斯来说是一个胜利的举动,所以我们返回 true 。
提示:
2 <= piles.length <= 500
piles.length 是偶数。
1 <= piles[i] <= 500
sum(piles) 是奇数。

思路:典型的dp?因为每次选择只能选择前后二者之一。如果到了某点:不管是选前还是选后都一定输,那么这个人就输了。或者换个思路。当一个人先拿到总数的一半以上,就是赢了。之前做过石子游戏4好像是差不多的思路,我去琢磨琢磨递推公式。
第一版代码:

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

这个代码怎么说呢,性能一言难尽,二维dp。其实这个理论就是一个博弈论的思路:不仅我要多拿,而且还得让你少拿。这个才是精髓。所以每次取值是取当前减去下一个(从头拿就是第二个,从尾拿就是倒数第二个)的较大值。也就是拿了这个不仅是我拿的多,还有你的选择少。然後为啥性能不咋地我也不知道,我去看看性能第一的代码:
手动滑稽,性能第一的代码:


leetCode进阶算法题+解析(八十一)_第1张图片
性能第一的代码

怎么说呢,我看了题解,发现竟然挺有道理。因为根据题目条件,先拿的人占了绝对优势!下面是官方讲解:

假设有 nn 堆石子,nn 是偶数,则每堆石子的下标从 0 到 n-1。根据下标将 n堆石子分成两组,每组有 n/2堆石子,下标为偶数的石子堆属于第一组,下标为奇数的石子堆属于第二组。
初始时,行的开始处的石子堆位于下标 00,属于第一组,行的结束处的石子堆位于下标 n-1n−1,属于第二组,因此作为先手的 \text{Alex}Alex 可以自由选择取走第一组的一堆石子或者第二组的一堆石子。如果 \text{Alex}Alex 取走第一组的一堆石子,则剩下的部分在行的开始处和结束处的石子堆都属于第二组,因此 \text{Lee}Lee 只能取走第二组的一堆石子。如果 \text{Alex}Alex 取走第二组的一堆石子,则剩下的部分在行的开始处和结束处的石子堆都属于第一组,因此 \text{Lee}Lee 只能取走第一组的一堆石子。无论 \text{Lee}Lee 取走的是开始处还是结束处的一堆石子,剩下的部分在行的开始处和结束处的石子堆一定是属于不同组的,因此轮到 \text{Alex}Alex 取走石子时,\text{Alex}Alex 又可以在两组石子之间进行自由选择。
根据上述分析可知,作为先手的 \text{Alex}Alex 可以在第一次取走石子时就决定取走哪一组的石子,并全程取走同一组的石子。既然如此,\text{Alex}Alex 是否有必胜策略?
答案是肯定的。将石子分成两组之后,可以计算出每一组的石子数量,同时知道哪一组的石子数量更多。\text{Alex}Alex 只要选择取走数量更多的一组石子即可。因此,\text{Alex}Alex 总是可以赢得比赛。

这个题是脑筋急转弯吧,可惜我一开始完全想着dp,完全没想明白这个。。下一题了。

完成所有工作的最短时间

题目:给你一个整数数组 jobs ,其中 jobs[i] 是完成第 i 项工作要花费的时间。请你将这些工作分配给 k 位工人。所有工作都应该分配给工人,且每项工作只能分配给一位工人。工人的 工作时间 是完成分配给他们的所有工作花费时间的总和。请你设计一套最佳的工作分配方案,使工人的 最大工作时间 得以 最小化 。返回分配方案中尽可能 最小 的 最大工作时间 。

示例 1:
输入:jobs = [3,2,3], k = 3
输出:3
解释:给每位工人分配一项工作,最大工作时间是 3 。
示例 2:
输入:jobs = [1,2,4,7,8], k = 2
输出:11
解释:按下述方式分配工作:
1 号工人:1、2、8(工作时间 = 1 + 2 + 8 = 11)
2 号工人:4、7(工作时间 = 4 + 7 = 11)
最大工作时间是 11 。
提示:
1 <= k <= jobs.length <= 12
1 <= jobs[i] <= 107

思路:首先这个题的数据范围好小。job的数量和k最大只能12。然后这个题我的理解是如何把数组分成k份,使得份数的最大和最小。说的比较绕,但是 我觉得还是能理解。然后就12个数据,我的想法是先排序,然后二分查找最合适的值。有点类似上面的题。我去试试代码。
两次ac,而且第一次没过的原因是马虎了写错一个细节,简直觉得自己行了,哈哈,贴上代码:

class Solution {
    public int minimumTimeRequired(int[] jobs, int k) {
        Arrays.sort(jobs);
        int sum = 0;
        int max = 0;
        for(int i : jobs){
            sum += i;
            max = Math.max(i,max);
        }
        if(jobs.length == k) return max;
        if(k == 1) return sum;
        //二分查看最小和.这里sum是最大值,max是最小值。
        while (max

其实我注释写的挺清楚了。然后说一下思路变化:首先感谢上上一道题也是二分查找,所以这个题我最开始就计划二分,这个主体思路是没问题的。最大值是总和,就是这么多任务分给一个人。最小值是所有工作时间的最大值(因为k小于等于长度,也就是每个工人最少做一个工作,所以最小长度就是给定的这工作的最大值)。但是重点这个题是怎么确定能不能将所有数据分成k份,每份最大和不大于给定值cur。
因为这里涉及到工作的组合,而且题目中标签就是回溯和递归,所以这个很容易能想到思路就是回溯。问题是怎么落实到实际呢?
首先我觉得从大时间往小时间插入是一个比较优的解法。毕竟N个小数据先组合了,剩下大的都没地方放肯定要做很多无用功,所以我这里所有的遍历都是倒叙遍历。其次还有一个优化点我在注释中写的很明白了。当所有工人都没工作的时候,谁接了某一个活是无所谓的,大家都是从0开始,不涉及到组合什么的。另外就是当前工人正好做到了给定值的工作时长。就一定是当前人的最优解了,因为是从大到小遍历,也不存在耽误别人什么的,总而言之这两种情况都不用再往下判断。
于是给出了上面的代码。性能超过了百分之九十九,所以我就不打算看题解和性能第一的代码了,哈哈。下一题下一题。

索引处的解码字符串

题目:给定一个编码字符串 S。请你找出 解码字符串 并将其写入磁带。解码时,从编码字符串中 每次读取一个字符 ,并采取以下步骤:如果所读的字符是字母,则将该字母写在磁带上。如果所读的字符是数字(例如 d),则整个当前磁带总共会被重复写 d-1 次。现在,对于给定的编码字符串 S 和索引 K,查找并返回解码字符串中的第 K 个字母。

示例 1:
输入:S = "leet2code3", K = 10
输出:"o"
解释:
解码后的字符串为 "leetleetcodeleetleetcodeleetleetcode"。
字符串中的第 10 个字母是 "o"。
示例 2:
输入:S = "ha22", K = 5
输出:"h"
解释:
解码后的字符串为 "hahahaha"。第 5 个字母是 "h"。
示例 3:
输入:S = "a2345678999999999999999", K = 1
输出:"a"
解释:
解码后的字符串为 "a" 重复 8301530446056247680 次。第 1 个字母是 "a"。
提示:
2 <= S.length <= 100
S 只包含小写字母与数字 2 到 9 。
S 以字母开头。
1 <= K <= 10^9
题目保证 K 小于或等于解码字符串的长度。
解码后的字符串保证少于 2^63 个字母。

思路:个人感觉这个题有点简单的离谱啊。。首先绝对不可能把这个字符串都拼出来。毕竟2的63次方有点吓人。。其次也没啥必要。就比如示例3中,那么多数字倍数,但是要求的下标1,第一个乘2就能获取到了。而且注意这个K是小于等于10的九次方的,所以我的想法就是遇到数字每次数字计算都算一下当前长度是不是包含k了,包含的话直接取k下标的值。以后不要计算了。大概思路就是这样,我去落实到代码上。
怎么说呢,果不其然超内存了。。然后敲之前的代码的时候我就有想过能不能不把这个字符串打印出来,直接往上计算长度。然后逆推取模K。。我感觉这个思路没问题,我去试试看:
勉强过了,附上代码:

class Solution {
    public String decodeAtIndex(String S, int K) {
        long size = 0;
        int idx = -1;
        for(int i = 0;i=K){
                    idx = i;
                    break;
                }
            }
        }
        //从idx往前遍历就行了
        for(int i = idx;i>=0;i--){
            K %= size;
            if(K == 0 && Character.isLetter(S.charAt(i))) return S.charAt(i)+"";
            if(Character.isLetter(S.charAt(i))){
                size--;
            }else {
                size /= S.charAt(i)-'0';
            }
        }
        return null;
    }
}

性能就是低空略过。然后这个是分两次计算:第一次计算到K的长度需要字符串解析到哪里(整个字符串解析完可能比K大的多,但是那些都没必要管,我们只解析到k就可以了)。然后我们从这个字符开始往前逆推。改除的就除(之前乘来的),该减的减(之前加来的)。然后一定要找到K那个点。这里有两个需要注意的:一个是如果size正好等于K,一开始我是直接返回这个对应的i的元素,后来发现可能是数字,所以改了。
第二个也是差不多的问题,一开始我是K=0就返回那个i对应的元素。同理可能是数字,所以又改了。
剩下别的就没啥了。这个代码3ms,只超过了百分之十九的人,我去看看性能第一的代码:

class Solution {
    public String decodeAtIndex(String S, int K) {
        long size = 0;
        int N = S.length();

        // Find size = length of decoded string
        for (int i = 0; i < N; ++i) {
            char c = S.charAt(i);
            if (Character.isDigit(c))
                size *= c - '0';
            else
                size++;
        }

        for (int i = N-1; i >= 0; --i) {
            char c = S.charAt(i);
            K %= size;
            if (K == 0 && Character.isLetter(c))
                return Character.toString(c);

            if (Character.isDigit(c))
                size /= c - '0';
            else
                size--;
        }
        throw null;
    }
}

万万没想到这个代码居然是我的代码的阉割版??有点日了狗的心情。我当时想着k以后的没用就不要遍历了。可能因此多出了许多的判断??反正是这个性能第一代码比我写的要简单的多,而且性能也好。。是测试案例的问题吧。。。这个题就这样了。
本篇笔记就记到这里,如果稍微帮到你了记得点个喜欢点个关注,也祝大家工作顺顺利利,生活健健康康!

你可能感兴趣的:(leetCode进阶算法题+解析(八十一))