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);
    }
};

 

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