Skip to content

Commit 02ea8e9

Browse files
committed
solve(w05): 139. Word Break
1 parent 95b89d9 commit 02ea8e9

File tree

1 file changed

+131
-0
lines changed

1 file changed

+131
-0
lines changed

word-break/seungriyou.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# https://leetcode.com/problems/word-break/
2+
3+
from typing import List
4+
5+
class Solution:
6+
def wordBreak_bfs(self, s: str, wordDict: List[str]) -> bool:
7+
"""
8+
[Complexity]
9+
- TC: O(n^3 + m * k) (n = len(s), m = len(wordDict), k = avg(len(word) for word in wordDict))
10+
- O(n^3): (1) s[start:end] slicing * (2) end * (3) start
11+
- O(m * k): set(wordDict)
12+
- SC: O(n + m * k)
13+
- O(n): seen, q
14+
- O(m * k): set(wordDict)
15+
16+
[Approach]
17+
BFS로 접근할 수 있다.
18+
이때, queue는 확인할 substring의 시작 index를 기록하면 되며, seen을 이용하여 각 start 당 한 번씩만 확인한다.
19+
"""
20+
from collections import deque
21+
22+
n, words = len(s), set(wordDict)
23+
24+
q = deque([0]) # 확인할 substring의 시작 index
25+
seen = set() # 중복 X
26+
27+
while q:
28+
start = q.popleft()
29+
30+
# base condition
31+
if start == n:
32+
return True
33+
34+
# string slicing에서 사용할 끝 index를 순회하며 확인
35+
for end in range(start + 1, n + 1):
36+
# (1) 방문하지 않았으며 (2) wordDict에 현재 보고 있는 substring이 있는 경우
37+
if end not in seen and s[start:end] in words:
38+
q.append(end)
39+
seen.add(end)
40+
41+
return False
42+
43+
def wordBreak_dp(self, s: str, wordDict: List[str]) -> bool:
44+
"""
45+
[Complexity]
46+
- TC: O(n^3 + m * k)
47+
- O(n^3): (1) s[start:end] slicing * (2) end * (3) start
48+
- O(m * k): set(wordDict)
49+
- SC: O(n + m * k)
50+
- O(n): dp
51+
- O(m * k): set(wordDict)
52+
53+
[Approach]
54+
DP로 접근할 수 있다.
55+
이때, dp[i] = s[i:]가 wordDict의 원소만으로 구성될 수 있는지 여부이다.
56+
따라서 핵심은 뒤에서부터 확인하는 것이다!
57+
"""
58+
59+
n, words = len(s), set(wordDict)
60+
61+
# dp[i] = s[i:]가 wordDict의 원소만으로 구성될 수 있는지 여부
62+
dp = [False] * (n + 1)
63+
dp[n] = True
64+
65+
for start in range(n - 1, -1, -1):
66+
for end in range(start + 1, n + 1):
67+
# (1) s[end:]가 wordDict 원소로 구성 가능하고 (2) wordDict에 현재 보고 있는 substring이 있는 경우
68+
if dp[end] and s[start:end] in words:
69+
dp[start] = True
70+
break
71+
72+
return dp[0]
73+
74+
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
75+
"""
76+
[Complexity]
77+
- TC: O(n^2 + m * k)
78+
- O(n^2): 루프 수행 (slicing 제거)
79+
- O(m * k): trie 생성
80+
- SC: O(n + m * k)
81+
- O(n): dp list
82+
- O(m * k): trie
83+
84+
[Approach]
85+
trie를 이용하면, dp에서 매 루프마다 s[start:end] slicing으로 O(n)이 소요되던 것을 O(1)로 최적화 할 수 있다.
86+
"""
87+
from collections import defaultdict
88+
89+
class TrieNode:
90+
def __init__(self):
91+
self.is_word = False
92+
self.children = defaultdict(TrieNode)
93+
94+
def add_word(self, word):
95+
curr = self
96+
97+
for w in word:
98+
curr = curr.children[w]
99+
100+
curr.is_word = True
101+
102+
# 1. trie 구성하기
103+
root = TrieNode()
104+
for word in wordDict:
105+
root.add_word(word)
106+
107+
# 2. dp 준비하기
108+
n, words = len(s), set(wordDict)
109+
# dp[i] = s[i:]가 wordDict의 원소만으로 구성될 수 있는지 여부
110+
dp = [False] * (n + 1)
111+
dp[n] = True
112+
113+
# 3. trie를 이용하여 최적화된 dp 수행
114+
for start in range(n - 1, -1, -1):
115+
curr = root
116+
for end in range(start + 1, n + 1): # -- trie 사용으로 각 단계 O(n) -> O(1)
117+
w = s[end - 1]
118+
119+
# 현재 보고 있는 substring의 마지막 문자가 curr.children에 없다면 중단
120+
if w not in curr.children:
121+
break
122+
123+
# 있다면 타고 내려가기
124+
curr = curr.children[w]
125+
126+
# (1) s[end:]가 wordDict 원소로 구성 가능하고 (2) wordDict의 word가 발견되었다면 dp 리스트에 기록 후 중단
127+
if dp[end] and curr.is_word:
128+
dp[start] = True
129+
break
130+
131+
return dp[0]

0 commit comments

Comments
 (0)