Skip to content

Commit 0b2ef08

Browse files
committed
add week06 solutions
1 parent 74db2c7 commit 0b2ef08

File tree

5 files changed

+300
-0
lines changed

5 files changed

+300
-0
lines changed
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Intuition
2+
높이가 양 끝 라인에 의해 결정된다는 것을 응용하여 투포인터 해결법을 고안했다.
3+
4+
# Approach
5+
1. 높이(`h`)를 0 ~ 최대까지 순회한다.
6+
2. 높이를 초과하는 선까지 양 끝(`l`, `r`)을 조절한다.
7+
3. 조절한 양 끝을 최댓값 갱신에 이용한다.
8+
9+
# Complexity
10+
- Time complexity: $O(n)$
11+
- 배열의 크기 `n`에 대하여, 반복문이 배열의 인덱스를 모두 순회하면 종료되기에 (`l < r`이 깨질 때) 이에 따라 시간복잡도가 결정된다.
12+
- Space complexity: $O(n), inline$
13+
- 배열의 크기 `n`에 대하여, 입력값으로 배열을 받으니 inline인 n이 소요된다.
14+
15+
# Code
16+
## Two Pointer
17+
```go
18+
func maxArea(height []int) int {
19+
l, r := 0, len(height)-1
20+
var maxArea int
21+
for l < r {
22+
minH := min(height[l], height[r])
23+
maxArea = max(minH*(r-l), maxArea)
24+
25+
if minH == height[l] {
26+
l++
27+
} else {
28+
r--
29+
}
30+
}
31+
32+
return maxArea
33+
}
34+
35+
```
36+
: 원래는 deque를 이용해 해결했는데, 솔루션의 투포인터를 이용한 것과 동일한 방식이어서 깔끔한 후자로 수정했다.
37+
38+
## Deque
39+
```go
40+
type Deque struct {
41+
Nodes []int
42+
}
43+
44+
func NewDeque(arr []int) Deque {
45+
return Deque{Nodes: arr}
46+
}
47+
48+
func (dq *Deque) PushFront(node int) {
49+
dq.Nodes = append([]int{node}, dq.Nodes...)
50+
}
51+
52+
func (dq *Deque) PushBack(node int) {
53+
dq.Nodes = append(dq.Nodes, node)
54+
}
55+
func (dq *Deque) Front() int {
56+
return dq.Nodes[0]
57+
}
58+
59+
func (dq *Deque) Back() int {
60+
return dq.Nodes[len(dq.Nodes)-1]
61+
}
62+
63+
func (dq *Deque) PopFront() int {
64+
ret := dq.Front()
65+
66+
dq.Nodes = dq.Nodes[1:]
67+
68+
return ret
69+
}
70+
71+
func (dq *Deque) PopBack() int {
72+
ret := dq.Back()
73+
74+
dq.Nodes = dq.Nodes[0 : len(dq.Nodes)-1]
75+
76+
return ret
77+
}
78+
79+
func (dq *Deque) Size() int {
80+
return len(dq.Nodes)
81+
}
82+
83+
func maxArea(height []int) int {
84+
dq := NewDeque(height)
85+
86+
var max int
87+
for h := 0; dq.Size() > 1; h++ {
88+
for dq.Size() != 0 && dq.Front() < h {
89+
dq.PopFront()
90+
}
91+
for dq.Size() != 0 && dq.Back() < h {
92+
dq.PopBack()
93+
}
94+
95+
area := h * (dq.Size() - 1)
96+
97+
if area > max {
98+
max = area
99+
}
100+
}
101+
102+
return max
103+
}
104+
105+
```
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Intuition
2+
전형적인 이분탐색 문제였다.
3+
4+
# Approach
5+
1. `nums[0]보다 작은지 여부`를 검사하도록 설계했다.
6+
2. `lo`는 항상 F이며, `hi`는 항상 T이다.
7+
2. 배열의 원소들에 대해 FFFFFFFTTTTTT가 되는데, 이 때 `T`가 처음 등장하는 지점을 찾는다.
8+
3. 등장하지 않는 경우 (`nums[0]`이 최소인 경우 = 회전이 없는 경우)는 hi가 배열 범위 밖이므로, 인덱스 0을 반환하게 했다.
9+
10+
(문제 해결 이후 타 문제 재활용을 위해 인덱스를 찾도록 리팩터링했다.)
11+
# Complexity
12+
- Time complexity: $O(log(n))$
13+
- 배열의 길이 `n`에 대하여, 범위를 반으로 줄여가며 이분 탐색하므로 `log(n)`이 발생한다.
14+
15+
- Space complexity: $O(n), inline$
16+
- 배열의 길이 `n`에 대하여, 입력(`nums`)의 비용이 존재한다.
17+
18+
# Code
19+
```go
20+
func findMinIdx(nums []int) int {
21+
lo, hi := -1, len(nums)
22+
for lo+1 < hi {
23+
mid := (lo + hi) / 2
24+
25+
if nums[mid] < nums[0] {
26+
hi = mid
27+
} else {
28+
lo = mid
29+
}
30+
}
31+
if hi == len(nums) {
32+
return 0
33+
}
34+
return hi
35+
}
36+
37+
func findMin(nums []int) int {
38+
return nums[findMinIdx(nums)]
39+
}
40+
41+
```
42+
43+
# 여담
44+
이분탐색 문제는 타 솔루션을 참고하지 않는 편이다. 왜냐하면 `lo`, `hi`, 종료 조건, 반환 값 설정이 비슷해보이지만 엄청 다른 의미이기에 큰 도움이 안된다고 생각한다.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Intuition
2+
문자열 내에 영문자들만 등장한다는 것에서 DP, 그리디가 아니라 빈도수 계산 + 투포인터임을 알게되었다.
3+
4+
# Approach
5+
1. A-Z에 대해 순회한다.
6+
2. 포함하는 배열이 커지며(`j++`) 순회하는 문자가 몇 번 연속해서 등장하는지 계산한다. (`have`)
7+
3. 연속하지 않은 문자가 등장하더라도 `k`번은 봐준다.
8+
4. `k`번이상이라 봐줄 수 없는 경우 (`have+k < j-i+1`인 경우) 포함하는 배열을 줄인다. (`i++`)
9+
10+
# Complexity
11+
- Time complexity: $O(n)$
12+
- 순회하는 문자들은 상수 개수(26)이므로 무시한다.
13+
- 문자열의 길이 `n`에 대하여, 이를 순회하는 비용이 든다. (`j<len(s)`)
14+
15+
- Space complexity: $O(n) inline$
16+
- 문자열의 길이 `n`에 대하여, 입력으로 받은 `s`만큼 비용이 든다.
17+
18+
# Code
19+
```go
20+
func characterReplacement(s string, k int) int {
21+
var maxLen int
22+
for ch := 'A'; ch <= 'Z'; ch++ {
23+
i, j := 0, 0
24+
var have int
25+
for j < len(s) {
26+
if ch == rune(s[j]) {
27+
have++
28+
}
29+
for have+k < j-i+1 {
30+
if ch == rune(s[i]) {
31+
have--
32+
}
33+
i++
34+
}
35+
maxLen = max(maxLen, j-i+1)
36+
j++
37+
}
38+
}
39+
return maxLen
40+
}
41+
42+
```
43+
# 여담
44+
- 가장 많이 등장한 문자를 검색하는 솔루션을 확인했다. 동일 문제를 다르게 접근한다는 게 신기했다.
45+
- 비교해보자면, A~Z중 한 문자가 등장하는 것만 계산한다는 본인의 풀이가 더욱 직관적이라는 생각이 들었다.
46+
- 다만, 코드로 나타냈을 때는 덜 직관적이었다...;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Intuition
2+
DP, 그리디 등을 시도하려다가 오류를 발견했다. 화이트보드에 예제 문제들을 손으로 풀어보니 직관적으로 떠올랐다.
3+
# Approach
4+
1. 똑같은 음식을 먹지 않는 지렁이를 생각하자.
5+
2. 다음 음식을 이미 먹었다면, 먹었던 걸 뱉는다. (`l++`)
6+
3. 다음 음식을 먹지 않았다면, 먹는다. (`r++`)
7+
4. 지렁이의 길이를 최댓값 갱신에 활용한다.
8+
# Complexity
9+
- Time complexity: $O(n)$
10+
- 문자열의 길이 `n`에 대하여, 이를 순회하는 비용이 소모된다.
11+
- Space complexity: $O(n)$
12+
- 문자열의 길이 `n`에 대하여, 등장 여부를 저장하는 자료구조(`contains`)는 최대 `n`개 만큼을 저장할 수 있으므로 `n`이다.
13+
<!-- Add your space complexity here, e.g. $$O(n)$$ -->
14+
15+
# Code
16+
```go
17+
func lengthOfLongestSubstring(s string) int {
18+
if len(s) == 0 {
19+
return 0
20+
}
21+
contains := make(map[uint8]bool)
22+
23+
l, r := 0, 0
24+
maxLen := 1
25+
26+
for r < len(s) {
27+
for contains[s[r]] {
28+
contains[s[l]] = false
29+
l++
30+
}
31+
contains[s[r]] = true
32+
maxLen = max(maxLen, r-l+1)
33+
r++
34+
}
35+
36+
return maxLen
37+
}
38+
39+
```
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# Intuition
2+
이분 탐색인 게 직관적으로 보였다.
3+
4+
# Approach
5+
1. 찾으려는 값(`target`)이 최솟값의 인덱스(`minIdx`) 이전/이후인지 판단한다. (`target < nums[0] || minIdx == 0`)
6+
2. 판단한 범위(이전, 이후)에 대해서만 배열을 잘라 `lowerBound`를 실행한다.
7+
3. 결과를 통해 타겟의 인덱스(`targetIdx`)를 획득한다.
8+
4. 예외처리(범위 벗어난 경우, `lowerBound`은 맞으나 일치하진 않는 경우)엔 `-`을 아닌 경우는 인덱스를 반환한다.
9+
10+
# Complexity
11+
- Time complexity: $O(log(n))$
12+
- 배열의 길이 `n`에 대하여, 범위를 반으로 줄여가며 이분 탐색하므로 `log(n)`이 발생한다.
13+
- 두 이분탐색 함수를 사용하긴 하지만, 합연산이므로 복잡도에는 영향이 없다.
14+
- Space complexity: $O(n), inline$
15+
- 배열의 길이 `n`에 대하여, 입력(`nums`)의 비용이 존재한다.
16+
17+
# Code
18+
```go
19+
func findMinIdx(nums []int) int {
20+
lo, hi := -1, len(nums)
21+
for lo+1 < hi {
22+
mid := (lo + hi) / 2
23+
24+
if nums[mid] < nums[0] {
25+
hi = mid
26+
} else {
27+
lo = mid
28+
}
29+
}
30+
if hi == len(nums) {
31+
return 0
32+
}
33+
return hi
34+
}
35+
36+
func lowerBound(nums []int, target int) int {
37+
lo, hi := -1, len(nums)
38+
for lo+1 < hi {
39+
mid := (lo + hi) / 2
40+
41+
if nums[mid] < target {
42+
lo = mid
43+
} else {
44+
hi = mid
45+
}
46+
}
47+
return hi
48+
}
49+
50+
func search(nums []int, target int) int {
51+
minIdx := findMinIdx(nums)
52+
var targetIdx int
53+
if target < nums[0] || minIdx == 0 {
54+
targetIdx = lowerBound(nums[minIdx:], target) + minIdx
55+
} else {
56+
targetIdx = lowerBound(nums[0:minIdx], target)
57+
}
58+
59+
if len(nums) <= targetIdx || target != nums[targetIdx] {
60+
return -1
61+
}
62+
return targetIdx
63+
64+
}
65+
66+
```

0 commit comments

Comments
 (0)