N 포털 사이트에서 우연히 한 출판사에서 올린 '<오늘의 스도쿠> 77번 문제'를 보게 됐습니다.
위 이미지 왼쪽에 있는 게 바로 그 문제입니다. 오른쪽은 당연히 그 문제 정답.
이번 포스트에서는 왼쪽처럼 스도쿠 퍼즐이 들어왔을 때 오른쪽처럼 해답을 출력하는 R 프로그램을 만들어 보도록 하겠습니다.
모르시는 분은 아니 계시겠지만 스도쿠(數獨)는 기본적으로 가로 9칸, 세로 9칸인 표에 1부터 9까지 숫자를 채워넣는 숫자 퍼즐입니다.
스도쿠는 '숫자는 한 번씩만 쓸 수 있다(數字は獨身に限る)'를 줄인 말.
각 행(Row)과 열(Column) 그리고 각 블록(Block)에 1~9를 한 번씩만 쓸 수 있어서 이런 이름이 붙었습니다.
다들 알고 계실 규칙을 새삼 강조한 건 코딩 과정에서 당연히 이 부분이 기본이기 때문입니다.
자 그럼 이제 본격적으로 스도쿠를 풀어주는 코드를 짜보겠습니다.
언제나 그렇듯 시작은 tidyverse (설치하고) 불러오기.
#install.packages('tidyverse')
library('tidyverse')
## -- Attaching packages ------------------------ tidyverse 1.2.1 --
## √ ggplot2 3.2.1 √ purrr 0.3.2 ## √ tibble 2.1.3 √ dplyr 0.8.3 ## √ tidyr 0.8.3 √ stringr 1.4.0 ## √ readr 1.3.1 √ forcats 0.3.0
## -- Conflicts --------------------------- tidyverse_conflicts() -- ## x dplyr::filter() masks stats::filter() ## x dplyr::lag() masks stats::lag()
이어서 행렬(matrix) 형식으로 퍼즐을 입력하도록 하겠습니다. 빈 칸은 0.
sudoku <- rbind(c(0,0,9,0,5,0,3,0,7),
c(2,0,0,0,7,6,5,1,0),
c(7,0,0,1,0,0,0,0,2),
c(0,8,0,0,0,0,0,0,0),
c(0,0,4,0,1,0,9,0,0),
c(0,0,0,0,0,0,0,8,0),
c(4,0,0,0,0,1,0,0,8),
c(0,2,8,7,3,0,0,0,5),
c(9,0,1,0,8,0,4,0,0))
입력을 마쳤으니 내용을 확인하면 이렇게 보입니다.
sudoku
## [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] ## [1,] 0 0 9 0 5 0 3 0 7 ## [2,] 2 0 0 0 7 6 5 1 0 ## [3,] 7 0 0 1 0 0 0 0 2 ## [4,] 0 8 0 0 0 0 0 0 0 ## [5,] 0 0 4 0 1 0 9 0 0 ## [6,] 0 0 0 0 0 0 0 8 0 ## [7,] 4 0 0 0 0 1 0 0 8 ## [8,] 0 2 8 7 3 0 0 0 5 ## [9,] 9 0 1 0 8 0 4 0 0
이 행렬은 기본적으로 '깔끔한 데이터' 형태가 아닙니다.
깔끔한 데이터는 기본적으로 롱(long) 포맷이고 지금 이 행렬은 와이드(wide) 포맷이니까요.
crossing() 함수를 써서 한번 데이터 형태를 바꿔보겠습니다.
이 함수는 R 기본 함수는 expand.grd()처럼 결합 가능한 모든 조합을 만드는 구실을 합니다.
이 행렬은 행 9개, 열 9개고 각 행(row)과 열(column)을 담고 있는 데이터 프레임은 이렇게 만들 수 있습니다.
crossing(r=1:9, c=1:9)
## # A tibble: 81 x 2 ## r c ## <int> <int> ## 1 1 1 ## 2 1 2 ## 3 1 3 ## 4 1 4 ## 5 1 5 ## 6 1 6 ## 7 1 7 ## 8 1 8 ## 9 1 9 ## 10 2 1 ## # ... with 71 more rows
이 데이터 프레임에 원해 행렬 각 행과 열에 있는 데이터를 가져와 보겠습니다.
우리는 세로로 길게 있는 여러 데이터를 활용해 = 각 행에 있는 값을 가지고 데이터를 반복적으로 가져와 넣고 싶은 거니까 purrr 패키지에 들어 있는 pmap_dfr() 함수를 쓰면 됩니다.
purrr 패키지가 낯선 분이 계시면 'R에서 깔끔하게 반복 작업 처리하기(feat. purrr)' 포스트가 도움이 될 수 있습니다.
이렇게 가져온 값은 새로운 열로 만들어 넣어야겠죠? 새로운 열을 추가할 때는 mutate() 함수를 쓰면 됩니다.
이게 무슨 말씀인지 이해가 가지 않으시다면 이번에는 '최대한 친절하게 쓴 R로 데이터 뽑아내기(feat. dplyr)' 포스트를 참조하시면 좋습니다.
자, 다시 본론으로 돌아오면 위에서 말씀드린 건 아래처럼 코드로 정리할 수 있습니다.
crossing(r=1:9, c=1:9) %>%
pmap_dfr(function(...){
df <- tibble(...)
df %>% mutate(cell=sudoku[r, c])
})
## # A tibble: 81 x 3 ## r c cell ## <int> <int> <dbl> ## 1 1 1 0 ## 2 1 2 0 ## 3 1 3 9 ## 4 1 4 0 ## 5 1 5 5 ## 6 1 6 0 ## 7 1 7 3 ## 8 1 8 0 ## 9 1 9 7 ## 10 2 1 2 ## # ... with 71 more rows
일단 앞으로도 계속 필요한 열을 추가하는 방식으로 퍼즐 풀이에 도전해 보겠습니다.
이제 앞서 봤던 스도쿠 큐칙을 적용할 차례.
각 행과 열 그리고 블록에는 숫자 1~9가 한 번만 들어갈 수 있습니다.
이를 뒤집어 생각하면 숫자 1~9 가운데 이미 있는 숫자를 뺀 나머지가 빈 칸에 들어갈 후보가 될 수 있습니다.
샘플이 여럿 있을 때 unique() 함수를 쓰면 고유한 값만 골라낼 수 있습니다.
예컨대 아래처럼 1~5가 반복해 등장할 때 이 함수를 쓰면 1, 2, 3, 4, 5만 골라냅니다.
unique(c(1, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 5))
## [1] 1 2 3 4 5
이번 작업에서 이 함수가 필요한 건 각 행, 열, 블록 등 세 군데에서 숫자가 오기 때문입니다.
1~9 가운데 여기 없는 숫자가 후보가 되겠죠?
이런 숫자만 골라낼 때는 setdiff() 함수를 쓰면 됩니다.
1~9 가운데 1~5에 없는 나머지 숫자를 골라내라고 아래처럼 쓰면 6, 7, 8, 9가 나옵니다.
setdiff(1:9, 1:5)
## [1] 6 7 8 9
행과 열은 숫자가 하나로 떨어져서 따로 선택하는 데 별 문제가 없지만 블록은 다릅니다.
예를 들어 첫 번째 블록은 1~3행, 1~3열에 자리잡고 있습니다.
이 부분만 골라내려면 어떻게 해야 할까요?
한 가지 방법은 R에서 나눗셈 몫을 찾아주는 '%/%' 기호를 쓰는 겁니다.
1을 3으로 나누면 몫은 0입니다.
1%/%3
## [1] 0
숫자 1~9를 3으로 나눌 때 몫이 각각 얼마인지 출력하는 코드는 이렇게 쓸 수 있습니다.
map_dbl(1:9, ~.x%/%3)
## [1] 0 0 1 1 1 2 2 2 3
일단 잘 나왔습니다. 단, 우리가 원하는 형태는 1~3, 4~6, 7~9을 같은 그룹으로 묶는 것.
이렇게 나타내려면 1~9에서 각각 1을 뺀 값을 3으로 나누고 나서 다시 1을 더하면 됩니다.
map_dbl(1:9, ~3*((.x-1)%/%3)+1)
## [1] 1 1 1 4 4 4 7 7 7
위에서는 1만 더했지만 1~3을 더해야 3열, 3행으로 된 블록을 출력할 수 있습니다.
첫 행, 첫 열이 속한 블록을 보여달라는 코드는 이렇게 쓰면 됩니다.
sudoku[3*((1-1)%/%3)+1:3, 3*((1-1)%/%3)+1:3]
## [,1] [,2] [,3] ## [1,] 0 0 9 ## [2,] 2 0 0 ## [3,] 7 0 0
행렬에서 어떤 위치가 같은 블록에 있을 때는 행과 열을 바꿔도 계속 그 블록을 유지해야겠죠?
첫 행, 두 번째 열이 속한 블록을 보여달라는 코드는 이렇게 쓰면 됩니다.
sudoku[3*((1-1)%/%3)+1:3, 3*((2-1)%/%3)+1:3]
## [,1] [,2] [,3] ## [1,] 0 0 9 ## [2,] 2 0 0 ## [3,] 7 0 0
위에서 우리가 본 것과 같은 결과가 나왔습니다.
첫 행, 네 번째 열이 속한 블록을 보여달라고 하면 결과가 바뀌어야겠죠?
sudoku[3*((1-1)%/%3)+1:3, 3*((4-1) %/% 3)+1:3]
## [,1] [,2] [,3] ## [1,] 0 5 0 ## [2,] 0 7 6 ## [3,] 1 0 0
네, 그렇습니다. 이 코드가 제대로 작동하고 있습니다.
이제 지금까지 따로 따로 작업한 걸 한 군데 묶으면 됩니다.
빈 칸에 들어갈 수 있는 후보는 숫자 1~9 가운데 각 행, 각 열 그리고 각 블록에 없는 숫자입니다.
이런 숫자가 하나가 아닐 테니 list() 함수로 묶어서 possible_answers라는 열에 넣겠습니다.
crossing(r=1:9, c=1:9) %>%
pmap_dfr(function(...){
df <- tibble(...)
df %>% mutate(cell=sudoku[r, c],
possible_answers=setdiff(1:9,
c(sudoku[r,],
sudoku[,c],
sudoku[3*((r-1)%/%3) + 1:3,
3*((c-1)%/%3)+1:3]) %>%
unique()) %>% list())
})
## # A tibble: 81 x 4 ## r c cell possible_answers ## <int> <int> <dbl> <list> ## 1 1 1 0 <int [3]> ## 2 1 2 0 <int [3]> ## 3 1 3 9 <int [1]> ## 4 1 4 0 <int [3]> ## 5 1 5 5 <int [2]> ## 6 1 6 0 <int [3]> ## 7 1 7 3 <int [2]> ## 8 1 8 0 <int [2]> ## 9 1 9 7 <int [2]> ## 10 2 1 2 <int [2]> ## # ... with 71 more rows
리스트를 중접한(nested) 형태로 결과가 나왔습니다.
리스트 안에 들어 있는 숫자를 실제로 확인하고 싶을 때는 unnest() 함수만 쓰면 됩니다.
우리는 possible_answers를 열어보고 싶은 거니까 unnest(possible_answers)라고 씁니다.
crossing(r=1:9, c=1:9) %>%
pmap_dfr(function(...){
df <- tibble(...)
df %>% mutate(cell=sudoku[r, c],
possible_answers=setdiff(1:9,
c(sudoku[r,],
sudoku[,c],
sudoku[3*((r-1)%/%3) + 1:3,
3*((c-1)%/%3)+1:3]) %>%
unique()) %>% list()) %>%
unnest(possible_answers)
})
## # A tibble: 271 x 4 ## r c cell possible_answers ## <int> <int> <dbl> <int> ## 1 1 1 0 1 ## 2 1 1 0 6 ## 3 1 1 0 8 ## 4 1 2 0 1 ## 5 1 2 0 4 ## 6 1 2 0 6 ## 7 1 3 9 6 ## 8 1 4 0 2 ## 9 1 4 0 4 ## 10 1 4 0 8 ## # ... with 261 more rows
첫 행, 첫 열에 들어갈 후보는 1, 6, 8이라는 사실을 알 수 있습니다.
그런데 자세히 보시면 이 결과가 조금 이상합니다.
첫 행, 세 번째 열에는 이미 9라는 숫자가 들어가 있는데 이 코드는 6이 후보라고 제시하고 있습니다.
이를 바로 잡으려면 각 셀에 이미 숫자가 들어 있을 때는 = 이 숫자가 0이 아닐 때는 원래 값을 넣으라고 코드를 짜면 됩니다.
이럴 때는 조건문을 쓰게 되고 R에서는 ifelse() 함수가 이 기능을 담당합니다.
crossing(r=1:9, c=1:9) %>%
pmap_dfr(function(...){
df <- tibble(...)
df %>% mutate(cell=sudoku[r, c],
possible_answers=ifelse(cell==0,
setdiff(1:9,
c(sudoku[r,],
sudoku[,c],
sudoku[3*((r-1)%/%3) + 1:3,
3*((c-1)%/%3)+1:3]) %>%
unique()) %>% list(),
cell %>% list())) %>%
unnest(possible_answers)
})
## # A tibble: 217 x 4 ## r c cell possible_answers ## <int> <int> <dbl> <dbl> ## 1 1 1 0 1 ## 2 1 1 0 6 ## 3 1 1 0 8 ## 4 1 2 0 1 ## 5 1 2 0 4 ## 6 1 2 0 6 ## 7 1 3 9 9 ## 8 1 4 0 2 ## 9 1 4 0 4 ## 10 1 4 0 8 ## # ... with 207 more rows
이번에는 첫 행, 세 번째 열에 9가 들어 있다는 사실을 확인할 수 있습니다.
이제 각 셀에 어떤 후보가 올 수 있는지 알아보는 함수를 만들어 보겠습니다.
그냥 지금까지 우리가 작업한 걸 function()으로 묶으면 그만입니다.
아래는 어떤 스도쿠 행렬(mx)이 있을 때 각 행(r)과 열(c)에 맞는 후보를 골라내는 함수입니다.
possible_answers_rc <- function(mx, r, c){
possible_answers=ifelse(mx[r,c]!=0,
mx[r,c],
setdiff(1:nrow(mx),
c(mx[r,],
mx[,c],
mx[(nrow(mx)/3)*((r-1)%/%(nrow(mx)/3))+1:(nrow(mx)/3),
(ncol(mx)/3)*((c-1)%/%(ncol(mx)/3))+1:(ncol(mx)/3)]) %>%
unique()) %>% list())
return(possible_answers %>% unlist())
}
이 함수가 잘 작동하는지 볼까요? 첫 행, 첫 열에 들어갈 수 있는 숫자를 보여달라고 입력하면 아래처럼 나옵니다.
possible_answers_rc(sudoku, 1, 1)
## [1] 1 6 8
위에서 우리가 확인한 것과 같은 결과가 나옵니다.
이미 답이 들어 있는 셀도 제대로 나오는지 확인해 봐야겠죠?
possible_answers_rc(sudoku, 1, 3)
## [1] 9
역시 잘 나옵니다.
이제 첫 빈 칸에 첫 번째 후보를 넣습니다. 두 번째 빈 칸도 마찬가지. 이어서 세번째 빈칸에도 첫 번째 후보를 넣습니다.
이런 식으로 계속 작업을 진행하다가 문제가 생기면 마지막으로 답을 넣은 칸에 두 번째 후보를 넣고 다시 답을 찾습니다.
일단 스도쿠 퍼즐 원본을 sudoku_temp라는 변수로 복사합니다.
sudoku_temp <- sudoku
이어서 첫 번째 칸에 어떤 숫자가 들어갈 수 있는지 확인한 다음
possible_answers_rc(sudoku_temp, 1, 1)
## [1] 1 6 8
첫 번째 후보 - 여기서는 1을 이 빈 칸에 넣습니다.
sudoku_temp[1, 1] <- 1
그래야 다음 번에 이 함수를 쓸 때 1을 함수가 각 열, 셀, 블록에서 1을 빼고 후보를 찾을 테니까요.
계속해서 두 번째 칸에 들어갈 수 있는 숫자를 확인하고
possible_answers_rc(sudoku_temp, 1, 2)
## [1] 4 6
마찬가지로 맨 처음 후보로 나온 4를 이 셀에 넣습니다.
sudoku_temp[1, 2] <- 4
세 번째 칸은 이미 답이 있습니다.
possible_answers_rc(sudoku_temp, 1, 3)
## [1] 9
그러면 그냥 이 숫자를 넣으면 됩니다.
sudoku_temp[1, 3] <- 9
나머지 4~9열에 대해서도 같은 작업을 진행합니다.
possible_answers_rc(sudoku_temp, 1, 4)
## [1] 2 8
sudoku_temp[1, 4] <- 2
possible_answers_rc(sudoku_temp, 1, 5)
## [1] 5
sudoku_temp[1, 5] <- 5
possible_answers_rc(sudoku_temp, 1, 6)
## [1] 8
sudoku_temp[1, 6] <- 8
possible_answers_rc(sudoku_temp, 1, 7)
## [1] 3
sudoku_temp[1, 7] <- 3
possible_answers_rc(sudoku_temp, 1, 8)
## [1] 6
sudoku_temp[1, 8] <- 6
possible_answers_rc(sudoku_temp, 1, 9)
## [1] 7
sudoku_temp[1, 9] <- 7
다행히도 아무 문제 없이 첫 행 작업이 끝났습니다.
지금까지 우리가 작업한 결과를 확인하면 이렇습니다.
sudoku_temp
## [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] ## [1,] 1 4 9 2 5 8 3 6 7 ## [2,] 2 0 0 0 7 6 5 1 0 ## [3,] 7 0 0 1 0 0 0 0 2 ## [4,] 0 8 0 0 0 0 0 0 0 ## [5,] 0 0 4 0 1 0 9 0 0 ## [6,] 0 0 0 0 0 0 0 8 0 ## [7,] 4 0 0 0 0 1 0 0 8 ## [8,] 0 2 8 7 3 0 0 0 5 ## [9,] 9 0 1 0 8 0 4 0 0
반복 작업을 이렇게 Ctrl+C/V로 진행하는 건 진짜 재미없는 일. 대신 반복문을 쓰면 됩니다.
위에서는 crossing() 함수와 pmap_dfr() 함수를 써서 반복 작업을 진행했지만 이번에는 가장 기본적인 반복문 함수인 for()를 쓸 겁니다.
첫 번째 이유는 스도쿠 퍼즐이 행렬 형태이기 때문입니다.
tidyverse 생태계 아버지 해들리 위컴 박사부터 행렬은 아직 purrr 범위 바깥이라고 인정했습니다.
위컴 박사는 또 'Advanced R'에 "기존 데이터 프레임 일부를 수정할 필요가 있다면 for 루프를 사용하는 것이 더 좋다"고 쓰기도 했습니다.
우리가 하려는 작업도 행렬 데이터 일부를 수정하면 더 편합니다.
그런 이유로 for()를 써서 작업하도록 하겠습니다.
이에 앞서 먼저 sudoku_temp를 원래대로 되돌리겠습니다.
sudoku_temp <- sudoku
그리고 우리가 위에서 했던 걸 반복하는 작업을 코드로 쓰면 됩니다.
for(r in 1:9){
for(c in 1:9){
candidates <- possible_answers_rc(sudoku_temp, r, c)
for(candidate in candidates){
sudoku_temp[r, c] <- candidate
}
}
}
어떤 결과가 나왔을지 sudoku_temp를 열어보겠습니다.
sudoku_temp
## [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] ## [1,] 8 6 9 4 5 2 3 0 7 ## [2,] 2 4 3 9 7 6 5 1 0 ## [3,] 7 5 0 1 0 8 6 9 2 ## [4,] 6 8 7 5 9 4 2 3 1 ## [5,] 5 3 4 8 1 7 9 6 0 ## [6,] 1 9 2 6 0 3 7 8 4 ## [7,] 4 7 6 2 0 1 0 0 8 ## [8,] 0 2 8 7 3 9 1 0 5 ## [9,] 9 0 1 0 8 5 4 7 6
여전히 군데군데 0이 자리를 차지하고 있는 걸 알 수 있습니다.
이건 작업을 한 번만 진행해서 생긴 일입니다.
코드로 표현하자면 아래 이라고 쓴 부분에 전체 코드가 반복해서 들어가야 해답을 찾을 수 있습니다.
for(r in 1:9){
for(c in 1:9){
candidates <- possible_answers_rc(sudoku_temp, r, c)
for(candidate in candidates){
sudoku_temp[r, c] <- candidate
[]
}
}
}
이렇게 한 함수 안에서 자기 자신을 다시 불러오는 방법을 프로그래밍에서는 재귀(Recursion)라고 부릅니다.
지금껏 우리가 작업한 내용을 바탕으로 다시 자기 자신을 불러오는 함수를 짜보겠습니다.
getting_answers <- function(mx, try=1){
if(try>ncol(mx)*nrow(mx)){
return(mx)
}
r <- ((try-1)%%nrow(mx))+1
c <- ((try-1)%/%ncol(mx))+1
candidates <- possible_answers_rc(mx, r, c)
for(candidate in candidates){
mx[r, c] <- candidate
the_answer <- getting_answers(mx, try+1)
if(!is.null(the_answer)){return(the_answer)}
}
}
이 코드가 어떻게 돌아가는지 감이 오시나요?
먼저 첫 번째 시도(try) 때는 첫 번째 행, 첫 번째 열에 들어갈 수 있는 후보를 추립니다.
그리고 각 후보를 그 칸에 넣은 다음 오류가 없으면 다음 단계로 넘어갑니다.
편의상 이 단계를 1-1이라고 부르면 이어서 1-2, 1-3, …하는 식으로 계속 빈칸을 채우는 겁니다.
여기서 문제가 없으면 다시 2-1을 시작합니다.
이런 식으로 모든 빈 칸에 오류가 없을 때까지 작업을 반복합니다.
정말 이 함수로 스도쿠 퍼즐을 풀 수 있을까요?
직접 문제를 넣어보면 알 수 있습니다.
getting_answers(sudoku)
## [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] ## [1,] 8 1 9 4 5 2 3 6 7 ## [2,] 2 4 3 8 7 6 5 1 9 ## [3,] 7 6 5 1 9 3 8 4 2 ## [4,] 1 8 6 3 2 9 7 5 4 ## [5,] 5 7 4 6 1 8 9 2 3 ## [6,] 3 9 2 5 4 7 6 8 1 ## [7,] 4 5 7 9 6 1 2 3 8 ## [8,] 6 2 8 7 3 4 1 9 5 ## [9,] 9 3 1 2 8 5 4 7 6
예, 우리가 맨 위에서 본 정답 그대로 출력했습니다.
혹시 이 문제만 우연히 정답을 맞춘 건 아닐까요?
그럴 확률이 별로 없다는 건 저도 알고 여러분도 아시겠지만 한 번 위키피디아에서 제시하고 있는 퍼즐을 하나 더 풀어보겠습니다.
일단 퍼즐과 정답은 이렇게 생겼습니다.
문제를 행렬로 만들어 sudoku_wikipedia 변수에 넣고,
sudoku_wikipedia <- rbind(c(5,3,0,0,7,0,0,0,0),
c(6,0,0,1,9,5,0,0,0),
c(0,9,8,0,0,0,0,6,0),
c(8,0,0,0,6,0,0,0,3),
c(4,0,0,8,0,3,0,0,1),
c(7,0,0,0,2,0,0,0,6),
c(0,6,0,0,0,0,2,8,0),
c(0,0,0,4,1,9,0,0,5),
c(0,0,0,0,8,0,0,7,9))
위에서 만든 함수로 해답을 찾으라고 명령하면,
getting_answers(sudoku_wikipedia)
## [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] ## [1,] 5 3 4 6 7 8 9 1 2 ## [2,] 6 7 2 1 9 5 3 4 8 ## [3,] 1 9 8 3 4 2 5 6 7 ## [4,] 8 5 9 7 6 1 4 2 3 ## [5,] 4 2 6 8 5 3 7 9 1 ## [6,] 7 1 3 9 2 4 8 5 6 ## [7,] 9 6 1 5 3 7 2 8 4 ## [8,] 2 8 7 4 1 9 6 3 5 ## [9,] 3 4 5 2 8 6 1 7 9
정답을 아주 잘 찾아냈다는 걸 확인할 수 있습니다.
사실 R에는 이미 스도쿠 퍼즐을 만들고, 직접 풀어 보고, 해답까지 알려주는 sudoku 패키지가 따로 있습니다.
그렇다고 직접 코드를 짜보는 게 재미없는 일은 아닐 겁니다.
그럼 모두들 Happy Tidyversing!
댓글,