2481. 分割圆的最少切割次数
圆内一个 有效切割 ,符合以下二者之一:
- 该切割是两个端点在圆上的线段,且该线段经过圆心。
- 该切割是一端在圆心另一端在圆上的线段。
一些有效和无效的切割如下图所示。
给你一个整数 n
,请你返回将圆切割成相等的 n
等分的 最少 切割次数。
提示:1 <= n <= 100
示例:
输入: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
行一的数目为 o n e s R o w i onesRow_i onesRowi 。 - 令第
j
列一的数目为 o n e s C o l j onesCol_j onesColj 。 - 令第
i
行零的数目为 z e r o s R o w i zerosRow_i zerosRowi 。 - 令第
j
列零的数目为 z e r o s C o l j zerosCol_j zerosColj 。 - d i f f [ i ] [ j ] = o n e s R o w i + o n e s C o l j − z e r o s R o w i − z e r o s C o l j diff[i][j] = onesRow_i + onesCol_j - zerosRow_i - zerosCol_j diff[i][j]=onesRowi+onesColj−zerosRowi−zerosColj
请你返回差值矩阵 diff
。
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 10^5
1 <= m * n <= 10^5
grid[i][j]
要么是0
,要么是1
示例:
输入: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个信息:
- 关于某个位置的字符
x
,是否在这个位置后,还存在某个位置,字符同样是x
- 关于某个区间内的长度为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的回文子序列的长度,由于i
和j
各自都能取到字符串长度这么大的值,那光是状态的个数都已经达到n^2
,即10^8
了,就算每个状态的计算只需要
O
(
1
)
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] != b
,R[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
×
E
2
)
O(n × E^2)
O(n×E2),其中
E
=
10
E = 10
E=10, 即 0-9
共10个数字
空间复杂度 O ( n × E 2 ) O(n × E^2) O(n×E2)
其实可以把L
和R
数组优化掉一维。
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
×
E
2
)
O(n × E^2)
O(n×E2),其中
E
=
10
E = 10
E=10, 即 0-9
共10个数字
空间复杂度 O ( E 2 ) O(E^2) O(E2)
还可以使用增量计算,再将时间复杂度优化掉一个 E E E,因为每个位置的字符是固定的。(这份代码不能完全理解,待后续更新)
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) O(n×E)