문제 풀이 날짜: 2023.08.10
포스트 작성일: 2023.08.21

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

SWEA 6808번: 규영이와 인영이의 카드게임 (D3)

 

키워드

순열, 백트래킹

 

풀이 접근법

  • 순열 문제로, Next Permutaion을 이용하면 시간을 더욱 줄일 수 있다.
  • 상대방이 내고 남은 카드 중에서 뽑아야 하기 때문에, 상대방이 뽑지 않은 나머지 카드 배열을 별도로 만들어주어야 한다. 카드는 종류 당 한 장씩 존재하므로 중복된 카드를 뽑을 수 없다.
  • 문제에 명시되지 않은 조건 중 하나로, 두 카드 값이 같아도 상대방에게 점수를 준다. 규영이가 점수를 얻을 수 있는 경우는 오직 인영이보다 큰 수를 냈을 때(kyuCards[i] > inCards[i])에만 해당한다.
  • 인영이가 내는 카드 순열을 생성하는 데에 비해, 구해야 하는 승/패 횟수는 규영이의 것이기 때문에 주의한다.
  • 이름이 비슷해서 문제 해석하는데 헷갈린다.

 

코드

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

public class Solution {

	private static final int SIZE = 9;
	private static final int MAX = 362_880;

	private static int[] kyuCards;
	private static int[] inCards;

	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		int T = Integer.parseInt(br.readLine());

		StringBuilder sb = new StringBuilder();
		for (int test_case = 1; test_case <= T; test_case++) {

			String[] input = br.readLine().split(" ");
			
			kyuCards = new int[SIZE];
			inCards = new int[SIZE];

			boolean[] used = new boolean[SIZE * 2 + 1];
			
			for (int i = 0; i < SIZE; i++) {
				kyuCards[i] = Integer.parseInt(input[i]);
				used[kyuCards[i]] = true;
			}
			
			int idx = 0;
			for(int i = 1; i <= SIZE * 2; i++) {
				if(!used[i]) {
					inCards[idx++] = i;
				}
			}
			
			Arrays.sort(inCards);
			
			//초기 상태 계산
			int count = 0;
			count += calculate();

			//마지막 순열이 될 때까지 반복
			while(nextPermutation(inCards)) {
				count += calculate();
			}

			sb.append("#").append(test_case).append(" ").append(count).append(" ")
				.append(MAX - count).append("\n");
		}

		System.out.print(sb);
	}

	private static int calculate() {
		int kScore = 0;
		int iScore = 0;
		
		for(int i = 0; i < SIZE; i++) {
			if(kyuCards[i] > inCards[i]) {
				kScore += (kyuCards[i] + inCards[i]);
			}
			else {
				iScore += (kyuCards[i] + inCards[i]);
			}
		}
		
		if(kScore > iScore) {
			return 1;
		}
		
		return 0;
	}

	public static boolean nextPermutation(int[] target) {

		int swapIdx = -1;
		for (int i = 1; i < target.length; i++) {
			if (target[i - 1] < target[i])
				swapIdx = i - 1;
		}

		if (swapIdx == -1)
			return false;

		int largerIdx = -1;
		for (int j = 1; j < target.length; j++) {
			if (target[swapIdx] < target[j])
				largerIdx = j;
		}

		swap(target, swapIdx, largerIdx);

		int j = target.length - 1;
		for (int i = swapIdx + 1; i < j; i++) {
			swap(target, i, j);
			j--;
		}
		
		inCards = target;

		return true;
	}

	public static void swap(int[] arr, int i, int j) {
		int temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;
	}

}

알고리즘 문제 풀이에는 독해력도 중요하게 작용된다는 걸 다시금 깨달은 문제였다. 구현 내용 자체는 크게 어렵지 않지만 혼동되는 정보들을 주의하자.

 

참고한 링크

https://comgong-man.tistory.com/23

 

git 링크

(git 링크 첨부)

문제 풀이 날짜: 2023.08.15
포스트 작성일: 2023.08.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

분할 정복

 

키워드

백준 온라인 저지 1992번: 쿼드트리 (실버1)

 

