Skip to content

[forest000014] Week 08 #968

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 7 commits into from
Jan 31, 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
77 changes: 77 additions & 0 deletions clone-graph/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
# Time Complexity: O(n * e), where e is the maximum number of edges of a node
- 전체 node를 순회하면서, 그 이웃에 해당하는 복제본을 생성해서 복제본끼리 연결해준다.
- 단, 중복 방문을 막기 위해, 복제본이 이미 이웃 복제본을 가지고 있는지 확인한다. 이 과정에서 O(e)만큼의 List 순회를 한다.

# Space Complexity: O(n)
- 전체 Node 만큼의 메모리가 필요하다.
(Space Complexity를 계산하기 애매한 측면이 있네요. 저는 지금까지 출력은 space complexity에 포함하지 않고 계산했었는데, 그 이유는 "어떤 알고리즘을 구현하든 출력은 동일하기 때문"인데요. 이 문제의 경우에 출력은 Node 하나이지만, 실제로는 Node 전체만큼의 메모리를 반드시 생성해야 한다는 특수성이 있습니다. 그래서 "어떻게 구현하든 동일하게 사용해야만 하는 메모리는 Space Complexity에서 배제한다" 라는 논리로만 보자면 O(1)일 것 같고, "출력을 제외한 메모리 사용은 Space Complexity에 포함한다" 라는 논리대로라면 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.

"어떻게 구현하든 동일하게 사용해야만 하는 메모리는 Space Complexity에서 배제한다" 라는 논리로만 보자면 O(1)일 것 같고

createNode 메서드가 map에 복제 노드들을 저장하고 있으니까 출력값을 제외하더라도 O(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.

으음... 위에서 언급하신 것처럼 map에 레퍼런스만 저장되긴 하겠지만, 그래도 O(n)만큼 저장되겠네요. 복제된 노드들만 생각했는데, map은 고려하지 못했습니다 😓 obzva님 말씀대로 O(1)보다는 O(n)이 맞을 것 같습니다



전체 노드를 DFS로 순회하면서 이웃 노드의 복제본을 생성하여 현재 노드의 복제본과 연결을 맺어줍니다.
다만, 중복 방문을 막기 위해, 복제본이 이미 이웃 복제본을 가지고 있는지 확인한다.
또한 순환 참조(cycle 구조)를 막기 위해서, 복제본 노드를 생성시 단순히 new 키워드를 사용하지 않고, 별도의 map을 통해 싱글톤으로 생성한다. (각 노드의 val은 distinct하다는 점을 이용)


// Definition for a Node.
class Node {
public int val;
public List<Node> neighbors;
public Node() {
val = 0;
neighbors = new ArrayList<Node>();
}
public Node(int _val) {
val = _val;
neighbors = new ArrayList<Node>();
}
public Node(int _val, ArrayList<Node> _neighbors) {
val = _val;
neighbors = _neighbors;
}
}
*/

class Solution {

Map<Integer, Node> map = new HashMap<>();

public Node cloneGraph(Node node) {
if (node == null) {
return null;
}

Node newNode = createNode(node.val);
dfs(node);

return newNode;
}

public Node createNode(int val) {
if (!map.containsKey(val)) {
map.put(val, new Node(val));
}
return map.get(val);
}

public void dfs(Node oldNode) {
Node newNode = map.get(oldNode.val);

for (Node oldNeighbor : oldNode.neighbors) {
boolean hasIt = false;
for (Node newNeighbor : newNode.neighbors) {
if (newNeighbor.val == oldNeighbor.val) {
hasIt = true;
break;
}
}

if (!hasIt) {
Node newNeighbor = createNode(oldNeighbor.val);
newNode.neighbors.add(newNeighbor);
newNeighbor.neighbors.add(newNode);
dfs(oldNeighbor, newNeighbor);
Copy link
Member

@DaleSeo DaleSeo Feb 1, 2025

Choose a reason for hiding this comment

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

@forest000014 dfs() 함수는 인자를 하나만 받으니 여기서 컴파일 에러가 나지 않을까요? 😨 이미 PR을 병합을 하셨으니 차주 답안 제출 때 정정해주시면 좋을 것 같습니다. 참여자 분들께 최고의 답안을 기대하는 것은 아니지만 미래 기수 분들께 혼선이 없도록 적어도 실행은 되는 코드를 목표로 하고 있습니다.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

아앗..! 이 부분을 놓쳤습니다 ㅠㅠ 수정하고 제출을 해봤어야 했는데 눈으로만 보고 넘어가서 컴파일 에러를 못 찾았네요. 9주차 PR에 포함해두겠습니다!

Copy link
Member

Choose a reason for hiding this comment

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

감사합니다! 🙏

}
}
}
}
51 changes: 51 additions & 0 deletions longest-common-subsequence/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
# Time Complexity: O(m * n), where m = text1.length(), n = text2.length()
# Space Complexity: O(m * n)

DP로 접근했습니다.
text1[0..i], text2[0..j]의 LCS의 길이를 lcs[i][j]라고 정의하겠습니다. (0 <= i < m, 0 <= j < n)
lcs[i - 1][j - 1], lcs[i - 1][j], lcs[i][j - 1]을 미리 구해두었다면, lcs[i][j]는 아래처럼 O(1)에 계산할 수 있습니다.

1. text1[i] != text2[j]인 경우
lcs[i - 1][j] 를 기준으로 생각해보면, text1[i] != text2[j]이기 때문에, text1[0..i-1]에 text1[i]를 추가한다 해도, LCS의 길이에는 변화가 없습니다.
마찬가지로 lcs[i][j - 1] 을 기준으로, text2[0..j-1]에 text2[j]를 추가해도, LCS의 길이에는 변화가 없습니다.
따라서 lcs[i][j] = max(lcs[i - 1][j], lcs[i][j - 1]) 로 구할 수 있습니다.

2. text1[i] == text2[j]인 경우
이 경우에는, lcs[i - 1][j - 1]에서 구한 LCS에 1글자가 추가되므로, lcs[i][j] = lcs[i - 1][j - 1] + 1 로 구할 수 있습니다.

3. i = 0 혹은 j = 0인 경우의 예외 로직을 추가하면, LCS의 길이를 구하는 2차원 DP를 구현할 수 있습니다.

*/

class Solution {
public int longestCommonSubsequence(String text1, String text2) {
int m = text1.length();
int n = text2.length();
int[][] lcs = new int[m][n];

for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (text1.charAt(i) == text2.charAt(j)) {
if (i == 0 || j == 0) {
lcs[i][j] = 1;
} else {
lcs[i][j] = lcs[i - 1][j - 1] + 1;
}
} else {
if (i == 0 && j == 0) {
lcs[i][j] = 0;
} else if (i == 0) {
lcs[i][j] = lcs[i][j - 1];
} else if (j == 0) {
lcs[i][j] = lcs[i - 1][j];
} else {
lcs[i][j] = Math.max(lcs[i][j - 1], lcs[i - 1][j]);
}
}
}
}

return lcs[m - 1][n - 1];
}
}
50 changes: 50 additions & 0 deletions longest-repeating-character-replacement/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
Time Complexity: O(n)
Space Complexity: O(1)

