Featured image of post [Algorithm] C++/Python 백준 1014번 : 컨닝

[Algorithm] C++/Python 백준 1014번 : 컨닝

백준 1014 컨닝 문제는 좌우 및 대각선 컨닝 제약과 일부 불능 좌석이 존재하는 교실에서, 비트마스킹과 동적 계획법을 활용해 최대 학생 수를 배치하는 최적화 알고리즘 구현을 다루는 대표적인 비트마스킹 DP 문제입니다.

서강대학교의 최백준 교수님은 “컨닝의 기술"이라는 과목을 가르치고 있다. 이 과목은 상당히 까다롭기로 유명하여, 일부 학생들은 시험 도중 다른 학생의 답안을 베끼려는 시도를 한다.

시험은 N행, M열의 직사각형 교실에서 진행되며, 각 칸은 학생이 앉을 수 있는 자리이다. 그러나 일부 학생들이 책상을 부숴버려서 앉을 수 없는 자리도 존재한다.

모든 학생은 자신의 왼쪽, 오른쪽, 왼쪽 대각선 위, 오른쪽 대각선 위에 있는 학생의 답안을 베낄 수 있다고 가정한다. 따라서 학생들이 서로 컨닝을 할 수 없도록 자리를 배치해야 한다.

교실의 배치도가 주어졌을 때, 컨닝이 불가능하도록 최대한 많은 학생을 배치할 수 있는 최대 학생 수를 구하는 프로그램을 작성하시오.

입력:

첫째 줄에 테스트 케이스의 개수 C가 주어진다.

각 테스트 케이스는 두 부분으로 이루어져 있다.

  • 첫 번째 줄: 교실의 세로 길이 N과 가로 길이 M이 주어진다. (1 ≤ N, M ≤ 10)
  • 두 번째 부분: N개의 줄에 걸쳐 교실의 배치도가 주어진다. 각 줄은 M개의 문자로 이루어져 있으며, ‘.‘은 앉을 수 있는 자리, ‘x’는 앉을 수 없는 자리를 의미한다.

출력:

각각의 테스트 케이스마다 컨닝이 불가능하도록 학생을 배치했을 때, 배치할 수 있는 최대 학생 수를 출력한다.

예제 입력:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
4
2 3
...
...
2 3
x.x
xxx
2 3
x.x
x.x
10 10
....x.....
..........
..........
..x.......
..........
x...x.x...
.........x
...x......
........x.
.x...x....

예제 출력:

1
2
3
4
4
1
2
46

문제 링크: https://www.acmicpc.net/problem/1014

/assets/images/undefined/algorithm.png

접근 방식

이 문제는 교실에 학생을 배치할 때, 컨닝이 불가능하도록 최대한 많은 학생을 배치하는 것이 목표이다. 학생들이 컨닝을 할 수 없는 조건을 만족하면서 최대 학생 수를 찾기 위해 다음과 같은 알고리즘과 전략을 사용한다.

사용된 알고리즘 및 전략

  • 동적 계획법(Dynamic Programming): 이전 상태의 결과를 이용하여 현재 상태의 결과를 계산함으로써 중복 계산을 방지한다.
  • 비트마스킹(Bitmasking): 각 자리의 상태(학생이 앉았는지 여부)를 비트로 표현하여 효율적으로 상태를 관리한다.
  • 메모이제이션(Memoization): 이미 계산한 상태를 저장하여 동일한 계산의 반복을 피한다.

해결 방법

  1. 자리 배치의 상태 표현: 각 행의 자리 배치를 비트마스크로 표현한다. 예를 들어, 3개의 자리에서 101은 첫 번째와 세 번째 자리에 학생이 앉았음을 의미한다.

  2. 유효한 자리 배치 생성: 한 행에서 가능한 모든 유효한 자리 배치를 생성한다. 유효한 자리 배치는 다음 조건을 만족해야 한다.

    • 학생이 앉을 수 없는 자리에 학생이 배치되지 않아야 한다.
    • 한 행에서 인접한 자리에 학생이 배치되지 않아야 한다.
  3. 동적 계획법 적용:

    • dp[row][mask]row번째 행까지 배치했을 때, 현재 행의 자리 배치가 mask일 때의 최대 학생 수를 저장한다.
    • 현재 행의 자리 배치와 이전 행의 자리 배치가 컨닝 조건을 만족하는지 확인한다.
      • 현재 행의 학생이 이전 행의 왼쪽 위 대각선이나 오른쪽 위 대각선에 앉은 학생과 컨닝할 수 없도록 해야 한다.
    • 이 조건을 만족하면 dp 테이블을 갱신한다.
  4. 최대 학생 수 계산: 모든 행에 대해 동적 계획법을 수행한 후, 마지막 행의 모든 상태 중 최대 값을 선택한다.

C++ 코드와 설명

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
#include <iostream>
#include <vector>
#include <algorithm>
#include <cstring>

using namespace std;