풀이 접근법

  • 1 / 4 분할정복 문제이다. 2 x 2 배열이 될 때까지 쪼개는 방식으로 풀이했다. (재귀 호출)
  • (0, 0) 칸에서 시작하고 한 변의 절반 (size / 2)만큼 칸을 옮겨가며 재귀를 호출한다.
  • 2 x 2 범위 내 비트가 모두 같다면 압축해서 한 자릿수로 나타내고, 서로 다른 비트는 압축하지 않고 모두 나열한다.
  • 괄호는 재귀를 시작하면서 열고, 재귀를 마치고 반환받으면서 닫는다.

 

코드

import java.io.*;

public class Main {

	private static int[][] matrix;
	
	private static StringBuilder sb;
	
	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		int N = Integer.parseInt(br.readLine());

		matrix = new int[N][N];
		
		sb = new StringBuilder();
		
		for(int i = 0; i < N; i++) {
			String line = br.readLine();
			for(int j = 0; j < N; j++) {
				matrix[i][j] = line.charAt(j) - '0';
			}
		}
		
		quadTree(N, 0, 0);
		
		System.out.println(sb);
	}

	private static void quadTree(int size, int r, int c) {
		//모두 같은 비트라면 압축
		if(isAllSameBit(size, r, c)) {
			sb.append(matrix[r][c]);
			return;
		}
		
		if(size == 2) { // 2 * 2 matrix까지 쪼개졌을 때
			sb.append("(");
			for(int i = r; i < r + size; i++) {
				for(int j = c; j < c + size; j++) {
					sb.append(matrix[i][j]);
				}
			}
			sb.append(")");
			
			return;
		}
		
		sb.append("("); //재귀를 시작하며 괄호 열기
		int newSize = size / 2;
		quadTree(newSize, r, c);
		quadTree(newSize, r, c + newSize);
		quadTree(newSize, r + newSize, c);
		quadTree(newSize, r + newSize, c + newSize);
		sb.append(")"); //4등분 재귀가 끝나면 괄호 닫기
	}

	private static boolean isAllSameBit(int size, int r, int c) {
		int initialBit = matrix[r][c];
		
		for(int i = r; i < r + size; i++) {
			for(int j = c; j < c + size; j++) {
				if(matrix[i][j] != initialBit) return false;
			}
		}
		
		return true;
	}
}

 

 

git 링크

(git 링크 첨부)

문제 풀이 날짜: 2023.08.16
포스트 작성일: 2023.08.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 3109번: 빵집 (골드2)

 

키워드

그리디, DFS (그래프), 백트랙킹

 

풀이 접근법

  • 가능한 공간내에 파이프를 많이 설치해야하기 때문에 0번째 행부터 탐색한다. (그리디)
  • 탐색 순서는 오른쪽 위, 오른쪽, 오른쪽 아래 순으로 한다.
  • 그리디하게 접근해도 되는 문제이기 때문에, 이미 하나의 파이프를 설치했다면 그 외의 경우를 고려하지 않고 가지치기 해야 한다. ( if(finished) continue; )
    • 즉, 유망하지 않은 길이 있다면 자식노드의 탐색을 중단하고 다른 경우를 탐색한다.
    • 이 문제의 경우 1. 길이 막혀있을 경우 2. 이미 완성된 루트를 지나가려고 하는 경우의 두 가지를 유망하지 않은 것으로 본다.

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

	private static int R;
	private static int C;
	
	private static int answer = 0;
	private static boolean finished;
	
	private static final char BUILDING = 'x';
	private static final char ROAD = '.';
	
	private static int[] dy = {-1, 0, 1};
	private static int[] dx = {1, 1, 1};
	
	private static char[][] map;
	private static boolean[][] visited;
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		String[] input = br.readLine().split(" ");
		
		R = Integer.parseInt(input[0]);
		C = Integer.parseInt(input[1]);
		
		map = new char[R][C];
		visited = new boolean[R][C];
		
		for(int i = 0; i < R; i++) {
			String line = br.readLine();
			for(int j = 0; j < C; j++) {
				map[i][j] = line.charAt(j);
			}
		}
		
		for(int i = 0; i < R; i++) {
			finished = false; //새 루트를 찾을 때 초기화
			DFS(i, 0);
		}

		System.out.println(answer);
	}
	
	public static void DFS(int row, int col) {
		if(col == C - 1) {
			answer++;
			finished = true; //한 루트를 발견했으므로 체크
			return;
		}
		
		for(int i = 0; i < dy.length; i++) {
			int nr = row + dy[i];
			int nc = col + dx[i];
			
			if(!isValid(nr, nc)) {
				continue;
			}
			
			if(map[nr][nc] == BUILDING) {
				continue;
			}
			
			if(visited[nr][nc]) {
				continue;
			}
			
			if(finished) continue; //이미 최적 경로를 찾았다면 중단
			
			visited[nr][nc] = true;
			DFS(nr, nc);
		}
		
		return;
	}

	private static boolean isValid(int nr, int nc) {
		return (nr > -1 && nc > -1 && nr < R && nc < C);
	}

}