투 포인터 방식으로 접근했습니다.
현재 상태에서, 두 포인터 사이의 가장 많은 문자를 제외한 나머지 문자의 개수가 k개 이하면 right를 오른쪽으로 1칸,
k개 초과면 left를 오른쪽으로 1칸씩 이동합니다.

다만, 개인적으로 에너지가 고갈된 상태에서 풀다보니,
현재 상태에서 가장 많은 문자를 카운트하는 방식을 counts[26] 배열을 순회하는 식으로 단순하게 짰습니다.
Comment on lines +9 to +10
Copy link
Contributor

Choose a reason for hiding this comment

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

고생많으셨습니다~~ 저는 배열 로직도 충분히 좋은 것 같아요 😄
다만 말씀하셨던 것처럼 mostCount가 윈도우 크기가 변경될 때만 업데이트되면 더 좋을 것 같아요~

mostCount = Math.max(mostCount, counts[s.charAt(r) - 'A'])

Copy link
Contributor Author

Choose a reason for hiding this comment

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

이렇게 하면 더 효율적이겠네요~! 나중에 보니 Dale님의 풀이에서도 O(26)으로 찾으셨더라구요 😃

PQ를 사용하면 조금 더 시간이 개선될 것 같습니다.
*/
class Solution {
public int characterReplacement(String s, int k) {
int[] counts = new int[26];

int l = 0;
int r = 0;
int mostCount = 1;

counts[s.charAt(0) - 'A'] = 1;
int ans = 0;

while (r < s.length()) {
mostCount = 0;
for (int i = 0; i < 26; i++) {
if (counts[i] > mostCount) {
mostCount = counts[i];
}
}

if (r - l + 1 - mostCount <= k) {
if (r - l + 1 > ans) {
ans = r - l + 1;
}

r++;
if (r == s.length()) {
break;
}
Comment on lines +38 to +40
Copy link
Contributor

Choose a reason for hiding this comment

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

while문이 r < s.length() 조건인 경우에만 실행되고 있어서 이 로직은 없어도 될 것 같아요!

counts[s.charAt(r) - 'A']++;
} else {
counts[s.charAt(l) - 'A']--;
l++;
}
}

return ans;
}
}
88 changes: 88 additions & 0 deletions number-of-1-bits/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
solution 1. bit operation
# Time Complexity: O(1)
- n의 사이즈는 32 bit로 일정
- 2 * 32 회 연산 필요
# Space Complexity: O(1)

bit를 하나씩 센다.


solution 2. bit operation (advanced)
# Time Complexity: O(1)
- 2 * b 회 연산 필요 (b는 1인 bit의 개수)
# Space Complexity: O(1)

n &= (n - 1) 연산을 통해, 마지막 bit를 한번에 하나씩 제거하면서 bit를 센다.
1인 bit의 개수만큼 연산을 하므로, 평균적으로 solution 1보다는 연산 횟수가 적다.