const int MAX_N = 12; // 최대 행 수
const int MAX_M = 12; // 최대 열 수
const int MAX_STATE = 1 << MAX_M; // 가능한 상태 수 (2^M)

int N, M; // 교실의 크기
char classroom[MAX_N][MAX_M]; // 교실의 자리 정보
vector<int> valid_masks[MAX_N]; // 각 행에서 가능한 유효한 자리 배치
int dp[MAX_N + 1][MAX_STATE]; // 동적 계획법 테이블

// 비트마스크에서 1의 개수를 세는 함수
int count_bits(int mask) {
    int count = 0;
    while (mask) {
        count++;
        mask &= (mask - 1); // 최하위 비트 제거
    }
    return count;
}

// 해당 행에서의 자리 배치(mask)가 유효한지 확인하는 함수
bool is_valid_mask(int row, int mask) {
    for (int j = 0; j < M; ++j) {
        if (mask & (1 << j)) { // 학생이 앉는 자리라면
            if (classroom[row][j] == 'x') return false; // 앉을 수 없는 자리
            if (j > 0 && (mask & (1 << (j - 1)))) return false; // 왼쪽에 학생이 있는 경우
        }
    }
    return true;
}

int main() {
    int C; // 테스트 케이스 수
    cin >> C;
    while (C--) {
        cin >> N >> M;
        for (int i = 0; i < N; ++i) {
            cin >> classroom[i]; // 교실 배치 입력
            valid_masks[i].clear(); // 유효한 마스크 초기화
        }
        // 각 행에서 가능한 유효한 자리 배치 생성
        for (int i = 0; i < N; ++i) {
            for (int mask = 0; mask < (1 << M); ++mask) {
                if (is_valid_mask(i, mask)) {
                    valid_masks[i].push_back(mask);
                }
            }
        }
        memset(dp, -1, sizeof(dp)); // DP 테이블 초기화
        dp[0][0] = 0; // 초기 상태
        // 동적 계획법 진행
        for (int row = 0; row < N; ++row) {
            for (int prev_mask = 0; prev_mask < (1 << M); ++prev_mask) {
                if (dp[row][prev_mask] == -1) continue; // 이전 상태가 유효하지 않으면 패스
                for (int curr_mask : valid_masks[row]) {
                    // 이전 행과 현재 행의 자리 배치가 컨닝 조건을 만족하는지 확인
                    if ((curr_mask & (prev_mask << 1)) == 0 && (curr_mask & (prev_mask >> 1)) == 0) {
                        int curr_count = count_bits(curr_mask); // 현재 행에서 앉은 학생 수
                        // 최대 학생 수 갱신
                        if (dp[row + 1][curr_mask] < dp[row][prev_mask] + curr_count) {
                            dp[row + 1][curr_mask] = dp[row][prev_mask] + curr_count;
                        }
                    }
                }
            }
        }
        // 결과 계산
        int result = 0;
        for (int mask = 0; mask < (1 << M); ++mask) {
            if (dp[N][mask] > result) {
                result = dp[N][mask];
            }
        }
        cout << result << endl; // 최대 학생 수 출력
    }
    return 0;
}

코드의 동작 설명:

  1. 입력 처리 및 초기화:

    • 테스트 케이스 수 C를 입력받는다.
    • 각 테스트 케이스마다 교실의 크기 N, M과 교실 배치 classroom을 입력받는다.
    • valid_masks를 초기화하여 각 행에서 가능한 유효한 자리 배치를 저장할 준비를 한다.
  2. 유효한 자리 배치 생성:

    • 각 행에 대해 가능한 모든 자리 배치(0부터 2^M - 1까지의 비트마스크)를 검사한다.
    • is_valid_mask 함수를 통해 해당 자리 배치가 유효한지 확인한다.
      • 앉을 수 없는 자리에 학생이 앉아 있으면 유효하지 않다.
      • 같은 행에서 인접한 두 자리에 학생이 앉아 있으면 유효하지 않다.
    • 유효한 자리 배치는 valid_masks에 저장된다.
  3. 동적 계획법(DP) 수행:

    • dp 배열을 -1로 초기화하고, dp[0][0]0으로 설정한다.
    • 각 행에 대해 이전 행의 모든 유효한 자리 배치(prev_mask)를 순회한다.
    • 현재 행의 모든 유효한 자리 배치(curr_mask)를 순회하면서 다음을 확인한다.
      • 이전 행과 현재 행의 자리 배치가 컨닝 조건을 만족하는지 확인한다.
        • curr_mask & (prev_mask << 1)curr_mask & (prev_mask >> 1)가 모두 0이어야 한다.
        • 이는 현재 행의 학생이 이전 행의 왼쪽 위 또는 오른쪽 위 대각선에 있는 학생과 겹치지 않음을 의미한다.
      • 조건을 만족하면 dp[row + 1][curr_mask]를 갱신한다.
        • dp[row + 1][curr_mask] = max(dp[row + 1][curr_mask], dp[row][prev_mask] + 현재 행의 학생 수)
  4. 결과 계산 및 출력:

    • 마지막 행의 모든 자리 배치에 대해 최대 학생 수를 계산한다.
    • 결과를 출력한다.