이미 최적의 경로를 찾았다면 자식노드를 탐색하지 않고 반환해주는 것이 아주 중요한 문제였다. 그 부분을 빠트려서 제대로 된 답을 내지 못했었다. 백트랙킹의 본질을 다시 공부할 필요가 있겠다.  

 

참고한 링크

https://velog.io/@qwerty1434/백준-3109번-빵집

 

git 링크

(git 링크 첨부)

문제 풀이 날짜: 2023.08.16
포스트 작성일: 2023.08.16

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 6987번: 월드컵 (골드4)

 

키워드

백트랙킹, 상태 탐색

 

풀이 접근법

  • 단순히 이긴 횟수, 비긴 횟수, 패배한 횟수만을 비교해서 답을 구할 수 없다.
  • 예를 들면 '5 0 0 5 0 0 5 0 0 0 0 5 0 0 5 0 0 5' 과 같은 경우 승패가 짝이 맞지만, 한 팀이 5승을 하기 위해서는 다른 팀 전적에 최소 1패 씩은 있어야 한다.
  • 따라서 해당 문제를 풀기 위해선 각 팀별로 모두 최소 1번씩 승리 - 무승부 - 패배를 체크하며 풀이해야 한다.
    • 1 → 2, 3, 4, 5, 6
    • 2 3, 4, 5, 6
    • 3 4, 5, 6
    • 4 5, 6
    • 5 6
  • 재귀를 승리 - 무승부 - 패배세 갈래로 호출시킨다. (자식이 3개인 상태 탐색 트리 형태)  
  • 만약 '5 0 0 3 0 2 2 0 3 0 0 5 4 0 1 1 0 4' 이라는 입력이 있을 때…
    • 모든 경기의 승패를 따지면서 입력 값을 감소시켜 '0 0 0 0 0 0 0 0 0 ...' 으로 만들어보자.
    • 만약 경기 횟수를 감소시키다가 음수값이나온다면 불가능한 경우의 수이다. (승-패 짝이 맞지 않음)
    • 백트래킹을 완료했을 때 모두 0으로 채워진 결과물을 얻게 된다면 가능한 경우의 수 이다.
  • 대진표(games 리스트)를 미리 생성하여 참조하면 보다 쉽게 풀 수 있다.

 

코드

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

public class Main {

	private static final int TEAM = 6;
	private static final int COUNT = 3;

	private static final int WIN = 0;
	private static final int DRAW = 1;
	private static final int LOSE = 2;

	private static final int MATCH_COUNT = 15;
	
	private static int result;
	private static List<String> games;

	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		StringBuilder sb = new StringBuilder();
		for (int tc = 0; tc < 4; tc++) {
			StringTokenizer st = new StringTokenizer(br.readLine(), " ");

			//games에 대결 정보를 미리 생성한다.
			games = new ArrayList<>();
			
			for(int i = 0; i < TEAM; i++) {
				for(int j = i + 1; j < TEAM; j++) {
					games.add(i + "" + j);
				}
			}
			
			int[][] matches = new int[TEAM][COUNT];
			for (int i = 0; i < TEAM; i++) {
				for (int j = 0; j < COUNT; j++) {
					matches[i][j] = Integer.parseInt(st.nextToken());
				}
			}

			result = 0;
			backTracking(matches, 0);

			sb.append(result).append(" ");
		}

