diff --git a/clone-graph/haklee.py b/clone-graph/haklee.py new file mode 100644 index 00000000..e3edcf00 --- /dev/null +++ b/clone-graph/haklee.py @@ -0,0 +1,79 @@ +"""TC: O(n + e), SC: - + +노드 개수 n개, 엣지 개수 e개 + +아이디어: +문제 설명부터가 deepcopy를 하라는 것이니 내장함수를 써서 deepcopy를 해주자. + +SC: +- 내장함수가 필요한 공간들을 따로 잘 관리해주지 않을까? 아마 변수를 읽고 그대로 리턴값으로 바꿔줄듯. +- 그렇다면 추가적으로 관리하는 공간은 필요 없다. + +TC: +- deepcopy는 필요한 정보를 그대로 다 deepcopy 뜰 뿐이다. 아마 node 개수 + edge 개수에 비례해서 시간이 + 걸릴것 같다. O(n + e). +""" + +""" +# Definition for a Node. +class Node: + def __init__(self, val = 0, neighbors = None): + self.val = val + self.neighbors = neighbors if neighbors is not None else [] +""" + +import copy +from typing import Optional + + +class Solution: + def cloneGraph(self, node: Optional["Node"]) -> Optional["Node"]: + return copy.deepcopy(node) + + +"""TC: O(e), SC: O(e) + +노드 개수 n개, 엣지 개수 e개 + +아이디어: +dfs 돌면서 노드들을 메모해두자. neighbors에 특정 노드를 추가해야 할때 메모에 있으면 바로 가져다 +쓰고, 없으면 새로 만들어서 메모에 노드를 추가한다. + +SC: +- 노드 총 n개가 memo에 올라간다. O(n). +- 각 노드마다 neighbor가 있다. 각 edge마다 neighbor 리스트들의 총 아이템 개수에 2개씩 기여한다. O(e). +- 더하면 O(n + e). 즉, 둘 중 더 큰 값이 공간복잡도를 지배한다. + ...고 생각하는 것이 일차적인 분석인데, 여기서 더 나아갈 수 있다. +- 주어진 조건에 따르면 우리에게 주어진 그래프는 connected graph다. 즉, 엣지 개수가 n-1개 이상이다. +- 즉, O(n) < O(e)가 무조건 성립하므로, O(e) < O(n + e) < O(e + e) = O(e). 즉, O(e). + +TC: +- SC와 비슷한 방식으로 분석 가능. O(e). +""" + +""" +# Definition for a Node. +class Node: + def __init__(self, val = 0, neighbors = None): + self.val = val + self.neighbors = neighbors if neighbors is not None else [] +""" + +from typing import Optional + + +class Solution: + def cloneGraph(self, node: Optional["Node"]) -> Optional["Node"]: + if node is None: + return node + + memo = {} + + def dfs(node): + if node not in memo: + new_node = Node(node.val, []) + memo[node] = new_node + new_node.neighbors = [dfs(i) for i in node.neighbors] + return memo[node] + + return dfs(node) diff --git a/longest-common-subsequence/haklee.py b/longest-common-subsequence/haklee.py new file mode 100644 index 00000000..2d384d92 --- /dev/null +++ b/longest-common-subsequence/haklee.py @@ -0,0 +1,64 @@ +"""TC: O(m * n), SC: O(m * n) + +주어진 문자열의 길이를 각각 m, n이라고 하자. + +아이디어: +- 두 문자열이 주어졌는데 끝이 같은 문자라고 하자. 이 경우 lcs의 길이는 각각의 문자열에서 + 끝 문자를 제거한 문자열로 lcs의 길이를 구한 값에 1을 더한 값이다. + - e.g.) abcz, bcdefz의 lcs 길이를 `x`라고 한다면, + abc/z, bcdef/z에서 끝의 z가 같은 문자니까 이게 lcs에 들어간다 칠 수 있으므로, + abc, bcdef의 lcs 길이는 `x - 1`이 된다. +- 두 문자열의 끝 문자가 다를 경우, 첫 번째 문자열의 끝 문자를 제거하고 구한 lcs의 길이나 + 두 번째 문자열의 끝 문자를 제고하고 구한 lcs의 길이 둘 중 큰 값이 원래 문자열로 구한 lcs + 의 길이다. + - e.g.) abcz, bcdefy의 lcs 길이를 `x`라고 한다면, + abc, bcdefy의 lcs 길이와 + abcz, bcdef의 lcs 길이 + 둘 중 더 큰 값을 취하면 된다. +- LCS는 유명한 알고리즘이므로 위의 설명을 시각적으로 잘 표현한 예시들을 온라인상에서 쉽게 + 찾을 수 있다. +- 위의 아이디어를 점화식으로 바꾸면 + - 첫 번째 문자열의 앞 i글자로 만든 문자열과 두 번째 문자열의 앞 j글자로 만든 문자열의 + lcs의 길이를 lcs(i, j)라고 하자. + - 첫 번째 문자열의 i번째 글자와 두 번째 문자열의 j번째 글자가 같은 경우 다음의 식이 성립. + - lcs(i, j) = lcs(i-1, j-1) + 1 + - 다를 경우, 다음의 식이 성립. + - lcs(i, j) = max(lcs(i-1, j), lcs(i, j-1)) +- 위의 아이디어를 memoize를 하는 dp를 통해 구현할 수 있다. 자세한 내용은 코드 참조. + +SC: +- 첫 번째 문자열의 앞 i글자로 만든 문자열과 두 번째 문자열의 앞 j글자로 만든 문자열의 lcs의 + 길이를 관리. +- 그런데 아이디어에 제시된 점화식을 보면 i, j값에 대한 전체 배열을 저장할 필요 없이 i=k일때 + 값을 구하려 한다면 i=k-1일때 구한 lcs값만 알고 있으면 충분하다. +- 즉, 배열은 현재 구하고자 하는 i값에 대한 j개의 아이템과 직전에 구한 j개의 아이템만 저장하면 + 충분하다. 즉, text2의 길이인 O(n)이라고 볼 수 있다. +- 그런데 만약 text2의 길이가 text1보다 길면 이 둘을 바꿔치기해서 위의 공간복잡도를 O(m)이라고 + 봐도 아이디어 자체는 똑같이 작동하지 않는가? +- 즉, O(min(m, n)) + +TC: +- dp 배열을 채우는 데에 마지막 문자가 같을 경우 단순 덧셈, 다를 경우 두 값 비교. 둘 다 O(1). +- 배열 채우는 것을 m * n회 반복하므로 총 O(m * n). +""" + + +class Solution: + def longestCommonSubsequence(self, text1: str, text2: str) -> int: + if len(text2) > len(text1): + # 이 최적화까지 해주면 사용하는 메모리 크기가 많이 줄어들 수 있다. + text1, text2 = text2, text1 + + dp = [[0 for _ in range(len(text2) + 1)] for _ in range(2)] + + for i in range(1, len(text1) + 1): + i_prev = (i + 1) % 2 + i_cur = i % 2 + for j in range(1, len(text2) + 1): + dp[i_cur][j] = ( + dp[i_prev][j - 1] + 1 + if text1[i - 1] == text2[j - 1] + else max(dp[i_prev][j], dp[i_cur][j - 1]) + ) + + return dp[i_cur][-1] diff --git a/longest-repeating-character-replacement/haklee.py b/longest-repeating-character-replacement/haklee.py new file mode 100644 index 00000000..f9e8be41 --- /dev/null +++ b/longest-repeating-character-replacement/haklee.py @@ -0,0 +1,36 @@ +"""TC: O(n), SC: O(1) + +n은 주어진 문자열의 길이 + +아이디어: +- 투 포인터를 써서 문자열의 시작, 끝을 관리하면서 부분 문자열을 만든다. +- 부분 문자열에 들어있는 문자 중 가장 많은 문자와 k의 합이 문자열의 길이 보다 크면 조건 만족. +- 부분 문자열에 들어있는 문자 개수를 dict를 써서 관리하자. + +SC: +- 부분 문자열에 들어있는 문자 개수를 관리하는 dict에서 O(1). +- 부분 문자열의 시작, 끝 인덱스 관리 O(1). +- 종합하면 O(1). + +TC: +- 부분 문자열의 끝 인덱스를 하나 늘릴 때마다 반환할 값 업데이트. O(1)을 n번 시행하므로 O(n). +- 시작, 끝 인덱스 수정할 때마다 부분 문자열에 들어있는 문자 개수 업데이트. 시작, 끝 인덱스는 + 많이 수정해봐야 합쳐서 2*n번. 즉, O(1)을 2*n번 시행하므로 O(n). +- 시작, 끝 인덱스에 1을 더하는 시행. O(1)을 2*n번 시행하므로 O(n). +- 종합하면 O(n). +""" + + +class Solution: + def characterReplacement(self, string: str, k: int) -> int: + char_cnt = {c: 0 for c in set(string)} + s = e = 0 + sol = -1 + while e < len(string): + char_cnt[string[e]] += 1 + while e - s + 1 > max(char_cnt.values()) + k: + char_cnt[string[s]] -= 1 + s += 1 + sol = max(e - s + 1, sol) + e += 1 + return sol diff --git a/merge-two-sorted-lists/haklee.py b/merge-two-sorted-lists/haklee.py new file mode 100644 index 00000000..42de1fba --- /dev/null +++ b/merge-two-sorted-lists/haklee.py @@ -0,0 +1,65 @@ +"""TC: O(n), SC: - + +n은 주어진 두 리스트의 길이 중 큰 값 + +아이디어: +- 주어진 조건에 의해 두 리스트에 들어있는 값들은 non-decreasing이므로, 새로운 리스트를 만들고 + 두 리스트의 앞에 있는 값 중 작은 값을 하나씩 뽑아서 더해주면 된다. +- 빈 리스트가 주어질 수 있는 것만 유의하자. + +SC: +- 특별히 관리하는 값이 없다. + +TC: +- 모든 노드에 한 번씩 접근해서 리턴할 값에 이어준다. 이어주는 시행마다 O(1). +- 리턴할 값에 새 노드를 추가할 때마다 값 비교를 한 번씩 한다. O(1). +- n이 두 리스트 길이 중 큰 값이므로 이어주는 시행은 x는 n <= x <= 2*n 만족. +- 즉, 총 O(n). +""" + +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next +class Solution: + def mergeTwoLists( + self, list1: Optional[ListNode], list2: Optional[ListNode] + ) -> Optional[ListNode]: + # 1. init head + # - 두 리스트를 보고 혹시 하나라도 비어있으면 다른 리스트를 리턴한다. + # - 둘 다 비어있지 않을 경우 첫 아이템을 보고 둘 중 작은 값을 결과물의 첫 아이템으로 씀. + if list1 is None: + return list2 + if list2 is None: + return list1 + # 여기 도달했으면 둘 다 최소한 한 아이템씩은 존재. + sol = None + if list1.val < list2.val: + sol = ListNode(list1.val) + list1 = list1.next + else: + sol = ListNode(list2.val) + list2 = list2.next + + sol_head = sol + + # 2. add item + # - 앞의 과정을 비슷하게 반복한다. + while True: + # 언젠가 둘 중 한 리스트는 비게 되므로 무한 루프를 돌지 않는다. + if list1 is None: + sol_head.next = list2 + return sol + if list2 is None: + sol_head.next = list1 + return sol + + if list1.val < list2.val: + sol_head.next = ListNode(list1.val) + list1 = list1.next + else: + sol_head.next = ListNode(list2.val) + list2 = list2.next + + sol_head = sol_head.next diff --git a/sum-of-two-integers/haklee.py b/sum-of-two-integers/haklee.py new file mode 100644 index 00000000..f7b7dde1 --- /dev/null +++ b/sum-of-two-integers/haklee.py @@ -0,0 +1,133 @@ +"""TC: O(1), SC: O(n^2) + +-n <= a, b <= n 가정. +문제에서는 n이 1000으로 주어져있다고 볼 수 있다. + +아이디어: +덧셈을 못 쓰게 한다면 전처리를 통해 모든 a, b 조합에 대한 덧셈 값을 만들어놓고 a, b의 값에 따라서 +필요한 값을 출력하도록 하자. python에서는 음수로 된 인덱스를 지원하므로 이를 활용하자. + +- 아래의 코드를 통해서 전처리된 값을 준비한다. 이 코드는 leetcode에서 실행되지 않으므로 더하기를 + 써도 상관 없다. +```py +n = 3 +with open("foo.txt", "w") as f: + a = [ + [ + (i if i <= n else i - 2 * n - 1) + (j if j <= n else j - 2 * n - 1) + for j in range(0, 2 * n + 1) + ] + for i in range(0, 2 * n + 1) + ] + f.write(str(a)) +``` + +SC: +- O(n^2). 정확히는, (2*n+1)^2 개의 정수를 배열에 저장한다. + +TC: +- 인덱스를 통해 바로 접근. O(1). +""" + + +class Solution: + # n = 3일때 예시. + def getSum(self, a: int, b: int) -> int: + x = [ + [0, 1, 2, 3, -3, -2, -1], + [1, 2, 3, 4, -2, -1, 0], + [2, 3, 4, 5, -1, 0, 1], + [3, 4, 5, 6, 0, 1, 2], + [-3, -2, -1, 0, -6, -5, -4], + [-2, -1, 0, 1, -5, -4, -3], + [-1, 0, 1, 2, -4, -3, -2], + ] + return x[a][b] + + +# 단, n = 1000일때 이런 식으로 코드를 짜려고 하면 +# `For performance reasons, the number of characters per line is limited to 10,000.` +# 하는 문구와 함께 리스트를 복붙할 수가 없다... + + +"""TC: O(n), SC: O(n) + +-n <= a, b <= n 가정. +문제에서는 n이 1000으로 주어져있다고 볼 수 있다. + +아이디어: +전처리 한 것을 가져오는 방법은 못 쓰게 되었지만, 인덱스를 통한 접근은 아직 그대로 쓰고 싶다. + +- 문제의 조건을 바꿔서 0 <= a, b <= n라고 해보자. 그리고 n이 3이라고 해보자. +- a가 0으로 고정되어 있다면, 다음과 같은 배열이 주어졌을때 a+b의 값을 인덱스로 접근할 수 있다. + - v = [0, 1, 2, 3] 일때 a + b 값은 v[b] +- a가 1로 고정되어 있다면, + - v = [1, 2, 3, 4] 일때 a + b 값은 v[b] +- a가 2로 고정되어 있다면, + - v = [2, 3, 4, 5] 일때 a + b 값은 v[b] +- a가 3으로 고정되어 있다면, + - v = [3, 4, 5, 6] 일때 a + b 값은 v[b] +- 위의 배열을 보면 겹치는 숫자들이 많다. 그렇다면 0~6까지 숫자들이 들어있는 배열을 slicing해서 + 쓰면 되지 않을까? + - a가 0일때 v = [0, 1, 2, 3, 4, 5, 6] 중 + [0, 1, 2, 3] 사용. + 즉, v[0:4] 사용. + + - a가 1일때 v = [0, 1, 2, 3, 4, 5, 6] 중 + [1, 2, 3, 4] 사용. + 즉, v[1:5] 사용. + ... + - 일반화하면, v[a:a+n+1] 사용. 이때 a+b 값은 v = list(range(0, 2 * n + 1))일때 v[a:a+n+1][b]. +- 그런데 v[a:a+n+1][b]를 보면 슬라이싱 하는 부분에서 + 기호를 사용했다. + - 그렇다면 저기서 더하기 기호를 사용할 필요 없이 슬라이싱의 시작 값과 끝 값도 미리 리스트로 만들고, + 이 리스트에서 a번째 아이템을 가져오는 방식을 활용해보자. + - s = [0, 1, 2, 3], e = [4, 5, 6, 7]일때, v[a:a+n+1][b] = v[s[a]:e[a]][b]가 된다. + - 일반화하면, s = list(range(0, n)), e = list(range(n+1, 2*n+1))이면 된다. + - e를 만들면서 더하기를 쓴 것처럼 보이지만, 실제로는 n이 주어진 상수이므로 값을 계산해서 넣으면 된다. + - 예를 들어, n=3일때 e = list(range(4, 7))이다. + +큰 아이디어는 위의 방식으로 설명이 끝났다. 이제 문제는 0 <= a, b <= n이 아니라 -n <= a, b <= n 범위에서도 +위의 방식이 작동하도록 하는 것인데, 먼저 a값은 양수 범위에 두고 b값만 음수로 확장한 상태에서 v를 구해보고, +그 다음 a도 음수까지 확장하는 식으로 접근하면 된다. 자세한 설명은 생략하고, 둘 다 음수 범위까지 확장한 뒤 +실제로 작동하는 결과물을 설명하는 것으로 대신하겠다. + +- n은 3이라고 가정하겠다. +- v = [0, 1, 2, 3, 4, 5, 6, -6, -5, -4, -3, -2, -1, 0, 1, 2, 3, 4, 5, 6, -6, -5, -4, -3, -2, -1] + - list(range(0, n+1))에 list(range(-n, 0))을 이어붙인 것을 두 번 반복했다. + - 두 번 반복한 이유는 a값이 음수가 된 상황에 대응하기 위함이다. 아래의 설명을 이어서 보도록 하자. +- s = list(range(0, 4 * n + 1)) = list(range(0, 13))이다. + - 이렇게 하면 a가 음수가 되었을때도 slicing을 시작할 인덱스는 양수로 유지할 수 있다. + - b를 0으로 고정하면 slicing을 시작하는 인덱스에 있는 아이템이 a+b의 값이 되어야 한다. + - 이를 위의 v와 같이 생각하면 v의 앞 13개 아이템을 취한 [0, 1, 2, 3, 4, 5, 6, -6, -5, -4, -3, -2, -1] + 배열이 있을때 a가 취할 수 있는 값의 범위는 -3~3이므로 [0, 1, 2, 3, ..., -3, -2, -1] 중 하나부터 + slicing이 시작된다고 보면 된다. + - 그러니까 쉽게 말해서 a의 범위로 인해 slicing이 아이템 4부터 시작해서 [4, 5, 6, -6, ...] 하는 일이 + 일어나지는 않는다는 뜻. +- slicing한 배열의 크기는 4*n+1이어야 한다. e는 s의 각 아이템에 4*n+1을 더한 값이면 된다. + - 4*n+1은 관찰을 통해 얻을 수 있는 값이다.a, b의 합의 최소가 -2*n, 최대가 2*n이어서 그 사이에 있는 + 숫자들이 총 4*n+1개 있다는 것에서 비롯된 숫자다. + - 끝에 예시를 보면 이해가 좀 더 편하다. + - 정리하면, e = list(range(4*n+1, 8*n+2)) = list(range(13, 26))이다. +- a+b 값은 v[s[a] : e[a]][b] 로 구할 수 있다. +- 예를 들어, a=2, b=-3이라고 할때 + - v[s[a] : e[a]] = v[2:15] = [2, 3, 4, 5, 6, -6, -5, -4, -3, -2, -1, 0, 1]다. + - b가 -3이므로 위의 slicing된 배열에서 뒤에서 세 번째 아이템을 찾으면 된다. 즉, -1이다. + - 잘 관찰하면 덧셈의 결과가 될 수 있는 값은 [2, 3, 4, 5, ..., -1, 0, 1] 밖에 없다. 사이에 있는 숫자는 + b의 범위가 제한되어 있어서 접근 불가능한, 즉, 필요 없는 숫자들이라고 보면 된다. + +SC: +- 코드 참조. O(n). + +TC: +- 인덱스를 통해 바로 접근. O(1). +""" + + +class Solution: + def getSum(self, a: int, b: int) -> int: + x = list(range(0, 2001)) + x.extend(list(range(-2000, 0))) + v = x * 2 # SC: O(n) + s = list(range(0, 4001)) # SC: O(n) + e = list(range(4001, 8002)) # SC: O(n) + return v[s[a] : e[a]][b]