前言

这是 2021-06-12 的一场双周赛,这次周赛的前三道题相对简单,第四题如果没有做过表达式处理相关的题目,是比较难想到正确做法的。其中,比赛涉及到的知识点有模拟、枚举、前缀和、表达式求值和动态规划。

第 54 场双周赛链接:https://leetcode-cn.com/contest/biweekly-contest-54/

1. 检查是否区域内所有整数都被覆盖

题目链接:https://leetcode.cn/problems/check-if-all-the-integers-in-a-range-are-covered/

题意

给出 n n n 个小区间 [ l i , r i ] [l_i, r_i] [li,ri],表示各个区间中的整数都被覆盖。

然后给出询问区间 [ l , r ] [l,r] [l,r],让你判断该区间内所有整数是否都被覆盖了。

数据保证:

1 ≤ n ≤ 50 1 \leq n \leq 50 1n50

1 ≤ l i ≤ r i ≤ 50 1 \leq l_i \leq r_i \leq 50 1liri50

1 ≤ l ≤ r ≤ 50 1 \leq l \leq r \leq 50 1lr50

题解

因为数据范围比较小,所有可以考虑用暴力法解决。我们对各区间中的整数进行标记,然后遍历标记数组区间 [ l , r ] [l,r] [l,r] 中的整数,如果有未被标记的就返回 false ,否则返回 true

时间复杂度是 O ( ∑ i = 0 n ( r i − l i + 1 ) ) O(\displaystyle \sum_{i=0}^{n}(r_i - l_i + 1)) O(i=0n(rili+1))

class Solution {
public:
    bool isCovered(vector<vector<int>>& ranges, int left, int right) {
        vector<bool> isDiscovers(51, false);
        for (auto range : ranges) {
            for (int i = range[0]; i <= range[1]; i++) {
                isDiscovers[i] = true;
            }
        }
        for (int i = left; i <= right; i++) {
            if (!isDiscovers[i]) {
                return false;
            }
        }
        return true;
    }
};

2. 找到需要补充粉笔的学生编号

题目链接:https://leetcode.cn/problems/find-the-student-that-will-replace-the-chalk/

题意

班级有 n n n 个学生,从 0 → n − 1 0 \rightarrow n-1 0n1 编号。

老师有 k k k 根粉笔,且会按 0 → n − 1 0 \rightarrow n-1 0n1 的顺序对学生进行提问,学生回答问题需要粉笔,编号为 i i i 的学生回答问题消耗的粉笔数为 C i C_i Ci

老师会循环进行提问,当 k < C i k < C_i k<Ci 时,老师需要补充粉笔,返回此时的学生编号,即 i i i

数据保证:

0 ≤ n , C i ≤ 1 0 5 0 \leq n,C_i \leq 10_5 0n,Ci105

0 ≤ k ≤ 1 0 9 0 \leq k \leq 10_9 0k109

题解

我们可以通过模拟来解决这道题,但是当 k k k 比较大,而 C i C_i Ci 都比较小的时候,就会时间超限。

我们在模拟的时候,其实不难发现,只有在最后一轮提问的时候,才会决定最后答案。于是我们可以通过 k % ∑ i = 0 n C i k \% \sum_{i=0}^{n}{C_i} k%i=0nCi,让提问直接进入最后一轮,然后再模拟去寻找老师需要补充粉笔时学生的编号。

using ll = long long;
class Solution {
public:
    int chalkReplacer(vector<int>& chalk, int k) {
        int n = chalk.size();
        ll sum = 0;
        for (auto num : chalk) {
            sum += num;
        }
        k = k % sum;
        for (int i = 0; i < n; i++) {
            if (k < chalk[i]) {
                return i;
            } else {
                k -= chalk[i];
            }
        }
        return -1;
    }
};

3. 最大的幻方

题目链接:https://leetcode.cn/problems/largest-magic-square/

题意

一个 k × k k \times k k×k 的幻方指的是一个 k × k k \times k k×k 填满整数的方阵,且 每一行、每一列以及两条对角线的和 全部相等。

给你一个 n × m n \times m n×m 的整数矩阵 g r i d grid grid,要求返回矩阵中最大幻方的边长 k k k

数据保证:

1 ≤ n , m ≤ 50 1 \leq n,m \leq 50 1n,m50

1 ≤ g r i d i , j ≤ 1 0 6 1 \leq grid_{i,j} \leq 10^6 1gridi,j106

