Skip to content

[forest000014] Week 07 #943

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 8 commits into from
Jan 25, 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
16 changes: 8 additions & 8 deletions longest-increasing-subsequence/forest000014.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,20 @@
# 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)
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(1, j))를 구할 때, 모든 j에 대해 탐색한다면, 전체 시간 복잡도는 O(n^2)가 되기 때문에, 시간 복잡도를 줄일 필요가 있습니다.
max(LIS(0, j))를 구할 때, 모든 j에 대해 탐색한다면, 전체 시간 복잡도는 O(n^2)가 되기 때문에, 시간 복잡도를 줄일 필요가 있습니다.
이 탐색 과정을 줄이기 위해, 아래의 사고 과정을 거쳤습니다.

어떤 범위 내의 가장 큰 값을 O(logn) 시간에 구하기 위한 자료구조로, 인덱스 트리(혹은 세그먼트 트리)를 사용합니다.
(이 인덱스 트리의 x번째 leaf 노드에는 LIS(1, x) 값을 저장하고, internal 노드에는 자식 노드들 중 가장 큰 값을 저장합니다.)
(이 인덱스 트리의 x번째 leaf 노드에는 LIS(0, 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에 대해서만 최대값을 찾게 되므로,
그러기 위해서, 인덱스 트리에 모든 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)이 됩니다.
*/
Expand Down
39 changes: 39 additions & 0 deletions longest-substring-without-repeating-characters/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
Time Complexity: O(n)
Space Complexity: O(c)
(c는 사용되는 모든 character의 가짓수)

solution (two pointers)

begin, end 포인터를 각각 1에 두고 시작한다.
end 포인터를 1씩 증가하면서 탐색하다가, 현재 window 내에 이미 존재하는 문자가 또 추가된다면, 그 문자가 window에서 사라질 때까지 begin을 증가시킨다.

(1) end++을 하는 도중의 모든 end에 대해서는, 또 다른 begin을 찾을 필요성은 없는가?
- 현재 begin보다 더 왼쪽의 begin : 현재의 begin은, window 내에 중복 문자가 없게끔 하는 leftmost 인덱스이다. 따라서, 더 작은 begin은 중복이 있을 것이므로, 탐색할 필요가 없다.
- 현재 begin보다 더 오른쪽의 begin : 더 짧은 길이는 탐색할 필요가 없다.

(2) begin++을 하는 도중의 모든 begin에 대해서는, 또 다른 end를 찾을 필요성은 없는가?
- 현재 end보다 더 왼쪽의 end : 더 짧은 길이는 탐색할 필요가 없다.
- 현재 end보다 더 오른쪽의 end : 중복된 문자가 있는 구간은 LSWRC가 될 수 없으므로, 탐색할 필요가 없다.
*/
class Solution {
public int lengthOfLongestSubstring(String s) {
Set<Character> set = new HashSet<>();
int begin = 0, end = 0;

int ans = 0;
while (end < s.length()) {
if (set.contains(s.charAt(end))) {
while (begin < end && s.charAt(begin) != s.charAt(end)) {
set.remove(s.charAt(begin++));
}
Comment on lines +27 to +29
Copy link
Contributor

Choose a reason for hiding this comment

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

왠지 forest님께서는 관심 있으실 것 같아서 key-value hashmap을 이용하는 풀이도 남깁니다 :)

class Solution:
    def lengthOfLongestSubstring(self, s: str) -> int:
        length = 0
        left = 0
        lookup = {}

        for right, char in enumerate(s):
            if char in lookup and lookup[char] >= left:
                left = lookup[char] + 1
            lookup[char] = right
            length = max(length, right + 1 - left)

        return length

시간 복잡도는 둘 다 O(N)인데, key-value로 관리하는 풀이가 연산량이 최대 두 배 적을 것으로 생각합니다

  • set
    • 문자열의 각 문자마다 최대 두 번의 연산 실행 (set에 추가, set에서 삭제)
    • O(2N) = O(N)
  • key-value
    • 삭제하는 과정이 필요 없음
    • 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.

삭제 연산도 적고, left 포인터도 한번에 점프할 수 있어서 더 효율적이겠네요! 한 칸씩 전진하는 걸 더 줄여볼 생각을 했더라면 떠올렸을지도 모르겠네요... 다음 번엔 좀 더 끈질기게 고민해봐야겠습니다 😄

set.remove(s.charAt(begin++));
} else {
set.add(s.charAt(end++));
ans = Math.max(ans, end - begin);
}
}

return ans;
}
}
40 changes: 40 additions & 0 deletions number-of-islands/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
# Time Complexity: O(m * n)
모든 격자를 최대 2번씩(2중 for loop, dfs 호출) 방문

