BOJ 1749 점수따먹기 (Java)

사람·2025년 5월 10일
0

BOJ

목록 보기
46/74

문제

https://www.acmicpc.net/problem/1749

동주는 항상 혼자 노느라 심심하다. 하지만 혼자 놀기의 고수가 된 동주는 매일매일 게임을 개발하여 혼자놀기의 진수를 우리에게 보여준다. 어느 날 동주는 새로운 게임을 개발하였다. 바로 점수 따먹기라는 게임인데 그다지 재밌어 보이지는 않는다.

동주가 개발한 게임은 이렇다. 일단 N*M 행렬을 그린 다음, 각 칸에 -10,000 이상 10,000 이하의 정수를 하나씩 쓴다. 그런 다음 그 행렬의 부분행렬을 그려 그 안에 적힌 정수의 합을 구하는 게임이다.

동주가 혼자 재밌게 놀던 중 지나가는 당신을 보고 당신을 붙잡고 게임을 하자고 한다. 귀찮은 당신은 정수의 합이 최대가 되는 부분행렬을 구하여 빨리 동주에게서 벗어나고 싶다.

입력
첫째 줄에 N (1 < N < 200), M (1 < M < 200)이 주어진다. 그 다음 N개의 줄에 M개씩 행렬의 원소가 주어진다.

출력
첫째 줄에 최대의 합을 출력하라.

예제 입력 1
3 5
2 3 -21 -22 -23
5 6 -22 -23 -25
-22 -23 4 10 2
예제 출력 1
16

접근

동주야 한 번만 더 말 걸면 죽인다 진짜

일단 이 문제는 보자마자 DP로 풀어야 할 것 같은 느낌이 막연히 들었다. 그런데 누적 합을 구한 다음에 DP를 했어야 했는데 그냥 무지성 DP를 하려고 하니까 '?? 하위 문제가 중복되지가 않는데 뭐지??' 이러고 있었다.

사실 이 문제는 (1, 1)에서 (i, j)까지의 누적 합을 일단 싹 다 구해서 배열에 저장해둔 다음 이 값들을 재사용해가는 방식으로 부분행렬의 합을 구했어야만 했다.


일단 (1, 1)에서 (i, j)까지의 누적 합을 구하는 방법을 알아보자.

for (int i = 1; i <= N; i++) {
    for (int j = 1; j <= M; j++) {
        // ...
    }
}

위와 같은 이중 for문을 통해 i와 j를 차례로 증가시켜가며 누적 합을 순차적으로 구하려고 한다.

문제에 주어진 예제에서 (1, 1)부터 (2, 4)까지, 즉 위 사진에서 초록색으로 표시된 부분의 합을 구하려고 한다고 생각해보자.


일단 위 사진에서 노란색으로 칠해진 부분의 합은 이미 앞에서 구한 상태일 것이다.


위 사진에서 보라색으로 칠해진 부분의 합도 이미 앞에서 구한 상태일 것이다. 앞에서 본 노란색 부분의 합과 위 보라색 부분의 합을 더해준다.


그런데 이 두 부분 사이에는 중복이 있다. 중복된 부분은 두 번 더해졌을 것이므로 한 번 빼줘야 한다.

마지막으로 자기 자신을 더해주면 (1, 1)부터 자기 자신까지의 합이 완성된다.

이걸 일반화하여 식을 세우면, 자기 자신을 num이라고 할 때 (1, 1)부터 num까지의 합은
sum[i][j] = sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1] + num
가 된다.


이렇게 (1, 1)부터 모든 위치까지의 합을 구했다면, (1, 1)부터 시작하지 않는 부분행렬이더라도 부분합을 다음과 같이 구할 수 있다.
위에서 본 방식과 비슷하다.


예를 들어 문제에 나온 예제에서 위 빨간 네모 부분, 즉 (2, 3)에서 (3, 4)까지 행렬에 대한 부분합을 구하려고 한다고 생각해 보자.

일단 보라색으로 칠해진 부분, 즉 (1, 1)에서 (3, 4)까지의 합을 가져온다.

그 다음, 위 사진에서 빨간색으로 칠해진 부분, 즉 (1, 1)에서 (3, 2)까지의 합은 구하려고 하는 부분 행렬 범위 밖에 있으므로 빼준다.

위 사진에서 빨간색으로 칠해진 부분, 즉 (1, 1)에서 (1, 4)까지의 합 역시 구하려고 하는 부분행렬 범위 밖에 있으므로 빼준다.

그런데 위 사진에서 파란색으로 칠해진 부분, 즉 (1, 1)에서 (1, 2)까지의 합은 둘이 겹쳐져 있던 부분이라 뺄셈이 중복으로 이루어졌다. 그러므로 현재까지의 계산 결과에서 이 부분을 한 번 더해준다.

색상이 겹쳐 잘 구분은 안 가지만, 한 번에 정리하면 위 사진과 같다.
(1, 1)부터 임의의 모든 위치까지의 합이 구해진 상태일 때,
(전체 - 왼쪽 빨간색 - 위쪽 빨간색 + 빨간색이 겹치는 부분)
이런 식으로 부분 행렬의 합을 구할 수가 있는 것이다.

이걸 일반화해서 (i, j)에서 (p, q)까지의 부분행렬의 합을 구하는 식을 세우면
sum[p][q] - sum[p][j - 1] - sum[i - 1][q] + sum[i - 1][j - 1]
와 같은 형태가 된다.

이런 식으로 이차원 배열의 누적합을 구해서 활용하는 방식을 이전에도 본 적이 있다고 스터디원분께서 그러셨다. 간혹 나오는 접근법인 것 같으니 기억해두면 좋을 것 같다.

구현

그런데 여기까지 어찌 저찌 생각은 했는데, 구현을 하려면 (i, j)부터 (p, q)까지의 부분 행렬 합을 모두 구해서 최댓값을 찾아야 하는데 좌표 값이 총 4개이다보니 4차원 메모이제이션 배열이나 4중 for문이 불가피할 거 같아서 이게 맞나? 하는 생각을 했다.
그래서 결국 풀이를 찾아봤는데, 진짜 그게 맞더라,,,, 말 그대로 브루트포스였다.

import java.io.*;
import java.util.*;

class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        int N = Integer.parseInt(st.nextToken());
        int M = Integer.parseInt(st.nextToken());
        int[][] sum = new int[N + 1][M + 1];
        
        // (1, 1)에서 (i, j)까지의 누적합 구하기
        for (int i = 1; i <= N; i++) {
            st = new StringTokenizer(br.readLine());
            for (int j = 1; j <= M; j++) {
                int num = Integer.parseInt(st.nextToken());
                sum[i][j] = sum[i][j - 1] + sum[i - 1][j] - sum[i - 1][j - 1] + num;
            }
        }

        // (i, j)에서 (p, q)까지의 부분행렬의 합 구하기 -> 그 중에서 최댓값 찾기
        int result = sum[1][1];
        for (int i = 1; i <= N; i++) {
            for (int j = 1; j <= M; j++) {
                for (int p = i; p <= N; p++) {
                    for (int q = j; q <= M; q++) {
                        result = Math.max(result, sum[p][q] - sum[p][j - 1] - sum[i - 1][q] + sum[i - 1][j - 1]);
                    }
                }
            }
        }

        System.out.println(result);
    }
}


흥미로운 문제였다....ㅋㅋㅋ

profile
알고리즘 블로그 아닙니다.

0개의 댓글