visited
배열을 사용하여 이미 방문한 정점을 다시 방문하지 않도록 합니다. DFS의 기능을 생각하면 순서와 상관없이 처리해도 되지만, 코딩 테스트에서는 번호가 낮은 순서부터 처리하도록 명시하는 경우가 종종 있습니다. 따라서 관행적으로 번호가 낮은 순서부터 처리하도록 구현하는 편입니다.
public struct Stack<Element> {
private var storage: [Element] = []
public init() { }
public init(_ elements: [Element]) {
storage = elements
}
public mutating func push(_ element: Element) {
storage.append(element)
}
public mutating func pop() -> Element? {
storage.popLast()
}
// 스택의 마지막 요소 확인하기 (stack.isEmpty의 전초)
public func peek() -> Element? {
storage.last
}
public func isEmpty() -> Bool {
peek() == nil
}
}
extension Graph where Element: Hashable {
func depthFirstSearch(from source: Vertex<Element>) -> [Vertex<Element>] {
var stack = Stack<Vertex<Element>>()
var pushed: Set<Vertex<Element>> = []
var visited: [Vertex<Element>] = []
stack.push(source)
pushed.insert(source)
visited.append(source)
outer: while let vertex = stack.peek() { // 1
let neighbors = edges(from: vertex) // 2
guard !neighbors.isEmpty else { // 3
stack.pop()
continue
}
for edge in neighbors { // 4
if !pushed.contains(edge.destination) {
stack.push(edge.destination)
pushed.insert(edge.destination)
visited.append(edge.destination)
continue outer // 5
}
}
stack.pop() // 6
}
return visited
}
}
print(graph2.depthFirstSearch(from: tokyo2))
[1: Tokyo, 3: Detroit, 6: Austin Texas, 4: San Francisco, 5: Washington DC, 7: Seattle]
func depthFirstSearchNoStack(from source: Vertex<Element>) -> [Vertex<Element>] {
var visited: [Vertex<Element>] = []
var pushed: Set<Vertex<Element>> = []
func dfs(vertex: Vertex<Element>) {
pushed.insert(vertex)
visited.append(vertex)
let neighborEdges = edges(from: vertex)
for edge in neighborEdges {
if !pushed.contains(edge.destination) {
dfs(vertex: edge.destination)
}
}
}
dfs(vertex: source)
return visited
}
print(graph2.depthFirstSearchNoStack(from: tokyo2))
[1: Tokyo, 3: Detroit, 6: Austin Texas, 4: San Francisco, 5: Washington DC, 7: Seattle]
dfs
를 정의하고, 시작 정점 source
에서 재귀를 시작합니다. dfs
를 호출합니다. let testGraph = [
[],
[2,3,8],
[1,7],
[1,4,5],
[3,5],
[3,4],
[7],
[2,6,8],
[1,7]
]
var visited = [Bool](repeating: false, count: testGraph.count)
func dfs(_ graph: [[Int]], _ start: Int) {
visited[start] = true
print(start, terminator: " ")
for i in graph[start] {
if !visited[i] {
dfs(graph, i)
}
}
}
dfs(testGraph, 1)
print()
1 2 7 6 8 3 4 5
let testGraph = [
[],
[2,3,8],
[1,7],
[1,4,5],
[3,5],
[3,4],
[7],
[2,6,8],
[1,7]
]
var visited = [Bool](repeating: false, count: testGraph.count)
func dfsStack(_ graph: [[Int]],_ start: Int) {
var stack = Stack<Int>()
stack.push(start)
while let value = stack.pop() {
guard !visited[value] else {
continue
}
print(value, terminator: " ")
visited[value] = true
for i in graph[value] {
if !visited3[i] {
stack.push(i)
}
}
}
}
dfsStack(testGraph, 1)
1 8 7 6 2 3 5 4
begin target words return
"hit" "cog" ["hot", "dot", "dog", "lot", "log", "cog"] 4
"hit" "cog" ["hot", "dot", "dog", "lot", "log"] 0
count - 1개
즉, 한 단어만 빼고 동일해야지 변경 할 수 있다는 것을 알게되었고, 주어진 words 값 만큼 dfs를 수행하면 문제를 해결 할 수 있다고 생각했다. 또한 한 번 찾으면 끝나는 문제가 아니라 다시 돌아가서 다른 경로도 찾아봐야 하기 때문에 백트래킹 문제라고 생각했다. count -1
이 아니라 단어가 3개일때를 기준으로 2로 잡아서 그랬던 것이었다. import Foundation
func solution(_ begin:String, _ target:String, _ words:[String]) -> Int {
// 최소 값 비교를 위한 최초 값
let INF = 99999999
var result = INF
// 방문처리를 위한 배열
var visited = [Bool](repeating: false, count: words.count)
func dfs(_ value: String,_ changeNum: Int) {
// 조건을 넘으면 조기 종료
if changeNum > result { return }
// 해당 사이클에서 target과 같다면 result 최신화 후 사이클 종료
if value == target {
result = min(result, changeNum)
return
}
for (idx, word) in words.enumerated() {
// 방문 했거나, 본인을 가리키는 단어라면 무시
if visited[idx] || value == word { continue }
// zip을 활용하여 비교 값과 틀린 단어 몇개인지 확인
let diff = zip(value, word).filter { $0 != $1 }.count
if diff == 1 {
// 백트래킹
visited[idx] = true
dfs(word, changeNum + 1)
visited[idx] = false
}
}
}
dfs(begin,0)
// dfs 탐색 후 매칭된 값이 없을 땐 '0'
return result == INF ? 0 : result
}
import Foundation
func solution(_ begin: String, _ target: String, _ words: [String]) -> Int {
let INF = 999999999
var result = INF
var visited = [Bool](repeating: false, count: words.count)
// 스택에는 (현재 단어, 변환 횟수, 방문 상태 배열)을 저장
var stack: [(String, Int, [Bool])] = [(begin, 0, visited)]
while !stack.isEmpty {
let (currentWord, changeNum, currentVisited) = stack.removeLast()
if currentWord == target {
result = min(result, changeNum)
continue
}
for (idx, word) in words.enumerated() {
if currentVisited[idx] || currentWord == word { continue }
let diff = zip(currentWord, word).filter { $0 != $1 }.count
if diff == 1 {
var newVisited = currentVisited
newVisited[idx] = true
stack.append((word, changeNum + 1, newVisited))
}
}
}
return result == INF ? 0 : result
}
단계
를 만듭니다. 재귀 호출이 끝나고 이전 단계로 돌아갈 때, 이전 단계의 visited
상태로 되돌려야 합니다.visited[idx] = false
를 설정하는 것 입니다. visited
상태의 복사본을 만들어 스택에 넣습니다. 이렇게 하면, 각 탐색 상태는 완전히 독립적이게 됩니다. 응모자 아이디 |
---|
frodo |
fradi |
crodo |
abc12 |
frodoc |
다음과 같이 불량 사용자 아이디 목록이 전달된 경우,
불량 사용자 |
---|
frd |
abc1** |
불량 사용자에 매핑되어 당첨에서 제외되어야 야 할 제재 아이디 목록은 다음과 같이 두 가지 경우가 있을 수 있습니다.
제재 아이디 |
---|
frodo |
abc123 |
제재 아이디 |
---|
fradi |
abc123 |
- user_id 배열의 크기는 1 이상 8 이하입니다.
- user_id 배열 각 원소들의 값은 길이가 1 이상 8 이하인 문자열입니다.
- 응모한 사용자 아이디들은 서로 중복되지 않습니다.
- 응모한 사용자 아이디는 알파벳 소문자와 숫자로만으로 구성되어 있습니다.
- banned_id 배열의 크기는 1 이상 user_id 배열의 크기 이하입니다.
- banned_id 배열 각 원소들의 값은 길이가 1 이상 8 이하인 문자열입니다.
- 불량 사용자 아이디는 알파벳 소문자와 숫자, 가리기 위한 문자 '*' 로만 이루어져 있습니다.
- 불량 사용자 아이디는 '*' 문자를 하나 이상 포함하고 있습니다.
불량 사용자 아이디 하나는 응모자 아이디 중 하나에 해당하고 같은 응모자 아이디가 중복해서 제재 아이디 목록에 들어가는 경우는 없습니다.- 제재 아이디 목록들을 구했을 때 아이디들이 나열된 순서와 관계없이 아이디 목록의 내용이 동일하다면 같은 것으로 처리하여 하나로 세면 됩니다.
user_id banned_id result
["frodo", "fradi", "crodo", "abc123", "frodoc"] ["fr*d*", "abc1**"] 2
["frodo", "fradi", "crodo", "abc123", "frodoc"] ["*rodo", "*rodo", "******"] 2
["frodo", "fradi", "crodo", "abc123", "frodoc"] ["fr*d*", "*rodo", "******", "******"] 3
func checkId(_ user_id: String, _ banned_id: String) -> Bool {
guard user_id.count == banned_id.count else { return false }
let xCount = banned_id.filter { $0 == "*" }.count
var sameCount = 0
for (user, banned) in zip(user_id, banned_id) {
if user == banned { sameCount += 1 }
}
return sameCount + xCount == user_id.count ? true : false
}
func solution(_ user_id: [String], _ banned_id: [String]) -> Int {
var bannedList: [[String]] = []
for ban in banned_id {
var tempArray: [String] = []
for user in user_id {
if checkId(user, ban) {
tempArray.append(user)
}
}
bannedList.append(tempArray)
}
// bannedList index번호와 누적된 ban id list
var stack: [(index: Int, banIdList: [String])] = []
var result: [[String]] = []
// 최초 값 저장하기
for banId in bannedList[0] {
stack.append((0, [banId]))
}
while stack.count > 0 {
let current = stack.removeLast()
// banned id 끝까지 다 탐색했을 경우
if current.index == bannedList.count - 1 {
result.append(current.banIdList)
continue
}
let index = current.index + 1
// 다음 bannedId List에서 가능한 banend id 배열에 넣기
for banned in bannedList[index] {
var banIdList = current.banIdList
// visited되었다면 해당 값은 무시 (값 중복 필터링)
if banIdList.contains(banned) { continue }
banIdList.append(banned)
stack.append((index, banIdList))
}
}
// 중복 패턴 제거
return Set(result.map { $0.sorted() }).count
}
func dfs(
_ bannedList: [[String]],
_ index: Int,
_ banIdList: [String],
_ result: inout Set<[String]>) {
if index == bannedList.count {
result.insert(banIdList.sorted())
return
}
for banned in bannedList[index] {
if !banIdList.contains(banned) {
// 해당 값이 없다면 배열 값을 추가하고 재귀 함수 실행
dfs(bannedList, index + 1, banIdList + [banned], &result)
}
}
}
func solution(_ user_id: [String], _ banned_id: [String]) -> Int {
var bannedList: [[String]] = []
for ban in banned_id {
var tempArray: [String] = []
for user in user_id {
if checkId(user, ban) {
tempArray.append(user)
}
}
bannedList.append(tempArray)
}
var result: Set<[String]> = []
dfs(bannedList, 0, [], &result)
return result.count
}
제가 학습한 내용을 요약하여 정리한 것입니다. 내용에 오류가 있을 수 있으며, 어떠한 피드백도 감사히 받겠습니다.
감사합니다.