solution 3. 8-bit chunk lookup table
# Time Complexity: O(n)
- n / 8 회 연산 필요
# Space Complexity: O(1)
- 2^8 사이즈의 int 배열 사용

이진수 00000000 ~ 11111111 에 대해서, 각 수에 bit 1이 몇 개 등장하는지 미리 lookup table에 저장해둔다.
그리고 n을 8 bit 단위로 잘라서, loopup table에서 조회하여 누적해준다.
연산 횟수가 n / 8로 줄어든다는 장점이 있으나, lookup table을 미리 계산하거나 런타임에 계산해야 하고, lookup table 사이즈만큼의 메모리를 더 사용해야 한다는 트레이드 오프가 있다.


solution 4. population count 알고리즘
# Time Complexity: O(1)
- 5 회 연산 필요
# Space Complexity: O(1)

각 단계를 진행할 때마다, 2, 4, 8, 16, 32 bit chunk 안의 1 bit의 개수를 센다.


solution 5. 자바 내장 함수 Integer.bitCount() 사용
# Time Complexity: O(1)
# Space Complexity: O(1)

*/
class Solution {
// solution 1
// public int hammingWeight(int n) {
// int ans = 0;
// while (n > 0) {
// ans += (n & 1);
// n >>= 1;
// }
// return ans;
// }

// solution 2
// public int hammingWeight(int n) {
// int ans = 0;
// while (n > 0) {
// n &= (n - 1); // 최하위 1비트를 제거
// ans++;
// }
// return ans;
// }

// solution 3.
// lookup table (8-bit 단위로)
// 이 아이디어는 시간이 부족해서 구현하지 못했습니다.


// solution 4.
// population count 알고리즘
// https://blog.naver.com/jinhan814/222540111549
// http://shumin.co.kr/algorithm-hamming-weight-bit-count/
public int hammingWeight(int n) {
n = (n >> 1 & 0x55555555) + (n & 0x55555555);
n = (n >> 2 & 0x33333333) + (n & 0x33333333);
n = (n >> 4 & 0x0F0F0F0F) + (n & 0x0F0F0F0F);
n = (n >> 8 & 0x00FF00FF) + (n & 0x00FF00FF);
n = (n >> 16 & 0x0000FFFF) + (n & 0x0000FFFF);
return n;
}

// solution 5.
// 자바 내장 함수 사용 O(logn)
// public int hammingWeight(int n) {
// return Integer.bitCount(n);
// }
}
109 changes: 109 additions & 0 deletions sum-of-two-integers/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
# Time Complexity: O(n)
# Space Complexity: O(1)

음...... bit manipulation을 최대한 활용해서 했습니다.
이진법의 덧셈/뺄셈을 손으로 계산한다면, carry/borrow 개념을 활용해서 수행할 텐데, 이 과정을 그대로 코드로 옮겨보았습니다.
++은 increment operator이고, -는 unary minus operator로만 썼으니, 문제의 조건인 +, - operator를 쓰지 말라는 제약사항은 지켰다고 주장하고 싶습니다.
그런데 이렇게 지저분하게 구현하는 것은 출제자의 의도에 부합하지 않는 것 같네요. Dale님의 풀이를 보니 훨씬 간결하던데, 좀 더 공부해봐야 할 것 같습니다.
*/

class Solution {
public int getSum(int a, int b) {
if (a >= 0 && b >= 0) {
return sum(a, b);
} else if (a <= 0 && b <= 0) {
return -sum(-a, -b);
} else if (a < 0) {
if (-a >= b) {
return -subtract(-a, b);
} else {
return subtract(b, -a);
}
} else {
if (a >= -b) {
return subtract(a, -b);
} else {
return -subtract(-b, a);
}
}
}

public int sum(int a, int b) {
int sum = 0;
int carry = 0;
int bit = 0;
int digit = 0;

while (a > 0 || b > 0) {
if (((a & 1) & (b & 1) & (carry & 1)) == 1) {
carry = 1;
bit = 1;
} else if (((a & 1) | (b & 1) | (carry & 1)) == 1) {
if (((a & 1) ^ (b & 1) ^ (carry & 1)) == 0) {
carry = 1;
bit = 0;
} else {
carry = 0;
bit = 1;
}
} else {
carry = 0;
bit = 0;
}

sum |= (bit << digit);

a >>= 1;
b >>= 1;
digit++;
}

if (carry == 1) {
sum |= (1 << digit);
}

return sum;
}

public int subtract(int a, int b) {
int sub = 0;
int borrow = 0;
int bit = 0;
int digit = 0;

while (a > 0) {
if (borrow == 1) {
if ((a & 1) == (b & 1)) {
borrow = 1;
bit = 1;
} else if ((a & 1) == 1) {
borrow = 0;
bit = 0;
} else {
borrow = 1;
bit = 0;
}
} else {
if ((a & 1) == (b & 1)) {
borrow = 0;
bit = 0;
} else if ((a & 1) == 1) {
borrow = 0;
bit = 1;
} else {
borrow = 1;
bit = 1;
}
}

sub |= (bit << digit);

digit++;
a >>= 1;
b >>= 1;
}

return sub;
}
}