|
| 1 | +# [0631. 设计 Excel 求和公式](https://leetcode.cn/problems/design-excel-sum-formula/) |
| 2 | + |
| 3 | +- 标签:图、设计、拓扑排序、数组、哈希表、字符串、矩阵 |
| 4 | +- 难度:困难 |
| 5 | + |
| 6 | +## 题目链接 |
| 7 | + |
| 8 | +- [0631. 设计 Excel 求和公式 - 力扣](https://leetcode.cn/problems/design-excel-sum-formula/) |
| 9 | + |
| 10 | +## 题目大意 |
| 11 | + |
| 12 | +**描述**: |
| 13 | + |
| 14 | +请你设计 Excel 中的基本功能,并实现求和公式。 |
| 15 | + |
| 16 | +**要求**: |
| 17 | + |
| 18 | +实现 Excel 类: |
| 19 | + |
| 20 | +- `Excel(int height, char width)`:用高度 $height$ 和宽度 $width$ 初始化对象。该表格是一个大小为 $height \times width$ 的整数矩阵 $mat$,其中行下标范围是 $[1, height]$,列下标范围是 $['A', width]$。初始情况下,所有的值都应该为零。 |
| 21 | +- `void set(int row, char column, int val)`:将 $mat[row][column]$ 的值更改为 $val$。 |
| 22 | +- `int get(int row, char column)`:返回 $mat[row][column]$ 的值。 |
| 23 | +- `int sum(int row, char column, List<String> numbers)`:将 $mat[row][column]$ 的值设为由 $numbers$ 表示的单元格的和,并返回 $mat[row][column]$ 的值。此求和公式应该 **长期作用于** 该单元格,直到该单元格被另一个值或另一个求和公式覆盖。其中,$numbers[i]$ 的格式可以为: |
| 24 | + - `"ColRow"`:表示某个单元格。例如,`"F7"` 表示单元格 $mat[7]['F']$。 |
| 25 | + - `"ColRow1:ColRow2"`:表示一组单元格。该范围将始终为一个矩形,其中 `"ColRow1"` 表示左上角单元格的位置,`"ColRow2"` 表示右下角单元格的位置。例如,`"B3:F7"` 表示 $3 \le i \le 7$ 和 $'B' \le j \le 'F'$ 的单元格 $mat[i][j]$。 |
| 26 | + |
| 27 | +**说明**: |
| 28 | + |
| 29 | +- 注意:可以假设不会出现循环求和引用。例如,$mat[1]['A'] == sum(1, "B")$,且 $mat[1]['B'] == sum(1, "A")$。 |
| 30 | +- $1 \le height \le 26$。 |
| 31 | +- $'A' \le width \le 'Z'$。 |
| 32 | +- $1 \le row \le height$。 |
| 33 | +- $'A' \le column \le width$。 |
| 34 | +- $-10^3 \le val \le 10^3$。 |
| 35 | +- $1 \le numbers.length \le 5$。 |
| 36 | +- $numbers[i]$ 的格式为 `"ColRow"` 或 `"ColRow1:ColRow2"`。 |
| 37 | +- 最多会对 `set`、`get` 和 `sum` 进行 $10^3$ 次调用。 |
| 38 | + |
| 39 | +**示例**: |
| 40 | + |
| 41 | +- 示例 1: |
| 42 | + |
| 43 | +```python |
| 44 | +输入: |
| 45 | +["Excel", "set", "sum", "set", "get"] |
| 46 | +[[3, "C"], [1, "A", 2], [3, "C", ["A1", "A1:B2"]], [2, "B", 2], [3, "C"]] |
| 47 | +输出: |
| 48 | +[null, null, 4, null, 6] |
| 49 | + |
| 50 | +解释: |
| 51 | +执行以下操作: |
| 52 | +Excel excel = new Excel(3, "C"); |
| 53 | + // 构造一个 3 * 3 的二维数组,所有值初始化为零。 |
| 54 | + // A B C |
| 55 | + // 1 0 0 0 |
| 56 | + // 2 0 0 0 |
| 57 | + // 3 0 0 0 |
| 58 | +excel.set(1, "A", 2); |
| 59 | + // 将 mat[1]["A"] 设置为 2 。 |
| 60 | + // A B C |
| 61 | + // 1 2 0 0 |
| 62 | + // 2 0 0 0 |
| 63 | + // 3 0 0 0 |
| 64 | +excel.sum(3, "C", ["A1", "A1:B2"]); // 返回 4 |
| 65 | + // 将 mat[3]["C"] 设置为 mat[1]["A"] 的值与矩形范围的单元格和的和,该范围的左上角单元格位置为 mat[1]["A"],右下角单元格位置为 mat[2]["B"]。 |
| 66 | + // A B C |
| 67 | + // 1 2 0 0 |
| 68 | + // 2 0 0 0 |
| 69 | + // 3 0 0 4 |
| 70 | +excel.set(2, "B", 2); |
| 71 | + // 将 mat[2]["B"] 设置为 2 。注意 mat[3]["C"] 也应该更改。 |
| 72 | + // A B C |
| 73 | + // 1 2 0 0 |
| 74 | + // 2 0 2 0 |
| 75 | + // 3 0 0 6 |
| 76 | +excel.get(3, "C"); // 返回 6 |
| 77 | +``` |
| 78 | + |
| 79 | +## 解题思路 |
| 80 | + |
| 81 | +### 思路 1:哈希表 + 图 |
| 82 | + |
| 83 | +这道题目要求设计一个 Excel 表格,支持设置单元格值、获取单元格值和设置求和公式。关键在于求和公式需要 **长期作用**,即当依赖的单元格值改变时,公式单元格的值也要自动更新。 |
| 84 | + |
| 85 | +**核心思路**: |
| 86 | + |
| 87 | +- 使用哈希表存储每个单元格的值。 |
| 88 | +- 使用图结构记录单元格之间的依赖关系:如果单元格 $A$ 的值依赖于单元格 $B$,则 $B \to A$ 有一条边。 |
| 89 | +- 当某个单元格的值改变时,需要递归更新所有依赖它的单元格。 |
| 90 | + |
| 91 | +**算法步骤**: |
| 92 | + |
| 93 | +1. **初始化**:创建哈希表存储单元格值,创建图存储依赖关系。 |
| 94 | +2. **set 操作**:设置单元格值,清除该单元格的依赖关系,递归更新依赖它的单元格。 |
| 95 | +3. **get 操作**:直接返回单元格的值。 |
| 96 | +4. **sum 操作**:解析 $numbers$,计算和,设置单元格值,建立依赖关系。 |
| 97 | + |
| 98 | +### 思路 1:代码 |
| 99 | + |
| 100 | +```python |
| 101 | +class Excel: |
| 102 | + |
| 103 | + def __init__(self, height: int, width: str): |
| 104 | + self.height = height |
| 105 | + self.width = ord(width) - ord('A') + 1 |
| 106 | + # formulas[r][c] = (cells_dict, val) |
| 107 | + # cells_dict: 依赖的单元格及其计数,val: 当前值 |
| 108 | + self.formulas = [[None] * self.width for _ in range(height)] |
| 109 | + |
| 110 | + def get(self, row: int, column: str) -> int: |
| 111 | + r, c = row - 1, ord(column) - ord('A') |
| 112 | + if self.formulas[r][c] is None: |
| 113 | + return 0 |
| 114 | + return self.formulas[r][c][1] |
| 115 | + |
| 116 | + def set(self, row: int, column: str, val: int) -> None: |
| 117 | + r, c = row - 1, ord(column) - ord('A') |
| 118 | + # 设置为纯值,清空依赖 |
| 119 | + self.formulas[r][c] = ({}, val) |
| 120 | + # 拓扑排序更新依赖此单元格的所有单元格 |
| 121 | + stack = [] |
| 122 | + self._topological_sort(r, c, stack, set()) |
| 123 | + self._execute_stack(stack) |
| 124 | + |
| 125 | + def sum(self, row: int, column: str, numbers: List[str]) -> int: |
| 126 | + r, c = row - 1, ord(column) - ord('A') |
| 127 | + cells = self._convert(numbers) |
| 128 | + summ = self._calculate_sum(r, c, cells) |
| 129 | + self.formulas[r][c] = (cells, summ) |
| 130 | + # 拓扑排序更新依赖此单元格的所有单元格 |
| 131 | + stack = [] |
| 132 | + self._topological_sort(r, c, stack, set()) |
| 133 | + self._execute_stack(stack) |
| 134 | + return summ |
| 135 | + |
| 136 | + def _convert(self, strs: List[str]) -> dict: |
| 137 | + """将公式字符串转换为单元格计数字典""" |
| 138 | + res = {} |
| 139 | + for st in strs: |
| 140 | + if ':' not in st: |
| 141 | + res[st] = res.get(st, 0) + 1 |
| 142 | + else: |
| 143 | + parts = st.split(':') |
| 144 | + si, ei = int(parts[0][1:]), int(parts[1][1:]) |
| 145 | + sj, ej = parts[0][0], parts[1][0] |
| 146 | + for i in range(si, ei + 1): |
| 147 | + for j in range(ord(sj), ord(ej) + 1): |
| 148 | + key = chr(j) + str(i) |
| 149 | + res[key] = res.get(key, 0) + 1 |
| 150 | + return res |
| 151 | + |
| 152 | + def _topological_sort(self, r: int, c: int, stack: list, visited: set) -> None: |
| 153 | + """拓扑排序:找出所有依赖 (r,c) 的单元格""" |
| 154 | + key = chr(ord('A') + c) + str(r + 1) |
| 155 | + for i in range(len(self.formulas)): |
| 156 | + for j in range(len(self.formulas[0])): |
| 157 | + if self.formulas[i][j] is not None and key in self.formulas[i][j][0]: |
| 158 | + if (i, j) not in visited: |
| 159 | + self._topological_sort(i, j, stack, visited) |
| 160 | + if (r, c) not in visited: |
| 161 | + visited.add((r, c)) |
| 162 | + stack.append((r, c)) |
| 163 | + |
| 164 | + def _execute_stack(self, stack: list) -> None: |
| 165 | + """按拓扑顺序更新单元格""" |
| 166 | + while stack: |
| 167 | + r, c = stack.pop() |
| 168 | + if self.formulas[r][c] is not None and self.formulas[r][c][0]: |
| 169 | + self._calculate_sum(r, c, self.formulas[r][c][0]) |
| 170 | + |
| 171 | + def _calculate_sum(self, r: int, c: int, cells: dict) -> int: |
| 172 | + """计算单元格的和""" |
| 173 | + total = 0 |
| 174 | + for s, cnt in cells.items(): |
| 175 | + x, y = int(s[1:]) - 1, ord(s[0]) - ord('A') |
| 176 | + val = self.formulas[x][y][1] if self.formulas[x][y] else 0 |
| 177 | + total += val * cnt |
| 178 | + self.formulas[r][c] = (cells, total) |
| 179 | + return total |
| 180 | + |
| 181 | + |
| 182 | +# Your Excel object will be instantiated and called as such: |
| 183 | +# obj = Excel(height, width) |
| 184 | +# obj.set(row,column,val) |
| 185 | +# param_2 = obj.get(row,column) |
| 186 | +# param_3 = obj.sum(row,column,numbers) |
| 187 | +``` |
| 188 | + |
| 189 | +### 思路 1:复杂度分析 |
| 190 | + |
| 191 | +- **时间复杂度**: |
| 192 | + - $set$ 操作:$O(d)$,其中 $d$ 是依赖此单元格的单元格数量,需要更新所有依赖的单元格。 |
| 193 | + - $get$ 操作:$O(1)$,直接返回缓存的值。 |
| 194 | + - $sum$ 操作:$O(k + d)$,其中 $k$ 是公式中依赖的单元格数量,$d$ 是依赖此单元格的单元格数量。需要计算所有依赖的单元格,并更新依赖此单元格的其他单元格。 |
| 195 | +- **空间复杂度**:$O(n + m)$,其中 $n$ 是单元格的数量,$m$ 是依赖关系的数量。 |
0 commit comments