LeetCode第188場周賽(Weekly Contest 188)解題報告

就差那麼一分鐘,哎,第四題思路和第一名大佬一樣,但是思維 + 手速,還要繼續努力呀,奧裏給

第一題:模擬。

第二題:異或前綴和+ 暴力枚舉 O(n ^ 3) 或者 異或前綴和 + 優化 O(n ^ 2) 或者 異或前綴和 + 再優化 O(n)。

第三題:樹的遍歷。

第四題:動態規劃 DP。

詳細題解如下。


1.用棧操作構建數組

       AC代碼(C++)

2. 形成兩個異或相等數組的三元組數目

       AC代碼(方法一、異或前綴和 + 暴力枚舉 O(n ^ 3) C++)

       AC代碼(方法二、異或前綴和 + 優化 O(n ^ 2) C++)

       AC代碼(方法三、異或前綴和 + 再優化 O(n) C++)

3.收集樹上所有蘋果的最少時間

       AC代碼(C++)

4.切披薩的方案數

       AC代碼(C++)


LeetCode第188場周賽地址:

https://leetcode-cn.com/contest/weekly-contest-188/


1.用棧操作構建數組

題目鏈接

https://leetcode-cn.com/problems/build-an-array-with-stack-operations/

題意

給你一個目標數組 target 和一個整數 n。每次迭代,需要從  list = {1,2,3..., n} 中依序讀取一個數字。

請使用下述操作來構建目標數組 target :

  • Push:從 list 中讀取一個新元素, 並將其推入數組中。
  • Pop:刪除數組中的最後一個元素。
  • 如果目標數組構建完成,就停止讀取更多元素。

題目數據保證目標數組嚴格遞增,並且只包含 1 到 n 之間的數字。

請返回構建目標數組所用的操作序列。

題目數據保證答案是唯一的。

示例 1:

輸入:target = [1,3], n = 3
輸出:["Push","Push","Pop","Push"]
解釋: 
讀取 1 並自動推入數組 -> [1]
讀取 2 並自動推入數組,然後刪除它 -> [1]
讀取 3 並自動推入數組 -> [1,3]

示例 2:

輸入:target = [1,2,3], n = 3
輸出:["Push","Push","Push"]

示例 3:

輸入:target = [1,2], n = 4
輸出:["Push","Push"]
解釋:只需要讀取前 2 個數字就可以停止。

提示:

  • 1 <= target.length <= 100
  • 1 <= target[i] <= 100
  • 1 <= n <= 100
  • target 是嚴格遞增的

解題思路

一開始看到題目,有點暈,不知道啥意思,然後結合樣例才明白。

其實就是,我們按理來說,應該取 1 到 target 數組中的最後一個數完結(注意不是到 n,因爲可能不需要,比如 示例 2 ).

如果要處理的數,在 target 數組中,我們直接往答案數組中加 Push 即可

如果要處理的數,不在 target 數組中,我們就需要加 Push 和 Pop

比如示例 1,一開始我們數從 1 開始,1 在,那麼就是 Push。然後到 2 , 2 沒有,所以就是 Push Pop。到 3 有,所以Push。

因此,我們只需要,遍歷 數組 target,然後一個數 cur 從 0 開始,如果發現,當前 target 的數(遍歷的數)比 cur 大 不止 1,說明 target 中接下來的數,就不是我們 cur + 1 有的數,那麼就需要 Push Pop,然後 cur + 1,一直到滿足剛好 大 1,說明此時 cur 的下一個數 在 target中,那麼就 Push。

AC代碼(C++)

class Solution {
public:
    vector<string> buildArray(vector<int>& target, int n) {
        vector<string> ans;
        int cur = 0;
        for(auto c : target)
        {
            while(c - cur > 1)
            {
                ++cur;
                ans.push_back("Push");
                ans.push_back("Pop");
            }
            ans.push_back("Push");
            ++cur;
        }
        return ans;
    }
};

2. 形成兩個異或相等數組的三元組數目

題目鏈接

https://leetcode-cn.com/problems/count-triplets-that-can-form-two-arrays-of-equal-xor/

題意

給你一個整數數組 arr 。

現需要從數組中取三個下標 i、j 和 k ,其中 (0 <= i < j <= k < arr.length) 。

a 和 b 定義如下:

  • a = arr[i] ^ arr[i + 1] ^ ... ^ arr[j - 1]
  • b = arr[j] ^ arr[j + 1] ^ ... ^ arr[k]

注意:^ 表示 按位異或 操作。