# Space Complexity: O(m * n)
최악의 경우, 모든 격자가 '1'인 경우에 m * n회 dfs() 재귀 호출이 이뤄진다. 각 콜 스택에서의 파라미터와 지역변수가 상수개 필요하므로, O(m * n)
*/
class Solution {
public int numIslands(char[][] grid) {
int m = grid.length;
int n = grid[0].length;
int[] dr = {-1, 0, 1, 0};
int[] dc = {0, 1, 0, -1};
int ans = 0;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
if (grid[i][j] != '1') {
continue;
}
dfs(grid, i, j, dr, dc);
ans++;
}
}
return ans;
}

private void dfs(char[][] grid, int r, int c, int[] dr, int[] dc) {
grid[r][c] = '2'; // mark as visited

for (int i = 0; i < 4; i++) {
int nr = r + dr[i];
int nc = c + dc[i];
if (nr < 0 || nr >= grid.length || nc < 0 || nc >= grid[0].length
|| grid[nr][nc] != '1') {
continue;
}
dfs(grid, nr, nc, dr, dc);
}
}
}
37 changes: 37 additions & 0 deletions reverse-linked-list/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
Time Complexity: O(n)
Space Complexity: O(1)

head에서부터 하나씩 next를 탐색하면서, 연결을 반대로 맺어준다.
*/

/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null) {
return null;
}

ListNode curr = head;
ListNode next = head.next;

head.next = null; // 마지막 노드가 될 노드의 next는 null로 세팅
while (next != null) {
ListNode nnext = next.next; // 연결을 끊기 전에, 그 다음 노드를 미리 nnext로 참조해둔다.
next.next = curr; // 연결을 반대로 맺어준다.
curr = next;
next = nnext;
}

