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

 

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

 

문제 출처

SWEA 1251번: 하나로 (D4)

 

키워드

프림 알고리즘

 

풀이 접근법

  • 기초적인 프림 알고리즘을 활용하여 풀이한다. 입력 형식에 맞게 인접 리스트로 그래프를 구현한다.
  • 문제의 입력으로 각 정점 사이의 거리만이 주어지는데, 프림 알고리즘에서는 정점 기반으로 다음 정점으로 가는 가장 가중치가 작은 간선을 선택해야 한다. 따라서 한 점에서 다른 모든 점으로 향하는 간선을 일괄 생성해준다.
    • 정점 간의 거리(dist)와 도착 정점 정보(u)를 저장하는 Vertex 클래스를 생성하여 인접리스트에 넣어주었다.
    • 우선순위 큐에 저장된 정점들을 순회하며 거리가 최소인 간선부터 뽑는다. 방문한 적 있는 정점이면 다음 정점을 뽑고, 방문한 적 없는 정점이라면 해당 정점과 연결된 간선들을 큐에 넣어준다. 
    • 우선순위 큐가 빌 때까지 위 과정을 반복한다.

 

코드

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

public class Solution {

	static class Vertex implements Comparable<Vertex> {
		int num; // 정점 번호
		long weight; // 트리 정점과 연결했을 때 간선 비용

		public Vertex(int num, long weight) {
			this.num = num;
			this.weight = weight;
		}

		@Override
		public int compareTo(Vertex o) {
			return Long.compare(this.weight, o.weight);
		}
	}

	private static List<Vertex>[] adjList; // 인접 리스트

	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++) {
			int N = Integer.parseInt(br.readLine());

			adjList = new LinkedList[N];
			for (int i = 0; i < N; i++) {
				adjList[i] = new LinkedList<>();
			}
			
			boolean[] visited = new boolean[N];
			PriorityQueue<Vertex> pq = new PriorityQueue<>();

			int[] px = new int[N];
			int[] py = new int[N];

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

			double E = Double.parseDouble(br.readLine());
			
			for(int v = 0; v < N; v++) {
				for(int u = 0; u < N; u++) {
					if(v == u) continue;
					
					long distX = Math.abs(px[v] - px[u]);
					long distY = Math.abs(py[v] - py[u]);
					
					long dist = distX * distX + distY * distY;
					
					adjList[v].add(new Vertex(u, dist));
				}
			}
			
			pq.offer(new Vertex(0, 0));
			
			sb.append("#").append(test_case).append(" ");
			
			int count = 0;
			long result = 0; // 최소 신장 트리 비용
			long minWeight = 0;
			int minVertex = 0;
			
			while(!pq.isEmpty()) {
				Vertex curr = pq.poll();

				minWeight = curr.weight;
				minVertex = curr.num;
				if (visited[minVertex]) {
					continue;
				}

				visited[minVertex] = true;
				result += minWeight;
				if (++count == N) {
					break;
				}

				int size = adjList[minVertex].size();
				for (int i = 0; i < size; i++) {
					Vertex next = adjList[minVertex].get(i);
					if (!visited[next.num]) { //연결된 다음 노드와 연결되었는지 확인
						pq.offer(next);
					}
				}
			}
			
			sb.append(Math.round(result * E)).append("\n");
		}

		System.out.println(sb);
	}

}

 

 

git 링크

(git 링크 첨부)

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

 

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

 

문제 출처

백준 온라인 저지 17471번: 게리맨더링 (골드4)

 

키워드

부분 집합, 유니온 파인드, BFS

 

풀이 접근법

  • 여러 알고리즘을 섞어서 풀이하였다.
  • subSet으로 구역을 나눌 도시를 뽑고, 이렇게 나뉜 구역이 모두 연결되어 있는지 BFS로 검증해주어야 한다.
    • subSet이 아닌 DFS로 도시를 뽑으면 시간 초과가 나므로 주의해야 한다.
  • 가장 먼저 subSet으로 선거구역에 포함시킬 도시를 선택한다. (부분집합 생성)
    • 부분 집합의 생성을 마쳤다면 선택된 구역과 선택되지 않은 구역을 각각 List로 모은다.
    • 각각의 구역별로 BFS 탐색을 한다. BFS 탐색으로 각 구역의 노드가 모두 연결되어 있음이 확인되었다면 올바르게 나뉜 것이다. 연결되어 있지 않다면 false를 반환하고 다음 집합을 생성한다.
    • 모두 정상적으로 연결되어 있다면 최소 인구 차이를 계산하여 저장한다.

 