請返回能夠令 a == b 成立的三元組 (i, j , k) 的數目。

示例 1:

輸入:arr = [2,3,1,6,7]
輸出:4
解釋:滿足題意的三元組分別是 (0,1,2), (0,2,2), (2,3,4) 以及 (2,4,4)

示例 2:

輸入:arr = [1,1,1,1,1]
輸出:10

示例 5:

輸入:arr = [7,11,12,9,5,2,7,17,22]
輸出:8

提示:

  • 1 <= arr.length <= 300
  • 1 <= arr[i] <= 10^8

解題思路

看到題目,第一眼異或,而且是一個區間計算異或,就想到了前綴和,這是因爲

  • arr[ i ] ^ .... ^ arr[ j ] = ( arr[ 0 ] ^ ... ^ arr[ i - 1 ] ) ^ ( arr[ 0 ] ^ ... ^ arr[ i - j ] ) = sum[ i - 1] ^ sum[ j ]

所以我們用了前綴和之和,就可以 O(1) 時間求出 [i, j] 區間中的異或值

方法一、異或前綴和 + 暴力枚舉 O(n ^ 3)

因此,根據實際複雜度,我們可以直接暴力枚舉 i, j, k,這樣子時間複雜度 O(n ^ 3),不會超時

方法二、異或前綴和 + 優化 O(n ^ 2)

我們可以發現,其實上是要求 [i, j - 1] == [j, k] 相等,那麼此時,這兩個區間再進行異或,就可以得到 我們要求 [i, k] 區間要求異或值爲 0,其中 i < k,那麼此時在這個 [ i, k] 中,j 的可能個數有 k - i + 1 - 1 = k - i,因爲 j > i 的

因此,我們直接只需要枚舉 i, k,使得 [i, k] 的區間異或值 == 0,那麼在這個區間內,滿足條件的三元個數有 k - i 個。

那麼時間複雜度就優化爲了 O(n ^ 2)

這裏代碼有一個位置要注意:

if((sum[i] ^ sum[k + 1]) == 0)  // 注意這裏,因爲 位運算優先級低於 == ,所以要加上 (),不然會錯誤的

方法三、異或前綴和 + 再優化 O(n)

我們可以發現,我們要求的是  [i, k] 的異或值 == 0,那就說明是 [0, i - 1] == [0, k] ,這兩個區間的異或值要相等

所以,我們可以直接遍歷一次數組,只要此時我們計算到 k ,即 [0, k],那麼我們記錄了前面所有的 [0, 0 到 k - 1] 的 異或值,那麼這樣子,我們就可以找到 和 [0, k] 異或值相等的個數情況,那麼這個時候,總共有 滿足條件的幾個 三元區間呢?

比如我們有 [0, l1 ], [0, l2] [0, l3]   都和  [0, k] 一樣,那麼計算出其三元區間個數就是:

k - l1 + k - l2 - k - l3 = k * (前面相等的個數 ) - (l1 + l2 + l3)。

所以,我們不僅要記錄, [0, k] 的同樣 異或值 對應有多少個,同時還要記錄,相同異或值的這樣子 區間的 右端點 之和。

這樣子,我們利用 兩個哈希表 ,然後時間複雜度就變爲了 O(n)。空間換時間

AC代碼(方法一、異或前綴和 + 暴力枚舉 O(n ^ 3) C++)

class Solution {
public:
    int sum[350];
    int countTriplets(vector<int>& arr) {
        memset(sum, 0, sizeof(sum));
        int n = arr.size();
        for(int i = 0;i < n; ++i)
        {
            sum[i + 1] = arr[i] ^ sum[i];
        }
        int ans = 0;
        for(int i = 0;i < n; ++i)
        {
            for(int j = i + 1;j < n; ++j)
            {
                for(int k = j; k < n; ++k)
                {
                    int a = sum[i] ^ sum[j];
                    int b = sum[j] ^ sum[k + 1];
                    if(a == b) ++ans;
                }
            }
        }
        return ans;
    }
};

AC代碼(方法二、異或前綴和 + 優化 O(n ^ 2) C++)

class Solution {
public:
    int sum[350];
    int countTriplets(vector<int>& arr) {
        memset(sum, 0, sizeof(sum));
        int n = arr.size();
        for(int i = 0;i < n; ++i)
        {
            sum[i + 1] = arr[i] ^ sum[i];
        }
        int ans = 0;
        for(int i = 0;i < n; ++i)
        {
            for(int k = i + 1; k < n; ++k)
            {
                // [i, k] == 0
                if((sum[i] ^ sum[k + 1]) == 0)  // 注意這裏,因爲 位運算優先級低於 == ,所以要加上 (),不然會錯誤的
                {
                    ans = ans + (k - i);
                }
            }
        }
        return ans;
    }
};

