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 8 #506

Merged
merged 2 commits into from
Oct 5, 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
79 changes: 79 additions & 0 deletions clone-graph/haklee.py
Original file line number Diff line number Diff line change
@@ -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).
Comment on lines +42 to +48
Copy link
Contributor

@obzva obzva Oct 2, 2024

Choose a reason for hiding this comment

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

dfs + memo 방식의 공간 복잡도 분석을 제가 잘 이해하지 못했습니다 🥹

제가 생각했을 때는,

  1. memo dictionary의 공간 사용은 N에 비례하여 선형적으로 증가 -> O(N)
  2. dfs의 재귀호출 스택의 크기는, 한 노드가 가질 수 있는 간선의 최대 개수 즉, N - 1에 비례하여 증가 -> O(N)

이라고 생각되어서 공간 복잡도에서 E는 오히려 배제하여도 되는 변수이고, 최종적으로 공간복잡도는 O(N)이라고 �생각하는데 학님은 어떻게 생각하시는지 의견 궁금합니다

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

오... 생각해보니 재귀 호출 스택 크기는 어차피 n보다 커지지 않을 거라고 생각하고 분석에 안 쓰고 그냥 넘어가버렸네요...

극단적으로 n개의 노드들이 모두 서로에 연결되어 있는 상황을 생각해보면 위의 설명이 이해가 될 것이라고 생각합니다.

  • 노드 하나가 neighbor로 들고 있어야 하는 값이 크기 O(n)짜리 리스트입니다. 노드 객체를 직접 들고 있지 않고 노드 객체의 포인터만 들고 있다고 해도, 포인터 자체의 크기도 고려해야 하기 때문에 O(n)입니다.
  • 위와 같은 노드가 n개 있습니다. 즉, 각 노드가 들고 있는 neighbor를 표시하기 위해서 O(n^2)의 공간이 필요합니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

설명 감사합니다
저는 보통 return하는 객체는 공간 복잡도에 포함시키지 않는데, 이 부분에서 서로 생각의 차이가 비롯된 것 같습니다

Copy link
Contributor Author

Choose a reason for hiding this comment

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

그렇네요..! 항상 리턴하는 값에 할당되는 공간을 무시한다는 것을 잊습니다 ㅠㅠ '리턴값에 어차피 O(n^2) 공간이 필요한데 추가 공간이 O(n) 필요하면 어차피 실제 작동하는 상황에서 추가 공간은 무시할만하다고 보는 것이 맞지 않나...?' 하는 생각이 자꾸 들어서 그런것 같습니다...


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)
64 changes: 64 additions & 0 deletions longest-common-subsequence/haklee.py
Original file line number Diff line number Diff line change
@@ -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는 유명한 알고리즘이므로 위의 설명을 시각적으로 잘 표현한 예시들을 온라인상에서 쉽게
Copy link
Contributor

Choose a reason for hiding this comment

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

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]
36 changes: 36 additions & 0 deletions longest-repeating-character-replacement/haklee.py
Original file line number Diff line number Diff line change
@@ -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
65 changes: 65 additions & 0 deletions merge-two-sorted-lists/haklee.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""TC: O(n), SC: -

n은 주어진 두 리스트의 길이 중 큰 값
Copy link
Contributor

Choose a reason for hiding this comment

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

haklee님의 n 정의로도 납득 가능한 설명이지만 개인적으로는 n을 두 리스트의 길이의 총합으로 두는 것이 자연스럽지 않을까 싶습니다

Copy link
Contributor Author

Choose a reason for hiding this comment

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

덧글 남겨주신 내용 보고 제가 왜 n을 저렇게 가정했을까 다시 생각해봤는데요,

  • 실제 문제에서는 두 리스트 길이가 in the range [0, 50] 정도로만 설명되어 있었습니다.
  • 두 리스트가 주어진다는 것을 보고 서로 독립적인 절차를 거쳐서 생성된 두 개의 리스트를 받아서 무슨 처리를 하는 상황일 것이라고 상상했던것 같습니다.
  • 여기서 더 이어서, 이 두 리스트 길이가 각각 최대 50이라는 조건이 주어져 있긴 하지만, 이건 리스트를 처리하는 쪽에서 제시한 최소한의 조건이고, 두 리스트를 제공하는 쪽에서 지키는 국룰(예를 들어, 리스트 길이가 아무리 길어도 30이 넘지 않도록 만든다...)이 따로 존재할 수도 있다고 상상했습니다. 그런데 두 리스트를 제공하는 쪽이 서로 독립이라고 처음에 생각했으니, list 1은 최대 길이 n_1(단, n_1은 무조건 50 이하), list 2는 최대 길이 n_2(단, n_2는 무조건 50 이하)을 지키며, n_1, n_2는 서로에 영향을 받지 않는 값이라고 볼 수 있습니다.
  • 여기에서 n을 max(n1, n2)라고 생각하고 문제를 풀었습니다.

물론 위에는 무수히 많은 가정들을 하면서 n을 정한 것이기 때문에 n을 꼭 저렇게 둘 필요는 없습니다. 말씀하신 것처럼 n을 두 리스트 길이의 총합으로 두어도 괜찮고, 처음에 주어진 in the range [0, 50]에의 50을 n으로 두어도 괜찮을것 같습니다.

Copy link
Contributor

Choose a reason for hiding this comment

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

넵 ㅎㅎ 개인적인 의견에도 정성스러운 답변 감사합니다
배경내용 설명을 이해하기에 충분히 제공해주셔서, 사실 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
133 changes: 133 additions & 0 deletions sum-of-two-integers/haklee.py
Original file line number Diff line number Diff line change
@@ -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]