		System.out.println(sb);
	}

	private static void backTracking(int[][] matches, int depth) {
		if (MATCH_COUNT == depth) {
			if(!isValidResult(matches)) {
				return;
			}
			result = 1;
			return;
		}
		
		int curr = games.get(depth).charAt(0) - '0';
		int rival = games.get(depth).charAt(1) - '0';

		// 승부 결과 생성
		if(matches[curr][WIN] > 0 && matches[rival][LOSE] > 0) {
			// 1.승 & 패
			matches[curr][WIN]--;
			matches[rival][LOSE]--;
			backTracking(matches, depth + 1);
			matches[curr][WIN]++;
			matches[rival][LOSE]++;
		}

		if(matches[curr][DRAW] > 0 && matches[rival][DRAW] > 0) {
			// 2.무 & 무
			matches[curr][DRAW]--;
			matches[rival][DRAW]--;
			backTracking(matches, depth + 1);
			matches[curr][DRAW]++;
			matches[rival][DRAW]++;
		}

		if(matches[curr][LOSE] > 0 && matches[rival][WIN] > 0) {
			// 3.패 & 승
			matches[curr][LOSE]--;
			matches[rival][WIN]--;
			backTracking(matches, depth + 1);
			matches[curr][LOSE]++;
			matches[rival][WIN]++;
		}

		return;
	}

	private static boolean isValidResult(int[][] matches) {
		for (int i = 0; i < TEAM; i++) {
			for (int j = 0; j < COUNT; j++) {
				if(matches[i][j] != 0) return false;
			}
		}
		
		return true;
	}

}

어느 것을 백트랙킹으로 구하고 원복시킬지 정하기 어려운 문제였다. 처음에는 가능한 모든 승패횟수를 구해서 입력과 같은 것이 발견되면 그것을 답으로 삼으려고 했으나, 역으로 생각하여 입력 배열을 모두 0으로 만드는 것이 비교적 쉬운 접근법이었다. 풀이가 쉽게 구상되지 않아 다른 블로그를 참고하여 풀이했다.

 

참고한 링크

https://maivve.tistory.com/215
https://dkswnkk.tistory.com/653

 

git 링크

(git 링크 첨부)

문제 풀이 날짜: 2023.08.09
포스트 작성일: 2023.08.09

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 11286번: 절댓값 힙 (실버1)

 

키워드

우선순위 큐, Comparator

 

풀이 접근법

  • 원본 값(value)과 절댓값(abs)을 동시에 손쉽게 참조할 수 있도록 Node 클래스를 정의하였다.
  • 2가지 정렬 기준을 가지고 Comparator를 구현하는 것이 중요한 문제이다. 첫 번째 정렬 기준이 동일하다면 0을 반환하지 않고, 그 다음으로 매길 정렬 기준을 구현해준다.
  • 람다식으로 구현할 경우, 두 번째 정렬 조건(if문)에 걸리면 먼저 return해주고 조건에 걸리지 않으면 첫 번째 정렬 기준을 따르도록 작성하면 compare 메소드를 간단하고 빠르게 구현할 수 있다.
  • 아래의 람다식을 compare 메소드로 풀어서 써보면 다음과 같다.
PriorityQueue<Node> pq = new PriorityQueue<>( new Comparator<Node>() {
			@Override
			public int compare(Node o1, Node o2) {
				if(o1.abs > o2.abs) {
					return 1;
				}
				else if (o1.abs < o2.abs) {
					return -1;
				}
				else { //절댓값이 같다면
					if(o1.value > o2.value) {
						return 1; //원본값이 작은 것을 반환한다
					}
					else {
						return -1;
					}
				}
			}
		});

 

코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;

public class Main {

	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		int N = Integer.parseInt(br.readLine());
		
		StringBuilder sb = new StringBuilder();
		
		int curr;
		PriorityQueue<Node> pq = new PriorityQueue<>(
				(o1, o2) -> {
					if(o1.abs == o2.abs) return o1.value - o2.value;
					return o1.abs - o2.abs;
				});
		
		for(int i = 0; i < N; i++) {
			curr = Integer.parseInt(br.readLine());
			
			if(curr == 0) {
				if(!pq.isEmpty())
					sb.append(pq.poll().value).append("\n");
				else
					sb.append(0).append("\n");
				continue;
			}
			
			Node n = new Node(curr, Math.abs(curr));
			pq.add(n);
		}
		
		System.out.println(sb);
	}

}

class Node {
	public int value;
	public int abs;
	
	public Node(int value, int abs) {
		this.value = value;
		this.abs = abs;
	}
}

 


 

 

git 링크

(git 링크 첨부)

문제 풀이 날짜: 2023.08.09
포스트 작성일: 2023.08.09

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 16935번: 배열 돌리기 3 (골드5)

 

키워드

구현

 

