LeetCode 92 双周赛

22 阅读5分钟

2481. 分割圆的最少切割次数

圆内一个 有效切割 ,符合以下二者之一:

  • 该切割是两个端点在圆上的线段,且该线段经过圆心。
  • 该切割是一端在圆心另一端在圆上的线段。

一些有效和无效的切割如下图所示。

image.png

给你一个整数 n ,请你返回将圆切割成相等的 n 等分的 最少 切割次数。

提示1 <= n <= 100

示例

image.png

输入:n = 4
输出:2
解释:
上图展示了切割圆 2 次,得到四等分。

思路:

简单找规律。若需要切成偶数份,则需要的切割次数为份数的一半;若要切成奇数份,则切割次数等于份数。

注意当n = 1时,不需要任何切割,这一点容易被遗漏。

class Solution {
public:
    int numberOfCuts(int n) {
        if (n == 1) return 0;
        return n % 2 == 0 ? n / 2 : n;
    }
};

2482. 行和列种一和零的差值

给你一个下标从 0 开始的 m x n 二进制矩阵 grid

我们按照如下过程,定义一个下标从 0 开始的 m x n 差值矩阵 diff

  • 令第 i 行一的数目为 onesRowionesRow_i
  • 令第 j 列一的数目为 onesColjonesCol_j
  • 令第 i 行零的数目为 zerosRowizerosRow_i
  • 令第 j 列零的数目为 zerosColjzerosCol_j
  • diff[i][j]=onesRowi+onesColjzerosRowizerosColjdiff[i][j] = onesRow_i + onesCol_j - zerosRow_i - zerosCol_j

请你返回差值矩阵 diff

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 10^5
  • 1 <= m * n <= 10^5
  • grid[i][j] 要么是 0 ,要么是 1

示例:

image.png

输入:grid = [[0,1,1],[1,0,1],[0,0,1]]
输出:[[0,0,4],[0,0,4],[-2,-2,2]]
解释:
- diff[0][0] = onesRow0 + onesCol0 - zerosRow0 - zerosCol0 = 2 + 1 - 1 - 2 = 0 
- diff[0][1] = onesRow0 + onesCol1 - zerosRow0 - zerosCol1 = 2 + 1 - 1 - 2 = 0 
- diff[0][2] = onesRow0 + onesCol2 - zerosRow0 - zerosCol2 = 2 + 3 - 1 - 0 = 4 
- diff[1][0] = onesRow1 + onesCol0 - zerosRow1 - zerosCol0 = 2 + 1 - 1 - 2 = 0 
- diff[1][1] = onesRow1 + onesCol1 - zerosRow1 - zerosCol1 = 2 + 1 - 1 - 2 = 0 
- diff[1][2] = onesRow1 + onesCol2 - zerosRow1 - zerosCol2 = 2 + 3 - 1 - 0 = 4 
- diff[2][0] = onesRow2 + onesCol0 - zerosRow2 - zerosCol0 = 1 + 1 - 2 - 2 = -2
- diff[2][1] = onesRow2 + onesCol1 - zerosRow2 - zerosCol1 = 1 + 1 - 2 - 2 = -2
- diff[2][2] = onesRow2 + onesCol2 - zerosRow2 - zerosCol2 = 1 + 3 - 2 - 0 = 2

思路:

预处理出每一行和每一列的零和一的数量,然后模拟即可。(实际可以直接统计一的数量减去零的数量)

class Solution {
public:
    vector<vector<int>> onesMinusZeros(vector<vector<int>>& grid) {
        int m = grid.size(), n = grid[0].size();
        vector<int> rows(m, 0), cols(n, 0);
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                if (grid[i][j]) rows[i]++, cols[j]++;
                else rows[i]--, cols[j]--;
            }
        }
        
        vector<vector<int>> ans(m, vector<int>(n));
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                ans[i][j] = rows[i] + cols[j];
            }
        }
        return ans;
    }
};

2483. 商店的最少代价

给你一个顾客访问商店的日志,用一个下标从 0 开始且只包含字符 'N''Y' 的字符串 customers 表示:

  • 如果第 i 个字符是 'Y' ,它表示第 i 小时有顾客到达。
  • 如果第 i 个字符是 'N' ,它表示第 i 小时没有顾客到达。

