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

[forest000014] Week 06 #907

Merged
merged 9 commits into from
Jan 18, 2025
Merged

[forest000014] Week 06 #907

merged 9 commits into from
Jan 18, 2025

Conversation

forest000014
Copy link
Contributor

@forest000014 forest000014 commented Jan 14, 2025

답안 제출 문제

체크 리스트

  • 우측 메뉴에서 PR을 Projects에 추가해주세요.
  • Projects의 오른쪽 버튼(▼)을 눌러 확장한 뒤, Week를 현재 주차로 설정해주세요.
  • 바로 앞에 PR을 열어주신 분을 코드 검토자로 지정해주세요.
  • 문제를 모두 푸시면 프로젝트에서 StatusIn Review로 설정해주세요.
  • 코드 검토자 1분 이상으로부터 승인을 받으셨다면 PR을 병합해주세요.

@forest000014 forest000014 self-assigned this Jan 14, 2025
@forest000014 forest000014 requested a review from a team as a code owner January 14, 2025 15:02
@github-actions github-actions bot added the java label Jan 14, 2025
@forest000014
Copy link
Contributor Author

forest000014 commented Jan 14, 2025

Design Add and Search Words Data Structure 문제에서, AlgoDale 풀이의 풀이 3(Trie 2)과 동일한 접근으로 풀었습니다 (제가 맞게 이해했다면요 😅)

AlgoDale 풀이에서는 시간 복잡도를 O(26^w)라고 적어주셨는데요, 저는 .이 최대 2번만 나오기 때문에, 시간 복잡도가 O(26^2 * w) = O(w)라고 생각되어서요... 만약 .이 word 길이만큼 등장할 수 있다면 O(26^w)가 맞겠지만, 2번이라는 제한이 있어서 좀 다르게 볼 수 있지 않을까요?
(풀이를 다시 읽어보니 .이 최대 2번 등장한다는 제한 조건 없이 설명해주신 것 같은데, 혹시 그 사이에 문제의 제한 조건이 변경되었을까용 🤔)

@forest000014
Copy link
Contributor Author

Container With Most Water 문제는 열심히 고민해서 나름대로 잘 풀었다고(O(nlogn)으로) 생각했는데, 풀이를 보니 O(n)의 접근이 가능했었네요 (조금은 허탈..ㅎㅎ)
2 포인터를 쓸 수 있는 근거에 대해서 스스로 명확히 납득이 될 만한 논리를 좀 고민해봐야 할 것 같습니다 😄

@forest000014
Copy link
Contributor Author

forest000014 commented Jan 14, 2025

(셀프 피드백) 다른 분 (bus710님) 코드를 보니, Valid Parentheses 문제에서 문자열 길이가 홀수인 경우는 굳이 탐색할 필요 없이 false로 early return하면 더 좋을 것 같네요

@forest000014
Copy link
Contributor Author

(셀프 피드백) 호돌이님 코드👍를 보고, Design Add and Search Words Data Structure 문제의 addWord() 구현에서, curr.ends = true로 세팅하는 코드를 좀 더 간결하게 수정했습니다.

*/
class Solution {
public boolean isValid(String s) {
Stack<Character> st = new Stack<>();
Copy link
Contributor

Choose a reason for hiding this comment

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

안녕하세요! 제가 자바를 거의 써보지 않아서 리뷰가 정확하지 않을 수 있는 점 양해 부탁드립니다 :) 꼭 모두 고치지 않으셔도 되고, 참고만 해주세요!

혹시 ArrayDeque를 사용하면 Stack보다 성능이 더 나아질까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ArrayDeque은 잘 몰랐는데, 말씀해주신 덕분에 좀 찾아보았습니다.

시간 복잡도 측면에서는 ArrayDeque이나 Stack 모두 push/pop이 O(1)이고, 공간 복잡도도 O(n)으로 동일한 것 같습니다.
(capacity가 가득 찼을 때 특별한 설정이 없으면 둘 다 2배씩 resize하기 때문에, push()의 시간 복잡도가 amortized O(1)이 된다고 이해했습니다.)

다만, Stack은 Vector를 상속받았는데, Vector는 동기화 때문에 주요 메소드들의 속도가 좀 더 느리다고 하네요. 다음 번에 Stack 쓸 일이 있을 땐, ArrayDeque을 써봐야겠습니다 👍

}