코드

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

public class Main {

	private static int N;
	private static int min = Integer.MAX_VALUE;
	
	private static int[] populations; // 전체 구역의 인구수 기록
	private static int[] parents;
	private static boolean[] visited;
	private static boolean[] selected;
	
	private static List<Integer>[] graph;

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

		N = Integer.parseInt(br.readLine());

		populations = new int[N + 1]; // 0은 사용 안 함
		selected = new boolean[N + 1]; // 0은 사용 안 함
		graph = new LinkedList[N + 1]; // 0은 사용 안 함

		StringTokenizer st = new StringTokenizer(br.readLine(), " ");
		for (int i = 1; i <= N; i++) {
			populations[i] = Integer.parseInt(st.nextToken());
			graph[i] = new LinkedList<>();
		}

		for (int v = 1; v <= N; v++) {
			st = new StringTokenizer(br.readLine(), " ");

			int M = Integer.parseInt(st.nextToken());

			for (int j = 0; j < M; j++) {
				int u = Integer.parseInt(st.nextToken());
				graph[v].add(u);
				graph[u].add(v);
			}
		}
	
		subSet(1);

		if(min == Integer.MAX_VALUE) { min = -1; }
		
		System.out.println(min);
	}
	
	private static void subSet(int depth) {
		if(depth == N + 1) {
			int sum1 = 0; int sum2 = 0;
			
			ArrayList<Integer> selectedSpace = new ArrayList<>();
			ArrayList<Integer> unselectedSpace = new ArrayList<>();
			
			makeSet();
			
			for(int i = 1; i <= N; i++) {
				if(selected[i]) {
					selectedSpace.add(i);
					sum1 += populations[i];
				}
				else {
					unselectedSpace.add(i);
					sum2 += populations[i];
				}
			}
			
			if(selectedSpace.size() == 0 || unselectedSpace.size() == 0) {
				return;
			}
			
			for(int i = 0; i < selectedSpace.size() - 1; i++) {
				union(selectedSpace.get(i), selectedSpace.get(i + 1));
			}
			
			for(int i = 0; i < unselectedSpace.size() - 1; i++) {
				union(unselectedSpace.get(i), unselectedSpace.get(i + 1));
			}
			
			if(BFS(selectedSpace) && BFS(unselectedSpace)) {
				min = Math.min(min, Math.abs(sum1 - sum2));
			}
			
			return;
		}
		
		selected[depth] = true;
		subSet(depth + 1);
		selected[depth] = false;
		subSet(depth + 1);
	}
	
	private static boolean BFS(ArrayList<Integer> space) {
		Queue<Integer> queue = new ArrayDeque<>();
		visited = new boolean[N + 1]; // 0은 사용 안 함
		
		queue.add(space.get(0));
		visited[space.get(0)] = true;
		
		while(!queue.isEmpty()) {
			int v = queue.poll();
			for(int u : graph[v]) {
				if(visited[u] || parents[u] != parents[v]) {
					continue; //이미 방문했거나 부모가 같지 않다면 유효하지 않음
				}
				
				visited[u] = true;
				queue.add(u);
			}
		}
		
		for (int v : space) { //방문되지 않은 정점이 남아있다면
			if(!visited[v]) return false;
		}
		
		return true;
	}

	private static void union(int a, int b) {
		int aRoot = find(a);
		int bRoot = find(b);

		if (aRoot == bRoot)
			return;

		parents[bRoot] = aRoot;
	}

	private static int find(int a) {
		if (a == parents[a])
			return a;

		parents[a] = find(parents[a]);
		return parents[a];
	}

	private static void makeSet() {
		parents = new int[N + 1];
		for (int i = 0; i <= N; i++) {
			parents[i] = i;
		}
	}
}

처음에는 유니온 파인드만을 이용하여 풀이하려고 했으나, 한계가 있어 부분집합을 사용하였다. 유니온 파인드를 제외시키고 부분 집합과 BFS만으로 풀이가 가능해보인다.

 

git 링크

(git 링크 첨부)

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

 

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

 

문제 출처

SWEA 3289번: 서로소 집합 (D4)

 

