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<Integer> set = new HashSet<Integer>();
        Set<Integer> last = new HashSet<Integer>();
        for(int i : arr) {
            Set<Integer> temp = new HashSet<Integer>();
            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<Integer> 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<arr.length;i+=2) {
            if(arr[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<Integer> list;
    public StockSpanner() {
        this.list = new ArrayList<Integer>();
    }
    
    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<int[]> 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;i<tree.length;i++) {
            if(i>0 && tree[i] == tree[i-1]) continue;
            int f = -1;
            for(int j = i+1;j<tree.length;j++) {
                if(f == -1) {
                     if(tree[j] != tree[i]) f = tree[j];                
                }else {
                    if(tree[j] != tree[i] && tree[j] != f) {
                        max = Math.max(j-i, max);
                        break;
                    }
                }
                if(j == tree.length-1) {
                    max = Math.max(max, j-i+1);
                    return max;
                }
            }
        }
        return max;
    }
}

思路還是挺清晰的。就像我上面說的子數組中元素種類不超過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<arr.length;i++) {
            int temp = arr[i];
            ans += temp;
            for(int j = i+1;j<arr.length;j++) {
                temp = Math.min(temp, arr[j]);
                ans += temp;
            }
        }
        return (int)(ans%1000000007);
    }
}

我果然不能懷有僥倖心理。。然後繼續說,之前就說了這個題應該可以用單調棧來實現。能把時間複雜度大大的降低。然後看上面的代碼。因爲正常的思維邏輯應該是:
找到所有的子數組,找到所有子數組的最小值
但是我們可以用逆向思維:
找到所有的最小值,然後我們判斷這個最小值能湊出來的子數組的個數。
然後說一個一眼能看出來的小規律:
一個元素是一個範圍的最小值。這個元素左邊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<Integer> 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<sum.length;i++) sum[i] = sum[i-1]+candiesCount[i-1];
        boolean[] ans = new boolean[queries.length];
        for(int i = 0;i<ans.length;i++) {
            long max = queries[i][2];
            int type = queries[i][0];
            int day = queries[i][1]+1;
            if(max*day<=sum[type] || sum[type+1]<day) {
                //這個類型之前的糖果大於等於到今天能喫的最大值,所以false
                //如果一天一顆糖到今天會把這個類型的糖都喫完了也false
                ans[i] = false;
            }else {
                ans[i] = true;
            }
        }
        return ans;                
    }
}

思路沒啥問題,就是這裏天數是從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,剩下其實大體的判斷是一樣的,這個題的性能相差不大,就這麼過了吧。
本篇筆記就到這裏,如果稍微幫到你了記得點個喜歡點個關注。也祝大家工作順順利利,生活健健康康!另外今天兒童節,也祝大家兒童節快樂吧~!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章