如果商店在第 j 小时关门(0 <= j <= n),代价按如下方式计算:

  • 在开门期间,如果某一个小时没有顾客到达,代价增加 1
  • 在关门期间,如果某一个小时有顾客到达,代价增加 1

请你返回在确保代价 最小 的前提下,商店的 最早 关门时间。

注意,商店在第 j 小时关门表示在第 j 小时以及之后商店处于关门状态。

提示:

  • 1 <= customers.length <= 10^5
  • customers 只包含字符 'Y''N'

示例:

输入:customers = "YYNY"
输出:2
解释:
- 第 0 小时关门,总共 1+1+0+1 = 3 代价。
- 第 1 小时关门,总共 0+1+0+1 = 2 代价。
- 第 2 小时关门,总共 0+0+0+1 = 1 代价。
- 第 3 小时关门,总共 0+0+1+1 = 2 代价。
- 第 4 小时关门,总共 0+0+1+0 = 1 代价。
在第 2 或第 4 小时关门代价都最小。由于第 2 小时更早,所以最优关门时间是 2 。

思路:

对于每个位置i,预处理一下[0, i - 1]区间内N的数量,以及[i, n - 1]区间内Y的数量,然后遍历一次计算答案即可。注意边界的处理。

class Solution {
public:
    int bestClosingTime(string c) {
        int n = c.size();
        vector<int> prev(n + 1, 0), post(n + 1, 0);
        
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            prev[i] = cnt;
            if (c[i] == 'N') cnt++;
        }
        prev[n] = cnt;
        
        cnt = 0;
        for (int i = n - 1; i >= 0; i--) {
            if (c[i] == 'Y') cnt++;
            post[i] = cnt;
        }
        
        int ans = 0, minCost = n;
        for (int i = n; i >= 0; i--) {
            int cost = post[i] + prev[i];
            if (cost <= minCost) {
                minCost = cost;
                ans = i;
            }
        }
        return ans;
    }
};

2484. 统计回文子序列数目

给你数字字符串 s ,请你返回 s 中长度为 5回文子序列 数目。由于答案可能很大,请你将答案对 10^9 + 7 取余 后返回。

提示:

  • 如果一个字符串从前往后和从后往前读相同,那么它是 回文字符串
  • 子序列是一个字符串中删除若干个字符后,不改变字符顺序,剩余字符构成的字符串。
  • 1 <= s.length <= 10^4
  • s 只包含数字字符

示例:

输入:s = "103301"
输出:2
解释:
总共有 6 长度为 5 的子序列:"10330""10331""10301""10301""13301""03301" 。
它们中有两个(都是 "10301")是回文的。

思路:

由于从子序列数目非常多,所以周赛当晚又想到了计算单个元素对答案的贡献这样的思路。就开始想,由于回文长度为5,且回文具有对称性,那么只需要计算某个位置的字符作为回文串的第一个,第二个字符即可。如果某个位置的字符x作为回文序列的第一个字符,那么我们需要找到其对称的最后一个字符,只要确定最后一个字符的位置,我们就可以把问题转变为求解中间区间内,长度为3的回文子序列的个数。也就是我们需要求解中间某个区间[i, j]内长度为3的回文子序列的个数。

这个思路需要2个信息:

  1. 关于某个位置的字符x,是否在这个位置后,还存在某个位置,字符同样是x
  2. 关于某个区间内的长度为3的回文子序列的个数

第一点,可以通过一次遍历,将每个相同的字符,出现过的位置都记录下来;但是这样在枚举某个字符x时,假设该字符出现过的位置共有n个,那枚举该字符作为长度为5的回文子序列的最外侧两端字符,需要枚举n^2。由于整个字符串长度为10^4,只包含0-9这些数字,那么可以认为每个字符,平均会在10^3个位置上出现,那枚举一个字符作为长度为5的回文子序列的两侧的字符,都需要10^6的复杂度,共有10种字符,那么就至少需要10^7复杂度,这还不包括计算的时间开销。

