쉬는 날이기에 근 1~2주 동안 일찍 출근해서 풀었던 문제와 못 푼 문제에 대해 정리해보고자 한다.
물론 다시풀어보면서 작성해보고자 한다.
1. 백준 일곱 난쟁이 [2309]
아홉 개의 숫자를 주고 그중에서 일곱 난쟁이를 찾는 문제이다. 이중 일곱 난쟁이의 키의 합은 100이라는 전제조건이 붙으며 정답이 여러 개 존재할 경우 아무거나 하나출력하면 된다.
func solution() {
reader := bufio.NewReader(os.Stdin)
arr := make([]int, 9)
for i := range arr {
var a int
fmt.Fscanln(reader, &a)
arr[i] = a
}
sort.Ints(arr)
btc := func(arr, rs []int, idx, sum int) {}
btc = func(arr, rs []int, idx, sum int) {
if len(rs) > 7 || sum >= 100 || idx >= len(arr) {
if sum == 100 {
for _, v := range rs {
fmt.Println(v)
}
os.Exit(0)
}
} else {
btc(arr, append(rs, arr[idx]), idx+1, sum+arr[idx])
btc(arr, rs, idx+1, sum)
}
}
btc(arr, []int{}, 0, 0)
}
백트리킹 방식을 사용했다. 난쟁이를 선택한 경우와 그렇지 않은 경우
최초 정렬을 수행하고 다음 항목을 진행하는데 정답의 출력이 오름차순 출력이기에 최초 입력값 자체를 정렬해 주었다.
이 방법 외에도 간단하게 이중 포문을 돌려서 하나씩 제거하는 방법도 존재한다.
func solution() {
reader := bufio.NewReader(os.Stdin)
arr := make([]int, 9)
var (
total, x, y int
)
for i := range arr {
var a int
fmt.Fscanln(reader, &a)
arr[i] = a
total += a
}
sort.Ints(arr)
Loop:
for i := 0; i < len(arr); i++ {
for j := i + 1; j < len(arr); j++ {
if total-(arr[i]+arr[j]) == 100 {
x, y = i, j
break Loop
}
}
}
for i := range arr {
if i != x && i != y {
fmt.Println(arr[i])
}
}
}
이렇게 수를 받아오면서 전체 수의 값을 계산하고 이중포문으로 하나씩 날려주면 된다.
2. 백준 로또 [6603]
1~49의 숫자 중 6개를 고르는 것이다 단 집합 S를 만들고 해당 수의 번호만 선택하는 전략을 가져간다.
해당 부분집합은 총 6~13 개의 숫자로 되어있으며, 입력마지막 줄에는 0이 하나 주어진다.
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
func main() {
solution()
}
func solution() {
reader := bufio.NewReader(os.Stdin)
answer := strings.Builder{}
bitmask := func(arr, rs []string, flag, start int) {}
bitmask = func(arr, rs []string, flag, start int) {
if len(rs) >= 6 {
ans := ""
for _, v := range rs {
ans += v + " "
}
answer.WriteString(ans + "\n")
return
} else {
for i := start; i < len(arr); i++ {
if flag&(1<<i) != 0 {
continue
}
bitmask(arr, append(rs, arr[i]), flag|(1<<i), i+1)
}
}
}
for {
line, _ := reader.ReadString('\n')
if line[0] == '0' {
break
}
input := strings.TrimSpace(line)
target := strings.Split(input, " ")[1:]
var rs []string
bitmask(target, rs, 0, 0)
}
fmt.Println(answer.String())
}
해당 문제는 비트마스크를 이용해 해결했다. 여기서 비트마스크는 & | 연산을 활용하는 방법이다.
최초 0의 플래그를 가지고 이동하며 플래그 즉 이진수의 각 자릿수는 배열의 방문을 표기한다.
예를 들어 0의 플래그가 있고 최초방문 0의 자리를 방문한다고 했을 때 0 | (1<<0) 1을 0만큼 쉬프트 한다. 즉 1과 0을 or 연산을 하게 되고플 래그는 0000001의 값을 가진다. 이룰 활용한 방법이다.
나의 맥북은 64비트 메모리를 사용하고 있으니 int는 int64 고되고 이는 총 64개의 방문배열을 처리할 수 있음을 의미하니 마음 놓고 사용해도 된다.
지난번도 그렇고 이번에 다시 풀 때도 그렇고 input을 읽어올 때 "\n" 읽어오고 trimspace를 안 해 자꾸 출력값이 이상해서 오답을 받았다.
ㅎㅎ....
3. 부분수열의 합 [1182]
총 20 개의 숫자와 하나의 숫자가 주어지고 몇몇의 숫자들을 선택해 합을 구해 주어진 하나의 숫자와 같은 경우의 개수를 출력해야 한다.
여기서 부분수열의 합이라서 연속된 배열의 부분수열의 합이라고 생각할 수 있다. 그게 아닌 특정 숫자들을 선택해 더하는 것이다.
func solution() {
reader := bufio.NewReader(os.Stdin)
var (
n, answer, sum int
)
fmt.Fscanln(reader, &n, &sum)
arr := make([]int, n)
for i := range arr {
var a int
fmt.Fscan(reader, &a)
arr[i] = a
}
find := func(arr []int, total, idx, flag int) {}
find = func(arr []int, total, idx, flag int) {
if idx >= len(arr) {
if total == sum && flag > 0 {
answer++
}
return
} else {
find(arr, total+arr[idx], idx+1, flag|(1<<idx))
find(arr, total, idx+1, flag)
}
}
find(arr, 0, 0, 0)
fmt.Println(answer)
}
1번 백설공주 와 마찬가지로 숫자를 선택 혹은 넘기는 쪽을 선택했다. flag를 넣어준 이유는 총합이 0 이 되는 경우 answer는 0으로 초기화되기 때문에 최소 고른 횟수를 넣어주기 위해 위와 같이 넣어주었다.
4. 차이를 최대로 [10819]
범위 또한 3~8로 순열 완탐에 최적화된 전형적인 순열 문제이다. 각 배열을 바꿔서 차이를 최대로 만들면 되기 때문에 순열을 이용해서 재배치 후 계산을 해주면 된다.
func solution() {
reader := bufio.NewReader(os.Stdin)
abs := func(a int) int {
if a < 0 {
return -a
}
return a
}
cal := func(arr []int) (total int) {
for i := 1; i < len(arr); i++ {
total += abs(arr[i] - arr[i-1])
}
return total
}
var (
n, answer int
)
fmt.Fscanln(reader, &n)
arr := make([]int, n)
for i := range arr {
var a int
fmt.Fscan(reader, &a)
arr[i] = a
}
answer = cal(arr)
recur := func(arr, rs []int, depth, flag int) {}
recur = func(arr, rs []int, depth, flag int) {
if depth >= len(arr) {
sum := cal(rs)
if answer < sum {
answer = sum
}
return
} else {
for i := 0; i < len(arr); i++ {
if flag&(1<<i) == 0 {
recur(arr, append(rs, arr[i]), depth+1, flag|(1<<i))
}
}
}
}
recur(arr, []int{}, 0, 0)
fmt.Println(answer)
}
단순히 비트마스크를 이용해서 순열을 조회해서 다음으로 넘겨준다. 이게 전부다.
5. 스도쿠 [2580]
예전에 풀어봤던 문제이다. 확실히 한번 풀어봐서인지 나름 손쉽게 풀 수 있었다.
(https://guiwoo.tistory.com/3 - 자바풀이)
백트리킹의 조건과 패스 될 수 있는 수의 경우만 명확하게 해 주면 된다.
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
solution()
}
func solution() {
reader := bufio.NewReader(os.Stdin)
arr := make([][]int, 9)
for i := range arr {
sub := make([]int, 9)
for j := range sub {
var a int
fmt.Fscan(reader, &a)
sub[j] = a
}
arr[i] = sub
}
isPossible := func(arr [][]int, num, row, col int) bool {
//가로줄 체크
for i := 0; i < 9; i++ {
if arr[row][i] == num {
return false
}
}
//세로줄 체크
for i := 0; i < 9; i++ {
if arr[i][col] == num {
return false
}
}
// 3*3 체크
row = (row / 3) * 3
col = (col / 3) * 3
for i := row; i < row+3; i++ {
for j := col; j < col+3; j++ {
if arr[i][j] == num {
return false
}
}
}
return true
}
dfs := func(arr [][]int, cal int) {}
dfs = func(arr [][]int, cal int) {
if cal >= 81 {
for i := range arr {
for j := range arr[i] {
fmt.Printf("%d ", arr[i][j])
}
fmt.Println()
}
os.Exit(0)
}
row := cal / 9
col := cal % 9
if arr[row][col] != 0 {
dfs(arr, cal+1)
} else {
for i := 1; i <= 9; i++ {
if isPossible(arr, i, row, col) {
arr[row][col] = i
dfs(arr, cal+1)
arr[row][col] = 0
}
}
}
}
dfs(arr, 0)
}
이문제는 row, col을 하나의 숫자로 넘겨서 계산해 주는 게 보다 손쉽게 계산할 수 있다.
6. 단어나누기 [1251]
알파벳 소문자 가 주어지고 단어를 임의의 두 부분을 골라서 단어를 쪼개고 나눠진 단어들을 뒤집어서 다시 재조합해 사전에서 찾을 수 있는 가장 앞에 단어를 출력해야 한다.
단 나눠진 단어의 길이는 1 이상인 단어여야만 한다.
포인트는 임의의 두 부분을 골라서 단어를 나눈다는 것이다.
바로 슬라이스를 사용하면 된다. 예를 들어보자 "abcdefg"와 같은 단어가 있다면?
func Test_StringSplit(t *testing.T) {
a := "abcdefg"
fmt.Println(a[:1]) // a
fmt.Println(a[1:3]) // bc
fmt.Println(a[3:]) // defg
fmt.Println(a[7:]) //
}
단어의 특정 부분을 고르는 것이 아닌 모든 부분을 사용해야 하기 때문에 딱 2개의 숫자만 고르면 된다. 단 길이가 7이라면 6까지 골라야 한다. 위에서 언급한 나눠진 단어는 길이가 최소 1 이상이어야 하기 때문에
func solution() {
reader := bufio.NewReader(os.Stdin)
reverse := func(s string) (ans string) {
for i := len(s) - 1; i >= 0; i-- {
ans += string(s[i])
}
return ans
}
strCompare := func(a string, b string, n []int) string {
b1 := reverse(b[:n[0]])
b2 := reverse(b[n[0]:n[1]])
b3 := reverse(b[n[1]:])
if a > (b1 + b2 + b3) {
return b1 + b2 + b3
}
return a
}
var input string
answer := "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz"
fmt.Fscanln(reader, &input)
recur := func(n int, rs []int, start, flag int) {}
recur = func(n int, rs []int, start, flag int) {
if len(rs) == 2 {
answer = strCompare(answer, input, rs)
return
} else {
for i := start; i < n; i++ {
if flag&(1<<i) == 0 {
recur(n, append(rs, i), i+1, flag|(1<<i))
}
}
}
}
recur(len(input), []int{}, 1, 0)
fmt.Println(answer)
}
string을 reverse 해주는 함수가 없어 따로 작성했다(그립다 java stringBuilder) answer 같은 경우는 최대 50 개의 단어가 주어지기 때문에 위와 같이 초기화시켜주었으며 조합을 응용해서 작성했다.
7. 부등호 [2529]
<,> 두 종류 의 부등호가 주어지고 주어진 부등호의 관계를 만족시켜야 한다는 조건이 붙는다.
부등호 앞뒤로 넣을 수 있는 숫자는 0~9까지이며 숫자의 선택은 한 번씩 만 가능하다.
k의 범위가 2~9까지 이기 때문에 나올 수 있는 숫자는 총 10자리이며 0 또한 표기해주어야 하기 때문에 스트링으로 처음부터 표기하면서 넘어가자.
func solution() {
reader := bufio.NewReader(os.Stdin)
var a int
fmt.Fscanln(reader, &a)
arr := make([]string, a)
for i := range arr {
var b string
fmt.Fscan(reader, &b)
arr[i] = b
}
min, max := "9999999999", "0"
recur := func(rs []string, depth, flag int) {}
recur = func(rs []string, depth, flag int) {
if len(rs) >= a+1 {
result := strings.Join(rs, "")
if min > result {
min = result
}
if max < result {
max = result
}
return
} else {
for i := 0; i <= 9; i++ {
if depth == 0 {
recur(append(rs, fmt.Sprintf("%d", i)), depth+1, flag|(1<<i))
} else {
if flag&(1<<i) == 0 {
next := fmt.Sprintf("%d", i)
switch arr[depth-1] {
case "<":
if rs[depth-1] < next {
recur(append(rs, next), depth+1, flag|(1<<i))
}
default:
if rs[depth-1] > next {
recur(append(rs, next), depth+1, flag|(1<<i))
}
}
}
}
}
}
}
recur([]string{}, 0, 0)
fmt.Printf("%s\n%s", max, min)
}
최소 최댓값을 선정을 최초 그냥 스트링으로 지정하였다. 이후 기존 백트래킹 방식과 유사하게 작성하였다.
else 구문에는 switch 구문을 이요해 케이스의 분기를 나누어 주었다. 요즘 select 구문을 자주 사용하다 보니 switch 문이 조금 더 편한 기분이기도 하다.
8. 연산자 끼워넣기 [14888]
숫자 2~11개 가 주어지고 연산자는 주어진 숫자-1 만큼 주어진다. 이에 따라 숫자의 최댓값, 최솟값을 구하라는 문제이다.
자릿수가 최대 여도 11자리 부호연산자는 10개 총 필요한 연산자는 10! 이 될듯하기 때문에 완탐으로 때려도 된다.
위의 대부분의 완탐의 경우에는 주어지는 숫자에 대해 재귀를 돌았다면 이번에는 정해진 operation에 의해 재귀를 돌면 된다.
func solution() {
reader := bufio.NewReader(os.Stdin)
var a int
fmt.Fscanln(reader, &a)
arr := make([]int, a)
for i := range arr {
fmt.Fscan(reader, &a)
arr[i] = a
}
oper := make([]int, 4)
for i := range oper {
fmt.Fscan(reader, &a)
oper[i] = a
}
min, max := 1<<31, -(1 << 31)
recur := func(idx, sum int) {}
recur = func(idx, sum int) {
if idx == len(arr) {
if min > sum {
min = sum
}
if max < sum {
max = sum
}
return
} else if idx == 0 {
recur(idx+1, arr[idx])
} else {
for i := 0; i < len(oper); i++ {
if oper[i] > 0 {
switch i {
case 0:
oper[i]--
recur(idx+1, sum+arr[idx])
oper[i]++
case 1:
oper[i]--
recur(idx+1, sum-arr[idx])
oper[i]++
case 2:
oper[i]--
recur(idx+1, sum*arr[idx])
oper[i]++
default:
oper[i]--
recur(idx+1, sum/arr[idx])
oper[i]++
}
}
}
}
}
recur(0, 0)
fmt.Printf("%d\n%d", max, min)
}
연산자의 개수가 0 이상일 때만 switch 문의 재귀를 타도록 만들었으며 재귀호출 이후에는 다시 ++ 를 해주어 모든 경우에 대한 탐색 을 하고 마지막 수의 값을 min, max에 따라 가져간다.
9. 라그랑주의 네 제곱수 정리 [3933]
못 푼 문제이다.ㅠ 시간초과 의 벽을 어쩌지 못하고 주어지는 숫자 n에 대하여 최대 4개의 자연수의 제곱의 합이 주어지는 숫자 n과 일치하는 여부를 찾는 문제이다.
기존방식과 동일하게 백트래킹 을 사용했으나 실패했다. 시간초과로.... 그래서 방문한 부분에 대해 처리를 해주려고
visit [][][] 이렇게 사용했다가 메모리 플로우로 터져버렸다.
풀이를 참조해 보니 그냥 4중 for 문 돌리던가 아니면 dp를 사용한다.
func solution() {
reader := bufio.NewReader(os.Stdin)
ans := strings.Builder{}
getAnswer := func(n int) int {
var cnt int
for i := 1; i*i <= n; i++ {
if i*i == n {
cnt++
continue
} else if i*i > n {
break
}
for j := i; i*i+j*j <= n; j++ {
if i*i+j*j == n {
cnt++
continue
} else if i*i+j*j > n {
break
}
for k := j; i*i+j*j+k*k <= n; k++ {
if i*i+j*j+k*k == n {
cnt++
continue
} else if i*i+j*j+k*k > n {
break
}
for l := k; i*i+j*j+k*k+l*l <= n; l++ {
if i*i+j*j+k*k+l*l == n {
cnt++
continue
} else if i*i+j*j+k*k+l*l > n {
break
}
}
}
}
}
return cnt
}
for {
var n int
fmt.Fscanln(reader, &n)
if n == 0 {
break
}
ans.WriteString(strconv.Itoa(getAnswer(n)) + "\n")
}
fmt.Println(ans.String())
}
디피로 푸는 방법도 있다.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
//fmt.Println(1 << 15)
solution()
}
func solution() {
reader := bufio.NewReader(os.Stdin)
ans := strings.Builder{}
var dp [32768][5]int
for i := 1; i*i < 32768; i++ {
dp[i*i][1] = 1
for j := i * i; j < 32768; j++ {
dp[j][2] += dp[j-i*i][1]
dp[j][3] += dp[j-i*i][2]
dp[j][4] += dp[j-i*i][3]
}
}
getAnswer := func(n int) int {
var cnt int
for i := 0; i < 5; i++ {
cnt += dp[n][i]
}
return cnt
}
for {
var n int
fmt.Fscanln(reader, &n)
if n == 0 {
break
}
ans.WriteString(strconv.Itoa(getAnswer(n)) + "\n")
}
fmt.Println(ans.String())
}
이 테이블 dp [i][j]은 “수 i를 j개의 제곱수의 합으로 표현하는 방법의 수”를 저장한다.
초기에 i*i (제곱수 자체)는 i*i를 1개의 제곱수로 표현하는 방법이므로 dp [i*i][1]=1로 설정되고.
dp를 채우는 이 반복문에서, j는 새로운 합을 나타내는 인덱스이고 i는 제곱수를 나타냅니다. j에 대해 반복할 때마다 i의 제곱값을 현재의 j에 뺀 값 (j-i*i)에 대한 DP 값을 가지고 온 후, j*(i*i)에 해당하는 값을 경신합니다. 이렇게 하면, 각 j에 대해 최대 j 개의 제곱수로 수를 표현하는 방법의 수를 누적하여 저장할 수 있다.
즉, dp [j][2]는 j를 2개의 제곱수의 합으로 표현하는 경우의 수를 나타내며, dp [j][3]과 dp [j][4] 역시 동일한 방식으로 계산된다.
현실적으로 이 방법에 대해 어렴풋이 느낌만 있었지 구현하라 그러면 못하지 않았을까 싶다...
10. 스타트와 링크 [14889]
arr로 각 번호들의 능력치를 입력 값으로 주고 arr [i][j] + arr [j][i]는 같은 팀이 된다면 앞에와 같은 힘을 낸다 따라서 조합으로 팀원을 2개의 팀으로 구분 짓고 각각의 팀의 능력치의 최소 값을 찾아주면 된다.
한 명의 플레이어가 선택된다면 그건 가로 세로 다 선수의 능력치를 사용할 수 없다 다시 말해 저 2차원 배열은 1차원 배열로 다시 나열될 수 있다.
위 그림에서 해당 인원들의 능력치를 다시 1차원 배열로 변경한다면
[6 15 10 12] [14 6 12 11] 이렇게 변경될 수 있다. 이 모든 수의 합은 43이 되고 이는 모두 중복되는 값이다. 여기서 1,4를 선택한다고 하면 1(6+14), 4(12+11) 이 들어가고 total에서 빼주면 반대편 값이 나온다.
이해가 잘 안 된다면 풀어서 작성해 보자 중복된 값들의 덧셈이 존재해 이것이 가능하다.
1번 (6(1+2+3) + 14(4+7+3)) 4번 (12(3+4+5) + 11(3+6+2))
2번 (15(4+5+6) + 6(1+1+4)) 3번 (10(7+1+2) + 12(2+5+5))
위에 보이는 것처럼 중복된 값들이 계산되기 때문에 모두 더한 후 total에서 빼주게 되면 값이 일치하게 된다.
func solution() {
reader := bufio.NewReader(os.Stdin)
abs := func(a int) int {
if a < 0 {
return -a
}
return a
}
var a int
fmt.Fscanln(reader, &a)
arr := make([][]int, a)
row := make([]int, a)
col := make([]int, a)
total := 0
for i := range arr {
sub := make([]int, a)
for j := range sub {
var b int
fmt.Fscan(reader, &b)
sub[j] = b
total += b
row[i] += b
col[j] += b
}
arr[i] = sub
}
min := 1 << 31
recur := func(start, depth, sum, flag int) {}
recur = func(start, depth, sum, flag int) {
if depth == a/2 {
target := abs(total - sum)
if target < min {
min = target
}
return
} else {
for i := start; i < a; i++ {
if flag&(1<<i) == 0 {
recur(i+1, depth+1, sum+row[i]+col[i], flag|(1<<i))
}
}
}
}
recur(0, 0, 0, 0)
fmt.Println(min)
}
사실 해당 문제의 다른 버전을 친구에게 제공받아 풀었지만 실패하고 이 문제를 찾아서 풀게 되었다.
11. 링크와 스타트 [15661]
해당 문제는 위에서 언급한 것처럼 해결하지 못했다.
위와 동일한 조건이지만 단 팀이 무조건 공평한 숫자가 아닌 최소 한 명 이상만 있으면 팀의 조건을 만족하게 된다.
위에서 제시한 조건을 그대로 이용해서 사용한다면?
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
//fmt.Println(1 << 15)
solution()
}
func solution() {
reader := bufio.NewReader(os.Stdin)
abs := func(a int) int {
if a < 0 {
return -a
}
return a
}
min := func(a, b int) int {
if a < b {
return a
}
return b
}
var a int
fmt.Fscanln(reader, &a)
arr := make([][]int, a)
row := make([]int, a)
col := make([]int, a)
total := 0
for i := range arr {
sub := make([]int, a)
for j := range sub {
var b int
fmt.Fscan(reader, &b)
total += b
row[i] += b
col[j] += b
}
}
res := 1 << 31
recur := func(idx, cur int) {}
recur = func(idx, cur int) {
res = min(res, abs(cur))
for i := idx + 1; i < a; i++ {
recur(i, cur-row[i]-col[i])
}
}
recur(0, total)
fmt.Println(res)
}
계산된 total 값을 이용해 한 칸씩 없애주면 된다. 오셀로 하듯이
나는 완탐을 잘못하기 때문에 이렇게 하루 쉬는날을 이용해서 풀었던 완탐을 정리해 봤다.
또 하루 날 잡고 완탐에 대해 다시 풀어보고자 한다.
완벽하게 완탕이 될 때까지 해보고 싶다.
'PS > Boj' 카테고리의 다른 글
백준-완탐 연습(Brute-Force)-2 (0) | 2023.08.20 |
---|---|
백준 1251 단어나누기[Java] (0) | 2022.06.26 |
백준 1120 문자열[Java] (0) | 2022.06.26 |
백준 10448 유레카[Java] (0) | 2022.06.26 |