C++ without library 코드와 설명

stdio.hmalloc.h만을 사용하여 코드를 작성하면 다음과 같다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_N 12
#define MAX_M 12
#define MAX_STATE (1 << MAX_M)

int N, M;
char classroom[MAX_N][MAX_M + 1];
int valid_masks[MAX_N][MAX_STATE];
int valid_mask_count[MAX_N];
int dp[MAX_N + 1][MAX_STATE];

int count_bits(int mask) {
    int count = 0;
    while (mask) {
        count++;
        mask &= (mask - 1); // 최하위 비트 제거
    }
    return count;
}

int is_valid_mask(int row, int mask) {
    int prev = -2;
    for (int j = 0; j < M; ++j) {
        if (mask & (1 << j)) {
            if (classroom[row][j] == 'x') return 0; // 앉을 수 없는 자리
            if (j - prev == 1) return 0; // 인접한 학생
            prev = j;
        }
    }
    return 1;
}

int main() {
    int C;
    scanf("%d", &C);
    while (C--) {
        scanf("%d %d", &N, &M);
        for (int i = 0; i < N; ++i) {
            scanf("%s", classroom[i]);
            valid_mask_count[i] = 0;
        }
        for (int i = 0; i < N; ++i) {
            for (int mask = 0; mask < (1 << M); ++mask) {
                if (is_valid_mask(i, mask)) {
                    valid_masks[i][valid_mask_count[i]++] = mask;
                }
            }
        }
        memset(dp, -1, sizeof(dp));
        dp[0][0] = 0;
        for (int row = 0; row < N; ++row) {
            for (int prev_mask = 0; prev_mask < (1 << M); ++prev_mask) {
                if (dp[row][prev_mask] == -1) continue;
                for (int k = 0; k < valid_mask_count[row]; ++k) {
                    int curr_mask = valid_masks[row][k];
                    if ((curr_mask & (prev_mask << 1)) == 0 && (curr_mask & (prev_mask >> 1)) == 0) {
                        int curr_count = count_bits(curr_mask);
                        if (dp[row + 1][curr_mask] < dp[row][prev_mask] + curr_count) {
                            dp[row + 1][curr_mask] = dp[row][prev_mask] + curr_count;
                        }
                    }
                }
            }
        }
        int result = 0;
        for (int mask = 0; mask < (1 << M); ++mask) {
            if (dp[N][mask] > result) {
                result = dp[N][mask];
            }
        }
        printf("%d\n", result);
    }
    return 0;
}

코드의 동작 설명:

  • 헤더 파일 및 상수 정의:

    • 표준 입출력과 메모리 관리를 위한 헤더 파일 stdio.h, stdlib.h, string.h를 포함한다.
    • 최대 행 수 MAX_N, 최대 열 수 MAX_M, 최대 상태 수 MAX_STATE를 정의한다.
  • 전역 변수 선언:

    • 교실 크기 N, M과 교실 배치 classroom을 선언한다.
    • 각 행에서의 유효한 마스크를 저장할 valid_masks와 그 개수를 저장할 valid_mask_count를 선언한다.
    • 동적 계획법 테이블 dp를 선언한다.
  • 비트 개수 세기 함수 count_bits:

    • 비트마스크에서 설정된 비트(학생 수)의 개수를 센다.
  • 유효한 마스크 검사 함수 is_valid_mask:

    • 주어진 행에서의 자리 배치가 유효한지 검사한다.
    • 인접한 자리에 학생이 앉아있지 않고, 앉을 수 없는 자리에 학생이 앉지 않아야 한다.
  • 메인 함수:

    • 테스트 케이스 수를 입력받는다.
    • 각 테스트 케이스마다 입력을 처리하고, 유효한 마스크를 생성한다.
    • 동적 계획법을 수행하여 최대 학생 수를 계산한다.
    • 결과를 출력한다.
  • 차이점:

    • C++의 vector와 같은 STL을 사용할 수 없으므로, 배열과 직접적인 메모리 관리를 통해 구현한다.
    • memset과 같은 C 표준 라이브러리 함수를 사용한다.

결론

이 문제는 동적 계획법과 비트마스킹을 결합하여 효율적으로 해결할 수 있었다. 각 행의 자리 배치를 비트마스크로 표현하고, 컨닝 조건을 만족하는지 검사하여 상태를 전이하는 방식이 핵심이었다.

문제를 풀면서 상태 공간을 효율적으로 관리하는 방법과, 메모이제이션을 통해 중복 계산을 피하는 동적 계획법의 중요성을 다시 한번 느낄 수 있었다.

추가적으로, C++에서 표준 라이브러리를 사용하지 않고도 동일한 알고리즘을 구현할 수 있었다.

이번 문제를 통해 복잡한 조건이 있는 최적화 문제에서도 알고리즘의 기본 원칙을 적용하면 해결할 수 있음을 알게 되었다.