Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[haklee] week 6 #472

Merged
merged 1 commit into from
Sep 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions container-with-most-water/haklee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
"""TC: O(n), SC: O(1)

아이디어:
높이 리스트 l이 주어졌다고 하자. 그리고 이 안에 있는 최대 수조 면적을 f(l)이라고 하자.

□ □ □
□ □ □ □
□ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □
□ □ □ □ □ □ □ □ □ □ □ □ □ □
2,5,3,3,1,3,1,2,2,5,3,5,3,4
^
l


그러면 다음이 항상 성립한다.

- l의 양 끝의 값 중 작은 값 x가 앞쪽에 있었다고 해보자. x를 뺀 리스트 l`을 만든다.
- 이때 작은 값이 뒷쪽에 있었어도 일반성을 잃지 않는다.

│□ □ □
│□ □ □ □
│□ □ □ □ □ □ □ □ □
□│□ □ □ □ □ □ □ □ □ □ □
□│□ □ □ □ □ □ □ □ □ □ □ □ □
2│5,3,3,1,3,1,2,2,5,3,5,3,4
^ ^
x l`


- f(l)은 그렇다면
- f(l`)이거나(즉, x를 쓰지 않았거나)
- x를 써서 만든 수조 면적 중에 있다.

- 그런데 x는 l의 양 끝 값 중에 작은 값이므로, 아래와 같은 분석을 할 수 있다.
1) x를 써서 만드는 수조의 높이는 아무리 높아도 x다. x보다 작아질 수는 있어도, x보다 커질 수는 없다.
2) x를 써서 만드는 수조의 폭은 l의 다른쪽 끝에 있는 높이를 선택했을때 최대가 된다.
- 그러므로, x를 써서 만들 수 있는 수조의 최대 크기는 l의 다른쪽 끝에 있는 높이를 선택한 경우 나온다.

위의 내용을 아래의 설명을 통해 시각적으로 확인할 수 있다.

- 양 끝을 선택한 경우 x로 만들 수 있는 최대 면적이다.

│□ □ □
│□ □ □ □
│□ □ □ □ □ □ □ □ □
■│■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
■│■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■
2│5,3,3,1,3,1,2,2,5,3,5,3,4
^ ^

- x는 그대로 둔 채 다른쪽 끝을 안쪽으로 더 이동하면 수조 높이는 동일한데 폭은 더 작아진다.

│□ □ □
│□ □ □ □
│□ □ □ □ □ □ □ □ □
■│■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ □ □
■│■ ■ ■ ■ ■ ■ ■ ■ ■ ■ ■ □ □
2│5,3,3,1,3,1,2,2,5,3,5,3,4
^ ^

- 심지어 x보다 작은 높이 값을 선택한 경우 수조 높이도 작아지고 폭도 작아지는 일이 일어난다.

│□ □ □
│□ □ □ □
│□ □ □ □ □ □ □ □ □
□│□ □ □ □ □ □ □ □ □ □ □
■│■ ■ ■ ■ ■ ■ □ □ □ □ □ □ □
2│5,3,3,1,3,1,2,2,5,3,5,3,4


즉, 위의 내용을 종합하면 다음을 확인할 수 있다.
- f(l) = max( (l의 양 끝 높이를 선택해서 만든 수조 넓이), f(l`) )
- 그런데 f(l`)도 f(l)을 구한 것과 같은 방식으로 구할 수 있다. 즉, 새로 만들어진 l`의 양 끝 높이 중
짧은 쪽을 뺀 리스트 l``을 만들어서 위의 과정을 반복할 수 있다.
- 즉, f(l)은 아래의 과정을 반복하여 구할 수 있다.
- l의 양 끝 높이를 써서 수조 넓이를 구하고, 기존 최대 넓이와 비교하여 더 큰 값을 최대 넓이에 대입한다.
- l에서 짧은 쪽 높이를 뺀다.
- 위 과정을 l에 아이템이 하나만 남을 때까지 반복.
Comment on lines +3 to +79
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전달력 좋은 글에 다이어그램까지 제공해주시니까 최고였습니다

저는 ~니까 ~일 수밖에 없다라는 식으로만 접근했었는데, 이렇게 보다 정제된 표현들로 설명하니까 제 이해도도 함께 높아졌습니다 :)



SC:
- 투 포인터를 써서 l의 시작, 끝 인덱스를 관리하면 O(1).
- 수조 최대 넓이값 관리, O(1).
- 종합하면 O(1).

TC:
- 리스트의 양 끝 높이를 통해 면적 구하기, O(1).
- 포인터 이동 O(1).
- 포인터 이동시 두 포인터 사이의 거리가 1씩 항상 감소하므로 위 과정을 최대 n-2번 반복.
- 종합하면 O(n).
"""


class Solution:
def maxArea(self, height: List[int]) -> int:
max_area = -1
s, e = 0, len(height) - 1
while s < e:
max_area = max(max_area, (e - s) * min(height[s], height[e]))
if height[s] > height[e]:
e -= 1
else:
s += 1
return max_area
59 changes: 59 additions & 0 deletions design-add-and-search-words-data-structure/haklee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""search시: TC: O(n), SC: O(n)

word의 max length가 w, `addWord`를 통해 만들어진 노드 개수가 n개라고 하자.

아이디어:
`implement-trie-prefix-tree` 문제에서 구현한 trie에서 search 부분만 recursive한 dfs 구현으로 수정.

SC:
- trie의 노드 개수 O(n).
- dfs시 스택 깊이는 최대 w. 즉, O(w). 그런데 스택 깊이는 노드 개수보다 클 수 없다. 즉, O(w) < O(n).
- 종합하면, O(n) + O(w) < O(n) + O(n) = O(n).

TC:
- 최악의 경우 모든 노드 순회. O(n).
Comment on lines +8 to +14
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

misc)

저는 Big O 분석을 input 값이 증가함에 따라 '함수'의 실행시간 및 공간사용량의 증가 추세가 어떻게 되느냐에 대해 이야기하는 것으로 이해하고 있습니다

그래서 Big O 분석을 WordDictionary 클래스에 대해서 논하는 것보다 각 method에 대해서 다루는 것이 좀 더 적절하다고 생각이 드는데, @haklee 님께서는 어떻게 생각하시나요?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그래서 맨 위에 search시: TC: O(n), SC: O(n) 라는 단서를 달아두었는데 헷갈릴 수 있었겠네요..! trie 자체는 이전 구현을 그대로 가져오기도 했고, 이전 구현에서도 잘 알려진 일반적인 trie라 TC, SC 분석을 생략하고 넘어갔었어서 이번에도 은근슬쩍 넘어갔다가 딱 걸렸습니다 ㅠㅠ

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아이고 맨 위에 코멘트를 제가 놓쳤네요..!


"""


class WordDictionary:

def __init__(self):
self.next: dict[str, WordDictionary] = {}
self.end: bool = False

def addWord(self, word: str) -> None:
cur = self

for c in word:
cur.next[c] = cur.next.get(c, WordDictionary())
cur = cur.next[c]

cur.end = True

def search(self, word: str) -> bool:
cur = self

return self._search(cur, word, 0)

def _search(self, trie, word: str, ind: int) -> bool:
if ind == len(word):
return trie.end

c = word[ind]

if c == ".":
return any(
[self._search(node, word, ind + 1) for node in trie.next.values()]
)

if c not in trie.next:
return False

return self._search(trie.next[c], word, ind + 1)


# Your WordDictionary object will be instantiated and called as such:
# obj = WordDictionary()
# obj.addWord(word)
# param_2 = obj.search(word)
74 changes: 74 additions & 0 deletions spiral-matrix/haklee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""search시: TC: O(m*n), SC: O(1)

아이디어:
2차원 배열에서 아직 훑고 가지 않은 영역을 →, ↓, ←, ↑ 방향으로 훑는다. 이때, 한 번에 훑고 갈 선의
시작과 끝 역할을 할 값들(즉, boundary 값들)을 잘 관리해준다.
- top, bottom, left, right의 역할을 할 값을 각각 0, m, 0, n으로 초기화.
- → 방향으로 훑고 나서 top을 한 칸 아래로(즉, top++).
- ↓ 방향으로 훑고 나서 right를 한 칸 왼쪽으로(즉, right--).
- ← 방향으로 훑고 나서 bottom을 한 칸 위로(즉, bottom--).
- ↑ 방향으로 훑고 나서 left를 한 칸 오른쪽으로(즉, left++).
- right가 left보다 왼쪽에 있거나 top이 bottom보다 아래 있으면 탐색 종료.

SC:
- boundary 값만 관리. O(1).

TC:
- 배열의 모든 아이템에 정확히 한 번씩 접근. O(m*n).
- 선을 한 번 긋는 과정에서 일어나는 일이 총 O(1)인데(코드 참조), 이게 아무리 많아도 O(m*n)을 넘지는
못한다. 한 선에 최소 하나의 값은 들어있기 때문.
- 종합하면 O(m*n).

"""


class Solution:
def spiralOrder(self, matrix: List[List[int]]) -> List[int]:
def read_line(start: tuple[int, int], end: tuple[int, int]) -> list[int]:
if start[0] == end[0]:
# column 방향
s, e = sorted([start[1], end[1]]) # TC: O(1)
line = range(s, e + 1)
if start[1] > end[1]: # TC: O(1)
line = reversed(line)
return [matrix[i][start[0]] for i in line]
else:
# row 방향
s, e = sorted([start[0], end[0]]) # TC: O(1)
line = range(s, e + 1)
if start[0] > end[0]: # TC: O(1)
line = reversed(line)
return [matrix[start[1]][i] for i in line]

top, bottom, left, right = 0, len(matrix) - 1, 0, len(matrix[0]) - 1
sol = []
while True:
# 위
if right < left:
break
line = read_line((left, top), (right, top))
sol += line
top += 1 # TC: O(1)

# 오른쪽
if top > bottom:
break
line = read_line((right, top), (right, bottom))
sol += line
right -= 1 # TC: O(1)

# 아래
if right < left:
break
line = read_line((right, bottom), (left, bottom))
sol += line
bottom -= 1 # TC: O(1)

# 왼쪽
if top > bottom:
break
line = read_line((left, bottom), (left, top))
sol += line
left += 1 # TC: O(1)

return sol
45 changes: 45 additions & 0 deletions valid-parentheses/haklee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""TC: O(n), SC: O(n)

문자열 길이 n

아이디어:
- 문자열을 앞에서부터 훑으면서
- 괄호 시작을 발견하면 스택에 넣는다.
- 괄호 끝을 발견하면 스택의 제일 마지막에 있는 아이템을 pop해서 매칭이 되는지 확인한다.
- 이때 다음과 같은 예외 상황들을 조심한다.
- 스택에 아이템이 하나도 없는데 pop을 하려는 상황 방지.
- e.g.) "))))"
- 모든 문자열을 훑고 지나갔는데 스택에 아이템이 남아있는 경우도 실패다.
- e.g.) "(((("

SC:
- 시작 괄호 리스트 O(1).
- 끝 괄호와 매칭되는 시작 괄호 dict O(1).
- 시작 괄호만 있는 경우 스택에 쌓이는 아이템은 총 n개다. O(n).
- 종합하면 O(n).

TC:
- 문자 총 n개에 대해 아래를 반복한다.
- 시작 괄호인지 체크. O(1).
- 시작 괄호인 경우 스택에 쌓기 O(1).
- 끝 괄호인 경우 스택에서 pop할때 O(1), 괄호 매칭 체크시 O(1).
- 종합하면 O(n).
"""


class Solution:
def isValid(self, s: str) -> bool:
stack = []
openings = ["(", "{", "["]
ending_match = {
")": "(",
"}": "{",
"]": "[",
}
for c in s:
if c in openings:
stack.append(c)
else:
if not stack or ending_match[c] != stack.pop():
return False
return False if stack else True