for (char ch : s.toCharArray()) {
if (ch == '(' || ch == '{' || ch == '[') {
Copy link
Contributor

Choose a reason for hiding this comment

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

각 pair들을 담은 Map을 사용하면 조건문을 많이 줄일 수 있을 것 같습니다.

@forest000014
Copy link
Contributor Author

Container With Most Water를 먼저 풀고 나서 Longest Increasing Subsequence를 풀어서인지, 왠지 비슷한 결의 풀이가 나왔네요. 인덱스 트리를 사용했는데, 구현량이 좀 있다보니 실전에서 실수 없이 짤 수 있을지 고민이 됩니다...ㅎㅎ

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

Choose a reason for hiding this comment

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

또한 LIS 문제는 널리 알려진 효율적인 알고리즘이 wikipedia에 소개되어 있으므로 관심 있으시다면 일독 추천드립니다
https://en.wikipedia.org/wiki/Longest_increasing_subsequence#Efficient_algorithms

아래 링크는 제가 2기로 참여할 당시에 wiki를 읽고 혼자 이해해보려고 정리한 내용입니다
https://github.com/DaleStudy/leetcode-study/blob/main/longest-increasing-subsequence/obzva.cpp

Copy link
Contributor Author

@forest000014 forest000014 Jan 17, 2025

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.

링크까지 남겨주셔서 감사합니다 :) 읽어보고 LIS는 확실히 정리해 두어야겠어요

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.

제 풀이를 주석에 추가 했습니다! 코드를 바로 머지할 거라, 혹시나 싶어서 코멘트로도 남겨놓겠습니다.

이 문제는 DP로 접근했습니다.

LIS(0, x)를 범위 [0, x] 내의 LIS(단, nums[x]를 반드시 포함)의 길이라고 정의하겠습니다. --- (1)
1 <= j < i 인 모든 j에 한 LIS(1, j)를 알고 있다면, LIS(0, i)는 아래와 같이 구할 수 있습니다.
LIS(0, i) = max(LIS(0, j)) (단, j는 0 <= j < i 이고, nums[j] < nums[i]) --- (2)

max(LIS(0, j))를 구할 때, 모든 j에 대해 탐색한다면, 전체 시간 복잡도는 O(n^2)가 되기 때문에, 시간 복잡도를 줄일 필요가 있습니다.
이 탐색 과정을 줄이기 위해, 아래의 사고 과정을 거쳤습니다.

어떤 범위 내의 가장 큰 값을 O(logn) 시간에 구하기 위한 자료구조로, 인덱스 트리(혹은 세그먼트 트리)를 사용합니다.
(이 인덱스 트리의 x번째 leaf 노드에는 LIS(0, x) 값을 저장하고, internal 노드에는 자식 노드들 중 가장 큰 값을 저장합니다.)

다만, 단순히 해당 범위 내의 가장 큰 값을 구하는 것만으로는 부족하고, nums[j] < nums[i]인 j만을 후보로 삼아야 할 텐데요,
그러기 위해서, 인덱스 트리에 모든 leaf 노드를 미리 삽입해두는 것이 아니라 아래처럼 순차적으로 max(LIS(0, i))의 계산과 삽입을 번갈아 수행합니다.
nums[i]의 크기가 작은 것부터 순서대로, "max(LIS(0, j))를 계산하고, leaf를 하나 삽입"하는 과정을 반복합니다.
nums[i]보다 더 큰 값은 아직 인덱스 트리에 삽입되지 않은 상태이기 때문에, 인덱스 트리에서 구간 [0, i-1]의 최대값을 조회하면 nums[j] < num[i]인 j에 대해서만 최대값을 찾게 되므로, (2)번 과정을 O(logn) 시간에 구할 수 있습니다.
따라서 전체 시간 복잡도는 O(nlogn)이 됩니다.

Comment on lines +46 to +47
2 포인터를 활용하면, PQ도 없이 시간 복잡도를 O(n)으로 줄일 수 있었다.
단순히 "큰 쪽을 줄이기보다는, 작은 쪽을 줄이는 게 유리하겠지" 정도의 greedy한 논리는 충분하지 않은 것 같고, 더 명확한 근거가 있을 것 같은데 시간 관계상 고민해보지는 못했다.
Copy link
Contributor

@obzva obzva Jan 17, 2025

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])

  1. container에 담기는 물의 양 v는 v = w * h입니다

  2. l, r의 초기값을 각각 0과 len(height) - 1로 설정합니다

  3. 알고리즘을 전개함에 따라 우린 l, r 사이의 간격 즉 밑변 w를 줄여나가게 됩니다

  4. w는 알고리즘을 전개할수록 감소하는데, v가 증가하려면 (기존에 계산했던 v보다 더 큰 v를 찾으려면) h가 증가할 수 있도록 해야 합니다

  5. h는 min(height[l], height[r])이므로 h 값을 증가시키기 위해서는 height[l], height[r] 중 더 작은 쪽을 변경해야 합니다 (그렇다고 h가 무조건 증가하는 것은 아니지만, 이렇게 해야 h가 증가할 가능성이 있다는 뜻입니다)