AC代碼(方法三、異或前綴和 + 再優化 O(n) C++)

class Solution {
public:
    unordered_map<int, int> pre_xor;
    unordered_map<int, int> pre_sum;
    int countTriplets(vector<int>& arr) {
        int n = arr.size();

        int ans = 0;
        pre_xor[0] = 1;  // 一開始,沒有任何數,異或值爲 0
        pre_sum[0] = 0;  // 此時是 [0, 0]

        int xor_val = 0;
        for(int k = 0; k < n; ++k)
        {
            // [0, k] 找和前面有幾個相同的
            xor_val ^= arr[k];

            if(pre_xor[xor_val])
            {
                ans = ans + k * pre_xor[xor_val] - pre_sum[xor_val];
            }

            ++pre_xor[xor_val];
            pre_sum[xor_val] += (k + 1);  // 注意,這裏的下標,是 k + 1,因此是從 [1, n]
        }
        return ans;
    }
};

3.收集樹上所有蘋果的最少時間

題目鏈接

https://leetcode-cn.com/problems/minimum-time-to-collect-all-apples-in-a-tree/

題意

給你一棵有 n 個節點的無向樹,節點編號爲 0 到 n-1 ,它們中有一些節點有蘋果。通過樹上的一條邊,需要花費 1 秒鐘。你從 節點 0 出發,請你返回最少需要多少秒,可以收集到所有蘋果,並回到節點 0 。

無向樹的邊由 edges 給出,其中 edges[i] = [fromi, toi] ,表示有一條邊連接 from 和 toi 。除此以外,還有一個布爾數組 hasApple ,其中 hasApple[i] = true 代表節點 i 有一個蘋果,否則,節點 i 沒有蘋果。

示例 1:

【示例有圖,具體看鏈接】
輸入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,true,true,false]
輸出:8 
解釋:上圖展示了給定的樹,其中紅色節點表示有蘋果。一個能收集到所有蘋果的最優方案由綠色箭頭表示。

示例 2:

【示例有圖,具體看鏈接】
輸入:n = 7, edges = [[0,1],[0,2],[1,4],[1,5],[2,3],[2,6]], hasApple = [false,false,true,false,false,true,false]
輸出:6
解釋:上圖展示了給定的樹,其中紅色節點表示有蘋果。一個能收集到所有蘋果的最優方案由綠色箭頭表示。

提示:

  • 1 <= n <= 10^5
  • edges.length == n-1
  • edges[i].length == 2
  • 0 <= fromi, toi <= n-1
  • fromi < toi
  • hasApple.length == n

 

解題分析

根據題目分析,其實就是不斷的遞歸,從根節點 dfs 到葉節點,然後又從葉節點不斷的返回值回到 根節點。

所以就相當於是一個 樹的遍歷

此時分析,當我們 dfs 到某個節點的時候,我們繼續往下 dfs,然後會有返回值,那麼此時對於這個節點而言,走的路,相當於是,其所有子節點來的。

如果子節點回到節點,那麼當前節點的返回值如何計算呢?根據題意,我們可以直接令 返回值就是 當前節點 拿到其子樹 所有蘋果時,走的路。

那麼此時,假設子節點返回值(因爲可能是多個,所有要遍歷其子節點)是 v,

那麼如果 v == 0,說明這個子節點以下沒有蘋果,但是可能這個子節點本身有蘋果,如果這個子節點本身有蘋果(hasApple 可以判斷)那麼當前 節點的返回值 res += 2,因此子節點有,從節點到子節點來回。

如果 v != 0,說明這個子節點下面肯定有蘋果,那麼無論這個子節點本身有沒有蘋果,都需要從當前節點去這個子節點,因此,返回值 res += v + 2。

所以對於某個節點,我們 dfs 其所有子節點,然後根據其子節點的返回值,又來計算 當前這個節點的返回值。

AC代碼(C++)

const int MAXN = 1e5 + 15;
class Solution {
public:
    vector<int> G[MAXN];
    vector<bool> hA;
    int dfs(int x, int fa)
    {
        int cur = 0;
        
        for(int i = 0;i < G[x].size(); ++i)
        {
            if(G[x][i] == fa) continue;
            
            int v = dfs(G[x][i], x);
            
            if(v) cur += v + 2;
            else
            {
                if(hA[G[x][i]]) cur += 2;
            }
        }

        return cur;
    }
    
