Skip to content

Commit e4f837c

Browse files
committed
feat: leetcode 587
1 parent 80da20f commit e4f837c

File tree

8 files changed

+455
-1
lines changed

8 files changed

+455
-1
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<p align="center">
44
<!-- TOPICS COUNT START -->
5-
<img src="https://img.shields.io/badge/-进度:329-green" alt="进度:329">
5+
<img src="https://img.shields.io/badge/-进度:330-green" alt="进度:330">
66
<!-- TOPICS COUNT END -->
77
<a href="./assets/docs/TOPICS.md"><img src="https://img.shields.io/badge/-题库目录-blue" alt="题库目录"></a>
88
<a href="./assets/docs/CATEGORIES.md"><img src="https://img.shields.io/badge/-题库分类-red" alt="题库分类"></a>

assets/data/categories.json

+6
Original file line numberDiff line numberDiff line change
@@ -1583,6 +1583,12 @@
15831583
"path": "./problemset/find-the-closest-palindrome/README.md",
15841584
"difficulty": "困难"
15851585
},
1586+
{
1587+
"id": "587",
1588+
"title": "安装栅栏",
1589+
"path": "./problemset/erect-the-fence/README.md",
1590+
"difficulty": "困难"
1591+
},
15861592
{
15871593
"id": "661",
15881594
"title": "图片平滑器",

assets/data/topics.json

+10
Original file line numberDiff line numberDiff line change
@@ -2669,6 +2669,16 @@
26692669
"url": "https://leetcode-cn.com/problems/find-the-closest-palindrome/",
26702670
"path": "./problemset/find-the-closest-palindrome/README.md"
26712671
},
2672+
{
2673+
"id": "587",
2674+
"title": {
2675+
"cn": "安装栅栏",
2676+
"en": "erect-the-fence"
2677+
},
2678+
"difficulty": "困难",
2679+
"url": "https://leetcode-cn.com/problems/erect-the-fence/",
2680+
"path": "./problemset/erect-the-fence/README.md"
2681+
},
26722682
{
26732683
"id": "589",
26742684
"title": {

assets/docs/CATEGORIES.md

+1
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@
305305
| 521. [最长特殊序列](../../problemset/longest-uncommon-subsequence/README.md) | 简单 |
306306
| 537. [复数乘法](../../problemset/complex-number-multiplication/README.md) | 中等 |
307307
| 564. [寻找最近的回文数](../../problemset/find-the-closest-palindrome/README.md) | 困难 |
308+
| 587. [安装栅栏](../../problemset/erect-the-fence/README.md) | 困难 |
308309
| 661. [图片平滑器](../../problemset/image-smoother/README.md) | 简单 |
309310
| 682. [棒球比赛](../../problemset/baseball-game/README.md) | 简单 |
310311
| 709. [转换成小写字母](../../problemset/to-lower-case/README.md) | 简单 |

assets/docs/TOPICS.md

+2
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,8 @@
534534

535535
[564. 寻找最近的回文数](../../problemset/find-the-closest-palindrome/README.md)
536536

537+
[587. 安装栅栏](../../problemset/erect-the-fence/README.md)
538+
537539
[589. N 叉树的前序遍历](../../problemset/n-ary-tree-preorder-traversal/README.md)
538540

539541
[590. N 叉树的后序遍历](../../problemset/n-ary-tree-postorder-traversal/README.md)

problemset/erect-the-fence/README.md

+225
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
# 安装栅栏
2+
3+
> 难度:困难
4+
>
5+
> https://leetcode-cn.com/problems/erect-the-fence/
6+
7+
## 题目
8+
9+
在一个二维的花园中,有一些用 `(x, y)` 坐标表示的树。由于安装费用十分昂贵,你的任务是先用**最短**的绳子围起所有的树。只有当所有的树都被绳子包围时,花园才能围好栅栏。你需要找到正好位于栅栏边界上的树的坐标。
10+
11+
12+
示例 1:
13+
14+
```
15+
输入: [[1,1],[2,2],[2,0],[2,4],[3,3],[4,2]]
16+
输出: [[1,1],[2,0],[4,2],[3,3],[2,4]]
17+
解释:
18+
```
19+
![erect_the_fence_1](https://user-images.githubusercontent.com/54696834/164755705-16dd2c5e-973e-4d5b-b7d3-3575f5319b6c.png)
20+
21+
示例 2:
22+
23+
```
24+
输入: [[1,2],[2,2],[4,2]]
25+
输出: [[1,2],[2,2],[4,2]]
26+
解释:
27+
```
28+
![erect_the_fence_2](https://user-images.githubusercontent.com/54696834/164755708-020b6c10-df7d-4ece-be29-0336da304a5f.png)
29+
```
30+
即使树都在一条直线上,你也需要先用绳子包围它们。
31+
```
32+
33+
## 解题
34+
35+
### Jarvis 算法
36+
37+
```ts
38+
/**
39+
* Jarvis 算法
40+
* @desc 时间复杂度 O(N²) 空间复杂度 O(N)
41+
* @param trees
42+
* @returns
43+
*/
44+
export function outerTrees(trees: number[][]): number[][] {
45+
const len = trees.length
46+
if (len < 4)
47+
return trees
48+
49+
let leftMost = 0
50+
for (let i = 0; i < len; i++) {
51+
if (trees[i][0] < trees[leftMost][0])
52+
leftMost = i
53+
}
54+
55+
const res: number[][] = []
56+
const cross = (p: number[], q: number[], r: number[]) => (q[0] - p[0]) * (r[1] - q[1]) - (q[1] - p[1]) * (r[0] - q[0])
57+
58+
const visit = new Array(len).fill(false)
59+
let p = leftMost
60+
do {
61+
let q = (p + 1) % len
62+
for (let r = 0; r < len; r++) {
63+
// 如果 r 在 pq 右侧,则 q = r
64+
if (cross(trees[p], trees[q], trees[r]) < 0)
65+
q = r
66+
}
67+
68+
// 是否存在点 i,使得 p、q、i 在同一条直线上
69+
for (let i = 0; i < len; i++) {
70+
if (visit[i] || i === p || i === q)
71+
continue
72+
73+
if (cross(trees[p], trees[q], trees[i]) === 0) {
74+
res.push(trees[i])
75+
visit[i] = true
76+
}
77+
}
78+
79+
if (!visit[q]) {
80+
res.push(trees[q])
81+
visit[q] = true
82+
}
83+
84+
p = q
85+
} while (p !== leftMost)
86+
87+
return res
88+
}
89+
```
90+
91+
### Graham 算法
92+
93+
```ts
94+
/**
95+
* Graham 算法
96+
* @desc 时间复杂度 O(NlogN) 空间复杂度 O(N)
97+
* @param trees
98+
* @returns
99+
*/
100+
export function outerTrees2(trees: number[][]): number[][] {
101+
const len = trees.length
102+
if (len < 4)
103+
return trees
104+
105+
let bottom = 0
106+
// 找到 y 最小的点 bottom
107+
for (let i = 0; i < len; i++) {
108+
if (trees[i][1] < trees[bottom][i])
109+
bottom = i
110+
}
111+
112+
const swap = (trees: number[][], i: number, j: number) => {
113+
[
114+
trees[i][0],
115+
trees[i][1],
116+
trees[j][0],
117+
trees[j][1],
118+
]
119+
= [
120+
trees[j][0],
121+
trees[j][1],
122+
trees[i][0],
123+
trees[i][1],
124+
]
125+
return trees
126+
}
127+
128+
const cross = (p: number[], q: number[], r: number[]) => (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1])
129+
130+
const distance = (p: number[], q: number[]) => (p[0] - q[0]) * (p[0] - q[0]) + (p[1] - q[1]) * (p[1] - q[1])
131+
132+
trees = swap(trees, bottom, 0)
133+
134+
// 以 bottom 原点,按照极坐标的角度大小进行排序
135+
trees.sort((a, b) => {
136+
const diff = cross(trees[0], a, b) - cross(trees[0], b, a)
137+
return diff === 0 ? distance(trees[0], a) - distance(trees[0], b) : diff > 0 ? 1 : -1
138+
})
139+
140+
// 对于凸包最后且在同一条直线的元素按照距离从小到大进行排序
141+
let r = len - 1
142+
while (r > 0 && cross(trees[0], trees[len - 1], trees[r]) === 0)
143+
r--
144+
145+
for (let l = r + 1, h = len - 1; l < h; l++, h--)
146+
trees = swap(trees, l, h)
147+
148+
const stack = [trees[0], trees[1]]
149+
for (let i = 2; i < len; i++) {
150+
let top = stack.pop()!
151+
// 如果当前元素与栈顶的两个元素构成的向量顺时针旋转,则弹出栈顶元素
152+
while (cross(stack[stack.length - 1], top, trees[i]) > 0)
153+
top = stack.pop()!
154+
155+
stack.push(top)
156+
stack.push(trees[i])
157+
}
158+
return stack
159+
}
160+
```
161+
162+
### Andrew 算法
163+
164+
```ts
165+
/**
166+
* Andrew 算法
167+
* @desc 时间复杂度 O(NlogN) 空间复杂度 O(N)
168+
* @param trees
169+
* @returns
170+
*/
171+
export function outerTrees3(trees: number[][]): number[][] {
172+
const len = trees.length
173+
if (len < 4)
174+
return trees
175+
176+
// 按照 x 大小进行排序,如果 x 相同,则按照 y 的大小进行排序
177+
trees.sort((a, b) => {
178+
if (a[0] === b[0])
179+
return a[1] - b[1]
180+
181+
return a[0] - b[0]
182+
})
183+
184+
const hull = []
185+
const used = new Array(len).fill(false)
186+
187+
// hull[0] 需要入栈两次,不进行标记
188+
hull.push(0)
189+
190+
const cross = (p: number[], q: number[], r: number[]) => (q[0] - p[0]) * (r[1] - q[1]) - (q[1] - p[1]) * (r[0] - q[0])
191+
192+
// 求出凸包的下半部分
193+
for (let i = 1; i < len; i++) {
194+
while (hull.length > 1 && cross(trees[hull[hull.length - 2]], trees[hull[hull.length - 1]], trees[i]) < 0) {
195+
used[hull[hull.length - 1]] = false
196+
hull.pop()
197+
}
198+
used[i] = true
199+
hull.push(i)
200+
}
201+
202+
const m = hull.length
203+
// 求出凸包的上半部分
204+
for (let i = len - 2; i >= 0; i--) {
205+
if (!used[i]) {
206+
while (hull.length > m && cross(trees[hull[hull.length - 2]], trees[hull[hull.length - 1]], trees[i]) < 0) {
207+
used[hull[hull.length - 1]] = false
208+
hull.pop()
209+
}
210+
used[i] = true
211+
hull.push(i)
212+
}
213+
}
214+
215+
// hull[0] 同时参与凸包的上半部分检测,需要去掉重复的 hull[0]
216+
hull.pop()
217+
218+
const size = hull.length
219+
const res: number[][] = []
220+
for (let i = 0; i < size; i++)
221+
res[i] = trees[hull[i]]
222+
223+
return res
224+
}
225+
```
+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { outerTrees, outerTrees2, outerTrees3 } from '.'
3+
4+
describe('安装栅栏', () => {
5+
describe('Jarvis 算法', () => {
6+
testCase(outerTrees)
7+
})
8+
9+
describe('Graham 算法', () => {
10+
testCase(outerTrees2)
11+
})
12+
13+
describe('Andrew 算法', () => {
14+
testCase(outerTrees3)
15+
})
16+
})
17+
18+
function testCase(fn: (trees: number[][]) => number[][]) {
19+
it.each([
20+
[
21+
[[1, 1], [2, 2], [2, 0], [2, 4], [3, 3], [4, 2]],
22+
[[1, 1], [2, 0], [4, 2], [3, 3], [2, 4]],
23+
],
24+
[
25+
[[1, 2], [2, 2], [4, 2]],
26+
[[1, 2], [2, 2], [4, 2]],
27+
],
28+
])('示例%#', (trees, expected) => {
29+
expect(fn(trees).sort()).toStrictEqual(expected.sort())
30+
})
31+
}

0 commit comments

Comments
 (0)