5404. 用栈操作构建数组
因为 target 是严格升序的,且只包含 1 到 n 之间的数字,即 target 是 list 的一个子序列,所以一定有解。对于 list 中的每个数字 x:
- 如果 x 在 target 中,则需执行一次 Push。
- 如果 x 不在 target 中,且小于target 中的最大值,则需执行一个 Push 和 Pop。
- 如果 x 不在 target 中,且大于target 中的最大值,则无需处理。
按照上述规则,依次处理 list 中的元素即可。代码如下:
class Solution {
public:
vector<string> buildArray(vector<int>& target, int n) {
vector<string> res;
int last = 0;
for(int i = 0; i < target.size(); i++) {
for(int j = last+1; j < target[i]; j++) {
res.push_back("Push");
res.push_back("Pop");
}
res.push_back("Push");
last = target[i];
}
return res;
}
};
5405. 形成两个异或相等数组的三元组数目
首先暴力枚举三元组(i,j,k),时间复杂度 O(n^3)。 然后利用异或运算的性质,O(1)的计算区间 [i,j-1],[j,k] 的异或值。 异或运算有如下性质:
- a ⊕ a = 0
- a ⊕ b = b ⊕ a
- a ⊕b ⊕ c = a ⊕ (b ⊕ c) = (a ⊕ b) ⊕ c;
- d = a ⊕ b ⊕ c 可以推出 a = d ⊕ b ⊕ c.
- a ⊕ b ⊕ a = b.
利用第五条性质,可以先O(n)的预处理所有前缀 pre(i) 的异或值。 当要计算[i,j]的异或值时,有:
- 当 i = 0 时,结果为 pre(i)。
- 当 i 不为 0 时,结果为 pre(j)⊕pre(i-1) 。
代码如下:
class Solution {
int get(const vector<int> &val, int L, int R) {
if(L < 0) {
return val[R];
}
return val[R]^val[L];
}
public:
int countTriplets(vector<int>& arr) {
vector<int> val; //预处理前缀
for(int a = 0, i = 0 ; i < arr.size(); i++) {
a ^= arr[i];
val.push_back(a);
}
int cnt = 0; //暴力枚举三元组
for(int i = 0; i < arr.size(); i++) {
for(int j = i+1; j < arr.size(); j++) {
for(int k = j; k < arr.size(); k++) {
// 根据预处理,O(1)计算异或值
if(get(val, i-1, j-1) == get(val, j-1, k)) {
cnt++;
}
}
}
}
return cnt;
}
};
5406. 收集树上所有苹果的最少时间
设 dfs(i) 表示收集以 i 为根节点的子树,然后返回到 i 的父节点的时间花费。
- 当子树 i 没有苹果时,显然 dfs(i) = 0。因为不需要到达 i,所以也不需要返回 i 的 父节点。
- 当子树有苹果时,dfs(i) = sum(dfs(son(i)))。
那么最终的答案为,dfs(0) - 2。
代码如下:
class Solution {
vector<int> E[100000];
public:
int dfs(int root, int pre, const vector<bool> &hasApple) {
int sum = 0;
for(auto v : E[root]) {
if(pre == v) { //pre 为父节点。
continue;
}
// 计算遍历所有子树的时间花费。
sum += dfs(v, root, hasApple);
}
// sum != 0 说明 root 的子树有苹果
// hasApple[root] == true,说明root结点有苹果。
// 说明此时需要到达 root 结点,sum += 2 是从root 回到 pre 的花费。
if((sum != 0 || hasApple[root])) {
sum += 2;
}
return sum;
}
int minTime(int n, vector<vector<int>>& edges, vector<bool>& hasApple) {
//先根据边表,构造邻接表,方便dfs。
for(const auto &e : edges) {
E[e[0]].push_back(e[1]);
E[e[1]].push_back(e[0]);
}
// 0 没有父节点,所有也不需要花费 2 返回了。
return dfs(0, -1, hasApple) - 2;
}
};
5407. 切披萨的方案数
设 dp(x, y, k) 表示pizza还剩 x 行,y列且还需要分成 k 块时的方案数。x, y, k 满足下述取值范围:
- 0 <= x <= pizza.size()
- 0 <= y <= pizza[0].size()
- 1 <= k
当 k = 1 时有:
- 如果 pizza 有苹果,则方案数为 1 。
- 如果 pizza 没有苹果,则方案数为 0 。
当 k > 1 时,有 (x-1) + (y-1) 种(可能合法的)切法,则 dp(x,y,k) = sum(dp(i,y,k-1)) + sum(dp(x,j,k-1))。 其中 1 <= i < x, 1 <= j < y。
代码如下:
class Solution {
public:
int64_t dp[51][51][11];
//pre[i][j] 表示i行j列的左上角子矩阵的苹果数量
int pre[51][51];
int count(int x, int y, const vector<string> &pizza) {
int r = pizza.size();
int c = pizza[0].size();
//根据容斥原理计算右下角 i 行 j 列的苹果数量。
return pre[r][c] - pre[r-x][c] - pre[r][c-y] + pre[r-x][c-y];
}
void update(int64_t &x, int64_t y) {
x += y;
while(x >= 1000000007) {
x -= 1000000007;
}
}
int dfs(int x, int y, int k, const vector<string> &pizza) {
if(dp[x][y][k] != -1) {
return dp[x][y][k];
}
int cnt = count(x, y, pizza);
if(cnt < k) {
//苹果总数都小于 k 了,还分个毛线。
return dp[x][y][k] = 0, 0;
}
if(k == 1) {
// k == 1,不用分了,所以就一种方案。
return dp[x][y][k] = 1, 1;
}
int64_t &val = dp[x][y][k];
val = 0;
//横着切,枚举剩下的pizza的行数
for(int i = 1; i < x; i++) {
//需要保证上半部分有苹果
if(count(x-i, y, pizza) == cnt) {continue;}
update(val, dfs(x-i, y, k-1, pizza));
}
//竖着切,枚举剩下的pizza的列数
for(int i = 1; i < y; i++) {
//需要保证左半部分有苹果
if(count(x, y-i, pizza) == cnt) {continue;}
update(val, dfs(x, y-i, k-1, pizza));
}
return val;
}
int ways(vector<string>& pizza, int k) {
memset(dp, -1, sizeof(dp));
memset(pre, 0, sizeof(pre));
//根据容斥原理预处理子矩阵的苹果数量。
for(int i = 0; i < pizza.size(); i++) {
for(int j = 0; j < pizza[0].size(); j++) {
pre[i+1][j+1] = pre[i+1][j] + pre[i][j+1] - pre[i][j];
if(pizza[i][j] == 'A') {
pre[i+1][j+1] ++;
}
}
}
return dfs(pizza.size(), pizza[0].size(), k, pizza);
}
};