    int minTime(int n, vector<vector<int>>& edges, vector<bool>& hasApple) {
        int ans = 0;
        this->hA = hasApple;
        for(auto e : edges)  // 雙向圖
        {
            G[e[0]].push_back(e[1]);
            G[e[1]].push_back(e[0]);
        }
        
        ans = dfs(0, -1);
        
        return ans;
    }
};

4.切披薩的方案數

題目鏈接

https://leetcode-cn.com/problems/find-the-kth-smallest-sum-of-a-matrix-with-sorted-rows/

題意

給你一個 rows x cols 大小的矩形披薩和一個整數 k ,矩形包含兩種字符: 'A' (表示蘋果)和 '.' (表示空白格子)。你需要切披薩 k-1 次,得到 k 塊披薩並送給別人。

切披薩的每一刀,先要選擇是向垂直還是水平方向切,再在矩形的邊界上選一個切的位置,將披薩一分爲二。如果垂直地切披薩,那麼需要把左邊的部分送給一個人,如果水平地切,那麼需要把上面的部分送給一個人。在切完最後一刀後,需要把剩下來的一塊送給最後一個人。

請你返回確保每一塊披薩包含 至少 一個蘋果的切披薩方案數。由於答案可能是個很大的數字,請你返回它對 10^9 + 7 取餘的結果。

示例 1:

【示例有圖,具體看鏈接】
輸入:pizza = ["A..","AAA","..."], k = 3
輸出:3 
解釋:上圖展示了三種切披薩的方案。注意每一塊披薩都至少包含一個蘋果。

示例 2:

【圖和 示例1 的一樣】
輸入:pizza = ["A..","AA.","..."], k = 3
輸出:1

提示:

  • 1 <= rows, cols <= 50
  • rows == pizza.length
  • cols == pizza[i].length
  • 1 <= k <= 10
  • pizza 只包含字符 'A' 和 '.' 。

 

解題分析

拿到題目,一看到是 計算方案的,想到的就是,動態規劃問題。

第一步,設狀態

那麼根據題目,我們可以直接設 dp[ k ][ i ][ j ],也就是,此時割了 第 k 刀,此時還剩下的部分爲 左上角 (i, j) 到右下角 (n - 1, m - 1)

 第二步,狀態轉移

對於 還剩下 (i, j) ,那麼此時應該是可以從 dp[ k - 1 ] [ 0 到 i - 1][ 0 到 j - 1 ] 轉移過來的,根據下面畫圖,應該比較好理解

也就是說, 原本是 (ii, jj) 那麼大,現在多切了一刀 (從 k - 1 到 k),這樣子就變成了 (i, j)

那麼此時轉移,因此對於 (i, j),我們要枚舉其 所有可能的 (ii, jj) 。

當時切這一刀的時候,要麼橫着切,要麼豎着切,所以第一種情況,就是豎着切,此時相當於 ii == i,那麼只有枚舉 jj 從 0 到 j - 1。同樣的,第二種情況,就是橫着切,那麼就是 jj == j,也就是隻有 ii 枚舉 所有 0 到 i - 1

那麼無論什麼情況,我們都需要進行判斷,能否從 dp[ k - 1][ ii ][ jj ] 轉移來 dp[ k - 1 ] [ i ] [ j ]。我們需要檢查,切了這一刀之後,分成了兩個塊,至少要求,這兩個塊分別都要有蘋果(後面繼續分析)

所以,如果可以切成功,就可以累加了

第三步,初始化

一開始,切第一刀,我就枚舉所有可能的情況,如果可以(也就是上面第二步那裏),那麼 dp[ 1 ][ ][ ] = 1,否則不可以就 = 0。

(其實也,可以直接 dp[ 0 ][ 0 ][ 0 ] = 1,)

最後答案,應該是,dp[ k - 1] [ all ] [ all ] 的和,因爲只要滿足總共切了 k - 1 刀

(如果是我們最上面的那個情況,初始化最開始已經是 dp[ 1 ]... 了,那麼當 k = 1,計算 k - 1  = 0,就沒有答案,所以直接對 k = 1,進行特判即可,也就是一整塊中,只要有蘋果就可以)

 

這裏還有一個問題需要討論:對於切一刀,是否可以切,也就是,對於 (ii, jj)  到 (i, j),要保證切後的兩部分都有蘋果

那麼我們先分析上面的時間複雜度,枚舉所有 k, i, j ,然後 ii 或者 jj,所以時間複雜度是 O(k * n * m * n)