따라서 작은 쪽을 줄이는게 유리합니다

Copy link
Contributor Author

Choose a reason for hiding this comment

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

자세한 설명 감사드립니다!!
실은 obzva님의 설명처럼, 매번 선택할 때마다 작은 쪽을 줄이면 항상 증가하는(혹은 동일한) 선택을 하게 되니 정답을 찾을 수 있다는 말이 처음에는 맞는 말인 것처럼 보였습니다.
하지만 매번 증가(혹은 동일한)하는 방향으로 greedy한 선택을 하는 것이, 과연 최적해를 보장해줄까- 하는 의문이 들어서요. 만에 하나, '한두번은 감소하는 선택을 한 뒤에, 그 뒤에 증가하는 선택을 하다보면 오히려 더 큰 답을 찾을 수 있는 케이스가 발생할 수 있을까?', '이 2 포인터 로직이 그런 케이스를 배제한다는 것을 어떻게 수학적으로 엄밀하게 증명하지?' 이런 의문이었던 것 같습니다!

Copy link
Contributor

@obzva obzva Jan 17, 2025

Choose a reason for hiding this comment

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

하지만 매번 증가(혹은 동일한)하는 방향으로 greedy한 선택을 하는 것이, 과연 최적해를 보장해줄까- 하는 의문이 들어서요. 만에 하나, '한두번은 감소하는 선택을 한 뒤에, 그 뒤에 증가하는 선택을 하다보면 오히려 더 큰 답을 찾을 수 있는 케이스가 발생할 수 있을까?'

이 부분을 짚고 넘어가야할 것 같은데요, 우리는 매번 증가하거나 동일한 값을 유지하는 방향으로 선택하지 않습니다.
즉 이 투포인터 알고리즘이 찾아내는 값들은 단조증가하지 않아요.

정확히 말하면, ij 중에서 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 님 말씀처럼 한 두번은 감소하는 선택이 이뤄질 수도 있다가 나중에 확 증가하는 값을 찾아서 문제에서 원하는 값을 찾게 되는 경우가 있을 수도 있죠.

제 조심스러운 생각으로는 해당 알고리즘의 전개 과정을 잘못 이해하시고 계신 것 같아요

Copy link
Contributor

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 님 말씀처럼 한 두번은 감소하는 선택이 이뤄질 수도 있다가 나중에 확 증가하는 값을 찾아서 문제에서 원하는 값을 찾게 되는 경우가 있을 수도 있죠.

이 부분이 말로만 적어놓으면 제가 읽기에도 좀 헷갈리는 것 같아서 다시 수식으로 적어놓으려 합니다

area(x, y)를 이렇게 정의하자
    area(x, y) = (y - x) * min(heights[x], heights[y]) (0 <= x < y < n)

구간 [0, n) 사이의 임의의 원소 i, j (i < j)에 대하여, i와 j 사이에서 담을 수 있는 물의 양은 다음과 같다
    area(i, j) = (j - i) * min(heights[i], heights[j]) --- (1)

heights[i] <= heights[j]인 상황이라고 가정하자 --- (2)

i < k < j 인 임의의 원소 k에 대해 --- (3) 다음이 성립한다
    area(i, k) = (k - i) * min(heights[i], heights[k]) --- (4)

min(heights[i], heights[k])은 heights[i]를 넘어설 수 없으므로 (4)로부터 다음이 성립한다
    area(i, k) <= (k - i) * heights[i] --- (5)

(3)으로부터 다음이 성립한다
    k - i < j - i --- (6)

(5), (6)으로부터 다음이 성립한다
    area(i , k) < (j -i) * heights[i] = area(i, j)

따라서 i와 j 중 `i`를 유지하는 방향으로 알고리즘을 전개하면 area(i, j)보다 더 큰 area를 찾을 가능성이 전혀 없다

그러므로 우린 area(i, j)를 탐색한 후에 area(i + 1, j)를 탐색하여야 한다

하지만 이러한 방향의 탐색이 area(i, j)보다 더 큰 area(k, j)를 찾는 것을 보장하진 않는다

Copy link
Contributor Author

Choose a reason for hiding this comment

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

상세한 설명 감사합니다!
설명해주신 내용 덕분에 이해가 되었습니다.

  1. 단조증가하지 않는 점에 대해서는 제가 잘못 이해하고 있었습니다

