[TIL] 동적 프로그래밍 - 돗자리 깔기 문제
📕학습 개요
유니티 캠프 20일차.
날씨도 많이 풀리고, 연휴 기간이 다가오다 보니 마음이 해이해지고 집중력이 떨어지는 느낌이다.
📖학습 내용
문제 1: 지폐 접기 (Java)
문제 요약
- 지갑 크기 (
wallet)와 지폐 크기 (bill)가 주어질 때, 지폐를 규칙에 맞게 접어 지갑에 넣기 위한 최소 접기 횟수를 구하는 문제.
규칙 및 조건
- 접기: 항상 지폐의 긴 쪽을 기준으로 반으로 접는다.
- 홀수 처리: 접기 전 길이가 홀수였다면, 접은 후 소수점 이하는 버린다 (정수 나눗셈).
- 종료 조건: 접힌 지폐를 그대로 또는 90도 돌려서 지갑에 넣을 수 있으면 멈춘다.
- 이는 지폐의 긴 쪽이 지갑의 긴 쪽보다 작거나 같고 AND 지폐의 짧은 쪽이 지갑의 짧은 쪽보다 작거나 같으면 만족 (
bill_large <= wallet_large && bill_small <= wallet_small).
- 이는 지폐의 긴 쪽이 지갑의 긴 쪽보다 작거나 같고 AND 지폐의 짧은 쪽이 지갑의 짧은 쪽보다 작거나 같으면 만족 (
접근 방식 및 개선
- 초기 코드 분석: 가로/세로 중 더 긴 값과 짧은 값의 인덱스를 추적 (
walletBigIndex,billSmallIndex등)하여 구현. 로직은 정확했으나 인덱스 관리 부분이 다소 복잡하게 느껴질 수 있음. - 개선 아이디어:
Math.max,Math.min을 사용하여 지갑과 지폐의 긴/짧은 길이를 값 자체로 직접 다루는 것이 가독성 측면에서 더 유리함. - 핵심 로직:
while루프를 사용하여 지폐가 지갑에 들어갈 수 있을 때까지 (max(bill) <= max(wallet) && min(bill) <= min(wallet)) 반복하며, 매번 현재 지폐의 긴 쪽 길이를/= 2연산으로 줄이고 접은 횟수를 증가시킴.
int walletLarge = Math.max(wallet[0], wallet[1]);
int walletSmall = Math.min(wallet[0], wallet[1]);
int currentBillW = bill[0];
int currentBillH = bill[1];
int foldCount = 0;
while (true) {
int billLarge = Math.max(currentBillW, currentBillH);
int billSmall = Math.min(currentBillW, currentBillH);
if (billLarge <= walletLarge && billSmall <= walletSmall) {
break;
}
foldCount++;
if (currentBillW >= currentBillH) { // 같을 땐 아무 쪽이나 접어도 됨
currentBillW /= 2;
} else {
currentBillH /= 2;
}
}
문제 2: 돗자리 깔기(Java)
문제 요약
- 장애물(‘X’ 또는 다른 문자)과 빈칸(‘-1’ 또는 ‘.’)으로 이루어진 공원 지도(park)와 여러 크기의 정사각형 돗자리(mats)가 주어질 때, 공원에 깔 수 있는 가장 큰 돗자리의 한 변 길이를 구하는 문제.
접근 방식: 동적 프로그래밍 (DP)
- DP 최적화: dp[i][j]를 (i, j)를 오른쪽 아래 꼭짓점으로 하는 가장 큰 빈 정사각형의 한 변 길이로 정의.
- DP 점화식:
- park[i][j]가 장애물이면: dp[i][j] = 0
- park[i][j]가 빈칸이면:
- i == 0 또는 j == 0 (가장자리): dp[i][j] = 1
- 그 외: dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1])) + 1
- 최종 답 찾기:
- DP 테이블을 채우면서 가능한 최대 빈 정사각형 크기 maxSize를 구한다.
- mats 배열을 내림차순으로 정렬한다. (또는 오름차순 정렬 후 역순으로 순회)
- 정렬된 mats를 순회하며 mat_size <= maxSize인 첫 번째 값 (가장 큰 값)을 찾아 반환한다.
-
트러블슈팅:
- 언어 혼용 실수: 초기 코드 작성 시 C#과 Java 문법 혼용 (e.g., Math.Min/min, foreach :/in, Array.Reverse/Reverst typo) -> Java 문법으로 통일 및 수정.
- Java 특징: 문자열 비교는 .equals(), 배열 길이는 .length, Math.min은 두 인자만 받으므로 중첩 필요, 기본 타입 배열(int[]) 내림차순 정렬 시 Integer[] 변환 후 Collections.reverseOrder() 사용 또는 오름차순 정렬 후 역순 반복.
- 로직 오류 디버깅:
- 문제: mats를 오름차순 정렬 후, 앞에서부터 순회하며 조건을 만족하는 첫 번째 값에서 break하여 가장 작은 돗자리가 반환됨.
- 해결: 오름차순 정렬 후 뒤에서부터 순회하거나, 내림차순 정렬 후 앞에서부터 순회하여 maxSize 이하인 가장 큰 돗자리 크기를 찾아야 함.
public int solution(int[] mats, String[][] park) { int answer = -1; int maxSize = 0; int h = park.length; int w = park[0].length; int[][] dp = new int[h][w]; for(int i = 0; i < h; i++){ for(int j = 0; j < w; j++){ String rect = park[i][j]; if(!rect.equals("-1")){ dp[i][j] = 0; } else if(i == 0 || j == 0){ dp[i][j] = 1; } else{ dp[i][j] = Math.min(dp[i-1][j], Math.min(dp[i][j-1], dp[i-1][j-1])) + 1; } if(dp[i][j] > maxSize) maxSize = dp[i][j]; } } if(maxSize > 0){ Arrays.sort(mats); for (int k = mats.length - 1; k >= 0; k--) { int mat_size = mats[k]; if (mat_size <= maxSize) { answer = mat_size; break; } } } return answer; }
🏁오늘 배운 핵심 내용 정리
- 알고리즘 선택: 문제의 제약 조건과 특성을 파악하여 적절한 알고리즘(단순 반복 vs DP)을 선택하는 것이 중요.
- DP 패턴: ‘최대 정사각형 찾기’ DP 문제는 유사한 다른 문제에도 적용 가능한 유용한 패턴. 점화식을 정확히 세우는 연습 필요.
- 배열 처리: 정렬 후 원하는 값을 찾기 위해 순회하는 방향(앞->뒤, 뒤->앞)이 로직의 정확성에 큰 영향을 미친다는 것을 디버깅을 통해 명확히 인지함.
- 언어 정확성: 사용하는 프로그래밍 언어의 문법과 표준 라이브러리 함수 사용법을 정확히 아는 것이 중요 (e.g., Math.min 인자 개수, 정렬 방법).
- 디버깅: 코드가 컴파일되더라도 논리적인 오류가 있을 수 있음. 예제 입력을 통해 단계별로 값을 추적하며 예상과 다른 부분을 찾아내는 것이 중요.
댓글남기기