题解

从大到小枚举可能组成幻方的所有边长和左上顶点的坐标,由此枚举出所有可能成为幻方的方阵,然后计算该方阵每一行、每一列以及两条对角线的和,判断它们是否都相等。如果能找到幻方就立即返回,否则到最后返回 1 1 1(每个方格都是一个 1 × 1 1 \times 1 1×1 的幻方)。

因为直接暴力计算该方阵每一行、每一列以及两条对角线的和会非常耗时,这里我们可以采用前缀和的方式来进行优化,这里我维护了行、列、两条对角线方向等四个前缀和,需要用到的时候按照坐标进行差分计算即可。

using ll = long long;
class Solution {
public:
    int largestMagicSquare(vector<vector<int>>& grid) {
        int n = grid.size(), m = grid[0].size();
        vector<vector<ll>> sum1(n + 1, vector<ll>(m + 1, 0)); // 每行前缀和
        vector<vector<ll>> sum2(sum1); // 每列前缀和
        vector<vector<ll>> sum3(sum1); // 左上到右下前缀和
        vector<vector<ll>> sum4(n + 1, vector<ll>(m + 2, 0)); // 右上到左下前缀和

        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                sum1[i][j] = sum1[i][j - 1] + grid[i - 1][j - 1];
                sum2[i][j] = sum2[i - 1][j] + grid[i - 1][j - 1];
            }
        }
        
        for (int i = 1; i <= n; i++) {
            for (int j = 1; j <= m; j++) {
                sum3[i][j] = sum3[i - 1][j - 1] + grid[i - 1][j - 1];
                sum4[i][j] = sum4[i - 1][j + 1] + grid[i - 1][j - 1];
            }
        }

        for (int k = min(n, m); k >= 2; k--) {
            for (int i = 0; i <= n - k; i++) {
                for (int j = 0; j <= m - k; j++) {
                    int x1 = i, y1 = j, x2 = i + k - 1, y2 = j + k - 1;
                    vector<ll> lens;
                    // 计算各行的和
                    for (int a = x1; a <= x2; a++) {
                        lens.push_back(sum1[a + 1][y2 + 1] - sum1[a + 1][y1]);
                    }
                    // 计算各列的和
                    for (int a = y1; a <= y2; a++) {
                        lens.push_back(sum2[x2 + 1][a + 1] - sum2[x1][a + 1]);
                    }
                    // 计算两条对角线的和
                    lens.push_back(sum3[x2 + 1][y2 + 1] - sum3[x1][y1]);
                    lens.push_back(sum4[x2 + 1][y1 + 1] - sum4[x1][y2 + 2]);

                    int l;
                    for (l = 1; l < lens.size(); l++) {
                        if (lens[l] != lens[l - 1]) {
                            break;
                        } 
                    }
                    if (l == lens.size()) {
                        return k;
                    }
                }
            }
        }
        return 1;
    }
};

4. 反转表达式值的最少操作次数

题目链接:https://leetcode.cn/problems/minimum-cost-to-change-the-final-value-of-expression/

题意

给你一个有效的布尔表达式字符串 s s s,这个字符串包含字符 '1''0''&''|''('')'

你可以对表达式 s s s 进行如下操作:

  • 将一个 '1' 变成一个 '0'
  • 将一个 '0' 变成一个 '1'
  • 将一个 '&' 变成一个 '|'
  • 将一个 '|' 变成一个 '&'

请你返回将表达 s s s 的结果反转( 0 → 1   o r   1 → 0 0 \rightarrow 1 \ or \ 1\rightarrow0 01 or 10),所需要的最小操作次数。

数据保证:

1 ≤ s . l e n g t h ≤ 1 0 5 1 \leq s.length \leq 10^5 1s.length105

题解

这道题是表达式求值动态规划的结合,通过在表达式计算的过程中进行转态转移,最后得到最优解。

首先,我们通常以双栈(操作数栈和操作符栈)的形式对表达式求值问题进行模拟求解。在这里,操作数栈中存储的元素要更换为该操作数所对应的一个二元状态 ( x , y ) (x, y) (x,y)。其中, x x x 表示将对应表达式变为 1 1 1 所需要的最小操作次数, y y y 表示将对应表达式变为 0 0 0 所需要的最小操作次数。

于是,在初始化时,操作数 1 1 1 对应的状态为 ( 0 , 1 ) (0,1) (0,1),操作数 0 0 0 对应的状态为 ( 1 , 0 ) (1,0) (1,0)