并且对于第2点,如何计算某个区间内的长度为3的回文子序列,这我想了半天也都无法解决。设dp[i][j]表示某个区间[i, j]内长度为3的回文子序列的长度,由于ij各自都能取到字符串长度这么大的值,那光是状态的个数都已经达到n^2,即10^8了,就算每个状态的计算只需要O(1)O(1),这也不可行。

但我就还是想用动态规划来做。我当时想的是,设dp[i][j][k]表示区间[i, j]内的长度为k的回文子序列的个数。由于直接自底向上进行状态计算,一定会计算至少n^2个状态,但其实有很多状态是无效的。所以我想用自顶向下,用记忆化搜索来做。于是便写出了如下代码

class Solution {
public:

    int INF = 1e9 + 7;
    
    vector<vector<vector<int>>> dp;

    // 找到 <= limit的最后一个位置
    int find(vector<vector<int>>& f, int x, int limit) {
        int n = f[x].size();
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (f[x][mid] <= limit) l = mid;
            else r = mid - 1;
        }
        return l;
    }

    // 计算区间[l, r]内, 长度为k的回文子序列的个数
    int dfs(int l, int r, int k, string& s, vector<vector<int>>& f) {
        if (dp[l][r][k] != -1) return dp[l][r][k];
        if (k == 1) return r - l + 1;
        int ans = 0;
        // 枚举这个区间内每个位置作为回文序列的两端字符
        for (int i = l; i <= r; i++) {
            int u = s[i - 1] - '0';
            // 找到这个字符出现的<= r的最远的位置, 并开始往回遍历
            for (int j = find(f, u, r); j >= 0; j--) {
                int v = f[u][j]; // 找到这个字符作为右侧对称的位置
                if (v - i + 1 < k) break; // 两个位置之间的字符数量 < k, 则不可能, 剪枝
                ans = (ans + dfs(i + 1, v - 1, k - 2, s, f)) % INF;
            }
        }
        return dp[l][r][k] = ans;
    }

    int countPalindromes(string s) {
        vector<vector<int>> f(10);
        int n = s.size();
        for (int i = 0; i < n; i++) {
            f[s[i] - '0'].push_back(i + 1); // 下标从1开始
        }
        dp = vector<vector<vector<int>>>(n + 1, vector<vector<int>>(n + 1, vector<int>(6, -1)));
        return dfs(1, n, 5, s, f);
    }
};

提交后,意料之中的超时了。稍微算了下时间复杂度,最外层的递归中,枚举了[1, n],共n个位置,10^4,内层枚举了该位置字符出现过的所有位置,平均是10^3,这就已经10^7了。然后换了另一种枚举方式,对于每个区间不枚举[l, r]内的每个位置,而枚举0-9,改写了一版代码

class Solution {
public:

    int INF = 1e9 + 7;
    
    vector<vector<vector<int>>> dp;

    // 找到 <= high 的最后一个位置
    int findLess(vector<vector<int>>& f, int x, int high) {
        int n = f[x].size();
        if (n == 0) return -1;
        int l = 0, r = n - 1;
        while (l < r) {
            int mid = l + r + 1 >> 1;
            if (f[x][mid] <= high) l = mid;
            else r = mid - 1;
        }
		if (f[x][l] <= high) return l;
        return -1;
    }
	
	// 找到 >= low 的第一个位置
	int findGreat(vector<vector<int>>& f, int x, int low) {
		int n = f[x].size();
        if (n == 0) return -1;
		int l = 0, r = n - 1;
		while (l < r) {
			int mid = l + r >> 1;
			if (f[x][mid] >= low) r = mid;
			else l = mid + 1;
		}
		if (f[x][l] >= low) return l;
		return -1;
	}