풀이 접근법

  • 90도 회전의 경우 배열의 가로, 세로 길이가 뒤바뀜에 주의해야 한다.
  • 원본 배열 값을 새 배열의 새 위치에 배치하는 식으로 구현하였다.
    • 현재 matrix[i][j]를 보고 있다고 할 때, 새 배열의 몇 번 인덱스에 현재 값(matrix[i][j])이 위치해야 하는지 인덱스를 구해준다.
    • 이전 배열의 row, col 값이 새 배열의 x, y가 되려면 어떠한 계산을 거쳐야 하는지 방정식(?)을 세우고 반복문을 돌려준다.
  • 배열을 네 구역으로 나눠서 값을 옮겨주는 연산은 배열의 mid 값(배열의 가운데 인덱스)을 활용한다.
    • 현재 인덱스 i, j 가 mid를 넘었는지 넘지 않았는지를 기준으로 새 인덱스를 다르게 계산한다.

 

코드

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

public class Main {
	
	private static int N;
	private static int M;
	private static int[][] matrix;
	private static int[][] tempMatrix;
	
	public static void main(String[] args) throws NumberFormatException, IOException {
		//배열 회전이 더 중요하므로 main 생략
	}
	
	private static int[][] flipUpDown() { //1
		int N = tempMatrix.length;
		int M = tempMatrix[0].length;
		
		int[][] result = new int[N][M];
		//col 고정
		for(int j = 0; j < M; j++) {
			for(int i = 0; i < N / 2; i++) {
				result[(N - 1) - i][j] = tempMatrix[i][j];
				result[i][j] = tempMatrix[(N - 1) - i][j];
			}
		}
		
		return result;
	}
	
	private static int[][] flipLeftRight() { //2
		int N = tempMatrix.length;
		int M = tempMatrix[0].length;
		
		int[][] result = new int[N][M];
		//row 고정
		for(int i = 0; i < N; i++) {
			for(int j = 0; j < M / 2; j++) {
				result[i][(M - 1) - j] = tempMatrix[i][j];
				result[i][j] = tempMatrix[i][(M - 1) - j];
			}
		}
		
		return result;
	}
	
	private static int[][] rotateRight() { //3
		int N = tempMatrix.length;
		int M = tempMatrix[0].length;
		
		int[][] result = new int[M][N];
		
		for(int i = 0; i < N; i++) { 
			for(int j = 0; j < M; j++) { 
				result[j][(N - 1) - i] = tempMatrix[i][j];
			}
		}
		
		return result;
	}

	private static int[][] rotateLeft() { //4
		int N = tempMatrix.length;
		int M = tempMatrix[0].length;
		
		int[][] result = new int[M][N];
		
		for(int i = 0; i < N; i++) { 
			for(int j = 0; j < M; j++) { 
				result[(M - 1) - j][i] = tempMatrix[i][j];
			}
		}
		
		return result;
	}
	
	private static int[][] rotateClockwise() { //5
		int N = tempMatrix.length;
		int M = tempMatrix[0].length;
		
		int[][] result = new int[N][M];
		
		int rMid = (N / 2) - 1;
		int cMid = (M / 2) - 1;
		for(int i = 0; i < N; i++) {
			for(int j = 0; j < M; j++) {
				if(i <= rMid && j <= cMid) { //왼쪽위
					result[i][j + (cMid + 1)] = tempMatrix[i][j];
				} else if(i <= rMid && j > cMid) { //오른쪽위
					result[i + (rMid + 1)][j] = tempMatrix[i][j];
				} else if(i > rMid && j > cMid) { //오른쪽아래
					result[i][j - (cMid + 1)] = tempMatrix[i][j];
				} else { //왼쪽아래
					result[i - (rMid + 1)][j] = tempMatrix[i][j];
				}
			}
		}
		
		return result;
	}
	
	private static int[][] rotateCounterclockwise() { //6
		int N = tempMatrix.length;
		int M = tempMatrix[0].length;
		
		int[][] result = new int[N][M];
		
		int rMid = (N / 2) - 1;
		int cMid = (M / 2) - 1;
		for(int i = 0; i < N; i++) {
			for(int j = 0; j < M; j++) {
				if(i <= rMid && j <= cMid) { //왼쪽위
					result[i + (rMid + 1)][j] = tempMatrix[i][j];
				} else if(i <= rMid && j > cMid) { //오른쪽위
					result[i][j - (cMid + 1)] = tempMatrix[i][j];
				} else if(i > rMid && j > cMid) { //오른쪽아래
					result[i - (rMid + 1)][j] = tempMatrix[i][j];
				} else { //왼쪽아래
					result[i][j + (cMid + 1)] = tempMatrix[i][j];
				}
			}
		}
		
		return result;
	}
}

