聰明不如鈍筆
총명불여둔필
assignment KindeR

R로 스도쿠 퍼즐을 풀어보자(feat. function(function(x)))


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!


Tags  , , ,

댓글, 0

account_circle
vpn_key
web

security

mode_edit
KindeR | 카테고리 다른 글 더 보기