如果旁段兩部分都有蘋果,直接枚舉的話,是 O(n * m),這樣子,總的時間複雜度就是 O(k * n * m * n * n * m),根據數據範圍,會超時。

因此,我們要想辦法,優化計算 切後兩部分計算有沒有蘋果

這個時候,就 想到了 二維前綴和,這個可以快速求出一個矩形區域的和。那麼我們可以講 有蘋果 = 1,沒 = 0。這樣子,我們就可以 O(1) 計算切後兩部分的各自和(二維前綴和),只要和 >= 1,說明至少有蘋果存在。

 

AC代碼(C++)

typedef long long LL;
const LL MOD = 1e9 + 7;
class Solution {
public:
    
    // dp[k][i][j]
    
    LL dp[25][65][65];
    int sum[65][65];
    
    int get(int x1, int y1, int x0, int y0){
        return sum[x1][y1] - sum[x1][y0] - sum[x0][y1] + sum[x0][y0];
    }
    
    bool check(int x, int y, int lx, int ly, vector<vector<int> >& grid, int n, int m)
    {
        // 判斷從 (lx, ly) 到 (x, y),分出來的兩塊有沒有
        // 然後,其實每一次只能橫着切或者豎着切,所以只要有 x == lx || y== ly,那麼分兩種情況

        if(x == lx)
        {
            // 此時相當於是豎着切,所以左半邊 和 右半邊
            bool left = false, right = false;
            
            if(get(lx, ly, n, y)) left = true;   // 直接利用前綴和快速計算這塊有沒有蘋果
            if(get(lx, y, n, m)) right = true;

            return left && right;
            
        }
        else
        {
            // 類似的
            bool up = false, down = false;
            if(get(lx, ly, x, m)) up = true;
            if(get(x, ly, n, m)) down = true;
            return up && down;
        }
        return false;
    }

    
    int ways(vector<string>& pizza, int k) {
        int n = pizza.size(), m = pizza[0].size();
        vector<vector<int> > grid(n, vector<int> (m, 0));
        for(int i = 0;i < n; ++i)
        {
            for(int j = 0;j < m; ++j)
            {
                if(pizza[i][j] == 'A') grid[i][j] = 1;
            }
        }
        // 特判
        if(k == 1)
        {
            for(int i = 0;i < n; ++i)
            {
                for(int j = 0;j < m; ++j)
                {
                    if(pizza[i][j] == 'A') return 1;
                }
            }
            return 0;
        }
        // 計算前綴和
        memset(sum, 0, sizeof(sum));
        for (int i = 0; i < n; i++)
            for (int j = 0; j < m; j++)
                sum[i + 1][j + 1] = grid[i][j];
        
        for (int i = 1; i <= n; i++)
            for (int j = 1; j <= m; j++) 
                sum[i][j] += sum[i][j - 1];
        
        for (int j = 1; j <= m; j++)
            for (int i = 1; i <= n; i++)
                sum[i][j] += sum[i - 1][j];
        
        // 初始化
        memset(dp, 0, sizeof(dp));
        for(int i = 1;i < n;++i)
        {
            if(check(i, 0, 0, 0, grid, n, m))
                dp[1][i][0] = 1;
        }
        
        for(int j = 1;j < m;++j)
        {
            if(check(0, j, 0, 0, grid, n, m))
                dp[1][0][j] = 1;
        }
        
        // 狀態轉移
        for(int kk = 2;kk < k; ++kk)
        {
            for(int i = 0;i < n; ++i)
            {
                for(int j = 0;j < m; ++j)
                {
                    for(int ii = 0; ii < i; ++ii)
                    {
                        if(check(i, j, ii, j, grid, n, m)) dp[kk][i][j] = (dp[kk][i][j] + dp[kk - 1][ii][j]) % MOD;
                    }
                    
                    
                    for(int jj = 0;jj < j; ++jj)
                    {
                        if(check(i, j, i, jj, grid, n, m)) dp[kk][i][j] = (dp[kk][i][j] + dp[kk - 1][i][jj]) % MOD;
                    }

                }
            }
        }
        
        // 最後答案,枚舉所有情況的 k - 1 刀的和,就是答案。
        LL ans = 0;
        for(int i = 0;i < n; ++i)
        {
            for(int j = 0;j < m; ++j)
            {
                ans = (ans + dp[k - 1][i][j]) % MOD;
            }
        }
        ans %= MOD;
        return int(ans);
    }
};

 

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