배열 돌리기 문제는 대부분 배열의 새 인덱스 계산이 핵심이다.  달팽이 문제처럼 배열값이 재배치되는 방향이 지속적으로 바뀌지 아닌 이상 고정된 식으로 반복문을 돌려서 답을 구할 수 있을 것이다.

 

git 링크

(git 링크 첨부)

문제 풀이 날짜: 2023.08.08
포스트 작성일: 2023.08.08

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 1168번: 요세푸스 문제 2 (플래티넘 3)

 

키워드

구간 합, 세그먼트 트리

 

풀이 접근법

  • 세그먼트 트리에 담는 값은 앞으로 뽑아야 할 개수이다.
  • 세그먼트 트리의 자식 노드에는 앞으로 뽑아야 할 개수의 부분합을 넣는다. 루트노드가 7이라면 자식 노드로 4와 3을 가진다. (7 = 4 + 3) 
  • 부분합 노드를 따라가면 해당 구간에 몇부터 몇까지의 수가 위치해 있는지 알 수 있다. 
    • 예를 들어, 총 7개의 수를 뽑아야 하는 상황에서 3번째 값을 찾고 싶다고 하면, [4] / [3]으로 나뉜 세그먼트 트리에서 4 노드를 선택한다. 3번째 값은 {1, 2, 3, 4} / {5, 6, 7} 구간 중에서 전자에 존재하기 때문이다.
    • 만약 왼쪽 구간과 오른쪽 구간의 크기가 같을 때, 즉 {1, 2} / {3, 4} 로 나뉜 노드에서는 {3, 4}쪽을 더 유망하다고 본다.
  • K번째 수를 찾는다고 할 때는, K번째 수를 나타내는 인덱스(rank)를 계속 변화시켜야 한다.
    • 가령 3번째 수를 뽑았다면, 그 위치로부터 또 3번째 뒤에 있는 수를 찾아야 하므로 인덱스를 6으로 변화 시켜준다.
    • 그런데 배열의 길이를 넘은 만큼 인덱스를 늘릴 수는 없으므로, 다음과 같이 일정한 규칙에 따라서 인덱스를 조절해주어야 한다.
    • rank = (rank + K - 2) % (N - i - 1) + 1
    • 이 규칙대로 rank를 변화시키고, 매번의 트리 탐색에서 rank번째에 위치한 값을 뽑아 반환한다.
    • rank 번째 값을 찾고 반환 시키는 과정 중, 세그먼트 트리의 원칙에 맞추어 뽑아야 할 개수를 갱신 시킨다.

 

코드

package algo;

import java.io.*;

public class boj1168 {

	private static final int SIZE = 100_000;
	private static int[] tree;
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		String[] temp = br.readLine().split(" ");
		
		int N = Integer.parseInt(temp[0]);
		int K = Integer.parseInt(temp[1]);
		
		tree = new int[4 * SIZE];
		
		init(0, N - 1, 1); //0부터 N - 1까지 부분합을 초기화한다.
		
		int rank = K;
		
		StringBuilder sb = new StringBuilder();
		sb.append("<");
		for(int i = 0; i < N; i++) {
			int result = update(0, N - 1, 1, rank) + 1; 
			sb.append(result);
			
			if(i < N - 1) {
				rank = (rank + K - 2) % (N - i - 1) + 1;
				sb.append(", ");
			}
		}
		sb.append(">");
		
		System.out.println(sb);
	}
	
	private static int init(int start, int end, int node) {
		if(start == end) {
			return tree[node] = 1; //노드값으로 시작점을 달아준다.
		}
		int mid = (start + end) / 2; //이분 탐색
		return tree[node] 
				= init(start, mid, node * 2) + init(mid + 1, end, node * 2 + 1);
	}
	
	private static int update(int start, int end, int node, int rank) {
		if(start == end) {
			tree[node] -= 1;
			return start;
		}
		
		int mid = (start + end) / 2;
		
		int result = 0;
		if(rank <= tree[node * 2]) { //rank가 왼쪽 자식 노드보다 작거나 같다면
			result = update(start, mid, node * 2, rank); //왼쪽 서브트리 탐색
		} else { //rank > tree[node * 2] //오른쪽 서브트리 탐색
			result = update(mid + 1, end, node * 2 + 1, rank - tree[node * 2]);
		}
		
		/**
		 * 1씩 줄어든 리프 노드를 토대로 윗 부분을 갱신한다.
		 */
		tree[node] = tree[node * 2] + tree[node * 2 + 1];
		
		return result;
	}

}

