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

子数组按位或操作

题目:我们有一个非负整数数组 A。对于每个(连续的)子数组 B = [A[i], A[i+1], ..., A[j]] ( i <= j),我们对 B 中的每个元素进行按位或操作,获得结果 A[i] | A[i+1] | ... | A[j]。返回可能结果的数量。 (多次出现的结果在最终答案中仅计算一次。)

示例 1:
输入:[0]
输出:1
解释:
只有一个可能的结果 0 。
示例 2:
输入:[1,1,2]
输出:3
解释:
可能的子数组为 [1],[1],[2],[1, 1],[1, 2],[1, 1, 2]。
产生的结果为 1,1,2,1,3,3 。
有三个唯一值,所以答案是 3 。
示例 3:
输入:[1,2,4]
输出:6
解释:
可能的结果是 1,2,3,4,6,以及 7 。
提示:
1 <= A.length <= 50000
0 <= A[i] <= 10^9

思路:这个题的标签是位运算和动态规划。因为是连续的子数组,所以这里肯定是要记录用到当前元素前一个的所有结果。针对这些结果集才可以选择或当前元素。当然了每个元素都可以作为一个子数组的开始来进行计算,总而言之感觉dp的性质不强。而且这个重复结果是不作为计算的。所以初步计划set存结果集。然后用一个集合记录前一个元素的所有可能。再与当前元素挨个遍历。思路比较清晰,我去代码实现
第一版代码:

class Solution {
    public int subarrayBitwiseORs(int[] arr) {
        Set set = new HashSet();
        Set last = new HashSet();
        for(int i : arr) {
            Set temp = new HashSet();
            temp.add(i);
            for(int c : last) {
                temp.add(c|i);
            }
            last = temp;
            set.addAll(temp);
        }
        return set.size();
    }
}

其实这个也算是暴力过了吧。虽然性能不太好,接下来我去看看性能第一的代码:

class Solution {
    public int subarrayBitwiseORs(int[] arr) {
        int n = arr.length;
        if(n < 2){
            return n;
        }
        Set set = new HashSet<>(65536);
        for (int i = 0; i < n; i++) {
            set.add(arr[i]);
            for (int j = i-1; j >= 0; --j) {
                if((arr[j] & arr[i]) == arr[i]) {
                    break;
                }
                arr[j] |= arr[i];
                set.add(arr[j]);
            }
        }
        return set.size();
    }
}

思路是类似的思路。我感觉重点应该就是这个当前元素和之前某个相等直接break。因为遇到同样元素的话说明前面的结果都是一样的。没必要重复计算。剩下别的没啥了。下一题了。

RLE迭代器

题目:编写一个遍历游程编码序列的迭代器。迭代器由 RLEIterator(int[] A) 初始化,其中 A 是某个序列的游程编码。更具体地,对于所有偶数 i,A[i] 告诉我们在序列中重复非负整数值 A[i + 1] 的次数。迭代器支持一个函数:next(int n),它耗尽接下来的 n 个元素(n >= 1)并返回以这种方式耗去的最后一个元素。如果没有剩余的元素可供耗尽,则 next 返回 -1 。例如,我们以 A = [3,8,0,9,2,5] 开始,这是序列 [8,8,8,5,5] 的游程编码。这是因为该序列可以读作 “三个八,零个九,两个五”。

示例:
输入:["RLEIterator","next","next","next","next"], [[[3,8,0,9,2,5]],[2],[1],[1],[2]]
输出:[null,8,8,5,-1]
解释:
RLEIterator 由 RLEIterator([3,8,0,9,2,5]) 初始化。
这映射到序列 [8,8,8,5,5]。
然后调用 RLEIterator.next 4次。
.next(2) 耗去序列的 2 个项,返回 8。现在剩下的序列是 [8, 5, 5]。
.next(1) 耗去序列的 1 个项,返回 8。现在剩下的序列是 [5, 5]。
.next(1) 耗去序列的 1 个项,返回 5。现在剩下的序列是 [5]。
.next(2) 耗去序列的 2 个项,返回 -1。 这是由于第一个被耗去的项是 5,
但第二个项并不存在。由于最后一个要耗去的项不存在,我们返回 -1。
提示:
0 <= A.length <= 1000
A.length 是偶数。
0 <= A[i] <= 10^9
每个测试用例最多调用 1000 次 RLEIterator.next(int n)。
每次调用 RLEIterator.next(int n) 都有 1 <= n <= 10^9 。

