
[프로그래머스] 파괴되지 않은 건물(java)
zl존석동
·2022. 7. 18. 18:43
프로그래머스 level3 - 파괴되지 않은 건물 자바 풀이
프로그래머스
코드 중심의 개발자 채용. 스택 기반의 포지션 매칭. 프로그래머스의 개발자 맞춤형 프로필을 등록하고, 나와 기술 궁합이 잘 맞는 기업들을 매칭 받으세요.
programmers.co.kr
문제 3줄 요약
숫자가 들어있는 N x M 의 행렬이 주어진다. (board) (최대 1000행 1000열)
숫자 상태를 증감시킬 수 있는 특정 크기의 직사각형 정보가 주어진다 (skill).(25만개 이하)
[ [증감 여부] , [x시작 좌표], [y시작 좌표], [x 종료 좌표], [y 종료 좌표], [상태 변경 수치] ]
skill 다 적용하고 난 뒤 board 에서 0보다 큰 값의 개수를 구하라
나의 풀이
완전탐색
완전탐색으로 풀면 매우 쉬운 문제이다. 그냥 다 돌면서 다 더하고 0보다 큰 것 찾으면 된다.
완전탐색일 경우 그냥 스킬 배열 개수만큼 돌면서 좌표만큼의 적절한 2차원 배열을 만들고 board 에 반영하면 끝이다.
하지만 이 문제는 효율성 테스트가 있다.
최악의 경우 스킬이 25만개 주어지며 x,y 좌표를 통한 직사각형 크기는 1000 x 1000 이 될 것이기에 어마어마한 연산 시간을 마주할 것이고
정확하게 어느정도로 효율성 통과여부를 판단하는지는 모르겠으나 적어도 이 방법으로는 풀지 말라는 것은 알았다.
1차원 배열처럼 안되나
25만개야 입력이니 어쩔 수 없는거고 행렬 이중 루프를 어떻게 처리를 잘 해야 풀 수 있을 것이라 생각해서 1차원 배열처럼 풀 수 없나 먼저 생각해보았지만 좋은 방법이 떠오르지 않았다
누적합 공식 응용하기
모두 같은 수치를 더하거나 빼는 형태이기 때문에 누적합을 응용할 수 있을 것이라고 생각하고 접근했다.
디지탈로 그릴 수 있는 고급 기계가 없다보니 손으로 풀이과정을 그렸던 저퀄리티의 그림을 그려가며 이해했다.
먼저 해결해야 할 관심사를 요약한 형태이다. Skill 이 1개라고 생각했을 때 이 형태이다.
행렬이 주어지고 그보다 작거나 같은 상태변경용 행렬(Skill) 이 여러개 주어지는데 그것들을 모두 반영하고 그 결과를 나타내줘야 하는 것이다.
완전탐색이라면 루프를 돌아 상태변경 행렬을 크기만큼 반영하고 다 하면 루프를 돌아 주어진 행렬에 반영하는 형태로 갔을 것이다.
1차원에 누적합 써보기
단순한 1차원 배열이라고 생각하고 1행에 대해서만 반영해보았다.
[3, 1, 4] 라는 1차원 배열에 [2, 2] 라는 배열의 값을 반영해 [5, 3, 4] 가 되어야 한다.
이를 즉시 주어진 행렬에 반영하는게 아니라 다음과 같이 수치만 명시해두고
추후에 상태 업데이트를 가져갈 수 있다.
상태 업데이트 배열 하나의 시점에 대해서 어차피 단 하나의 숫자만으로 특정 범위의 값이 갱신되어야 하는 것이기 때문에
굳이 범위만큼 돌면서 인덱스 0에 2를 넣고 인덱스 1에 2를 넣고 할 필요가 없이
주어진 범위 시작 지점에 갱신값을 넣고 범위 바로 바깥에 그 반대값을 넣어주면 된다.
이렇게 하면 입력 상태 업데이트 배열 하나하나를 모두 순회할 필요 없이 상태 배열 하나에 단 한번의 연산으로 업데이트 반영 준비를 할 수 있다.
위 처럼 상태 변경 배열 하나에 한 번의 연산으로 업데이트 준비를 할 수 있고
마지막에 한번만 누적합 공식을 적용해 모든 업데이트 정보를 만들어낼 수 있다.
갱신 범위 바로 다음 인덱스에 갱신값의 반대값을 넣어 이후 범위에 해당 상태가 전파되는 것을 막아주는 것이 핵심이다.
그럼 2차원은요?
저걸 생각하고도 2차원은 도대체 어떻게 반영해야 하나 고민하느라 머리가 깨졌던 것 같다.
역시 직접 그려보면서 이해했다.
갱신이 필요한 범위 바로 바깥에 갱신 수치의 반대값을 넣는 공략을 응용했다.
첫 행과 첫 열에만 관심을 두고 맨 앞 인덱스에 부터 누적합을 해보고
이후에 중간의 값들은 어떻게 갱신되어야 하나 생각해보았다.
위 그림에서 단순히 (2,2) 좌표의 값만을 생각해보았을 때 이 좌표의 값이 2여야 한다는 것을 우리는 암묵적으로 알고 있다.
2가 어떻게 탄생했을까 보았더니 결국 상태값의 근원지였던 (1,1) 의 2로부터 상태가 업데이트 된 (1,2)와 (2,1) 의 값으로 부터 누적되는 것임을 알게 되었다.
이대로 행과 열로 누적합을 적용한다면 우리가 관심을 가지고 있던 2 라는 업데이트 값이 각각 이전의 위치였던 앞 대각좌표로 부터 두번 반영된 것이니 그 값을 빼주는 형태로 업데이트 해주면 된다.
갱신 범위 밖의 마지막 대각위치 행렬에 갱신값으로 설정해둔 이유는
추후에 누적합을 적용할 때 2라는 값으로 부터 얻었던 영향을 제거해 0으로 만들어주기 위함이라고 볼 수 있다.
이런식으로 제공되는 기존 Board 행렬 크기에 상응하는 상태 변경용 행렬을 만들고 Skill 정보를 하나하나 인덱스 정보만 가지고 업데이트 해주면 된다.
Java 풀이 코드
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
public class Solution {
private static Map<Integer, Integer> activationMap = new HashMap<>();
public static void main(String[] args) {
int[][] board = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
int[][] skill = {{1, 1, 1, 2, 2, 4}, {1, 0, 0, 1, 1, 2}, {2, 2, 0, 2, 0, 100}};
System.out.println(solution(board, skill));
}
public static int solution(int[][] board, int[][] skill) {
activationMap.put(1, -1);
activationMap.put(2, 1);
int[][] activationArray = new int[board.length + 1][board[0].length + 1];
for (int[] item : skill) {
setActivationArray(item, activationArray);
}
setCumulativeSumOfActivationArray(activationArray);
setBoard(activationArray, board);
return (int) Arrays.stream(board).flatMapToInt(ints -> Arrays.stream(ints).filter(value -> value > 0)).count();
}
private static void setActivationArray(int[] skill, int[][] activationArray) {
int activationQuantity = activationMap.get(skill[0]) * skill[5];
activationArray[skill[1]][skill[2]] += activationQuantity;
activationArray[skill[1]][skill[4] + 1] += -activationQuantity;
activationArray[skill[3] + 1][skill[2]] += -activationQuantity;
activationArray[skill[3] + 1][skill[4] + 1] += activationQuantity;
}
private static void setCumulativeSumOfActivationArray(int[][] activationArray) {
for (int i = 1; i < activationArray[0].length; i++) {
activationArray[0][i] += activationArray[0][i - 1];
}
for (int i = 1; i < activationArray.length; i++) {
activationArray[i][0] += activationArray[i - 1][0];
}
for (int i = 1; i < activationArray.length; i++) {
for (int j = 1; j < activationArray[i].length; j++) {
activationArray[i][j] = activationArray[i][j] + activationArray[i][j - 1] + activationArray[i - 1][j] - activationArray[i - 1][j - 1];
}
}
}
private static void setBoard(int[][] activationArray, int[][] board) {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[i].length; j++) {
board[i][j] += activationArray[i][j];
}
}
}
}
구현 세부사항은 별거 없는 것 같고 solution 메소드의 각 메소드들의 동작 순서들만 확인하면 문제의 흐름이 이해 될 것 같다.
주어진 2차원 행렬 크기만큼까지도 Skill 행렬 크기가 가능하기 때문에 한 사이즈 더 크게 Skill(상태변경값) 들의 상태를 저저장해줄 행렬을 선언해준다.
사실 크기만큼 해도 상관없지만 엣지 케이스 핸들링 하기 귀찮아서 그냥 더 크게 만들었던 것 같다.
int[][] activationArray = new int[board.length + 1][board[0].length + 1];
해당 상태 행렬에 스킬들로부터 낙관적으로 모든 업데이트 상태를 반영해준다.(누적합을 적용하지 않은)
for (int[] item : skill) {
setActivationArray(item, activationArray);
}
상태 행렬의 최종 누적합을 구한다.
setCumulativeSumOfActivationArray(activationArray);
주어진 기존 행렬 Board 에 이를 모두 반영한다.
setBoard(activationArray, board);
반영 후 행렬에서 0보다 큰 값 개수를 찾아 리턴한다.
return (int) Arrays.stream(board)
.flatMapToInt(ints -> Arrays.stream(ints).filter(value -> value > 0)).count();
완전탐색과 비교했을 때 핵심은 스킬 하나하나를 루프를 돌며 그때그때 상태반영 배열을 만들어 나가느냐
마지막에 한 번만 최종적인 상태 반영 행렬을 완성시킬 것이냐의 차이인 것 같다
매우 어려웠던 문제였다.
'알고리즘 & 자료구조 > 프로그래머스' 카테고리의 다른 글
[프로그래머스] 스킬트리 (java) (0) | 2022.08.24 |
---|---|
[프로그래머스] 파일명 정렬 (java) (0) | 2022.08.15 |
[프로그래머스] 소수찾기 (Java) (0) | 2022.02.03 |
[프로그래머스] 다리를 지나는 트럭 (java) (0) | 2022.01.13 |
[프로그래머스] 기능개발(Java) (0) | 2022.01.13 |