    // 计算区间[l, r]内, 长度为k的回文子序列的个数
    int dfs(int l, int r, int k, string& s, vector<vector<int>>& f) {
        if (dp[l][r][k] != -1) return dp[l][r][k];
        if (k == 1) return r - l + 1;
		if (r - l + 1 < k) return 0; // 区间的长度不足k
		if (r - l + 1 == k) {
			// 区间长度刚好为k, 直接判断区间是否是回文
			int i = l - 1, j = r - 1;
			bool ok = true;
			while (i < j) {
				if (s[i] != s[j]) {
					ok = false;
					break; // 不是回文
				}
				i++;
				j--;
			}
			dp[l][r][k] = ok ? 1 : 0;
			return dp[l][r][k];
		}
        int ans = 0;
		// 枚举所有的字符, 一共就10个
		for (int i = 0; i < 10; i++) {
			int begin = findGreat(f, i, l); // log 10^3 = 10
			int end = findLess(f, i, r);
			if (begin == -1 || end == -1) continue;
			// 计算所有的两侧端点
			for (int j = begin; j < end; j++) {
				for (int t = end; t > j; t--) {
					int ll = f[i][j], rr = f[i][t];
                    if (rr - ll + 1 < k) break; // 可以跳出这一轮循环了
					ans = (ans + dfs(ll + 1, rr - 1, k - 2, s, f)) % INF;
				}
			}
		}
        // 枚举这个区间内每个位置作为回文序列的两端字符
        return dp[l][r][k] = ans;
    }

    int countPalindromes(string s) {
        vector<vector<int>> f(10);
        int n = s.size();
        for (int i = 0; i < n; i++) {
            f[s[i] - '0'].push_back(i + 1); // 下标从1开始
        }
        dp = vector<vector<vector<int>>>(n + 1, vector<vector<int>>(n + 1, vector<int>(6, -1)));
        return dfs(1, n, 5, s, f);
    }
};

提交后发现还是超时,/(ㄒoㄒ)/

枚举中点+前后缀分离

其实我枚举每个字符,计算每个字符对答案的贡献,这样的思路是可以的。但是对于回文这一类的问题,比较好的做法是枚举回文的中点。而我上面是枚举了回文串的首尾两端。

对于枚举中点来说,看以当前元素作为回文序列的中间点,能构成的长度为5的回文序列,那么只要看当前位置之前的ab的回文子序列的数量,以及当前位置之后的ba的回文子序列的数量。由于每个字符都是0-9,所以对于两位的ab,一共能组合出的就只有100种情况。对于每个字符作为中点,只需要暴力枚举所有可能的ab组合,用乘法原理计算,并对答案进行累加即可。由于字符串长度最长为10^4,则总复杂度一共10^6

当我们遍历到位置i时,s[i] = x,我们此时求解一下以x作为中点的回文子序列有多少个,由于回文子序列长度为5,左右两侧是对称的,那么只需要枚举一侧的两位数字,对于每种组合ab,我们看一下,在区间[1, i - 1]内,有多少个ab这样的子序列,假设为L个;再看一下,在区间[i + 1, n]的区间内,有多少个ba这样的子序列,假设为R,那么根据乘法原理,回文子序列abxba一共有L * R个。我们对以x为中点,只需要枚举全部的ab组合即可。(全部的ab组合一共就100种)

L[i][a][b]表示区间[0, i]内的,形如ab的子序列的个数。我们可以用动态规划来计算。考虑第i个位置

  • s[i] != b,那么L[i][a][b] = L[i - 1][a][b]
  • s[i] == b,那么L[i][a][b] = L[i - 1][a][b] + 由s[i]构成的ab的数量

其中当s[i] == b时,还需要额外加上以s[i]构成的ab的个数,这就等于[0, i - 1]区间内所有a的数量

所以我们还需要这样一个东西,设cnt[i][a]表示区间[0, i]内的字符a的数量,由于a的取值只有0-9,所以第二维只需要开到10。那么上面的状态转移方程为:

  • s[i] == b,那么L[i][a][b] = L[i - 1][a][b] + cnt[i - 1][a]

所以我们需要进行一下预处理,对于a ∈ [0, 9],计算一下[1, i]区间内,共有多少个a

同理,对于中点的右侧,即[i + 1, n]区间内,我们需要知道有多少个形如ba的子序列。

我们设R[i][b][a]表示区间[i, n]中,形如ba的子序列的个数。同样的,其状态转移方程如下

  • s[i] != bR[i][b][a] = R[i + 1][b][a]
  • s[i] == b,则R[i][b][a] = R[i + 1][b][a] + 由s[i]构成的ba的数量

s[i] == b时,还需要额外加上以s[i]构成的ab的个数,这就等于[i + 1, n]区间内所有a的数量,计算方式同理,不再赘述