세그먼트 트리는 시간을 O(logN)까지 줄이기 위한 수단에 가깝고, 다음 인덱스(rank)를 잘 구하는 것이 특히 중요한 문제였다. 인덱스 규칙만 잘 찾으면 LinkedList로도 풀 수 있어 보인다. 세그먼트 트리의 내용물로 구간을 저장하는 것까지는 구상해냈으나, 인덱스 계산에서 막혀 다른 풀이를 참고했다.

 

참고 링크

https://byeo.tistory.com/entry/boj-1168-%EC%9A%94%EC%84%B8%ED%91%B8%EC%8A%A4-%EB%AC%B8%EC%A0%9C-2

 

git 링크

(git 링크 첨부)

문제 풀이 날짜: 2023.08.07
포스트 작성일: 2023.08.07

 

* 학습 목적으로 작성하는 글입니다. 풀이가 정석적이지 못할 수도 있습니다.

 

문제 출처

백준 온라인 저지 2493번: 탑 (골드5)

 

키워드

스택, 동적 계획법

 

풀이 접근법

  • 브루트포스가 안 되는 이유 ⇒ 내림차순일 때 최악의 경우 시간복잡도가 O(n!)까지 나타날 수 있다.
  • DP 배열에는 현재 위치한 i번째 탑에서 레이저가 닿는 가장 가까운 탑의 번호를 저장한다.
  • 맨 첫번째 탑은 왼쪽으로 레이저가 닿는 탑이 하나도 없으므로 DP배열의 0번째 값은 0으로 초기화 한다.
    • 반복문을 순회하며 i번째와 i - 1번째 탑의 높이를 비교한다.
    • 만약 i - 1번째 탑이 i번째 탑보다 높다면, i번째 탑의 레이저가 i - 1번째 탑에 닿는 것이다. 따라서 DP배열에 i - 1번째 탑의 순서를 저장한다.
    • 만약 i - 1번째 탑이 i번째 탑보다 낮다면, i번째 탑의 레이저가 닿는 가장 가까운 탑을 찾아야 한다. i - 1번째 탑의 DP값을 기준으로 그보다 왼쪽에 i보다 높은 탑이 있는지 찾는다. 이는 가장 유망한 높이의 탑부터 탐색하는 것이다.
    • 만약 i - 1번째 탑의 DP값을 기준으로 찾지 않고 i - 1번째 탑의 높이부터 탐색한다면 시간 초과가 발생한다.
  • 즉, 가장 유망한 탑으로부터 인덱스 j를 줄여가며 반복문을 순회하기 때문에 시간초과를 내지 않고 답을 찾을 수 있다.

 

코드

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

public class Main {

	public static void main(String[] args) throws NumberFormatException, IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		int N = Integer.parseInt(br.readLine());

		int[] towers = new int[N];

		StringTokenizer st = new StringTokenizer(br.readLine(), " ");
		for (int i = 0; i < N; i++) {
			towers[i] = Integer.parseInt(st.nextToken());
		}

		int[] DP = new int[N];
		DP[0] = 0;
		for (int i = 1; i < N; i++) {
			if(towers[i] >= towers[i - 1]) {
				int start = DP[i - 1];
				
                	//현재 탑보다 높으면서도 가장 가까운 것을 찾는다.
				for(int j = start; j >= 0; j--) {
					if(towers[j] > towers[i]) {
						DP[i] = j + 1;
						break;
					}
				}
			}
			else { //towers[i] < towers[i - 1]
				DP[i] = i;
			}
		}

		StringBuilder sb = new StringBuilder();
		for (int dp : DP) {
			sb.append(dp).append(" ");
		}

		System.out.println(sb);
	}
}

 

git 링크

https://github.com/jinju9553/23-CodingTest/commit/892fa83dcd931e182c45f31a53d5c028fe38f388

+ Recent posts