思路:这个题怎么说呢,感觉还是挺简单的。首先分析数据范围:虽然A的长度小于1000.但是因为A[I]的范围是10的九次方。所以这个字符串是没必要都凑出来的。然后好像可以直接处理。数数的时候奇数遍历。取值的时候下一个取值。大概思路是有了。我去实现下试试。
第一版代码:

class RLEIterator {
    int[] arr;
    int idx = 0;
    public RLEIterator(int[] encoding) {
        this.arr = encoding;
    }
    
    public int next(int n) {
        for(int i = idx;i=n) {
                arr[i] -= n;
                idx = i;
                return arr[i+1];
            }else {
                n -= arr[i];
            }
        }
        idx = arr.length+1;
        return -1;
    }
}

/**
 * Your RLEIterator object will be instantiated and called as such:
 * RLEIterator obj = new RLEIterator(encoding);
 * int param_1 = obj.next(n);
 */

这个题果然比较简单,做完了也没觉得有什么坑点。然后这个代码的性能还行。我再去看看性能第一的代码:

class RLEIterator {

    int index;
    int[] encoding;
    public RLEIterator(int[] encoding) {
        index = 0;
        this.encoding = encoding;
    }

    public int next(int n) {
        int res = -1;
        while (index < encoding.length && n > 0){
            if(n > encoding[index]){
                n -= encoding[index];
                index += 2;
            }else {
                encoding[index] -= n;
                n = 0;
                res = encoding[index + 1];
            }
        }
        return res;
    }
}

差不多的思路,也没啥两点,下一题了。

股票价格跨度

题目:编写一个 StockSpanner 类,它收集某些股票的每日报价,并返回该股票当日价格的跨度。今天股票价格的跨度被定义为股票价格小于或等于今天价格的最大连续日数(从今天开始往回数,包括今天)。例如,如果未来7天股票的价格是 [100, 80, 60, 70, 60, 75, 85],那么股票跨度将是 [1, 1, 1, 2, 1, 4, 6]。

示例:
输入:["StockSpanner","next","next","next","next","next","next","next"], [[],[100],[80],[60],[70],[60],[75],[85]]
输出:[null,1,1,1,2,1,4,6]
解释:
首先,初始化 S = StockSpanner(),然后:
S.next(100) 被调用并返回 1,
S.next(80) 被调用并返回 1,
S.next(60) 被调用并返回 1,
S.next(70) 被调用并返回 2,
S.next(60) 被调用并返回 1,
S.next(75) 被调用并返回 4,
S.next(85) 被调用并返回 6。
注意 (例如) S.next(75) 返回 4,因为截至今天的最后 4 个价格
(包括今天的价格 75) 小于或等于今天的价格。
提示:
调用 StockSpanner.next(int price) 时,将有 1 <= price <= 10^5。
每个测试用例最多可以调用 10000 次 StockSpanner.next。
在所有测试用例中,最多调用 150000 次 StockSpanner.next。
此问题的总时间限制减少了 50%。

思路:怎么说呢,这个题还特别说了时间限制。暂时没有什么特别的思路,我打算先暴力试水下。毕竟单个案例最多调用10000次。也就是说存储空间上将肯定是够用的。
第一版代码:

class StockSpanner {
    List list;
    public StockSpanner() {
        this.list = new ArrayList();
    }
    
    public int next(int price) {
        int n = 0;
        list.add(price);
        for(int i = list.size()-1;i>=0;i--) {
            if(list.get(i) <= price) {
                n++;
            }else {             
                return n;
            }
        }
        return n;
    }
}

/**
 * Your StockSpanner object will be instantiated and called as such:
 * StockSpanner obj = new StockSpanner();
 * int param_1 = obj.next(price);
 */

我简直起了怪了,我都做好了超时的准备,结果居然过了。神奇的一批。然后题目的标签是栈。其实我觉得优化点是跳跃寻找。比如前一天是10,今天比昨天还大的话就可以直接从前一天的前10个元素开始。因为比昨天还小的也一定比今天小。但是思路还不完善,所以先暴力法试试水,结果发现过了,然后继续说这个题,优化版我觉得还是像我刚刚上面说的那样。然后具体的实现可能是用空间换时间,用一个二维数组来计数。大概思路是这样。我去实现试试。
第二版代码:

class StockSpanner {
    int[][] d = new int[10000][2];
    int idx = 0;
    public StockSpanner() {
    }
    