typedef long long LL;
const int N = 1e4 + 10, INF = 1e9 + 7;
class Solution {
public:
	
	int L[N][10][10], R[N][10][10];
	
    int countPalindromes(string s) {
		int cnt[10] = {0};
		int n = s.size();
		for (int i = 1; i <= n; i++) {
			for (int j = 0; j <= 9; j++) {
				for (int k = 0; k <= 9; k++) {
					L[i][j][k] = L[i - 1][j][k];
				}
			}
			int u = s[i - 1] - '0';
			for (int j = 0; j <= 9; j++) {
				L[i][j][u] += cnt[j];
			}
			cnt[u]++;
		}
		
		memset(cnt, 0, sizeof cnt);
		for (int i = n; i >= 1; i--) {
			for (int j = 0; j <= 9; j++) {
				for (int k = 0; k <= 9; k++) {
					R[i][j][k] = R[i + 1][j][k];
				}
			}
			int u = s[i - 1] - '0';
			for (int j = 0; j <= 9; j++) {
				R[i][u][j] += cnt[j];
			}
			cnt[u]++;
		}
		
		int ans = 0;
		for (int i = 1; i <= n; i++) {
			for (int j = 0;j <= 9; j++) {
				for (int k = 0; k <= 9; k++) {
					LL m = (LL) L[i - 1][j][k] * R[i + 1][k][j];
					ans = (ans + m) % INF;
				}
			}
		}
		return ans;
    }
};

时间复杂度 O(n×E2)O(n × E^2),其中 E=10E = 10, 即 0-9 共10个数字

空间复杂度 O(n×E2)O(n × E^2)

其实可以把LR数组优化掉一维。

class Solution {
public:
    
    int preCnt[10];
    
    int postCnt[10];

    int L[10][10];

    int R[10][10];

    int countPalindromes(string s) {
        int n = s.size(), INF = 1e9 + 7;
        for (int i = n - 1; i >= 0; i--) {
            int u = s[i] - '0';
            for (int j = 0; j < 10; j++) {
                R[u][j] += postCnt[j];
            }
            postCnt[u]++;
        }

        int ans = 0;
        for (int i = 0; i < n; i++) {
            int u = s[i] - '0';
            // 先撤销该位置的R数组
            postCnt[u]--;
            for (int j = 0; j < 10; j++) {
                R[u][j] -= postCnt[j];
            }
            // L数组还未计算该位置
            for (int j = 0; j < 10; j++) {
                for (int k = 0; k < 10; k++) {
                    ans = (ans + (long long)L[j][k] * R[k][j]) % INF;
                }
            }
            // 计算L数组
            for (int j = 0; j < 10; j++) {
                L[j][u] += preCnt[j];
            }
            preCnt[u]++;
        }
        return ans;
    }
};

时间复杂度 O(n×E2)O(n × E^2),其中 E=10E = 10, 即 0-9 共10个数字

空间复杂度 O(E2)O(E^2)

还可以使用增量计算,再将时间复杂度优化掉一个 EE,因为每个位置的字符是固定的。(这份代码不能完全理解,待后续更新)

typedef long long LL;
class Solution {
public:
    
    int preCnt[10];
    
    int postCnt[10];

    int L[10][10];

    int R[10][10];

    int countPalindromes(string s) {
        int n = s.size(), INF = 1e9 + 7;
        for (int i = n - 1; i >= 0; i--) {
            int u = s[i] - '0';
            for (int j = 0; j < 10; j++) {
                R[u][j] += postCnt[j];
            }
            postCnt[u]++;
        }

        LL ans = 0, cur = 0;
        for (int i = 0; i < n; i++) {
            int u = s[i] - '0';
            postCnt[u]--;
            for (int j = 0; j < 10; j++) {
                cur -= (LL) postCnt[j] * L[j][u];
                R[u][j] -= postCnt[j];
            }
            ans += cur;
            for (int j = 0; j < 10; j++) {
                cur += (LL) preCnt[j] * R[u][j];
                L[j][u] += preCnt[j];
            }
            preCnt[u]++;
        }
        return ans % INF;
    }
};

时间复杂度 O(n×E)O(n × E)