-
-
Notifications
You must be signed in to change notification settings - Fork 195
[forest000014] Week 06 #907
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
Changes from all commits
5460104
40aff0f
daa753f
a59d354
7ae4344
f848beb
4d49d49
8d3745d
0b38772
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
/* | ||
solution 1. brute force | ||
Time Complexity: O(n^2) | ||
Space Complexity: O(1) | ||
모든 선분 i에 대해, 가장 많은 물을 담을 수 있는 반대쪽 선분 j를 찾는다. | ||
|
||
|
||
solution 2. PQ | ||
Time Complexity: O(nlogn) | ||
- 정렬 : O(nlogn) | ||
- i번째 선분에 대해 (자신보다 크거나 같은) 가장 멀리있는 선분 탐색(O(logn)) * n = O(nlogn) | ||
Space Complexity: O(n) | ||
|
||
(사고의 흐름을 적기 위해 편의상 반말로 적었습니다... ^^) | ||
brute force 에서 불필요한 탐색을 줄여보자. | ||
일단 어떤 선분 i가 주어졌다고 가정하자. i를 한쪽 벽으로 해서 가장 많은 물을 담는 방법을 찾으려면, 반드시 나머지 모든 선분을 탐색해야만 할까? | ||
|
||
(1) 자신보다 작은 선분은 탐색하지 않는다. | ||
|
||
자신보다 큰 선분만을 탐색해도 충분하다. | ||
왜냐하면, 설령 자신보다 더 작은 선분 중에 정답이 있었다고 하더라도, 그 선분을 기준으로 탐색할 때 정답에 해당하는 쌍을 확인하게 될 것이기 때문이다. | ||
|
||
(2) 자신보다 크거나 같은 선분만을 탐색 대상으로 삼는다면, 가장 멀리있는 선분만 확인하면 된다. | ||
|
||
탐색 대상이 자신보다 크거나 같은 선분들이라면, 어차피 담을 수 있는 물의 높이는 자신의 높이로 일정하다. | ||
따라서, 멀리있는 선분일수록 더 많은 물을 담게 된다. | ||
즉, "자신보다 크거나 같으면서", "가장 멀리 있는(오른쪽이든 왼쪽이든)" 선분만을 후보로 삼아 확인하면 충분하다. | ||
(그 외의 나머지, 즉 자신보다 크거나 같으면서, 가장 멀리있지 않은 나머지 선분들은, 자연스럽게 탐색하지 않을 수 있다.) | ||
|
||
정리하자면, 주어진 선분 i에 대해서, 단 2번(오른쪽, 왼쪽)의 탐색만 하면 충분하다. | ||
|
||
(3) 내림차순 정렬과 2개의 PQ를 아래처럼 활용하면, 위 탐색을 O(logn) 시간에 수행할 수 있다. | ||
PQ는 각각 x 좌표 기준 max heap(가장 오른쪽의 선분을 찾기 위함), x 좌표 기준 min heap(가장 왼쪽의 선분을 찾기 위함)을 사용한다. | ||
선분들을 길이 내림차순으로 정렬해놓고, 하나씩 순회하면서 (say, i번째 선분), 아래 과정을 반복한다. | ||
- 자신보다 크거나 같으면서 가장 오른쪽으로 멀리 있는 선분의 위치를 찾아서(= 현재 PQ(max heap)의 root), 최대 물의 양을 계산한다. | ||
- 자신보다 크거나 같으면서 가장 왼쪽으로 멀리 있는 선분의 위치를 찾아서(= 현재 PQ(min heap)의 root), 최대 물의 양을 계산한다. | ||
- i번째 선분을 PQ 2개에 각각 넣는다. | ||
|
||
|
||
solution 3. two pointers | ||
(AlgoDale 풀이를 참고함) | ||
|
||
Time Complexity: O(n) | ||
Space Complexity: O(1) | ||
|
||
2 포인터를 활용하면, PQ도 없이 시간 복잡도를 O(n)으로 줄일 수 있었다. | ||
단순히 "큰 쪽을 줄이기보다는, 작은 쪽을 줄이는 게 유리하겠지" 정도의 greedy한 논리는 충분하지 않은 것 같고, 더 명확한 근거가 있을 것 같은데 시간 관계상 고민해보지는 못했다. | ||
|
||
To-Do : 풀이가 대강 이해는 되었지만, 이게 왜 되는지, 엄밀하게 이해하기 위해 PQ를 사용했던 논리를 좀 더 발전시켜볼 필요가 있다. | ||
|
||
*/ | ||
class Solution { | ||
public int maxArea(int[] height) { | ||
int i = 0, j = height.length - 1; | ||
|
||
int ans = 0; | ||
while (i < j) { | ||
int area = (j - i) * Math.min(height[i], height[j]); | ||
if (area > ans) { | ||
ans = area; | ||
} | ||
|
||
if (height[i] <= height[j]) { | ||
i++; | ||
} else { | ||
j--; | ||
} | ||
} | ||
|
||
return ans; | ||
} | ||
|
||
////// 아래는 solution 2 | ||
private static class Tuple { | ||
private int x; | ||
private int h; | ||
|
||
Tuple(int x, int h) { | ||
this.x = x; | ||
this.h = h; | ||
} | ||
} | ||
|
||
public int maxArea2(int[] height) { | ||
List<Tuple> tuples = IntStream.range(0, height.length) | ||
.mapToObj(i -> new Tuple(i, height[i])) | ||
.collect(Collectors.toList()); | ||
Collections.sort(tuples, (a, b) -> b.h - a.h); | ||
|
||
PriorityQueue<Tuple> minPq = new PriorityQueue<>((a, b) -> a.x - b.x); | ||
PriorityQueue<Tuple> maxPq = new PriorityQueue<>((a, b) -> b.x - a.x); | ||
|
||
int ans = 0; | ||
for (int i = 0; i < height.length; i++) { | ||
minPq.add(tuples.get(i)); | ||
maxPq.add(tuples.get(i)); | ||
|
||
Tuple curr = tuples.get(i); | ||
|
||
Tuple left = minPq.peek(); | ||
int leftArea = (curr.x - left.x) * curr.h; | ||
if (leftArea > ans) { | ||
ans = leftArea; | ||
} | ||
|
||
Tuple right = maxPq.peek(); | ||
int rightArea = (right.x - curr.x) * curr.h; | ||
if (rightArea > ans) { | ||
ans = rightArea; | ||
} | ||
} | ||
|
||
return ans; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
/* | ||
Time Complexity: | ||
- add: O(w) | ||
- search: O(26^2 * w) = O(w) | ||
Space Complexity: O(w) | ||
|
||
Trie를 활용하되, '.'의 탐색이 필요한 경우에는 for문을 사용한다. | ||
|
||
*/ | ||
class WordDictionary { | ||
class Node { | ||
public char ch; | ||
public boolean ends; | ||
public Map<Character, Node> children; | ||
|
||
Node() { | ||
this.children = new HashMap<>(); | ||
} | ||
|
||
Node(char ch) { | ||
this.ch = ch; | ||
this.children = new HashMap<>(); | ||
} | ||
} | ||
|
||
Node root; | ||
|
||
public WordDictionary() { | ||
this.root = new Node(); | ||
} | ||
|
||
public void addWord(String word) { | ||
Node curr = this.root; | ||
|
||
for (int i = 0; i < word.length(); i++) { | ||
char ch = word.charAt(i); | ||
if (!curr.children.containsKey(ch)) { | ||
curr.children.put(ch, new Node(ch)); | ||
} | ||
|
||
curr = curr.children.get(ch); | ||
} | ||
|
||
curr.ends = true; | ||
} | ||
|
||
public boolean search(String word) { | ||
return searchChar(word, 0, this.root); | ||
} | ||
|
||
private boolean searchChar(String word, int idx, Node curr) { | ||
if (curr == null) { | ||
return false; | ||
} else if (idx == word.length()) { | ||
return curr.ends; | ||
} | ||
|
||
char ch = word.charAt(idx); | ||
|
||
if (ch == '.') { | ||
for (Character key : curr.children.keySet()) { | ||
if (searchChar(word, idx + 1, curr.children.get(key))) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} else { | ||
return searchChar(word, idx + 1, curr.children.get(ch)); | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* Your WordDictionary object will be instantiated and called as such: | ||
* WordDictionary obj = new WordDictionary(); | ||
* obj.addWord(word); | ||
* boolean param_2 = obj.search(word); | ||
*/ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
/* | ||
# Time Complexity: O(nlogn) | ||
- tuples ArrayList 정렬: O(nlogn) | ||
- 인덱스 트리 삽입: O(logn) * n times = O(nlogn) | ||
- 인덱스 트리 조회: O(logn) * n times = O(nlogn) | ||
|
||
# Space Complexity: O(nlogn) | ||
- tuples ArrayList: O(n) | ||
- 인덱스 트리: O(nlogn) | ||
|
||
# solution | ||
이 문제는 DP로 접근했습니다. | ||
|
||
LIS(1, x)를 범위 [1, x] 내의 LIS(단, nums[x]를 반드시 포함)의 길이라고 정의하겠습니다. - (1) | ||
1 <= j < i 인 모든 j에 한 LIS(1, j)를 알고 있다면, LIS(1, i)는 아래와 같이 구할 수 있습니다. | ||
LIS(1, i) = max(LIS(1, j)) (단, j는 1 <= j < i 이고, nums[j] < nums[i]) - (2) | ||
|
||
max(LIS(1, j))를 구할 때, 모든 j에 대해 탐색한다면, 전체 시간 복잡도는 O(n^2)가 되기 때문에, 시간 복잡도를 줄일 필요가 있습니다. | ||
이 탐색 과정을 줄이기 위해, 아래의 사고 과정을 거쳤습니다. | ||
|
||
어떤 범위 내의 가장 큰 값을 O(logn) 시간에 구하기 위한 자료구조로, 인덱스 트리(혹은 세그먼트 트리)를 사용합니다. | ||
(이 인덱스 트리의 x번째 leaf 노드에는 LIS(1, x) 값을 저장하고, internal 노드에는 자식 노드들 중 가장 큰 값을 저장합니다.) | ||
|
||
다만, 단순히 해당 범위 내의 가장 큰 값을 구하는 것만으로는 부족하고, nums[j] < nums[i]인 j만을 후보로 삼아야 할 텐데요, | ||
그러기 위해서, 인덱스 트리에 모든 leaf 노드를 미리 삽입해두는 것이 아니라 아래처럼 순차적으로 max(LIS(1, i))의 계산과 삽입을 번갈아 수행합니다. | ||
nums[i]의 크기가 작은 것부터 순서대로, "max(LIS(1, j))를 계산하고, leaf를 하나 삽입"하는 과정을 반복합니다. | ||
nums[i]보다 더 큰 값은 아직 인덱스 트리에 삽입되지 않은 상태이기 때문에, 인덱스 트리에서 구간 [1, i-1]의 최대값을 조회하면 nums[j] < num[i]인 j에 대해서만 최대값을 찾게 되므로, | ||
(2)번 과정을 O(logn) 시간에 구할 수 있습니다. | ||
따라서 전체 시간 복잡도는 O(nlogn)이 됩니다. | ||
*/ | ||
class Solution { | ||
|
||
int[] tree; | ||
int L = 1; | ||
|
||
public int lengthOfLIS(int[] nums) { | ||
init(nums); | ||
ArrayList<Tuple> tuples = new ArrayList<>(); | ||
|
||
for (int i = 0; i < nums.length; i++) { | ||
tuples.add(new Tuple(i, nums[i])); | ||
} | ||
|
||
Collections.sort(tuples, (a, b) -> { | ||
if (a.val == b.val) { | ||
return b.ref - a.ref; // 2순위 : ref 내림차순 | ||
} else { | ||
return a.val - b.val; // 1순위 : val 오름차순 | ||
} | ||
}); | ||
|
||
int ans = 0; | ||
for (int i = 0; i < nums.length; i++) { | ||
int curr = getMax(0, tuples.get(i).ref - 1) + 1; | ||
ans = Math.max(ans, curr); | ||
insert(tuples.get(i).ref, curr); | ||
} | ||
|
||
return ans; | ||
} | ||
|
||
public class Tuple { | ||
public int ref; | ||
public int val; | ||
|
||
public Tuple(int ref, int val) { | ||
this.ref = ref; | ||
this.val = val; | ||
} | ||
} | ||
|
||
public void init(int[] nums) { | ||
while (L < nums.length) { | ||
L *= 2; | ||
} | ||
|
||
tree = new int[L * 2]; | ||
|
||
for (int i = 1; i < L * 2; i++) { | ||
tree[i] = 0; | ||
} | ||
} | ||
|
||
public void insert(int idx, int v) { | ||
int i = idx + L; | ||
tree[i] = v; | ||
i /= 2; | ||
while (i >= 1) { | ||
tree[i] = Math.max(tree[i * 2], tree[i * 2 + 1]); | ||
i /= 2; | ||
} | ||
} | ||
|
||
public int getMax(int l, int r) { | ||
int i = l + L; | ||
int j = r + L; | ||
int ret = 0; | ||
while (i <= j) { | ||
if (i % 2 == 1) { | ||
ret = Math.max(ret, tree[i++]); | ||
} | ||
if (j % 2 == 0) { | ||
ret = Math.max(ret, tree[j--]); | ||
} | ||
i /= 2; | ||
j /= 2; | ||
} | ||
|
||
return ret; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
/* | ||
Time Complexity: O(m * n) | ||
Space Complexity: O(1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. input으로 주어진 2차원 배열 matrix와 정답으로 return하는 배열 ans를 공간복잡도 계산에 포함시키지 않아야 O(1)이란 분석 결과가 나올텐데, 이러한 조건 하에서 분석하신 것 맞을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 input과 output은 제외해서 계산했습니다. 그런데 말씀을 듣고 나니 제외하는 것이 과연 맞는 것인지... 한번 고민하게 되네요. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ㅎㅎㅎ 지금처럼 의도가 명확하시다면 전혀 문제가 없을 것으로 판단 됩니다! 수고하셨습니다 |
||
|
||
현재 위치를 r, c라는 변수를 사용해서 나타내고, 이 r, c를 직접 제어하는 방식으로 행렬을 순회한다. | ||
*/ | ||
class Solution { | ||
public List<Integer> spiralOrder(int[][] matrix) { | ||
List<Integer> ans = new ArrayList<>(); | ||
|
||
int m = matrix.length, n = matrix[0].length; | ||
int r = 0, c = 0; | ||
int[] dr = {0, 1, 0, -1}; | ||
int[] dc = {1, 0, -1, 0}; | ||
int d = 0; | ||
final int VISITED = -999; | ||
|
||
while (ans.size() < m * n) { | ||
ans.add(matrix[r][c]); | ||
matrix[r][c] = VISITED; | ||
|
||
int nr = r + dr[d]; | ||
int nc = c + dc[d]; | ||
if (nr < 0 || nr >= m || nc < 0 || nc >= n || matrix[nr][nc] == VISITED) { | ||
d = (d + 1) % 4; | ||
nr = r + dr[d]; | ||
nc = c + dc[d]; | ||
} | ||
r = nr; | ||
c = nc; | ||
} | ||
|
||
return ans; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
/* | ||
Time Complexity: O(n) | ||
Space Complexity: O(n) | ||
|
||
유효한 문자열이라면, 인접한 열고 닫는 괄호 쌍을 하나씩 캔슬시켰을 때, 빈 문자열만 남게 된다. | ||
매치되는 쌍이 서로 떨어져있을 수 있기 때문에, 그 안의 유효한 쌍들을 미리 모두 캔슬시킨 뒤에 판단해야 매칭 여부를 쉽게 판단할 수 있는데, 이 과정을 스택을 이용해 구현할 수 있다. | ||
*/ | ||
class Solution { | ||
public boolean isValid(String s) { | ||
Stack<Character> st = new Stack<>(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 안녕하세요! 제가 자바를 거의 써보지 않아서 리뷰가 정확하지 않을 수 있는 점 양해 부탁드립니다 :) 꼭 모두 고치지 않으셔도 되고, 참고만 해주세요! 혹시 ArrayDeque를 사용하면 Stack보다 성능이 더 나아질까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ArrayDeque은 잘 몰랐는데, 말씀해주신 덕분에 좀 찾아보았습니다. 시간 복잡도 측면에서는 ArrayDeque이나 Stack 모두 push/pop이 O(1)이고, 공간 복잡도도 O(n)으로 동일한 것 같습니다. 다만, Stack은 Vector를 상속받았는데, Vector는 동기화 때문에 주요 메소드들의 속도가 좀 더 느리다고 하네요. 다음 번에 Stack 쓸 일이 있을 땐, ArrayDeque을 써봐야겠습니다 👍 |
||
|
||
if (s.length() % 2 == 1) { | ||
return false; | ||
} | ||
|
||
for (char ch : s.toCharArray()) { | ||
if (ch == '(' || ch == '{' || ch == '[') { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 각 pair들을 담은 Map을 사용하면 조건문을 많이 줄일 수 있을 것 같습니다. |
||
st.push(ch); | ||
} else { | ||
if (st.empty()) | ||
return false; | ||
if (ch == ')') { | ||
if (st.peek() != '(') | ||
return false; | ||
} else if (ch == '}') { | ||
if (st.peek() != '{') | ||
return false; | ||
} else { | ||
if (st.peek() != '[') | ||
return false; | ||
} | ||
st.pop(); | ||
} | ||
} | ||
return st.empty(); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
안녕하세요 제가 도움이 될 수 있으면 좋겠습니다 :)
l, r: 왼쪽 오른쪽 포인터의 인덱스
w: l, r 사이의 간격 즉, container의 밑변
h: min(height[l], height[r])
container에 담기는 물의 양 v는 v = w * h입니다
l, r의 초기값을 각각 0과 len(height) - 1로 설정합니다
알고리즘을 전개함에 따라 우린 l, r 사이의 간격 즉 밑변 w를 줄여나가게 됩니다
w는 알고리즘을 전개할수록 감소하는데, v가 증가하려면 (기존에 계산했던 v보다 더 큰 v를 찾으려면) h가 증가할 수 있도록 해야 합니다
h는 min(height[l], height[r])이므로 h 값을 증가시키기 위해서는 height[l], height[r] 중 더 작은 쪽을 변경해야 합니다 (그렇다고 h가 무조건 증가하는 것은 아니지만, 이렇게 해야 h가 증가할 가능성이 있다는 뜻입니다)
따라서 작은 쪽을 줄이는게 유리합니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
자세한 설명 감사드립니다!!
실은 obzva님의 설명처럼, 매번 선택할 때마다 작은 쪽을 줄이면 항상 증가하는(혹은 동일한) 선택을 하게 되니 정답을 찾을 수 있다는 말이 처음에는 맞는 말인 것처럼 보였습니다.
하지만 매번 증가(혹은 동일한)하는 방향으로 greedy한 선택을 하는 것이, 과연 최적해를 보장해줄까- 하는 의문이 들어서요. 만에 하나, '한두번은 감소하는 선택을 한 뒤에, 그 뒤에 증가하는 선택을 하다보면 오히려 더 큰 답을 찾을 수 있는 케이스가 발생할 수 있을까?', '이 2 포인터 로직이 그런 케이스를 배제한다는 것을 어떻게 수학적으로 엄밀하게 증명하지?' 이런 의문이었던 것 같습니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분을 짚고 넘어가야할 것 같은데요, 우리는
매번 증가하거나 동일한 값을 유지하는 방향으로 선택
하지 않습니다.즉 이 투포인터 알고리즘이 찾아내는 값들은 단조증가하지 않아요.
정확히 말하면,
i
와j
중에서height[i] <= height[j]
인i
를 그대로 유지하고k ( i < k < j)
을 선택하는 방향으로 알고리즘을 진행한다면, 모든k
에 대해area(i, k) > area(i, j)
이므로 (즉 기존에 찾았던area
보다 더 큰area
를 찾을 수 있는 가능성이 아예 없으므로)j
를 유지하고i
값을 변경해야하는데 이 때area(k, j)
는area(i, j)
보다 클 수도 있고 작을 수도 있습니다.@forest000014 님 말씀처럼 한 두번은 감소하는 선택이 이뤄질 수도 있다가 나중에 확 증가하는 값을 찾아서 문제에서 원하는 값을 찾게 되는 경우가 있을 수도 있죠.
제 조심스러운 생각으로는 해당 알고리즘의 전개 과정을 잘못 이해하시고 계신 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 부분이 말로만 적어놓으면 제가 읽기에도 좀 헷갈리는 것 같아서 다시 수식으로 적어놓으려 합니다
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
상세한 설명 감사합니다!
설명해주신 내용 덕분에 이해가 되었습니다.
작은 쪽의 포인터를 이동시켰을 때 넓이가 줄어들 수도 있는데, 이 부분은 제대로 생각하지 않고 잘못된 표현을 했네요
그럼에도 불구하고, 원래 'greedy한 선택처럼 보이는 이 로직이 정답을 놓칠 가능성이 없는 근거'에 대해 확신을 가지지 못했던 이유는 여전히 있는데요,
AlgoDale 풀이에서 이런 표현이 있었습니다
이 설명을 보고, 마치 greedy하게 선택하는 것처럼 이해했던 것 같습니다. 'i < k < j 인 모든 k에 대해서 탐색할 필요가 없다'라는 게 아니라, '바로 다음 1번의 이동만 greedy하게 고려했을 때, 낮은 쪽을 이동하는 게 좋은 거야'처럼 이해했었습니다
(혹시라도 오해하실까 말씀드리자면, 풀이가 잘못되었다고 하는 것이 아니라 제가 이 부분을 그렇게 해석했다는 의미입니다..! 풀이는 매번 감사하게 잘 보고 있습니다 😄 좀 더 찬찬히 고민해서 'i < k < j 인 모든 k에 대해서 탐색할 필요가 없다'라는 관찰이 숨어있다는 걸 파악했어야 했는데, 제가 충분히 고민해보지 못했네요)
어찌보면 당연한 건데, 이 부분을 놓치고 계속 'greedy한 거 아닌가...?' 하면서 고민하고 있었네요
제가 부족해서 잘 이해하지 못했던 부분인데, 주말 아침부터 긴 시간 쓰시게 만든 것 같습니다 😭
정성껏 코멘트 남겨주셔서 정말 감사합니다!! 🙇
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
캐나다는 금요일 낮시간이었어서 괜찮습니다!! ㅎㅎㅎ
다음에도 또 재밌는 논의할 기회가 있으면 좋겠습니다 마구 소환해주세요~!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
느낌 혹은 직관적으로는 이해되지만 수학적으로 엄밀하게 정의하기는 난이도가 높은 문제들이 종종 있는데, 저도 그럴 때마다 고민이 깊었던 기억이 납니다..
자세가 너무 멋있습니다 화이팅!