    public int next(int price) {
        //数组的第一个元素存当前值。第二个元素存比它小的个数
        d[idx][0] = price;
        int temp = idx-1;
        while(temp>=0 && d[temp][0]<=price) {
            temp -= d[temp][1]; 
        }
        d[idx][1] = idx-temp;
        idx++;
        return d[idx-1][1];
    }
}

/**
 * Your StockSpanner object will be instantiated and called as such:
 * StockSpanner obj = new StockSpanner();
 * int param_1 = obj.next(price);
 */

跟第一版比性能大大的提升了,但是还没达到好的地步。不过我思路也就这样了,我去看看性能第一的代码:

class StockSpanner {

    private LinkedList stack;

    public StockSpanner() {
        stack = new LinkedList<>();
    }
    
    public int next(int price) {
        int weight=1;
        while(!stack.isEmpty() && stack.peekLast()[0]<=price){
            int[] last = stack.pollLast();
            weight+=last[1];
        }
        stack.add(new int[]{price,weight});
        return weight;
    }
}

/**
 * Your StockSpanner object will be instantiated and called as such:
 * StockSpanner obj = new StockSpanner();
 * int param_1 = obj.next(price);
 */

思路和我之前说的差不多,但是人家用的是栈,我用的是二维数组。别的也没啥特别的了,下一题。

水果成蓝

题目:在一排树中,第 i 棵树产生 tree[i] 型的水果。你可以从你选择的任何树开始,然后重复执行以下步骤:
把这棵树上的水果放进你的篮子里。如果你做不到,就停下来。
移动到当前树右侧的下一棵树。如果右边没有树,就停下来。
请注意,在选择一颗树后,你没有任何选择:你必须执行步骤 1,然后执行步骤 2,然后返回步骤 1,然后执行步骤 2,依此类推,直至停止。你有两个篮子,每个篮子可以携带任何数量的水果,但你希望每个篮子只携带一种类型的水果。用这个程序你能收集的水果树的最大总量是多少?

示例 1:
输入:[1,2,1]
输出:3
解释:我们可以收集 [1,2,1]。
示例 2:
输入:[0,1,2,2]
输出:3
解释:我们可以收集 [1,2,2]
如果我们从第一棵树开始,我们将只能收集到 [0, 1]。
示例 3:
输入:[1,2,3,2,2]
输出:4
解释:我们可以收集 [2,3,2,2]
如果我们从第一棵树开始,我们将只能收集到 [1, 2]。
示例 4:
输入:[3,3,3,1,2,1,1,2,3,3,4]
输出:5
解释:我们可以收集 [1,2,1,1,2]
如果我们从第一棵树或第八棵树开始,我们将只能收集到 4 棵水果树。
提示:
1 <= tree.length <= 40000
0 <= tree[i] < tree.length

思路:这个题怎么说呢,我觉得重点应该是找到起摘点,根据题目的两个条件来说,摘果子应该是连续的。所以说从哪个点开始摘很重要。因为只有两个篮子。所以我可以换个说法:数组中只包含两个数字的子数组最长是多少这个题的答案就是多少。大概思路就这样了,我去代码实现了。
第一版本代码:

class Solution {
    public int totalFruit(int[] tree) {
        int max = 1;
        for(int i = 0;i0 && tree[i] == tree[i-1]) continue;
            int f = -1;
            for(int j = i+1;j

思路还是挺清晰的。就像我上面说的子数组中元素种类不超过2个。打头的算一个。所以额外记录一个。如果出现了超出这两种的元素当前元素开头的数组不用计算了,走不下去了。
还有两个小优化点:一个是重复元素不用重复计算。所以当前元素和上一个相等直接跳过。
另外如果已经遍历到结尾了就不用再继续判断了。因为只能越往后数值越小。所以这个代码ac了,性能还凑合。我直接去看性能第一的代码了:

class Solution {
    public int totalFruit(int[] tree) {
        int res = 0, len = tree.length;
        int one = tree[0], two, begin = 0, end = 1; //2种树的状态(one, two), one初始化为tree数组的第1个元素
        while (end < len && tree[end] == one)   //寻找two的初始值,以构成初始(one, two)
            ++end;
        if (end == len) return len; //若整个数组的元素都由初始的(one, two)所构成,则直接返回数组长度
        two = tree[end++];  //构成初始的(one, two)
        for (; end < len; ++end) {
            if (one != tree[end] && two != tree[end]) { //遇到了第3种树
                res = Math.max(res, end - begin);   //更新最终返回结果
                one = tree[end - 1];    //(one, two)更新为(第3种树的左边的树, 第3种树)
                two = tree[end];
                begin = end - 1;    //更新由当前(one, two)所构成的连续子数组的左边界
                while (tree[begin - 1] == one)   //向左寻找由one构成的连续子数组
                    --begin;
            }
        }
        return Math.max(res, end - begin);
    }
}

首先这种做法我其实本来是想这么写来这,但是到了写代码的时候发现略复杂,所以换了思路。用双指针的好处就是可以少做很多无用功。比如子数组的内部不用重复计算了。例如1,2,1,2,1,2,3.如果是我的做法每个元素其实都要遍历一遍,但是人家的代码直接第一个1到了最后一个2.遇到3往前导一次。确实能理解人家的性能好。但是我最开始思路不清楚,所以代码没写出来就换思路了,哎。下一题吧。

子数组的最小值之和

题目:给定一个整数数组 arr,找到 min(b) 的总和,其中 b 的范围为 arr 的每个(连续)子数组。由于答案可能很大,因此 返回答案模 10^9 + 7 。

示例 1:
输入:arr = [3,1,2,4]
输出:17
解释:
子数组为 [3],[1],[2],[4],[3,1],[1,2],[2,4],[3,1,2],[1,2,4],[3,1,2,4]。
最小值为 3,1,2,4,1,1,2,1,1,1,和为 17。
示例 2:
输入:arr = [11,81,94,43,3]
输出:444
提示:
1 <= arr.length <= 3 * 10^4
1 <= arr[i] <= 3 * 10^4

思路:这个题怎么说呢,首先数据范围是30000.不算是很大。但是这种子数组的计算方式,感觉暴力法肯定会超时。然后题目的标签是栈。这种题目第一反应单调栈没跑了。暴力法的时间复杂度n方不行。用栈的话应该尽量o(n)时间复杂度,emmmm...我还是去试试代码吧
第一版暴力超时代码:

class Solution {
    public int sumSubarrayMins(int[] arr) {
        long ans = 0;
        for(int i = 0;i

我果然不能怀有侥幸心理。。然后继续说,之前就说了这个题应该可以用单调栈来实现。能把时间复杂度大大的降低。然后看上面的代码。因为正常的思维逻辑应该是:
找到所有的子数组,找到所有子数组的最小值
但是我们可以用逆向思维:
找到所有的最小值,然后我们判断这个最小值能凑出来的子数组的个数。
然后说一个一眼能看出来的小规律:
一个元素是一个范围的最小值。这个元素左边x个元素,右边y个元素。那么这个元素能组成的子数组的个数是x乘y。至于原因其实就是左边x种可能,右边y种,总数应该是笛卡尔积的形式。也就是xy。
接下来这个题就稍微好做一点了,我去敲第二版本代码:

class Solution {
    public int sumSubarrayMins(int[] arr) {
        int n = arr.length;
        int cost = 0;
        long ans = 0;
        Stack stack = new Stack<>();
        stack.push(-1); // 元素范围1开始,所以填充-1永远不会弹出
        for (int i = 0; i < n; i++) {
            while (stack.size() > 1 && arr[stack.peek()] > arr[i]) {
                int top = stack.pop();
                cost -= arr[top] * (top - stack.peek());
            }
            cost += arr[i] * (i - stack.peek());
            ans = cost + ans;
            stack.push(i);  // 压栈
        }
        return (int)(ans%1000000007);
    }
}

我感觉思路啥的说的挺清楚了,就不多说了,我去看看性能第一的代码:

class Solution {
    public int sumSubarrayMins(int[] arr) {
        final long factor = (long)Math.pow(10, 9) + 7;
        int[] dp = new int[arr.length + 1];
        int[] imin = new int[arr.length + 1];
        dp[0] = arr[0];
        imin[0] = -1;
        long result = dp[0];
        for (int i = 1; i < arr.length; i++) {
            int j = i - 1;
            while(j >= 0 && arr[j] > arr[i]) {
                j = imin[j];
            }
            imin[i] = j;
            dp[i] = (i - j) * arr[i] + (j >= 0 ? dp[j] : 0);
            result += dp[i];
        }

        return (int)(result % factor);
    }
}

大部分思路雷同,只不过人家是用dp的方式来记录的。然后其实本质上也还是分左右部分,然后乘法算出所有结果。这个题就不多说了,下一题。

你能在你最喜欢的那天吃到你最喜欢的糖果吗?

题目:给你一个下标从 0 开始的正整数数组 candiesCount ,其中 candiesCount[i] 表示你拥有的第 i 类糖果的数目。同时给你一个二维数组 queries ,其中 queries[i] = [favoriteTypei, favoriteDayi, dailyCapi] 。你按照如下规则进行一场游戏:
你从第 0 天开始吃糖果。
你在吃完 所有 第 i - 1 类糖果之前,不能 吃任何一颗第 i 类糖果。
在吃完所有糖果之前,你必须每天 至少 吃 一颗 糖果。
请你构建一个布尔型数组 answer ,满足 answer.length == queries.length 。answer[i] 为 true 的条件是:在每天吃 不超过 dailyCapi 颗糖果的前提下,你可以在第 favoriteDayi 天吃到第 favoriteTypei 类糖果;否则 answer[i] 为 false 。注意,只要满足上面 3 条规则中的第二条规则,你就可以在同一天吃不同类型的糖果。请你返回得到的数组 answer 。

示例 1:
输入:candiesCount = [7,4,5,3,8], queries = [[0,2,2],[4,2,4],[2,13,1000000000]]
输出:[true,false,true]
提示:
1- 在第 0 天吃 2 颗糖果(类型 0),第 1 天吃 2 颗糖果(类型 0),第 2 天你可以吃到类型 0 的糖果。
2- 每天你最多吃 4 颗糖果。即使第 0 天吃 4 颗糖果(类型 0),第 1 天吃 4 颗糖果(类型 0 和类型 1),你也没办法在第 2 天吃到类型 4 的糖果。换言之,你没法在每天吃 4 颗糖果的限制下在第 2 天吃到第 4 类糖果。
3- 如果你每天吃 1 颗糖果,你可以在第 13 天吃到类型 2 的糖果。
示例 2:
输入:candiesCount = [5,2,6,4,1], queries = [[3,1,2],[4,10,3],[3,10,100],[4,100,30],[1,3,1]]
输出:[false,true,true,false,false]
提示:
1 <= candiesCount.length <= 105
1 <= candiesCount[i] <= 105
1 <= queries.length <= 105
queries[i].length == 3
0 <= favoriteTypei < candiesCount.length
0 <= favoriteDayi <= 109
1 <= dailyCapi <= 109

思路:今天儿童节,题目都这么有童心。虽然这个题目本身除了名字可爱点外并不友好,我仔细看了下好像每次判断都是单独的计算,所以我有个大胆的想法:首先用累加的方法算出前面的总和。然后只要天数<前缀。并且天数乘可吃最大值小于等于前面的个数。差不多思路就这样,我去代码试试吧。
第一版代码:

class Solution {
    public boolean[] canEat(int[] candiesCount, int[][] queries) {
        long[] sum = new long[candiesCount.length+1];
        for(int i = 1;i

思路没啥问题,就是这里天数是从0开始的,想要参与计算要+1才是表示第几天,然后第二个坑点就是溢出!
连着wa了三次,两次都是溢出的问题,第一次是sum溢出,第二次是max乘法溢出。然后面向测试案例编程,好不容易过了。
思路比较清晰,也没啥好说的。这个代码性能不是很好,我觉得应该是细节处理上的问题,我去看看性能第一的代码:

class Solution {
    public boolean[] canEat(int[] candiesCount, int[][] queries) {
        int n = candiesCount.length;
        long[] preSum = new long[n + 1];
        for (int i = 0; i < n; i++) {
            preSum[i + 1] = preSum[i] + candiesCount[i];
        }

        boolean[] ans = new boolean[queries.length];
        for (int i = 0; i < queries.length; i++) {
            int type = queries[i][0];
            long day = queries[i][1];
            long cap = queries[i][2];
            long min = day + 1;
            long max = min * cap;
            if (max > preSum[type] && min <= preSum[type + 1]) {
                ans[i] = true;
            }
        }
        return ans;
    }
}

差不多一样一样的思路,细节处理上因为默认都是false,所以只要填充true就行了。还有人家全部变量用的long,剩下其实大体的判断是一样的,这个题的性能相差不大,就这么过了吧。
本篇笔记就到这里,如果稍微帮到你了记得点个喜欢点个关注。也祝大家工作顺顺利利,生活健健康康!另外今天儿童节,也祝大家儿童节快乐吧~!

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