키워드

유니온 파인드

 

풀이 접근법

  • 집합의 연산(합집합, 트리의 부모 찾기)을 구현하는 기본적인 문제이다.
  • 유니온 파인드 알고리즘은 크게 makeSet(), union(), find() 세 함수로 나눠 구현한다.
  • makeSet()
    • 자기만을 원소로 갖는 단위 서로소 집합을 생성한다. parents가 자기 자신을 가리키게 만들면 된다.
  • union()
    • 두 원소의 부모가 같지 않다면 같은 집합으로 합친다.
    • 집합을 합치는 것은 한 노드(a)의 부모를 나머지 노드(b) 또한 부모로 가리키도록 만들어주면 된다. (같은 부모를 가리킴)  
  • find()
    • 어떤 노드 a의 부모를 재귀적으로 찾는다.
    • 매개변수로 들어온 노드가 부모로 자기 자신을 가리키면 그것이 하위 노드의 부모로 리턴시킨다.

 

코드

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

public class Solution {

	private static int N;
	private static int M;
	
	private static int[] parents;
	
	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[] temp = br.readLine().split(" ");
			
			N = Integer.parseInt(temp[0]);
			M = Integer.parseInt(temp[1]);
			
			makeSet();
			
			sb.append("#").append(test_case).append(" ");
			StringTokenizer st;
			for(int i = 0; i < M; i++) {
				st = new StringTokenizer(br.readLine(), " ");
				
				int ope = Integer.parseInt(st.nextToken());
				int a = Integer.parseInt(st.nextToken());
				int b = Integer.parseInt(st.nextToken());
				
				if(ope == 0) {
					union(a, b);
				} else if (ope == 1) {
					if(find(a) == find(b)) {
						sb.append(1);
					} else sb.append(0);
				}
			}
			
			sb.append("\n");
		}

		System.out.println(sb);
	}
	
	private static void union(int a, int b) {
		int aRoot = find(a);
		int bRoot = find(b);
		
		if(aRoot == bRoot) return; //두 요소가 같은 집합에 속해있다면
		
		//bRoot 이하의 집합을 aRoot 아래로 붙인다.
		parents[bRoot] = aRoot;
	}
	
	private static int find(int a) {
		if(a == parents[a]) return a;
		
		parents[a] = find(parents[a]);
		
		return parents[a];
	}
	
	// 모든 요소는 자기만을 원소로 갖는 단위 서로소 집합임.
	private static void makeSet() {
		parents = new int[N + 1];
		for(int i = 0; i <= N; i++) {
			parents[i] = i;
		}
	}

}

 

 

git 링크

(git 링크 첨부)

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

 

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

 

문제 출처

백준 온라인 저지 2252번: 줄 세우기 (골드3)

 

키워드

위상 정렬, BFS (너비 우선 탐색)

 

풀이 접근법

  • 일부의 우선순위를 알고 있는 문제 ⇒ 위상 정렬을 활용하여 풀 수 있다.
  • 위상정렬은 DFS와 BFS 모두로 구현할 수 있다. 이 포스트에서는 BFS로 구현한 방식을 사용한다.
    • degree 배열로 각 노드마다의 진입간선 수를 저장한다.
    • 처음에는 진입간선 수가 0인 노드부터 시작하여, 다른 노드를 방문할 때마다 진입간선의 수를 1씩 줄인다.
    • 진입간선의 수가 0이 된다면 그 노드를 큐에 넣고 다시 탐색해나간다.

 

코드

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

public class Main {

	private static int N;
	private static int M;
	
	private static int[] degree;
	private static boolean[] visited;
	private static List<Integer>[] graph;
	
	private static StringBuilder sb = new StringBuilder();
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		
		String[] temp = br.readLine().split(" ");

		N = Integer.parseInt(temp[0]);
		M = Integer.parseInt(temp[1]);
		
		degree = new int[N + 1]; //0은 사용 안 함
		visited = new boolean[N + 1];
		graph = new LinkedList[N + 1];
		
		for(int i = 0; i < N + 1; i++) {
			graph[i] = new LinkedList<>();
		}
		
		for(int i = 0; i < M; i++) {
			temp = br.readLine().split(" ");
			
			int v = Integer.parseInt(temp[0]);
			int u = Integer.parseInt(temp[1]); 
			
			graph[v].add(u);
			degree[u]++;
		}
		
		topologicalSort();
		
		System.out.println(sb);
	}
	
	public static void topologicalSort() { //BFS
		Queue<Integer> queue = new ArrayDeque<>();
		
		for(int i = 1; i < N + 1; i++) {
			if(degree[i] == 0) {
				queue.add(i);
			}
		}
		
		while(!queue.isEmpty()) {
			int v = queue.poll();
			sb.append(v).append(" ");
			
			for(int u : graph[v]) {
				degree[u]--;
				
				if(degree[u] == 0) {
					queue.add(u);
				}
			}
		}
		
		return;
	}
}

 

 

git 링크

(git 링크 첨부)

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

 

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

 

문제 출처

백준 온라인 저지 1987번: 알파벳 (골드4)

 

키워드

DFS(깊이 우선 탐색)

 

풀이 접근법

  • DFS로 풀이한다. 보드 규격에 맞추어 4방 탐색을 한다.
  • 원래 시간 제한이 다소 빡빡한 문제라 느릴 만한 부분은 최대한 제거하는 것이 좋다.
  • Set은 매우 느린 자료구조이다 ⇒ 따라서 Set에 지금까지 방문한 알파벳을 넣지 않고도 더욱 효율적으로 관리하는 방법이 필요하다.
    • 이를 위해 각 알파벳의 방문 여부를 boolean 배열로 관리한다.
    • 이미 방문한 적 있는 알파벳을 다시 방문하였다면 그곳을 방문하지 않고(continue) delta값을 바꾸어 방향을 전환한다.

 

코드

import java.io.*;

public class Main {

	private static int R;
	private static int C;
	
	private static char[][] board;
	
	private static int[] dr = {0, 1, 0, -1};
	private static int[] dc = {1, 0, -1, 0};
	
	private static int maxDepth = 0;
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		String[] temp = br.readLine().split(" ");

		R = Integer.parseInt(temp[0]);
		C = Integer.parseInt(temp[1]);
		
		board = new char[R][C];
		
		boolean[] alphabets = new boolean['Z' - 'A' + 1];
		
		for(int i = 0; i < R; i++) {
			String line = br.readLine();
			for(int j = 0; j < C; j++) {
				board[i][j] = line.charAt(j);
			}
		}
		
		alphabets[board[0][0] - 'A'] = true;
		DFS(0, 0, alphabets);
		
		System.out.println(maxDepth);
	}

	private static void DFS(int r, int c, boolean[] alphabets) {
		if(r == R && c == C) {
			return;
		}
		
		int sum = 0; //maxDepth 계산
		for(boolean b : alphabets) {
			if(b) sum += 1;
		}
		maxDepth = Math.max(maxDepth, sum);
		
		int nr = 0;
		int nc = 0;
		for(int i = 0; i < dr.length; i++) {
			nr = r + dr[i];
			nc = c + dc[i];
			
			if(!isValid(nr, nc)) {
				continue;
			}
			
			if(alphabets[board[nr][nc] - 'A']) {
				continue;
			}
			
			alphabets[board[nr][nc] - 'A'] = true;
			DFS(nr, nc, alphabets);
			alphabets[board[nr][nc] - 'A'] = false;
		}
		
	}

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

}

 

 

git 링크

(git 링크 첨부)

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

 

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

 

문제 출처

백준 온라인 저지 1074번: Z (실버1)

 

키워드

분할 정복

 

풀이 접근법

  • 입력이 2의 제곱수 꼴로 주어졌으므로 이분탐색을 이용한다.
  • 주어진 배열의 크기는 2^N × 2^N이다. 만약 이것을 재귀적으로 풀고 싶다면 그 이전에 2^{N - 1} × 2^{N - 1} 크기 배열(4분의 1 사이즈)을 계산한다.
  • 찾는 좌표가 몇 사분면에 있는 지를 기준으로 각기 다르게 재귀 호출을 해야 한다.
  • 배열의 크기를 4분의 1로 나누어 Z모양으로 탐색하므로, 한 번의 메소드 호출에서 재귀 호출이 4회 일어난다.
  • 범위를 제한하지 않고 재귀 호출을 하면 시간 초과가 발생하므로, 두 가지 처리를 해준다.

1. 재귀 호출의 범위를 설정해주어야 한다.

  • 현재의 위치에 따라 호출할 방향이 결정된다. 배열을 4등분 하여, 찾고 싶은 위치가 몇 사분면에 있는지에 따라 현재 좌표를 해당 사분면으로 이동시킨다.
  • 좌표를 계속 이동시키다가 배열을 더이상 쪼갤 수 없거나, 현재 위치가 목표 좌표와 같은 경우 함수를 리턴시키고 결과를 출력한다.

2. 현재 위치가 몇 번째 칸인지 별도로 계산해주어야 한다.

  • 위에서 서술한 대로, 모든 칸을 하나하나 다 탐색하면서 개수를 구하면 시간 초과가 발생한다.
  • 따라서, 목표 좌표가 있는 곳으로 현재 좌표를 이동시키면서, 그동안 거쳐온 칸의 수를 별도로 더해주어야 한다.
  • 예를 들어서, 현재 좌표가 (0, 0)이고 1사분면에 있을 때, 이를 3사분면으로 옮겼다면 1사분면과 2사분면에 존재하는 모든 칸의 수를 더해주어야 목표 좌표까지 다다르는 데 존재하는 칸의 수를 구할 수 있다.

 

코드

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

public class Main {
	
	private static int R;
	private static int C;
	private static int SIZE;

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

		StringTokenizer st = new StringTokenizer(br.readLine(), " ");

		int N = Integer.parseInt(st.nextToken());
		R = Integer.parseInt(st.nextToken());
		C = Integer.parseInt(st.nextToken());
		
		SIZE = (int) Math.pow(2, N); //한 변의 크기

		recursion(SIZE, R, C);
		
		System.out.println(count);
	}
	
	private static void recursion(int size, int r, int c) {		
		if(size == 1) {
			return;
		}

		int half = size / 2;
		if(r < half && c < half) { //1사분면: row와 col이 모두 key보다 크면 호출
			recursion(half, r, c);
		} else if(r < half && c >= half) { //2사분면: R보다 row가 크면 호출
			count += size * size / 4; //1사분면의 수 skip
			
			recursion(half, r, c - half);
		} else if(r >= half && c < half) { //3사분면: C보다 col이 크면 호출
			count += (size * size / 4) * 2; //2사분면의 수 skip
			
			recursion(half, r - half, c);
		} else { //4사분면: row와 col이 모두 key보다 작거나 같으면 호출
			count += (size * size / 4) * 3; //3사분면의 수 skip
			
			recursion(half, r - half, c - half);
		}
	}

}

재귀 호출의 방향을 설정하는 데 어려움을 겪어, 다른 블로그를 참고하여 풀었다. 이와 유사한 matrix 분할정복 문제도 모두 좌표를 밀어주면서 계산한다.

 

참고한 링크

https://mygumi.tistory.com/284
https://wiselog.tistory.com/133

 

git 링크

(git 링크 첨부)

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

 

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

 

문제 출처

SWEA 9843번: 촛불 이벤트 (D5)

 

키워드

수학

 

풀이 접근법

  • 공차가 1인 등차수열의 합 공식을 응용하는 문제이다.
  • 문제는 이렇게도 설명 가능하다: k(k+1) = 2N 이다. N이 주어졌을 때 k를 구하시오.
  • 문제의 입력이 10^18까지이므로 최소 O(√n)시간 이하로 풀이해야 한다. (그 이상은 시간 초과)
    • 만약 10^18개의 수보다 적은 양을 조사해서 k를 구하고 싶다면 어떻게 해야 할까?
    • 다음과 같은 식을 통해 k의 최대치를 알아낼 수 있다. sqrt() 함수로 k값을 구하고 공식에 넣어 검증해보면 된다.

유도 과정

 

코드

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

public class Solution {

	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++) {
			long answer = -1;
			
			long N = Long.parseLong(br.readLine());
			
			if(N == 1) {
				sb.append("#").append(test_case).append(" ").append(1).append("\n");
				continue;
			}
			
              //k값은 최대 √n이다.
			long k = (long) Math.sqrt(2 * N);
			if(k * (k + 1) / 2 == N) { //만약 k를 넣어서 공식이 성립한다면
					answer = k; //그것이 답이 된다.
			}
            
			sb.append("#").append(test_case).append(" ").append(answer).append("\n");
		}
		
		System.out.print(sb);
	}

}

야매로 풀긴 했지만, 수학적 원리를 이용해서 푸는 문제이기 때문에 구현할 코드는 사실상 별로 없다. 이 외에도 수학적인 성질을 이용하면 O(1)시간만에 풀이할 수 있는 문제가 꽤 있으니 신경써보는 게 좋겠다.

 

git 링크

(git 링크 첨부)

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

 

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

 

문제 출처

백준 온라인 저지 15686번: 치킨 배달 (골드5)

 

키워드

완전 탐색, 백트래킹

 

풀이 접근법

  • 전체 치킨집 중, 순서에 상관 없이 M개를 뽑는다. (조합)
    • Next Permutation으로 풀면 재귀보다 시간을 더 줄일 수 있다.
    • 배열의 0은 선택되지 않은 것이고, 1은 선택되었음을 나타낸다.
    • 문제에 조금 헷갈리게 적혀있는데, 최대 M개라고 해서 1~M개를 다 뽑아보라는 게 아니라 M개만 뽑아도 된다.
  • List에 집의 좌표와 치킨집의 좌표를 모두 취합한다.
    • 이렇게 좌표를 취합한 뒤 순회하는 게 Matrix를 순회하는 것보다 공간을 적게 차지한다. (희소 그래프)
  • 조합으로 M개의 치킨집을 선정하고, 각 집으로부터 모든 치킨집까지의 거리를 구한다.
    • 치킨집까지의 최소 거리를 구하고, 매 시도마다 sum에 합한다.
    • sum을 구할 때마다 최솟값을 찾는다.

 

코드

import java.awt.Point;
import java.io.*;
import java.util.*;

public class Main {

	private static int N;
	private static int R;
	private static int answer = Integer.MAX_VALUE;
	
	private static List<Point> houses = new ArrayList<>();
	private static List<Point> stores = new ArrayList<>();
	
	public static void main(String[] args) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		StringTokenizer st = new StringTokenizer(br.readLine());
		N = Integer.parseInt(st.nextToken());
		R = Integer.parseInt(st.nextToken());
		
		for(int i = 0; i < N; i++) {
			st = new StringTokenizer(br.readLine(), " ");
			for(int j = 0; j < N; j++) {
				int input = Integer.parseInt(st.nextToken()); 
				
				if(input == 1) {
					houses.add(new Point(j, i));
				}
				
				if(input == 2) {
					stores.add(new Point(j, i));
				}
			}
		}
		
		int len = stores.size();
		int[] p = new int[len];
		
		for(int i = 0; i < R; i++) {
			p[len - 1 - i] = 1;
		}
		
		do {
			for(int i = 0; i < len; i++) {
				if(p[i] == 0) continue;
				
				answer = Math.min(calculateDist(p), answer);
			}
		} while(np(p));
		
		System.out.println(answer);
	}

	private static int calculateDist(int[] p) {
		int sum = 0;
		for(Point house : houses) {
				int minDist = Integer.MAX_VALUE;
				for(int k = 0; k < p.length; k++) {
					if(p[k] == 0) continue;
					
					Point store = stores.get(k);
					
					int d = Math.abs(house.y - store.y) + Math.abs(house.x - store.x);
					minDist = Math.min(d, minDist);
				}
				sum += minDist;
		}
		
		return sum;
	}

	private static boolean np(int[] p) {
		int N = p.length;
		
		int i = N - 1; //맨 뒤에서 출발
		while(i > 0 && p[i - 1] >= p[i]) --i;
		
		if(i == 0) //이미 마지막 순열임
			return false;
		
		int j = N - 1; //맨 뒤에서 출발
		while(p[i - 1] >= p[j]) --j;
		
		swap(p, i - 1, j);
		
		int k = N - 1; //맨 뒤에서 출발
		while(i < k) { //i, k가 양 끝의 수를 가리킴
			swap(p, i++, k--);
		}
		
		return true;
	}
	
	public static void swap(int[] arr, int i, int j) {
		int temp = arr[i];
		arr[i] = arr[j];
		arr[j] = temp;
	}
}

백트래킹의 심화 문제이다. Matrix에 담긴 정보를 조합으로 뽑아야 하기 때문에 Matrix 그대로 두고 재귀를 돌기에는 무리가 있다. 별개의 배열이나 리스트로 뽑아서 조합(또는 순열)을 만드는 것이 좋다.

 

git 링크

(git 링크 첨부)

+ Recent posts