return curr;
}
}
121 changes: 121 additions & 0 deletions set-matrix-zeroes/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
solution 1. 재귀 호출
Time Complexity: O(m * n * (m + n))
Space Complexity: O(m * n)
처음에는 공간 복잡도를 O(1)이라고 생각했으나, 검색해보니 함수 호출 스택도 공간 복잡도 계산에 포함시켜야만 한다. 따라서 이 방법은 공간 복잡도 제한을 만족시키지 못한다.
(참고 : https://en.wikipedia.org/wiki/In-place_algorithm),


solution 2. bit manipulation

long 변수를 선언해서, 각 bit에 x번째 row(혹은 col)를 0으로 바꿀지 여부를 기록한다.
(m + n) / 64 개의 변수를 써서 가능하긴 하지만, 64라는 factor가 다소 클 뿐, 결국 공간 복잡도는 O(m + n).


solution 3. matrix 내에 안 쓰이는 값 찾기 (probabilistic 접근)
int 범위 내에서, 쓰이는 값보다는 안 쓰이는 값의 갯수가 압도적으로 많다.(1 - (200 * 200 / 2^32) = 0.99999+)

matrix 내에 안 쓰이는 수를 찾을 때까지 int 범위 내의 랜덤하게 뽑는 행위를 10번만 반복해도,
O(m * n) 시간에 상당히 높은 확률로 안 쓰이는 값을 찾을 수 있다.
(10번 이내에 찾지 못할 확률은 10^(-50) 정도.)
이렇게 찾은 값을 x라고 하자. matrix의 모든 원소를 순회하며, 0인 원소가 있다면 같은 행/열에 존재하는 모든 원소(또다른 0은 제외)를 x로 바꾼 뒤에, 마지막에 한번에 모든 x를 0으로 바꾸는 식으로 풀 수 있다.

그러나 이 접근법의 확률은 문제의 제한 조건 m, n 범위 하에서 계산한 것이라는 한계가 있다.
m, n이 꽤나 커진다면 랜덤 추출로 안 쓰이는 값을 찾을 확률이 낮아지고, 극단적으로 m * n 이 2^32 이상이 되면, 쓸 수 없는 방법이기도 하다.
Comment on lines +15 to +24
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.

이 방법을 떠올렸을 땐 꽤나 괜찮아 보였는데, 다시 생각해보니 m, n 크기에 의존하는 풀이더라구요... 🥲 아쉬웠습니다



solution 4. in-place marking
(AlgoDale 풀이를 참고함)
Time Complexity: O(m * n)
Space Complexity: O(1)

*/
class Solution {

// solution 4. in-place marking
public void setZeroes(int[][] matrix) {
int m = matrix.length;
int n = matrix[0].length;

boolean should0thColumnBeZero = false;
for (int i = 0; i < m; i++) {
if (matrix[i][0] == 0) {
should0thColumnBeZero = true;
}
}

for (int i = 0; i < m; i++) {
for (int j = 1; j < n; j++) {
if (matrix[i][j] == 0) {
matrix[i][0] = matrix[0][j] = 0;
}
}
}

for (int i = 1; i < m; i++) {
if (matrix[i][0] == 0) {
for (int j = 1; j < n; j++) {
matrix[i][j] = 0;
}
}
}
for (int i = 1; i < n; i++) {
if (matrix[0][i] == 0) {
for (int j = 0; j < m; j++) {
matrix[j][i] = 0;
}
}
}
if (matrix[0][0] == 0) {
for (int i = 0; i < n; i++) {
matrix[0][i] = 0;
}
}
if (should0thColumnBeZero) {
for (int i = 0; i < m; i++) {
matrix[i][0] = 0;
}
}
}

/* solution 1. 재귀 호출
public void setZeroes(int[][] matrix) {
dfs(matrix, 0, 0);
}

public void dfs(int[][] matrix, int sr, int sc) {
int m = matrix.length;
int n = matrix[0].length;
for (int r = sr; r < m; r++) {
boolean found = false;
for (int c = (r == sr) ? sc : 0; c < n; c++) {
if (matrix[r][c] != 0) {
continue;
}

int nr = (c == n) ? (r + 1) : r;
int nc = (c == n) ? 0 : c + 1;
dfs(matrix, nr, nc);
setRowAndColumnZeroes(matrix, r, c);

found = true;
break;
}
if (found) {
break;
}
}
}

public void setRowAndColumnZeroes(int[][] matrix, int r, int c) {
int m = matrix.length;
int n = matrix[0].length;
for (int i = 0; i < n; i++) {
matrix[r][i] = 0;
}
for (int i = 0; i < m; i++) {
matrix[i][c] = 0;
}
}
*/
}
23 changes: 23 additions & 0 deletions unique-paths/forest000014.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
Time Complexity: O(m * n)
Space Complexity: O(n)
*/
class Solution {
public int uniquePaths(int m, int n) {
int[] dp = new int[n];

for (int i = 0; i < n; i++) {
dp[i] = 1;
}

for (int i = 1; i < m; i++) {
int prev = dp[0];
for (int j = 1; j < n; j++) {
dp[j] += prev;
prev = dp[j];
}
}

return dp[n - 1];
}
}
Loading