学习数据结构与算法的关键在于掌握问题背后的算法思维框架,你的思考越抽象,它能覆盖的问题域就越广,理解难度也更复杂。在这个专栏里,小彭与你分享每场 LeetCode 周赛的解题报告,一起体会上分之旅。
本文是 LeetCode 上分之旅系列的第 37 篇文章,往期回顾请移步到文章末尾~
T1. 故障键盘(Easy)
T2. 判断是否能拆分数组(Medium)
T3. 找出最安全路径(Medium)
T4. 子序列最大优雅度(Hard)
https://leetcode.cn/problems/faulty-keyboard/
简单模拟题。
i
字符时对已填入字符进行反转,时间复杂度是 O(n^2);i
时修改标记位和写入方向,在最后输出时根据标记位输出,避免中间的反转操作。class Solution { public: string finalString(string s) { vector<char> dst; for (auto& c : s) { if (c == 'i') { reverse(dst.begin(), dst.end()); } else { dst.push_back(c); } } return string(dst.begin(), dst.end()); } };
class Solution { public: string finalString(string s) { deque<char> dst; bool rear = true; for (auto& c : s) { if (c == 'i') { rear = !rear; } else { if (rear) { dst.push_back(c); } else { dst.push_front(c); } } } return rear ? string(dst.begin(), dst.end()) : string(dst.rbegin(), dst.rend()); } };
复杂度分析:
https://leetcode.cn/problems/check-if-it-is-possible-to-split-array/
思维题,主要题目的两个条件只要满足其中一个即可 😭
结合两个条件,如果我们能找到两个相邻的元素之和大于等于 m,那么总可以通过消除 1 个元素的方式完成题目要求。
例如在示例 3 [2, 3, 3, 2, 3] 中,我们以 [3,3] 为起点倒推:
class Solution { public: bool canSplitArray(vector<int>& nums, int m) { // 2 | 3, 3 | 2 | 3 // 1, 3, 2, 2, 3 // 1, 1, 1, 3, 3 if (nums.size() <= 2) return true; for (int i = 1; i < nums.size(); i++) { if (nums[i] + nums[i - 1] >= m) return true; } return false; } };
复杂度分析:
https://leetcode.cn/problems/find-the-safest-path-in-a-grid/
根据题目描述,每个节点的安全系数定位为该节点到「小偷」节点的最小曼哈顿距离,而题目要求是寻找从 [0][0] 到 [n-1][n-1] 的最大安全系数。「使得最小曼哈顿距离最大」暗示可能是需要使用二分答案的极大化极小问题。
class Solution { fun maximumSafenessFactor(grid: List<List<Int>>): Int { val INF = Integer.MAX_VALUE val directions = arrayOf(intArrayOf(0,1), intArrayOf(1,0), intArrayOf(0,-1), intArrayOf(-1,0)) val n = grid.size // 特判 if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return 0 // 多源 BFS(拓扑排序) val safe = Array(n) { IntArray(n) { -1 }} var queue = LinkedList<IntArray>() for (r in 0 until n) { for (c in 0 until n) { if (grid[r][c] == 1) { queue.offer(intArrayOf(r, c)) safe[r][c] = 0 } } } while (!queue.isEmpty()) { val temp = LinkedList<IntArray>() for (node in queue) { for (direction in directions) { val newX = node[0] + direction[0] val newY = node[1] + direction[1] if (newX < 0 || newX >= n || newY < 0 || newY >= n || safe[newX][newY] != -1) continue temp.offer(intArrayOf(newX, newY)) safe[newX][newY] = safe[node[0]][node[1]] + 1 } } queue = temp } // for (row in safe) println(row.joinToString()) // BFS(检查只通过大于等于 limit 的格子,能否到达终点) fun check(limit: Int) : Boolean { val visit = Array(n) { BooleanArray(n) } var queue = LinkedList<IntArray>() queue.offer(intArrayOf(0, 0)) visit[0][0] = true while (!queue.isEmpty()) { val temp = LinkedList<IntArray>() for (node in queue) { // 终止条件 if (node[0] == n - 1 && node[1] == n - 1) return true for (direction in directions) { val newX = node[0] + direction[0] val newY = node[1] + direction[1] if (newX < 0 || newX >= n || newY < 0 || newY >= n || visit[newX][newY] || safe[newX][newY] < limit) continue temp.offer(intArrayOf(newX, newY)) visit[newX][newY] = true } } queue = temp } return false } // 二分查找 var left = 0 var right = Math.min(safe[0][0], safe[n - 1][n - 1]) while (left < right) { val mid = (left + right + 1) ushr 1 if (!check(mid)) { right = mid - 1 } else { left = mid } } return left } }
复杂度分析:
思路参考雪景式的题解。
在题解一预处理的基础上,同样走一次 BFS 也能够算出最大安全系数,思路类似于 Dijkstra 最最短路算法中使用当前最短路最短的节点去松弛相邻边,我们优先让当前曼哈顿距离最大的节点去松弛相邻节点,以保证每个节点都能够从较大的路径转移过来。
class Solution { fun maximumSafenessFactor(grid: List<List<Int>>): Int { ... // 类最短路(使用曼哈顿距离最大的节点去松弛相邻边) val heap = PriorityQueue<IntArray>() { e1, e2 -> e2[0] - e1[0] } heap.offer(intArrayOf(safe[0][0], 0, 0)) val visit = Array(n) { BooleanArray(n) } visit[0][0] = true while (!heap.isEmpty()) { val node = heap.poll() if (node[1] == n - 1 && node[2] == n - 1) return node[0] for (direction in directions) { val newX = node[1] + direction[0] val newY = node[2] + direction[1] if (newX < 0 || newX >= n || newY < 0 || newY >= n || visit[newX][newY]) continue // 松弛相邻边 heap.offer(intArrayOf(Math.min(node[0], safe[newX][newY]), newX, newY)) visit[newX][newY] = true } } return 0 } }
复杂度分析:
思路参考灵神的题解。
其实,求从 [0][0] 到 [n - 1][n - 1] 的最大安全系数,也相当于连通性问题的变形,而连通性问题有并查集的解法。为了求得最大安全系数,我们使用分层并查集:
class Solution { fun maximumSafenessFactor(grid: List<List<Int>>): Int { val directions = arrayOf(intArrayOf(0,1), intArrayOf(1,0), intArrayOf(0,-1), intArrayOf(-1,0)) val n = grid.size // 特判 if (grid[0][0] == 1 || grid[n - 1][n - 1] == 1) return 0 // 多源 BFS(拓扑排序) val safe = Array(n) { IntArray(n) { -1 }} // 分层 val groups = LinkedList<LinkedList<IntArray>>() var queue = LinkedList<IntArray>() for (r in 0 until n) { for (c in 0 until n) { if (grid[r][c] == 1) { queue.offer(intArrayOf(r, c)) safe[r][c] = 0 } } } groups.add(queue) while (!queue.isEmpty()) { val temp = LinkedList<IntArray>() for (node in queue) { for (direction in directions) { val newX = node[0] + direction[0] val newY = node[1] + direction[1] if (newX < 0 || newX >= n || newY < 0 || newY >= n || safe[newX][newY] != -1) continue temp.offer(intArrayOf(newX, newY)) safe[newX][newY] = safe[node[0]][node[1]] + 1 } } queue = temp if (!queue.isEmpty()) groups.add(queue) } // for (row in safe) println(row.joinToString()) // for (row in groups) println(row.joinToString()) val helper = UnionFind(n) // 逆序合并 for (i in groups.size - 1 downTo 0) { for (node in groups[i]) { val x = node[0] val y = node[1] for (direction in directions) { val newX = x + direction[0] val newY = y + direction[1] // 合并曼哈顿距离大于等于当前层的节点 if (newX < 0 || newX >= n || newY < 0 || newY >= n || safe[newX][newY] < i) continue helper.union(x * n + y, newX * n + newY) } } if (helper.find(0) == helper.find(n * n - 1)) return i } return 0 } class UnionFind(private val n: Int) { private val parents = IntArray(n * n) { it } private val ranks = IntArray(n * n) fun find(x: Int): Int { var cur = x while (cur != parents[cur]) { parents[cur] = parents[parents[cur]] cur = parents[cur] } return cur } fun union(x: Int, y: Int) { val rootX = find(x) val rootY = find(y) if (ranks[rootX] < ranks[rootY]) { parents[rootX] = rootY } else if (ranks[rootX] > ranks[rootY]){ parents[rootY] = rootX } else { parents[rootY] = rootX ranks[rootX]++ } } } }
复杂度分析:
https://leetcode.cn/problems/maximum-elegance-of-a-k-length-subsequence/
class Solution { fun findMaximumElegance(items: Array<IntArray>, k: Int): Long { Arrays.sort(items) { e1, e2 -> e2[0] - e1[0] } var ret = 0L var totalProfit = 0L // duplicate:小顶堆 val duplicate = LinkedList<Int>() // categorySet:类目表 val categorySet = HashSet<Int>() for ((i, item) in items.withIndex()) { val profit = item[0] val category = item[1] if (i < k) { totalProfit += item[0] // 记录重复节点 if (categorySet.contains(category)) { duplicate.add(profit) } categorySet.add(category) } else if (!duplicate.isEmpty() && !categorySet.contains(category)){ // 替换 totalProfit += profit - duplicate.pollLast() categorySet.add(category) } else { // 不会让类目数量变大 } // println(duplicate.joinToString()) ret = Math.max(ret, totalProfit + 1L * categorySet.size * categorySet.size) } return ret } }
复杂度分析: