Skip to content

[SunaDu] Week 15 #1113

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

Merged
merged 4 commits into from
Mar 22, 2025
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
135 changes: 135 additions & 0 deletions longest-palindromic-substring/dusunax.py
Copy link
Contributor

Choose a reason for hiding this comment

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

선아님 안녕하세요!
Brute force가 아닌 다른 방법을 고민하고 있었는데, 주석을 정말 자세하게 달아주셔서 공부하는 데에 도움이 많이 됐습니다 😄👍

Copy link
Member Author

@dusunax dusunax Mar 21, 2025

Choose a reason for hiding this comment

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

안녕하세요 나연님~
풀고 개선하는 데 시간이 굉장히 많이 걸렸어요😅
개인적으로 15주 동안 푼 문제 중에서 손꼽히게 어려웠던 것 같아요.
도움이 되었다니 다행입니다!

Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
'''
# 5. Longest Palindromic Substring

DP 테이블을 사용하여 팰린드롬 여부를 저장하고 최대 길이를 찾기.
'''
class Solution:
'''
TC: O(n^2)
SC: O(n^2)
'''
def longestPalindrome(self, s: str) -> str:
n = len(s)
if n == 1:
return s

start, length = 0, 1
dp = [[False] * n for _ in range(n)] # SC: O(n^2)

# length 1, diagonal elements in 2d array
for i in range(n): # TC: O(n)
dp[i][i] = True

# length 2, are two elements same
for i in range(n - 1):
if s[i] == s[i + 1]:
dp[i][i + 1] = True
start, length = i, 2

# length 3+
for word_length in range(3, n + 1): # TC: O(n^2)
for i in range(n - word_length + 1):
j = i + word_length - 1

if s[i] == s[j] and dp[i + 1][j - 1]:
dp[i][j] = True
start, length = i, word_length

return s[start:start + length]

'''
## Check Leetcode Hints
- How can we reuse a previously computed palindrome to compute a larger palindrome?
- use dp table that stores isPalindrome computation.
- If “aba” is a palindrome, is “xabax” a palindrome? Similarly is “xabay” a palindrome?
- if it is palindrome, we only need to check the outermost chars, that wrapping our palindrome.
- Palindromic checks can be O(1) by reusing prev computations.
- DP!

## 동적 프로그래밍 풀이
- s[i:j]의 팰린드롬 여부를 저장하기 위해서는 2차원 배열을 사용하는 것이 간편하다.
- 길이가 1인 경우, 팰린드롬
- 길이가 2인 경우, 두 문자가 같다면 팰린드롬
- 3 이상은 dp 테이블을 활용하며 확인
- ex) 길이가 5인 문자열을 다음과 같이 탐색
```
len: 3, i: 0, j: 2
len: 3, i: 1, j: 3
len: 3, i: 2, j: 4
len: 4, i: 0, j: 3
len: 4, i: 1, j: 4
len: 5, i: 0, j: 4
```

## 탐구
- can we use sliding window to optimize?
슬라이딩 윈도우는 연속적인 구간을 유지하며 최적해를 찾을 때 유용하지만, 팰린드롬은 중앙 확장과 이전 계산 재사용이 핵심.

- can we solve this by counting char?
문자 개수를 세면 "어떤 문자가 몇 번 나왔는지"만 알 수 있지만, 팰린드롬 여부는 문자 순서가 중요하다.

- 그렇다면 개선 방법은?
> Manacher's Algorithm: https://www.geeksforgeeks.org/manachers-algorithm-linear-time-longest-palindromic-substring-part-1/
>
>팰린드롬의 대칭성을 활용해 중앙을 기준으로 확장하는 Manacher's Algorithm을 적용할 수 있다. 모든 문자 사이에 #을 넣어 짝수, 홀수를 동일하게 다루는 기법인데 구현이 다소 복잡하다. 시간 복잡도는 O(n)이다.
'''
class Solution:
'''
TC: O(n)
SC: O(n)
'''
def longestPalindromeManacher(self, s: str) -> str:
'''
1. 문자열 변환하기 (가운데에 # 추가)
2. 저장공간 초기화
3. 변환된 문자열 순회하기
(a) 미러 인덱스 계산
(b) 팰린드롬 반지름 확장
- i에서 양쪽 문자들을 비교해서 확장
(c) 새로운 팰린드롬 찾기
- 새로운 팰린드롬을 i로 설정
- 새로운 팰린드롬의 오른쪽 끝을 i + p[i]로 설정
(d) 가장 긴 팰린드롬 업데이트
'''
transformed = '#' + '#'.join(s) + '#'
n = len(transformed)
p = [0] * n # i 중심의 팰린드롬 크기
c = 0 # 현재 팰린드롬의 중심
r = 0 # 현재 팰린드롬의 오른쪽 끝
max_len = 0 # 가장 긴 팰린드롬의 길이
center = 0 # 가장 긴 팰린드롬의 중심

for i in range(n):
# 현재 위치 i의 미러 인덱스 (중앙을 기준으로 대칭되는 인덱스)
# ex) ababbaa
# 0123456
# i가 3이고 center가 2일 때, 2*2 - 3 = 1, 미러 인덱스는 1
# i가 5이고 center가 4일 때, 2*4 - 5 = 3, 미러 인덱스는 3
mirror = 2 * c - i

if i < r:
# r - i: 얼마나 더 확장 될 수 있는가 => 현재 팰린드롬의 오른쪽 끝에서 현재 인덱스까지의 거리
# p[mirror]: 미러 인덱스에서의 팰린드롬 반지름
p[i] = min(r - i, p[mirror])
# 작은 값만큼만 확장이 가능하다. 예를 들어, r - i가 더 작은 값이라면 팰린드롬을 그만큼만 확장할 수 있고, p[mirror]가 더 작은 값이라면 이미 그만큼 확장이 된 상태
# r - i가 3이고 p[mirror]가 2라면 팰린드롬을 2만큼 확장할 수 있고, r - i가 2이고 p[mirror]가 3이라면 팰린드롬을 2만큼 확장할 수 있다.

# 현재 중심에서 팰린드롬을 확장
# 양 끝이 같다면 팰린드롬 반지름 p[i]를 1씩 증가
while i + p[i] + 1 < n and i - p[i] - 1 >= 0 and transformed[i + p[i] + 1] == transformed[i - p[i] - 1]:
p[i] += 1

# 기존에 찾은 팰린드롬보다 더 큰 팰린드롬을 찾은 경우
# 현재 중심과 오른쪽 끝을 설정
if i + p[i] > r:
c = i
r = i + p[i]

# 가장 긴 팰린드롬 업데이트
if p[i] > max_len:
max_len = p[i]
center = i

# 변환된 문자열에서 가장 긴 팰린드롬을 원래 문자열에서 추출
start = (center - max_len) // 2
return s[start:start + max_len]
77 changes: 77 additions & 0 deletions rotate-image/dusunax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'''
# 48. Rotate Image

rotate 2d matrix 90 degree in place.
👉 transpose matrix and reverse each row.

- original matrix
1 2 3
4 5 6
7 8 9

- transpose matrix (swap i, j) (flip diagonally)
1 4 7
2 5 8
3 6 9

- reverse each row (horizontal flip)
7 4 1
8 5 2
9 6 3
'''
class Solution:
'''
TC: O(n^2)
SC: O(1)
'''
def rotate(self, matrix: List[List[int]]) -> None:
n = len(matrix)

for i in range(n):
for j in range(i, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

for i in range(n):
matrix[i].reverse()

'''
# 💡 other rotate in-place examples

## rotate 180 degree

### solution A. reverse each column (vertical flip) & reverse each row (horizontal flip)

```python
def rotate180(matrix: List[List[int]]) -> None:
n = len(matrix)
matrix.reverse()

for i in range(n):
matrix[i].reverse()
```

### solution B. (after transpose, reverse each row (horizontal flip)) * 2.

90 degree * 2 = 180 degree

```python
def rotate180(matrix: List[List[int]]) -> None:
rotate90(matrix)
rotate90(matrix)
```

## rotate -90 degree

after transpose, reverse each column (vertical flip)

```python
def rotate90CCW(matrix):
n = len(matrix)

for i in range(n):
for j in range(i, n):
matrix[i][j], matrix[j][i] = matrix[j][i], matrix[i][j]

matrix.reverse()
```
'''
62 changes: 62 additions & 0 deletions subtree-of-another-tree/dusunax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
'''
# 572. Subtree of Another Tree

## 1. Recursive
use a recursive function to check if the two trees are identical.
- if they are identical, return True.
- if they are not identical, recursively check the left subtree and right subtree.

### base case
- isSubtree
- if the current tree is None, return False.
- if the subRoot is None, return True. (empty tree is a subtree of any tree)
- isIdentical
- if both trees are None, return True. (they are identical)
- if one of the trees is None, return False.
- if the values of the current nodes are different, return False.
- left subtree and right subtree's results are must be True.

## 2. Snapshot
> reference: https://www.algodale.com/problems/subtree-of-another-tree/

use a snapshot string of the preorder traversal to check if the subRoot is a subtree of the current tree.
- if the subRoot is a subtree of the current tree, return True.
- if the subRoot is not a subtree of the current tree, return False.
'''
class Solution:
'''
1. Recursive
TC: O(n * m), m is the number of nodes in the subRoot.
SC: O(n) (recursive stack space)
'''
def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:
if not root:
return False
if not subRoot:
return True

if self.isIdentical(root, subRoot):
return True

return self.isSubtree(root.left, subRoot) or self.isSubtree(root.right, subRoot)

def isIdentical(self, s: Optional[TreeNode], t: Optional[TreeNode]) -> bool:
if not s and not t:
return True
if not s or not t:
return False

return s.val == t.val and self.isIdentical(s.left, t.left) and self.isIdentical(s.right, t.right)

'''
2. Snapshot
TC: O(n + m), n is the number of nodes in the root, m is the number of nodes in the subRoot.
SC: O(n + m) (recursive stack space)
'''
def isSubtree(self, root: Optional[TreeNode], subRoot: Optional[TreeNode]) -> bool:
def preorder(node):
if not node:
return '#'
return f'({node.val},{preorder(node.left)},{preorder(node.right)})'

return preorder(subRoot) in preorder(root)
26 changes: 26 additions & 0 deletions validate-binary-search-tree/dusunax.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
'''
# 98. Validate Binary Search Tree

check if is a valid BST.
- the left value is smaller than the current value
- the right value is greater than the current value

👉 all left and right subtrees must also valid continuously.
so, we need to check the low & high bound recursively. (not only check current node & immediate children)

## base case
- if the node is None, return True (empty tree is valid BST)
- valid node value has range of left < node.val < right, if not, return False
- recursively check the left and right subtree, update the low & high.
'''
class Solution:
def isValidBST(self, root: Optional[TreeNode]) -> bool:
def dfs(node, low = float('-inf'), high = float('inf')):
if not node:
return True
if not (low < node.val < high):
return False

return dfs(node.left, low, node.val) and dfs(node.right, node.val, high)

return dfs(root)