第 84 场周赛:翻转图像、字符串中的查找与替换、图像重叠、树中距离之和
Q1、[简单] 翻转图像
1、题目描述
给定一个 n x n
的二进制矩阵 image
,先 水平 翻转图像,然后 反转 图像并返回 结果 。
水平翻转图片就是将图片的每一行都进行翻转,即逆序。
- 例如,水平翻转
[1,1,0]
的结果是[0,1,1]
。
反转图片的意思是图片中的 0
全部被 1
替换, 1
全部被 0
替换。
- 例如,反转
[0,1,1]
的结果是[1,0,0]
。
示例 1:
输入:image = [[1,1,0],[1,0,1],[0,0,0]] 输出:[[1,0,0],[0,1,0],[1,1,1]] 解释:首先翻转每一行: [[0,1,1],[1,0,1],[0,0,0]];然后反转图片: [[1,0,0],[0,1,0],[1,1,1]]
示例 2:
输入:image = [[1,1,0,0],[1,0,0,1],[0,1,1,1],[1,0,1,0]] 输出:[[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]] 解释:首先翻转每一行: [[0,0,1,1],[1,0,0,1],[1,1,1,0],[0,1,0,1]];然后反转图片: [[1,1,0,0],[0,1,1,0],[0,0,0,1],[1,0,1,0]]
提示:
n == image.length
n == image[i].length
1 <= n <= 20
images[i][j]
==0
或1
.
2、解题思路
- 水平翻转:
- 对于每一行,将其元素逆序排列。例如,
[1,1,0]
变为[0,1,1]
。
- 对于每一行,将其元素逆序排列。例如,
- 反转:
- 对于每个元素,如果是
0
,则替换为1
;如果是1
,则替换为0
。
- 对于每个元素,如果是
- 合并操作:
- 在水平翻转的同时,直接对元素进行反转,避免额外的遍历。
3、代码实现
C++
class Solution {
public:vector<vector<int>> flipAndInvertImage(vector<vector<int>>& image) {int n = image.size(); // 获取矩阵的大小vector<vector<int>> ret(n, vector<int>(n)); // 创建结果矩阵for (int i = 0; i < n; ++i) { // 遍历每一行for (int j = 0; j < n; ++j) { // 遍历每一列// 水平翻转:取 image[i][n - j - 1]// 反转:对结果取反ret[i][j] = !image[i][n - j - 1];}}return ret; // 返回结果矩阵}
};
Java
class Solution {public int[][] flipAndInvertImage(int[][] image) {int n = image.length; // 获取矩阵的大小int[][] ret = new int[n][n]; // 创建结果矩阵for (int i = 0; i < n; i++) { // 遍历每一行for (int j = 0; j < n; j++) { // 遍历每一列// 水平翻转:取 image[i][n - j - 1]// 反转:对结果取反ret[i][j] = 1 - image[i][n - j - 1];}}return ret; // 返回结果矩阵}
}
Python
class Solution:def flipAndInvertImage(self, image: List[List[int]]) -> List[List[int]]:n = len(image) # 获取矩阵的大小ret = [[0] * n for _ in range(n)] # 创建结果矩阵for i in range(n): # 遍历每一行for j in range(n): # 遍历每一列# 水平翻转:取 image[i][n - j - 1]# 反转:对结果取反ret[i][j] = 1 - image[i][n - j - 1]return ret # 返回结果矩阵
4、复杂度分析
-
时间复杂度:
- 遍历矩阵的每个元素,时间复杂度为 O(n2)。
-
空间复杂度:
- 创建结果矩阵,空间复杂度为 O(n2)。
Q2、[中等] 字符串中的查找与替换
1、题目描述
你会得到一个字符串 s
(索引从 0 开始),你必须对它执行 k
个替换操作。替换操作以三个长度均为 k
的并行数组给出:indices
, sources
, targets
。
要完成第 i
个替换操作:
- 检查 子字符串
sources[i]
是否出现在 原字符串s
的索引indices[i]
处。 - 如果没有出现, 什么也不做 。
- 如果出现,则用
targets[i]
替换 该子字符串。
例如,如果 s = "abcd"
, indices[i] = 0
, sources[i] = "ab"
, targets[i] = "eee"
,那么替换的结果将是 "eeecd"
。
所有替换操作必须 同时 发生,这意味着替换操作不应该影响彼此的索引。测试用例保证元素间不会重叠 。
- 例如,一个
s = "abc"
,indices = [0,1]
,sources = ["ab","bc"]
的测试用例将不会生成,因为"ab"
和"bc"
替换重叠。
在对 s
执行所有替换操作后返回 结果字符串 。
子字符串 是字符串中连续的字符序列。
示例 1:
输入:s = "abcd", indices = [0,2], sources = ["a","cd"], targets = ["eee","ffff"] 输出:"eeebffff" 解释: "a" 从 s 中的索引 0 开始,所以它被替换为 "eee"。 "cd" 从 s 中的索引 2 开始,所以它被替换为 "ffff"。
示例 2:
输入:s = "abcd", indices = [0,2], sources = ["ab","ec"], targets = ["eee","ffff"] 输出:"eeecd" 解释: "ab" 从 s 中的索引 0 开始,所以它被替换为 "eee"。 "ec" 没有从原始的 S 中的索引 2 开始,所以它没有被替换。
提示:
1 <= s.length <= 1000
k == indices.length == sources.length == targets.length
1 <= k <= 100
0 <= indices[i] < s.length
1 <= sources[i].length, targets[i].length <= 50
s
仅由小写英文字母组成sources[i]
和targets[i]
仅由小写英文字母组成
2、解题思路
-
问题分析:
- 我们需要对字符串
s
执行多个替换操作。 - 每个替换操作由
indices[i]
,sources[i]
,targets[i]
定义。 - 替换操作必须同时发生,且不会相互影响。
- 我们需要对字符串
-
算法设计:
- 使用哈希表记录每个索引对应的替换操作。
- 遍历字符串
s
,对于每个字符,检查是否有替换操作需要执行。 - 如果有替换操作,则检查
sources[i]
是否匹配,如果匹配则替换为targets[i]
,否则保留原字符。
-
优化:
-
使用哈希表快速查找每个索引的替换操作。
-
在遍历字符串时,直接跳过已替换的部分。
-
3、代码实现
C++
class Solution {
public:string findReplaceString(string s, vector<int>& indices, vector<string>& sources, vector<string>& targets) {int n = s.size(), m = indices.size(); // 获取字符串和操作数组的长度unordered_map<int, vector<int>>ops; // 哈希表, 记录每个索引对应的替换操作for (int i = 0; i < m; ++i) {ops[indices[i]].push_back(i); // 将操作索引加入哈希表}string ret; // 存储结果字符串for (int i = 0; i < n;) {bool succeed = false; // 标记是否成功执行替换if (ops.count(i)) // 如果当前索引有替换操作{for (int pt : ops[i]) // 遍历所有替换操作{if (s.substr(i, sources[pt].size()) == sources[pt]) // 检查 sources[pt] 是否匹配{succeed = true; // 标记成功ret += targets[pt]; // 替换为 targets[pt]i += sources[pt].size(); // 跳过已替换的部分break;}}}if (!succeed) // 如果没有执行替换{ret += s[i]; // 保留原字符++i; // 移动到下一个字符}}return ret;}
};
Java
class Solution {public String findReplaceString(String s, int[] indices, String[] sources, String[] targets) {int n = s.length(), m = indices.length; // 获取字符串和操作数组的长度Map<Integer, List<Integer>> ops = new HashMap<>(); // 哈希表,记录每个索引对应的替换操作for (int i = 0; i < m; i++) {ops.computeIfAbsent(indices[i], k -> new ArrayList<>()).add(i); // 将操作索引加入哈希表}StringBuilder ans = new StringBuilder(); // 存储结果字符串for (int i = 0; i < n;) { // 遍历字符串boolean succeed = false; // 标记是否成功执行替换if (ops.containsKey(i)) { // 如果当前索引有替换操作for (int pt : ops.get(i)) { // 遍历所有替换操作if (s.startsWith(sources[pt], i)) { // 检查 sources[pt] 是否匹配succeed = true; // 标记成功ans.append(targets[pt]); // 替换为 targets[pt]i += sources[pt].length(); // 跳过已替换的部分break; // 跳出循环}}}if (!succeed) { // 如果没有执行替换ans.append(s.charAt(i)); // 保留原字符i++; // 移动到下一个字符}}return ans.toString(); // 返回结果字符串}
}
Python
class Solution:def findReplaceString(self, s: str, indices: List[int], sources: List[str], targets: List[str]) -> str:n, m = len(s), len(indices) # 获取字符串和操作数组的长度ops = {} # 哈希表,记录每个索引对应的替换操作for i in range(m):if indices[i] not in ops:ops[indices[i]] = []ops[indices[i]].append(i) # 将操作索引加入哈希表ans = [] # 存储结果字符串i = 0while i < n: # 遍历字符串succeed = False # 标记是否成功执行替换if i in ops: # 如果当前索引有替换操作for pt in ops[i]: # 遍历所有替换操作if s.startswith(sources[pt], i): # 检查 sources[pt] 是否匹配succeed = True # 标记成功ans.append(targets[pt]) # 替换为 targets[pt]i += len(sources[pt]) # 跳过已替换的部分break # 跳出循环if not succeed: # 如果没有执行替换ans.append(s[i]) # 保留原字符i += 1 # 移动到下一个字符return ''.join(ans) # 返回结果字符串
4、复杂度分析
-
时间复杂度:O(n+ml),其中 n 是字符串 s 的长度,m 是数组 indices 的长度,l 是数组 sources 和 targets 中字符串的平均长度。
-
空间复杂度:O(n+ml)。
Q3、[中等] 图像重叠
1、题目描述
给你两个图像 img1
和 img2
,两个图像的大小都是 n x n
,用大小相同的二进制正方形矩阵表示。二进制矩阵仅由若干 0
和若干 1
组成。
转换 其中一个图像,将所有的 1
向左,右,上,或下滑动任何数量的单位;然后把它放在另一个图像的上面。该转换的 重叠 是指两个图像 都 具有 1
的位置的数目。
请注意,转换 不包括 向任何方向旋转。越过矩阵边界的 1
都将被清除。
最大可能的重叠数量是多少?
示例 1:
输入:img1 = [[1,1,0],[0,1,0],[0,1,0]], img2 = [[0,0,0],[0,1,1],[0,0,1]] 输出:3 解释:将 img1 向右移动 1 个单位,再向下移动 1 个单位。两个图像都具有 1 的位置的数目是 3(用红色标识)。
示例 2:
输入:img1 = [[1]], img2 = [[1]] 输出:1
示例 3:
输入:img1 = [[0]], img2 = [[0]] 输出:0
提示:
n == img1.length == img1[i].length
n == img2.length == img2[i].length
1 <= n <= 30
img1[i][j]
为0
或1
img2[i][j]
为0
或1
2、解题思路
-
问题分析:
- 我们需要找到一种滑动方式,使得两个图像的重叠
1
的数量最大。 - 滑动操作可以看作是对一个图像进行平移,平移的范围是
[-n+1, n-1]
。
- 我们需要找到一种滑动方式,使得两个图像的重叠
-
算法设计:
- 使用一个二维数组
count
来记录所有可能的平移向量(dx, dy)
对应的重叠数量。 - 对于
img1
中的每个1
,计算它与img2
中的每个1
的平移向量(i-x, j-y)
,并在count
中累加对应的重叠数量。 - 最终,
count
中的最大值即为最大可能的重叠数量。
- 使用一个二维数组
-
优化:
- 使用
count
数组的大小为[2*n+1][2*n+1]
,以处理负的平移向量。
- 使用
3、代码实现
C++
class Solution {
public:int largestOverlap(vector<vector<int>>& img1, vector<vector<int>>& img2) {int n = img1.size(); // 获取矩阵的大小vector<vector<int>> count(2 * n + 1, vector<int>(2 * n + 1, 0)); // 记录平移向量的重叠数量for (int i = 0; i < n; ++i) // 遍历 img1 的每一行{for (int j = 0; j < n; ++j) // 遍历 img1 的每一列{if (img1[i][j] == 1) // 如果 img1[i][j] 是 1{for (int x = 0; x < n; ++x) // 遍历 img2 的每一行{for (int y = 0; y < n; ++y) // 遍历 img2 的每一列{if (img2[x][y] == 1) // 如果 img2[i][j] 是 1{// 计算平移向量 (i-x, j-y), 并累加对应的重叠数量count[i - x + n][j - y + n] += 1;}}}}}}int ret = 0; // 记录最大的重叠数for (const auto& row : count) { // 遍历 count 数组for (int v : row) {ret = max(ret, v); // 更新最大值}}return ret;}
};
Java
class Solution {public int largestOverlap(int[][] img1, int[][] img2) {int n = img1.length; // 获取矩阵的大小int[][] count = new int[2 * n + 1][2 * n + 1]; // 记录平移向量的重叠数量for (int i = 0; i < n; i++) { // 遍历 img1 的每一行for (int j = 0; j < n; j++) { // 遍历 img1 的每一列if (img1[i][j] == 1) { // 如果 img1[i][j] 是 1for (int x = 0; x < n; x++) { // 遍历 img2 的每一行for (int y = 0; y < n; y++) { // 遍历 img2 的每一列if (img2[x][y] == 1) { // 如果 img2[x][y] 是 1// 计算平移向量 (i-x, j-y), 并累加对应的重叠数量count[i - x + n][j - y + n] += 1;}}}}}}int ret = 0; // 记录最大的重叠数for (int[] row : count) { // 遍历 count 数组for (int v : row) {ret = Math.max(ret, v); // 更新最大值}}return ret; // 返回结果}
}
Python
class Solution:def largestOverlap(self, img1: List[List[int]], img2: List[List[int]]) -> int:n = len(img1) # 获取矩阵的大小count = [[0] * (2 * n + 1) for _ in range(2 * n + 1)] # 记录平移向量的重叠数量for i in range(n): # 遍历 img1 的每一行for j in range(n): # 遍历 img1 的每一列if img1[i][j] == 1: # 如果 img1[i][j] 是 1for x in range(n): # 遍历 img2 的每一行for y in range(n): # 遍历 img2 的每一列if img2[x][y] == 1: # 如果 img2[x][y] 是 1# 计算平移向量 (i-x, j-y), 并累加对应的重叠数量count[i - x + n][j - y + n] += 1ret = 0 # 记录最大的重叠数for row in count: # 遍历 count 数组for v in row:ret = max(ret, v) # 更新最大值return ret # 返回结果
4、复杂度分析
-
时间复杂度:
- 遍历
img1
和img2
的每个元素,时间复杂度为 O(N4),其中N
是矩阵的大小。
- 遍历
-
空间复杂度:
- 使用
count
数组记录平移向量的重叠数量,空间复杂度为 O(N2)。
- 使用
Q4、[困难] 树中距离之和
1、题目描述
给定一个无向、连通的树。树中有 n
个标记为 0...n-1
的节点以及 n-1
条边 。
给定整数 n
和数组 edges
, edges[i] = [ai, bi]
表示树中的节点 ai
和 bi
之间有一条边。
返回长度为 n
的数组 answer
,其中 answer[i]
是树中第 i
个节点与所有其他节点之间的距离之和。
示例 1:
输入: n = 6, edges = [[0,1],[0,2],[2,3],[2,4],[2,5]] 输出: [8,12,6,10,10,10] 解释: 树如图所示。 我们可以计算出 dist(0,1) + dist(0,2) + dist(0,3) + dist(0,4) + dist(0,5) 也就是 1 + 1 + 2 + 2 + 2 = 8。 因此,answer[0] = 8,以此类推。
示例 2:
输入: n = 1, edges = [] 输出: [0]
示例 3:
输入: n = 2, edges = [[1,0]] 输出: [1,1]
提示:
1 <= n <= 3 * 104
edges.length == n - 1
edges[i].length == 2
0 <= ai, bi < n
ai != bi
- 给定的输入保证为有效的树
2、解题思路
- 问题分析:
- 树是一个连通无向图,没有环。
- 我们需要计算每个节点到其他所有节点的距离之和。
- 算法设计:
- 使用 树形动态规划 的思想,通过两次深度优先搜索(DFS)来计算每个节点的距离和。
- 第一次 DFS:从根节点开始,计算每个节点的子树大小
sz
和子树中所有节点到当前节点的距离和dp
。 - 第二次 DFS:从根节点开始,利用第一次 DFS 的结果,计算每个节点到其他所有节点的距离和
ans
。
- 优化:
- 通过动态规划,避免重复计算,将时间复杂度优化到 O(n)。
3、代码实现
C++
class Solution {
private:vector<int> ret, sz, dp; // ret: 结果数组, sz: 子树大小, dp: 子树中所有节点到当前节点的距离和vector<vector<int>> graph; // 邻接表表示的树// 第一次 DFS: 计算子树大小 sz 和子树中所有节点到当前节点的距离和 dpvoid dfs1(int u, int f) {sz[u] = 1; // 当前节点的子树大小初始化为 1dp[u] = 0; // 当前节点的距离和初始化为 0// 遍历当前节点的所有邻居for (auto& v : graph[u]) {// 跳过父节点if (v == f) {continue;}dfs1(v, u); // 递归处理子节点dp[u] += dp[v] + sz[v]; // 更新当前节点的距离和sz[u] += sz[v]; // 更新当前节点的子树大小}}// 第二次 DFS: 计算每个节点到其他所有节点的距离和 ansvoid dfs2(int u, int f) {ret[u] = dp[u]; // 当前节点的距离和即为 dp[u]// 遍历当前节点的所有邻居for (auto& v : graph[u]) {// 跳过父节点if (v == f) {continue;}int pu = dp[u], pv = dp[v]; // 保存当前节点和子节点的 dp 值int su = sz[u], sv = sz[v]; // 保存当前节点和子节点的 sz 值// 将当前节点 u 从树中移除,重新计算 dp 和 szdp[u] -= dp[v] + sz[v];sz[u] -= sz[v];dp[v] += dp[u] + sz[u];sz[v] += sz[u];dfs2(v, u); // 递归处理子节点// 恢复 dp 和 sz 的值dp[u] = pu, dp[v] = pv;sz[u] = su, sz[v] = sv;}}public:vector<int> sumOfDistancesInTree(int n, vector<vector<int>>& edges) {ret.resize(n, 0); // 初始化结果数组sz.resize(n, 0); // 初始化字数大小数组dp.resize(n, 0); // 初始化距离和数组graph.resize(n, {}); // 初始化邻接表// 构建邻接表for (auto& edge : edges) {int u = edge[0], v = edge[1];graph[u].emplace_back(v);graph[v].emplace_back(u);}dfs1(0, -1); // 第一次 DFS, 以 0 为根节点dfs2(0, -1); // 第二次 DFS, 以 0 为根节点return ret;}
};
Java
class Solution {int[] ans, sz, dp; // ans: 结果数组, sz: 子树大小, dp: 子树中所有节点到当前节点的距离和List<List<Integer>> graph; // 邻接表表示的树// 第一次 DFS:计算子树大小 sz 和子树中所有节点到当前节点的距离和 dpprivate void dfs(int u, int f) {sz[u] = 1; // 当前节点的子树大小初始化为 1dp[u] = 0; // 当前节点的距离和初始化为 0for (int v : graph.get(u)) { // 遍历当前节点的所有邻居// 跳过父节点if (v == f) {continue;}dfs(v, u); // 递归处理子节点dp[u] += dp[v] + sz[v]; // 更新当前节点的距离和sz[u] += sz[v]; // 更新当前节点的子树大小}}// 第二次 DFS:计算每个节点到其他所有节点的距离和 ansprivate void dfs2(int u, int f) {ans[u] = dp[u]; // 当前节点的距离和即为 dp[u]for (int v : graph.get(u)) { // 遍历当前节点的所有邻居// 跳过父节点if (v == f) {continue;}int pu = dp[u], pv = dp[v]; // 保存当前节点和子节点的 dp 值int su = sz[u], sv = sz[v]; // 保存当前节点和子节点的 sz 值// 将当前节点 u 从树中移除,重新计算 dp 和 szdp[u] -= dp[v] + sz[v];sz[u] -= sz[v];dp[v] += dp[u] + sz[u];sz[v] += sz[u];dfs2(v, u); // 递归处理子节点// 恢复 dp 和 sz 的值dp[u] = pu;dp[v] = pv;sz[u] = su;sz[v] = sv;}}public int[] sumOfDistancesInTree(int n, int[][] edges) {ans = new int[n]; // 初始化结果数组sz = new int[n]; // 初始化子树大小数组dp = new int[n]; // 初始化距离和数组graph = new ArrayList<>(); // 初始化邻接表for (int i = 0; i < n; i++) {graph.add(new ArrayList<>());}for (int[] edge : edges) { // 构建邻接表int u = edge[0], v = edge[1];graph.get(u).add(v);graph.get(v).add(u);}dfs(0, -1); // 第一次 DFS,以 0 为根节点dfs2(0, -1); // 第二次 DFS,以 0 为根节点return ans; // 返回结果}
}
Python
class Solution:def sumOfDistancesInTree(self, n: int, edges: List[List[int]]) -> List[int]:self.ans = [0] * n # 结果数组self.sz = [0] * n # 子树大小数组self.dp = [0] * n # 子树中所有节点到当前节点的距离和self.graph = [[] for _ in range(n)] # 邻接表表示的树# 构建邻接表for u, v in edges:self.graph[u].append(v)self.graph[v].append(u)# 第一次 DFS:计算子树大小 sz 和子树中所有节点到当前节点的距离和 dpdef dfs(u, f):self.sz[u] = 1 # 当前节点的子树大小初始化为 1self.dp[u] = 0 # 当前节点的距离和初始化为 0for v in self.graph[u]: # 遍历当前节点的所有邻居if v == f: continue # 跳过父节点dfs(v, u) # 递归处理子节点self.dp[u] += self.dp[v] + self.sz[v] # 更新当前节点的距离和self.sz[u] += self.sz[v] # 更新当前节点的子树大小# 第二次 DFS:计算每个节点到其他所有节点的距离和 ansdef dfs2(u, f):self.ans[u] = self.dp[u] # 当前节点的距离和即为 dp[u]for v in self.graph[u]: # 遍历当前节点的所有邻居if v == f: continue # 跳过父节点pu, pv = self.dp[u], self.dp[v] # 保存当前节点和子节点的 dp 值su, sv = self.sz[u], self.sz[v] # 保存当前节点和子节点的 sz 值# 将当前节点 u 从树中移除,重新计算 dp 和 szself.dp[u] -= self.dp[v] + self.sz[v]self.sz[u] -= self.sz[v]self.dp[v] += self.dp[u] + self.sz[u]self.sz[v] += self.sz[u]dfs2(v, u) # 递归处理子节点# 恢复 dp 和 sz 的值self.dp[u], self.dp[v] = pu, pvself.sz[u], self.sz[v] = su, svdfs(0, -1) # 第一次 DFS,以 0 为根节点dfs2(0, -1) # 第二次 DFS,以 0 为根节点return self.ans # 返回结果
4、复杂度分析
- 时间复杂度:
- 两次 DFS 遍历所有节点和边,时间复杂度为 O(n)。
- 空间复杂度:
-
使用邻接表存储树,空间复杂度为 O(n)。
pu, pv = self.dp[u], self.dp[v] # 保存当前节点和子节点的 dp 值
su, sv = self.sz[u], self.sz[v] # 保存当前节点和子节点的 sz 值# 将当前节点 u 从树中移除,重新计算 dp 和 szself.dp[u] -= self.dp[v] + self.sz[v]self.sz[u] -= self.sz[v]self.dp[v] += self.dp[u] + self.sz[u]self.sz[v] += self.sz[u]dfs2(v, u) # 递归处理子节点# 恢复 dp 和 sz 的值self.dp[u], self.dp[v] = pu, pvself.sz[u], self.sz[v] = su, sv
dfs(0, -1) # 第一次 DFS,以 0 为根节点
dfs2(0, -1) # 第二次 DFS,以 0 为根节点
return self.ans # 返回结果
-
#### 4、复杂度分析1. **时间复杂度**:- 两次 DFS 遍历所有节点和边,时间复杂度为 O(n)。
2. **空间复杂度**:- 使用邻接表存储树,空间复杂度为 O(n)。<br>
<br>