然后,我们根据运算符优先级确定计算顺序。因为 &| 的优先级相同,所以我们只需要从左往右进行计算。考虑到有括号的存在,我们可以把每个 ( 之前的操作数先合并成一个,当遇到 ) 时再让 ( 出栈,之后再合并由出栈的 ( 所分隔的操作数。

在计算的过程中的两个状态 ( x 1 , y 1 ) , ( x 2 , y 2 ) (x_1,y_1),(x_2,y_2) (x1,y1),(x2,y2) 进行状态转移时,我们可以根据操作符分类讨论:

当操作符为 & 时,分下面两种情况讨论可得转移方程。

  • 要想得到 1 1 1,我们需要将两个操作数都为 1 1 1,代价为 x 1 + x 2 x_1+x_2 x1+x2;或者操作符变为 |,然后将其中一个操作数变为 1 1 1,此时代价为 m i n ( x 1 , x 2 ) + 1 min(x_1,x_2)+1 min(x1,x2)+1
  • 要想得到 0 0 0, 我们只需要将其中一个操作数变为 0 0 0,代价为 m i n ( y 1 , y 2 ) min(y_1,y_2) min(y1,y2)
    { x = m i n ( x 1 + x 2 , m i n ( x 1 , x 2 ) + 1 ) y = m i n ( y 1 , y 2 ) \begin{cases} x = min(x_1+x_2,min(x_1,x_2)+1) \\ y = min(y1, y2) \end{cases} {x=min(x1+x2,min(x1,x2)+1)y=min(y1,y2)

当操作符为 | 时,分下面两种情况讨论可得转移方程。

  • 要想得到 1 1 1,我们只需要将其中一个操作数变为 1 1 1,代价为 m i n ( x 1 , x 2 ) min(x_1,x_2) min(x1,x2)
  • 要想得到 0 0 0, 我们需要将两个操作数都为 0 0 0,代价为 y 1 + y 2 y_1+y_2 y1+y2;或者操作符变为 &,然后将其中一个操作数变为 0 0 0,此时代价为 m i n ( y 1 , y 2 ) + 1 min(y_1,y_2)+1 min(y1,y2)+1
    { x = m i n ( x 1 , x 2 ) y = m i n ( y 1 + y 2 , m i n ( y 1 , y 2 ) + 1 ) \begin{cases} x = min(x_1,x_2) \\ y = min(y_1+y_2,min(y_1,y_2)+1) \end{cases} {x=min(x1,x2)y=min(y1+y2,min(y1,y2)+1)

在完成表达式计算之后,操作数栈中最后一个元素就是最终得到的状态 ( x , y ) (x, y) (x,y) 。其中一个值为 0 0 0,表示表达式原本的计算结果,另个值为非 0 0 0,即我们想要的答案,所以最终的答案为 m a x ( x , y ) max(x,y) max(x,y)

using pii = pair<int, int>;
class Solution {
public:
    int minOperationsToFlip(string expression) {
        vector<pii> num_stk; // 数字栈
        vector<char> op_stk; // 操作数栈
        for (auto ch : expression) {
            if (ch == '0' || ch == '1' || ch == ')') {
                if (ch == '0') {
                    num_stk.push_back({1, 0});
                } else if (ch == '1') {
                    num_stk.push_back({0, 1});
                } else {
                    op_stk.pop_back(); // 弹出左括号
                }
				// 在计算过程中进行状态转移
                if (num_stk.size() >= 2 && op_stk.back() != '(') {
                    char op = op_stk.back();
                    op_stk.pop_back();
                    auto [x1, y1] = num_stk.back();
                    num_stk.pop_back();
                    auto [x2, y2] = num_stk.back();
                    num_stk.pop_back();
	
                    if (op == '&') {
                        num_stk.push_back({min(x1 + x2, min(x1, x2) + 1), min(y1, y2)});
                    } else if (op == '|') {
                        num_stk.push_back({min(x1, x2), min(y1 + y2, min(y1, y2) + 1)});
                    }
                }
            } else {
                op_stk.push_back(ch);
            }
        }
        return max(num_stk[0].first, num_stk[0].second);
    }
};

参考链接


这是一篇上古题解了,当时觉得到没有时效性了,就一直没有发。今年偶然翻到,发现压轴题有一定的学习价值,决定分享一下,建议看到的同学可以去写一下压轴题。

12-11 16:18