From 2308fe0f5e5a32fc7d72da8aa3edf5a23c5f57d2 Mon Sep 17 00:00:00 2001 From: jinvicky Date: Mon, 18 Aug 2025 09:45:56 +0900 Subject: [PATCH 1/8] best time to buy and sell stock solution and 4 week solution comment added --- best-time-to-buy-and-sell-stock/jinvicky.java | 30 +++++++++++ coin-change/jinvicky.java | 28 +++++++--- word-search/jinvicky.java | 52 +++++++++++++------ 3 files changed, 87 insertions(+), 23 deletions(-) create mode 100644 best-time-to-buy-and-sell-stock/jinvicky.java diff --git a/best-time-to-buy-and-sell-stock/jinvicky.java b/best-time-to-buy-and-sell-stock/jinvicky.java new file mode 100644 index 0000000000..67c8a50559 --- /dev/null +++ b/best-time-to-buy-and-sell-stock/jinvicky.java @@ -0,0 +1,30 @@ +class Solution { + public int maxProfit(int[] prices) { + /** + max라는 메모이제이션 변수를 int로 선언한다. + dp로 모든 경우의 수를 고려할 것이다. + + 주식의 최소가격을 담은 변수를 int로 선언한다. 맨 처음에 prices[0] 값이 되면 좋겠다. + + + */ + int max = 0; + int minStock = prices[0]; + + for (int i = 1; i < prices.length; i++) { + /** + 현재 주식은 팔 때의 주식 가격을 나타낸다. + max에는 현재 주식 가격 - 현재 최소 주식 가격의 값을 저장한다. (abs 금지) + 만약 현재 주식의 가격이 현재 최소 주식 가격보다 크다면 + 현재 최소 주식의 가격으로 업데이트한다. + */ + int prc = prices[i]; + max = Math.max(max, prc - minStock); + + if (minStock > prc) { + minStock = prc; + } + } + return max; + } +} diff --git a/coin-change/jinvicky.java b/coin-change/jinvicky.java index c7e0ce392b..c265c4a924 100644 --- a/coin-change/jinvicky.java +++ b/coin-change/jinvicky.java @@ -1,16 +1,28 @@ import java.util.Arrays; + +// 왜 greedy가 아닐까? amount가 7이라고 가정했을 때 greedy라면 [1,3,4,5]에서 5를 우선시해서 5+1+1 총 3개를 사용할 것이지만, +// 실제 정답은 3+4 로 총 2개가 된다. 그렇기에 단순히 욕심쟁이로 최댓값, 최솟값으로 접근하는 방식은 적합하지 않다. +// greedy가 아니라면 보통 DP로 풀이한다. +// DP의 기초인 1. Climbing Stairs 2. Best Time to Buy and Sell Stock 3. House Robber를 선행으로 풀고 접근했다. +// DP의 기본은 i-1, i-2식으로 기존 값을 재사용하는 메모이제이션인데 나는 bottom-up으로 모든 dp를 풀이하려고 노력한다. (일관성) +// DP는 값을 비교하기 위해서 Math.min(), Math.max()를 많이 사용하기에 초기값을 Integer.MAX_VALUE, Integer.MIN_VALUE로 염두했다. + class Solution { public int coinChange(int[] coins, int amount) { - int max=amount+1; - int [] dp=new int[amount+1]; - Arrays.fill(dp,max); - dp[0]=0; - for(int coin:coins){ - for(int j=coin;j<=amount;j++){ // coin부터 시작해서 일반 루프보다 성능 향상 - dp[j]=Math.min(dp[j],dp[j-coin]+1); + int max = amount + 1; // 값 미표현이라면 Integer.MAX_VALUE가 더 직관적이지 않나? -> testcase에서 음수값 나와서 실패함 + int[] dp = new int[amount + 1]; // 목표인 amount를 위한 공간 확보를 위해서 amount+1로 배열 사이즈를 측정 + Arrays.fill(dp, max); // 최소 개수를 구하는 것이기 때문에 max값으로 채웠다. + dp[0] = 0; // 첫 요소만 max가 아닌 0으로 설정한다. -> + // 그냥 0부터 amount까지 이중 for문 하는 것보다 + // j문에서 바깥 i의 코인값부터 인덱스로 시작하는 것이 같은 결과, 높은 성능. + for (int coin : coins) { + for (int j = coin; j <= amount; j++) { + // dp[j - coin]: coin 동전을 하나 사용했을 때, 남은 금액 j - coin을 만드는 최소 동전 개수 + // 코인값을 인덱스로서 계산해야 한다는 것을 몰랐음 j-coin에서 한참을 헤맨.... + dp[j] = Math.min(dp[j], dp[j - coin] + 1); } } - return dp[amount]>amount ? -1:dp[amount]; + return dp[amount] > amount ? -1 : dp[amount]; } } diff --git a/word-search/jinvicky.java b/word-search/jinvicky.java index e61731c16b..1e1f1d4bf3 100644 --- a/word-search/jinvicky.java +++ b/word-search/jinvicky.java @@ -2,9 +2,9 @@ class Solution { public boolean exist(char[][] board, String word) { int m = board.length; int n = board[0].length; - for(int i = 0; i < m; i++) { - for(int j = 0; j < n; j++) { - if(dfs(board, i, j, word, 0)) { + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (dfs(board, i, j, word, 0)) { return true; } } @@ -12,17 +12,17 @@ public boolean exist(char[][] board, String word) { return false; } - boolean dfs(char[][] board,int i,int j,String word,int index){ - if(index == word.length()) return true; - if(i<0 || j<0 || i>=board.length || j>=board[0].length) return false; // 범위를 벗어난 경우 - if(board[i][j] != word.charAt(index)) return false; // 일치 조건을 불만족하는 경우 + boolean dfs(char[][] board, int i, int j, String word, int index) { + if (index == word.length()) return true; + if (i < 0 || j < 0 || i >= board.length || j >= board[0].length) return false; // 범위를 벗어난 경우 + if (board[i][j] != word.charAt(index)) return false; // 일치 조건을 불만족하는 경우 char temp = board[i][j]; board[i][j] = '#'; - boolean found = dfs(board, i+1, j, word, index+1) - || dfs(board, i-1, j, word, index+1) - || dfs(board, i, j+1, word, index+1) - || dfs(board, i, j-1, word, index+1); + boolean found = dfs(board, i + 1, j, word, index + 1) + || dfs(board, i - 1, j, word, index + 1) + || dfs(board, i, j + 1, word, index + 1) + || dfs(board, i, j - 1, word, index + 1); board[i][j] = temp; return found; @@ -48,14 +48,36 @@ public boolean exist2(char[][] board, String word) { return false; } + // 백트래킹 인자 국룰 (인자로 받는다는 건 다음에 필요함 + 다음 재귀때 업데이트된 값이 필요해서) + // 재귀여야 하기 때문에 2차원 input과 방향을 위한 i, j를 받도록 한다. + // 매번 조건 일치를 확인하기 위한 target 값이 인자로 포함된다 (문제마다 다름) + + // private boolean backtrack (원본 2차원 배열, 타겟, 2차원 방문 배열, i 방향 인덱스, j방향 인덱스, 조건부 target) { + // break1. 조건을 만족하면 값을 반환 + // break2. i와 j의 범위가 0보다 작거나 원본 배열의 길이보다 크거나 같으면 탈락 + // break3. 방문배열[i][j]가 true거나 (이미 방문했음) 또는 문제의 만족 조건이 아닐 경우 탈락 + // 보통 break2, break3을 한번의 if문으로 작성하지만 난 그마저도 어려워서 한번 더 분리했다. + // + // action1. 방문배열[i][j] 를 true로 설정한다. + // action2. i와 j를 위한 2차원 방향배열 템플릿을 선언한다. + // action3. 방향은 무조건 동서남북 4방향 고정이다. 4만큼 반복되는 for문을 실행한다. + // action3-1. dir[0], dir[1]을 i와 j에게 더해서 nextI, nextJ로 만들어 backtrack()에 전달한다. (이때 조건이 맞으면 return으로 break) + // action4. return을 못한 경우 방문배열[i][j]를 false로 재설정한다. + // action5. false 등을 리턴한다. + // } + + private boolean backtrack(char[][] board, String word, boolean[][] visited, int i, int j, int index) { if (index == word.length()) { return true; } - // dfs를 풀 때는 배열 범위를 벗어나는 경우 break를 꼭 기억하기 - if (i < 0 || i >= board.length || j < 0 || j >= board[0].length || visited[i][j] - || board[i][j] != word.charAt(index)) { + // dfs를 풀 때는 배열 범위를 벗어나는 경우는 모든 문제 공통이니 그냥 i,j기준으로 0와 length 생각하며 암기 + if (i < 0 || i >= board.length || j < 0 || j >= board[0].length) { + return false; + } + + if (visited[i][j] || board[i][j] != word.charAt(index)) { return false; } @@ -63,7 +85,7 @@ private boolean backtrack(char[][] board, String word, boolean[][] visited, int // if문에서 작성하는 것이 눈에 안 익어서 // direction 템플릿을 만들어서 for문으로 해결하는 암기법으로 변환 - int[][] directions = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } }; // 그냥 암기 + int[][] directions = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; // 그냥 암기 for (int[] dir : directions) { // i가 y, j가 x인데 사실 1, -1, 0만 잘 설정하면 상관없음. 목적은 1, -1 1번씩 그리고 나머지는 0으로 채우는 것 From ef08b1d9a67cabfb09fe8e81106f64f25cb647cb Mon Sep 17 00:00:00 2001 From: jinvicky Date: Mon, 18 Aug 2025 10:17:33 +0900 Subject: [PATCH 2/8] group anagrams solution --- best-time-to-buy-and-sell-stock/jinvicky.java | 30 +++++++-------- group-anagrams/jinvicky.java | 37 +++++++++++++++++++ 2 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 group-anagrams/jinvicky.java diff --git a/best-time-to-buy-and-sell-stock/jinvicky.java b/best-time-to-buy-and-sell-stock/jinvicky.java index 67c8a50559..da6219b151 100644 --- a/best-time-to-buy-and-sell-stock/jinvicky.java +++ b/best-time-to-buy-and-sell-stock/jinvicky.java @@ -1,30 +1,30 @@ +/** + * 한글주석만 보고 코드를 작성해본다. + * 조건문 안의 조건만 코드를 비우거나, 조건문만 남기고 안의 액션코드를 비우고 다시 작성해본다. + * + */ class Solution { public int maxProfit(int[] prices) { /** max라는 메모이제이션 변수를 int로 선언한다. dp로 모든 경우의 수를 고려할 것이다. - 주식의 최소가격을 담은 변수를 int로 선언한다. 맨 처음에 prices[0] 값이 되면 좋겠다. - - */ - int max = 0; - int minStock = prices[0]; - + int max = 0; // dp memoization 변수 + int min = prices[0]; // 주식 배열의 최소값 for (int i = 1; i < prices.length; i++) { /** 현재 주식은 팔 때의 주식 가격을 나타낸다. - max에는 현재 주식 가격 - 현재 최소 주식 가격의 값을 저장한다. (abs 금지) - 만약 현재 주식의 가격이 현재 최소 주식 가격보다 크다면 - 현재 최소 주식의 가격으로 업데이트한다. + max값은 (기존 최대이익, 최소주식을 현재 주식값에 팔았을 때의 이익) 중 더 큰 값으로 업데이트된다. + 현재 주식값이 기존 최소 주식값보다 작다면 현재 주식값으로 최솟값을 업데이트한다. */ - int prc = prices[i]; - max = Math.max(max, prc - minStock); - - if (minStock > prc) { - minStock = prc; + int currentMax = prices[i] - min; + max = Math.max(max, currentMax); + if (prices[i] < min) { + min = prices[i]; } } + return max; } -} +} \ No newline at end of file diff --git a/group-anagrams/jinvicky.java b/group-anagrams/jinvicky.java new file mode 100644 index 0000000000..6a5bf9d299 --- /dev/null +++ b/group-anagrams/jinvicky.java @@ -0,0 +1,37 @@ +import java.util.*; +import java.util.stream.Collectors; + +class Solution { + /** + * 41ms를 가진 낮은 성능의 첫번째 정답 코드 + */ + public List> groupAnagrams(String[] strs) { + // 길이가 1이면 자체를 List로 감싸서 반환한다. + if (strs.length == 1) { + return List.of(List.of(strs[0])); + } + + List> groupList = new ArrayList<>(); + /** + * bf로 그룹핑을 할 수 있나? + * strs를 for문으로 돌면서 "정렬한" 단어가 map에 있는 지 확인하고 존재할 경우 key로 조회한 list에 추가한다. + * 마지막으로 map을 순회하면서 list들을 groupList.add(list); 한다. + */ + Map> map = new HashMap<>(); + for (String s : strs) { + String sortedS = Arrays.stream(s.split("")) + .sorted() + .collect(Collectors.joining()); + List v = map.get(sortedS); + if (v == null) { + map.put(sortedS, new ArrayList<>()); + } + map.get(sortedS).add(s); + } + + for (List list : map.values()) { + groupList.add(list); + } + return groupList; + } +} \ No newline at end of file From 18bd398c394650604e381f392e4e8fa9254e41da Mon Sep 17 00:00:00 2001 From: jinvicky Date: Mon, 18 Aug 2025 10:17:52 +0900 Subject: [PATCH 3/8] fix lint --- group-anagrams/jinvicky.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/group-anagrams/jinvicky.java b/group-anagrams/jinvicky.java index 6a5bf9d299..cfaa6bdec0 100644 --- a/group-anagrams/jinvicky.java +++ b/group-anagrams/jinvicky.java @@ -34,4 +34,4 @@ public List> groupAnagrams(String[] strs) { } return groupList; } -} \ No newline at end of file +} From bb07450e7d372ef605f184e9aa459fb6d7868891 Mon Sep 17 00:00:00 2001 From: jinvicky Date: Mon, 18 Aug 2025 10:34:39 +0900 Subject: [PATCH 4/8] implement trie prefix tree solution --- implement-trie-prefix-tree/jinvicky.java | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 implement-trie-prefix-tree/jinvicky.java diff --git a/implement-trie-prefix-tree/jinvicky.java b/implement-trie-prefix-tree/jinvicky.java new file mode 100644 index 0000000000..ad97529ca6 --- /dev/null +++ b/implement-trie-prefix-tree/jinvicky.java @@ -0,0 +1,40 @@ +import java.util.ArrayList; +import java.util.List; + +/** + * brute-force로 먼저 접근한 423ms의 최악의 성능 코드. + */ +class Trie { + + private List list; + + public Trie() { + this.list = new ArrayList<>(); + } + + public void insert(String word) { + this.list.add(word); + } + + public boolean search(String word) { + for(String s : list) { + if(s.equals(word)) return true; + } + return false; + } + + public boolean startsWith(String prefix) { + for(String s : list) { + if(s.startsWith(prefix)) return true; + } + return false; + } +} + +/** + * Your Trie object will be instantiated and called as such: + * Trie obj = new Trie(); + * obj.insert(word); + * boolean param_2 = obj.search(word); + * boolean param_3 = obj.startsWith(prefix); + */ \ No newline at end of file From 27a05464587e8deee85e436581faef9f2ecbad69 Mon Sep 17 00:00:00 2001 From: jinvicky Date: Mon, 18 Aug 2025 10:35:46 +0900 Subject: [PATCH 5/8] fix lint --- implement-trie-prefix-tree/jinvicky.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/implement-trie-prefix-tree/jinvicky.java b/implement-trie-prefix-tree/jinvicky.java index ad97529ca6..b5dc6f210f 100644 --- a/implement-trie-prefix-tree/jinvicky.java +++ b/implement-trie-prefix-tree/jinvicky.java @@ -37,4 +37,4 @@ public boolean startsWith(String prefix) { * obj.insert(word); * boolean param_2 = obj.search(word); * boolean param_3 = obj.startsWith(prefix); - */ \ No newline at end of file + */ From 1df0a9a4bab3c919007c995ad183cfb4bcd5d462 Mon Sep 17 00:00:00 2001 From: jinvicky Date: Mon, 18 Aug 2025 11:14:51 +0900 Subject: [PATCH 6/8] word break solution --- word-break/jinvicky.java | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 word-break/jinvicky.java diff --git a/word-break/jinvicky.java b/word-break/jinvicky.java new file mode 100644 index 0000000000..4ac9d98aa7 --- /dev/null +++ b/word-break/jinvicky.java @@ -0,0 +1,20 @@ +import java.util.*; + +class Solution { + public boolean wordBreak(String s, List wordDict) { + int n = s.length(); + boolean[] dp = new boolean[n + 1]; + dp[0] = true; // 빈 문자열은 항상 가능 + + for (int i = 0; i < n; i++) { + if (!dp[i]) continue; // 여기까지 못 오면 확장 불가 + for (String w : wordDict) { + int j = i + w.length(); + if (j <= n && s.startsWith(w, i)) { + dp[j] = true; + } + } + } + return dp[n]; + } +} From 2258086b75202ee56c99471c18c9fce600f60b7a Mon Sep 17 00:00:00 2001 From: jinvicky Date: Thu, 21 Aug 2025 10:44:40 +0900 Subject: [PATCH 7/8] work break comment added --- word-break/jinvicky.java | 50 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/word-break/jinvicky.java b/word-break/jinvicky.java index 4ac9d98aa7..15150ee4a6 100644 --- a/word-break/jinvicky.java +++ b/word-break/jinvicky.java @@ -3,7 +3,7 @@ class Solution { public boolean wordBreak(String s, List wordDict) { int n = s.length(); - boolean[] dp = new boolean[n + 1]; + boolean[] dp = new boolean[n + 1]; // 빈 문자열을 대비해서 +1 길이로 설정 dp[0] = true; // 빈 문자열은 항상 가능 for (int i = 0; i < n; i++) { @@ -17,4 +17,52 @@ public boolean wordBreak(String s, List wordDict) { } return dp[n]; } + + public boolean wordBreak2(String s, List wordDict) { + int n = s.length(); + // List를 HashSet 객체 생성시 인자로 넣어 초기화가 가능합니다. + // set은 List에서의 단어 탐색을 O(1) 성능으로 최적화하기 위해서 필요합니다. + Set wordSet = new HashSet<>(wordDict); + + // 빈 문자열일 경우를 대비해서 +1을 길이로 설정합니다. + boolean[] dp = new boolean[n + 1]; + // 빈 문자열을 항상 true이므로 0번째 dp[]에 true를 설정합니다. + dp[0] = true; + + // 앞서 빈문자열을 셋팅했으니 i는 1부터 n까지 반복합니다. + // j는 0부터 i보다 작을때까지 반복합니다. + /** + * j=0: s.substring(0, 1) = "l" | dp[0]=true | "l" in dict=false + * ----- + * j=0: s.substring(0, 2) = "le" | dp[0]=true | "le" in dict=false + * j=1: s.substring(1, 2) = "e" | dp[1]=false | "e" in dict=false + * ----- + * j=0: s.substring(0, 3) = "lee" | dp[0]=true | "lee" in dict=false + * j=1: s.substring(1, 3) = "ee" | dp[1]=false | "ee" in dict=false + * j=2: s.substring(2, 3) = "e" | dp[2]=false | "e" in dict=false + * ----- + * j=0: s.substring(0, 4) = "leet" | dp[0]=true | "leet" in dict=true → dp[4] = true! + * ----- + * j=0: s.substring(0, 5) = "leetc" | dp[0]=true | "leetc" in dict=false + * j=1: s.substring(1, 5) = "eetc" | dp[1]=false | "eetc" in dict=false + * j=2: s.substring(2, 5) = "etc" | dp[2]=false | "etc" in dict=false + * j=3: s.substring(3, 5) = "tc" | dp[3]=false | "tc" in dict=false + * j=4: s.substring(4, 5) = "c" | dp[4]=true | "c" in dict=false + * ----- + * j=0: s.substring(0, 8) = "leetcode" | dp[0]=true | "leetcode" in dict=false + * j=1: s.substring(1, 8) = "eetcode" | dp[1]=false | "eetcode" in dict=false + * j=2: s.substring(2, 8) = "etcode" | dp[2]=false | "etcode" in dict=false + * j=3: s.substring(3, 8) = "tcode" | dp[3]=false | "tcode" in dict=false + * j=4: s.substring(4, 8) = "code" | dp[4]=true | "code" in dict=true → dp[8] = true! + */ + for (int i = 1; i <= n; i++) { + for (int j = 0; j < i; j++) { + if (dp[j] && wordSet.contains(s.substring(j, i))) { + dp[i] = true; + break; + } + } + } + return dp[n]; + } } From 1f9548310209750ea47f673ed7e3f22f4b516f0a Mon Sep 17 00:00:00 2001 From: jinvicky Date: Thu, 21 Aug 2025 10:51:59 +0900 Subject: [PATCH 8/8] fix lint --- best-time-to-buy-and-sell-stock/jinvicky.java | 2 +- word-break/jinvicky.java | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/best-time-to-buy-and-sell-stock/jinvicky.java b/best-time-to-buy-and-sell-stock/jinvicky.java index da6219b151..f8f95a14f9 100644 --- a/best-time-to-buy-and-sell-stock/jinvicky.java +++ b/best-time-to-buy-and-sell-stock/jinvicky.java @@ -27,4 +27,4 @@ public int maxProfit(int[] prices) { return max; } -} \ No newline at end of file +} diff --git a/word-break/jinvicky.java b/word-break/jinvicky.java index 15150ee4a6..febc6868cd 100644 --- a/word-break/jinvicky.java +++ b/word-break/jinvicky.java @@ -57,6 +57,7 @@ public boolean wordBreak2(String s, List wordDict) { */ for (int i = 1; i <= n; i++) { for (int j = 0; j < i; j++) { + // 예시를 보면 (start, end)에서 start가 증가할 동안 end는 고정됩니다. 따라서 start=j, end=i가 되어야 합니다. if (dp[j] && wordSet.contains(s.substring(j, i))) { dp[i] = true; break;