작은 쪽의 포인터를 이동시켰을 때 넓이가 줄어들 수도 있는데, 이 부분은 제대로 생각하지 않고 잘못된 표현을 했네요

  1. greedy한 선택이라고 생각했던 이유

그럼에도 불구하고, 원래 'greedy한 선택처럼 보이는 이 로직이 정답을 놓칠 가능성이 없는 근거'에 대해 확신을 가지지 못했던 이유는 여전히 있는데요,
AlgoDale 풀이에서 이런 표현이 있었습니다

두 개의 높이 중 높은 쪽을 줄이면 넓이는 무조건 전보다 작아지게 됩니다. 왜냐하면 수조의 높이는 두 수 중에 낮은 값이 결정이 되고 너비는 무조건 1이 줄이들기 때문입니다.

이 설명을 보고, 마치 greedy하게 선택하는 것처럼 이해했던 것 같습니다. 'i < k < j 인 모든 k에 대해서 탐색할 필요가 없다'라는 게 아니라, '바로 다음 1번의 이동만 greedy하게 고려했을 때, 낮은 쪽을 이동하는 게 좋은 거야'처럼 이해했었습니다
(혹시라도 오해하실까 말씀드리자면, 풀이가 잘못되었다고 하는 것이 아니라 제가 이 부분을 그렇게 해석했다는 의미입니다..! 풀이는 매번 감사하게 잘 보고 있습니다 😄 좀 더 찬찬히 고민해서 'i < k < j 인 모든 k에 대해서 탐색할 필요가 없다'라는 관찰이 숨어있다는 걸 파악했어야 했는데, 제가 충분히 고민해보지 못했네요)

  1. obzva님의 마지막 코멘트 중 아래 부분 덕분에 제가 궁금해했던 부분이 해소되었습니다

(5), (6)으로부터 다음이 성립한다
area(i , k) < (j -i) * heights[i] = area(i, j)
따라서 i와 j 중 i를 유지하는 방향으로 알고리즘을 전개하면 area(i, j)보다 더 큰 area를 찾을 가능성이 전혀 없다

어찌보면 당연한 건데, 이 부분을 놓치고 계속 'greedy한 거 아닌가...?' 하면서 고민하고 있었네요

제가 부족해서 잘 이해하지 못했던 부분인데, 주말 아침부터 긴 시간 쓰시게 만든 것 같습니다 😭
정성껏 코멘트 남겨주셔서 정말 감사합니다!! 🙇

Copy link
Contributor

@obzva obzva Jan 18, 2025

Choose a reason for hiding this comment

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

캐나다는 금요일 낮시간이었어서 괜찮습니다!! ㅎㅎㅎ
다음에도 또 재밌는 논의할 기회가 있으면 좋겠습니다 마구 소환해주세요~!

Copy link
Contributor

Choose a reason for hiding this comment

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

느낌 혹은 직관적으로는 이해되지만 수학적으로 엄밀하게 정의하기는 난이도가 높은 문제들이 종종 있는데, 저도 그럴 때마다 고민이 깊었던 기억이 납니다..
자세가 너무 멋있습니다 화이팅!

@@ -0,0 +1,35 @@
/*
Time Complexity: O(m * n)
Space Complexity: O(1)
Copy link
Contributor

Choose a reason for hiding this comment

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

input으로 주어진 2차원 배열 matrix와 정답으로 return하는 배열 ans를 공간복잡도 계산에 포함시키지 않아야 O(1)이란 분석 결과가 나올텐데, 이러한 조건 하에서 분석하신 것 맞을까요?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

넵 input과 output은 제외해서 계산했습니다. 그런데 말씀을 듣고 나니 제외하는 것이 과연 맞는 것인지... 한번 고민하게 되네요.
leetcode discuss의 댓글 중, "output은 어떤 알고리즘을 택하든 동일하니 공간 복잡도 계산에저 제외해야 한다" 라는 의견이 있는데, 이 의견이 가장 설득력있게 느껴졌습니다 😄 다만, 인터뷰 중이라면 인터뷰어와 명확하게 논의를 해보는 것이 좋을 것 같습니다!

Copy link
Contributor

Choose a reason for hiding this comment

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

ㅎㅎㅎ 지금처럼 의도가 명확하시다면 전혀 문제가 없을 것으로 판단 됩니다! 수고하셨습니다

@forest000014 forest000014 merged commit 015ee61 into DaleStudy:main Jan 18, 2025
1 check passed
@forest000014 forest000014 changed the title [forest000014] Week 6 [forest000014] Week 06 Feb 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Completed
Development

Successfully merging this pull request may close these issues.

3 participants