📙 Algorithm Solving/Java
📙[Algo] 24.05.13 알고리즘
혜덕hyeduck
2024. 5. 13. 17:05
알고리즘 문제) BOJ 14676. 영우는 사기꾼?
문제 요약
- 우주 전쟁
- 건물을 짓는 순서가 정해져있음
- ex) 1번 건물을 지어야 2번, 3번 건설 가능 = 1번 건물이, 2,3,번에 영향을 미친다
- 한 건물은 최대 3개 건물에게만 영향을 미칠 수 있으며, 중복 건설이 가능하다.
- 치트키 ⇒ 원하는 건물을 마음대로 지을 수 있음
- 건물을 짓는 순서가 정해져있음
- 주어진 정보를 보고 영우가 치트키를 사용했는지 안헀는지 파악하기
시간 제한
- 1초
입력
- 첫째 줄 : 건물 종류 개수 N, 건물 사이 관계 수 M, 영우의 게임 정보 개수 K
- 1 ≤ N,M,K ≤ 10만
- 다음 M개의 줄에 걸쳐 건물 관계인 X, Y가 차례대로 중복 없이 주어짐
- X를 건설해야 Y건설 가능
- 1 ≤ X, Y ≤ N
- X를 건설해야 Y건설 가능
- 다음 줄부터 K줄에 걸쳐 영우의 게임 정보가 주어짐 ( 1 ≤ a ≤ N)
- 1 a
- 영우가 a번 건물을 1개 건설
- 2 a
- 영우의 a번 건물이 1개 파괴
- 1 a
출력
- 영우가 정상적으로 건물을 건설하고나, 건설한 만큼 건물만 파괴 되었다면 King-God-Emperor 출력
- 건설할 수 없는 건물을 건설하거나, 건설한적 없는 건물이 파괴되었다면 Lier! 출력
접근법
- 시간초과 해결하기 위해 위상정렬 알고리즘이 무엇인지 공부했다.
- 하지만 이 문제는 새로운 타입(?)으로 위상정렬 알고리즘을 푼 느낌이다.
- 결국 다른 사람 해설법을 참고해서 시간초과를 해결함
- 우선, 게임 정보마다 건물을 지을 때 부모 건물이 지어졌는지 체크해가면 최악의 경우 10만*10만으로 당연히 시간초과가 뜰 수밖에 없음
- 따라서 pCnt라는 배열을 추가로 만들어서 조건을 걸어서 체크해나갔다.
- pCnt배열 → 현재 부모 건물이 지어졌는지 판단하는 배열
- 우선 건물 관계정보를 입력 받을 때, 현재 건물의 부모 개수만큼 +1해준다.
- 부모 = 짓기 전에 미리 지어져 있어야 하는 건물
- 그리고 게임 정보가 주어질 때, 만약 건물을 짓게 된다면
- pCnt가 0인 경우에만 짓게 했다.
- 즉, 미리 지어야 할 건물이 모두 지어졌다는 의미
- 또한 건물을 짓게 되면, 내 자식들의 pCnt값을 1씩 감소 시킨다. ⇒ 즉, 부모가 건설 되어있으니, 이미 지어졌다는 의미로 1 감소
- 그러나, 모든 상황에서 해당 로직을 돌리는 게 아니다. 건물을 중복해서 건설할 수 있기 때문에, 지어진 건물 개수(isBuild)가 1개인 경우에만 돌리게 했다.
- pCnt가 0인 경우에만 짓게 했다.
- 건물을 파괴하게 된다면
- 지어진 건물 개수(isBuild)가 0이하면 파괴가 불가능
- 그게 아니라면,
- idBuild개수를 1감소 시키고
- 해당 건물이 파괴 되었으므로, 자식들의 pCnt를 1씩 증가시킨다. ⇒ 지어져야할 부모 건물이 지어지지 않았다는 의미
- 이거 역시 isBuild가 0일때만 돌릴것
- 중복 건설이 가능하기 때문에 isBuild가 양수일 경우에는 자식들의 pCnt를 감소시키면 X
- 이거 역시 isBuild가 0일때만 돌릴것
- 우선 건물 관계정보를 입력 받을 때, 현재 건물의 부모 개수만큼 +1해준다.
- pCnt배열 → 현재 부모 건물이 지어졌는지 판단하는 배열
코드
import java.io.*;
import java.util.*;
public class Main {
static int N, M, K;
static ArrayList<Integer>[] childs;
static int[] isBuild;
static int[] pCnt;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st;
st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken()); // 건물 종류 개수
M = Integer.parseInt(st.nextToken()); // 건물 사이 관계 개수
K = Integer.parseInt(st.nextToken()); // 영우 게임 정보 개수
childs = new ArrayList[N + 1];
for (int i = 0; i < N + 1; i++) {
childs[i] = new ArrayList<>();
}
isBuild = new int[N + 1];
pCnt = new int[N + 1];
int p, c;
for (int i = 0; i < M; i++) {
st = new StringTokenizer(br.readLine());
p = Integer.parseInt(st.nextToken());
c = Integer.parseInt(st.nextToken());
childs[p].add(c);
pCnt[c]++;
}
int cmd, no;
boolean isAble = true;
for (int i = 0; i < K; i++) {
st = new StringTokenizer(br.readLine());
cmd = Integer.parseInt(st.nextToken());
no = Integer.parseInt(st.nextToken());
if (cmd == 1) {
// 건물 짓기
// 조건 : 내 부모가 이미 지어져 있어야 함
if (pCnt[no] == 0) {
// 현재건물에 영향을 미치는 건물이 없다면
isBuild[no]++;
// 이미 지어져있으니, 자식들의 pCnt는 낮추기
if (isBuild[no] == 1) {
// isBuild[no] == 1조건 준 이유 --> 건물은 중복해서 건설이 가능
// 즉 같은 건물을 여러번 짓게 되면, pCnt값이 음수가 나올 수 있다.
for (Integer child : childs[no]) {
pCnt[child]--;
}
}
} else {
isAble = false;
}
} else if (cmd == 2) {
// 건물 파괴
// 조건 : 건설된 건물만 파괴해야함
if (isBuild[no] > 0){
isBuild[no]--;
if (isBuild[no] == 0) {
// 건물이 없다면, pCnt 늘리기 -> 이걸로 지을 수 있는지 판단하니까
// isBuild[no] == 0 조건을 준 이유 --> 건물은 중복해서 건설 가능
// 즉, 아예 건물이 존재하지 않은 경우에만 pCnt를 늘려준다.
for (Integer child : childs[no]) {
pCnt[child]++;
}
}
} else{
isAble = false;
}
}
}
if (!isAble) System.out.println("Lier!");
else System.out.println("King-God-Emperor");
br.close();
}
}
알고리즘 문제) BOJ 1761. 정점들의 거리
문제 요약
- N개의 정점으로 이루어진 트리가 주어지고, M개의 두 노드 쌍을 입력 받을 때 두 노드 사이 거리 출력
시간 제한
- 2초
입력
- 노드 개수 N
- 2 ≤ N ≤ 4만
- 다음 N-1개 줄에 트리 상 연결된 두 점과 거리가 주어짐
- 거리는 1만보다 작은 자연수
- 거리를 알고 싶은 노드 쌍 개수 M
- 1 ≤ M ≤ 1만
- M개 줄에 노드 쌍이 주어짐
출력
- M개의 줄에 차례대로 입력받은 두 노드 사이의 거리를 출력
접근법
- 공통 조상 알고리즘을 학습했다.
- 루트 노드를 1로 가정한 트리를 구성할 것
- 부모노드와 현재 노드의 깊이를 저장할 배열 parent, depth를 만들어서 진행한다.
- 그러나, 이 문제에서는 두 노드간 거리를 구하는 문제라서 추가로 루트로부터의 거리를 저장할 distFromRoot배열도 만들었다.
- dfs를 돌리면서 자식 노드의 부모 노드, 깊이, 루트로부터 거리를 각각 갱신해주었다.
- 그 다음 lca를 찾는 함수를 만든다.
- 매개변수로 lca를 찾을 두 노드 v1, v2를 넘겨준다.
- 먼저, 깊이가 더 깊은 노드를 v2로 맞춰준다.
- 그다음, 두 노드 깊이가 같아질 때까지 v2를 계속해서 올려준다.
- 두 노드 깊이가 같아진 상태에서 동시에, 두 노드가 같아질 때까지 두 노드의 부모 노드로 올려준다(위로 올리기).
- 최종적으로 v1또는 v2를 반환 → 해당 노드가 LCA
- 이제, 입력으로 주어진 v1, v2의 거리를 찾아보자
- 먼저 lca(v1,v2)를 통해 공통 조상(lca)를 찾는다.
- 두 노드의 거리는 각 노드가 lca까지의 거리의 합이 된다.
- 따라서 (distFromRoot[v1] - distFromRoot[lca]) + (distFromRoot[v2] - distFromRoot[lca]) 가 답이 된다.
- distFromRoot배열에는 루트노드로부터 거리가 저장 되어 있기 때문에, 각각 노드의 distFromRoot값에서 루트에서 lca까지 값인 distFromRoot[lca]를 뺀 거리의 합이 답이됨.
- 따라서 (distFromRoot[v1] - distFromRoot[lca]) + (distFromRoot[v2] - distFromRoot[lca]) 가 답이 된다.
- 루트 노드를 1로 가정한 트리를 구성할 것
코드
import java.io.*;
import java.util.*;
public class Main {
static int N, M;
static ArrayList<Node>[] lst;
static int[] parent;
static int[] depth;
static int[] distFromRoot;
static class Node {
int no;
int dist;
Node(int no, int dist) {
this.no = no;
this.dist = dist;
}
}
// 트리 구성하기
static void dfs(int cur, int prv, int dist) {
int nxt, nd;
for (Node node : lst[cur]) {
nxt = node.no;
nd = node.dist;
if (prv == nxt) continue;
// 깊이 갱신
depth[nxt] = depth[cur] + 1;
// 부모 노드 갱신
parent[nxt] = cur;
// 루트로부터 누적 거리 갱신
distFromRoot[nxt] = dist + nd;
dfs(nxt, cur, dist + nd);
}
}
// 공통 조상 LCA 찾기
static int lca(int v1, int v2) {
// 깊이가 더 큰 쪽을 n2로 맞춰주기
if (depth[v1] > depth[v2]) {
int tmp = v1;
v1 = v2;
v2 = tmp;
}
// 두 노드가 같은 깊이가 될 떄까지
while (depth[v1] != depth[v2]) {
v2 = parent[v2];
}
// LCA 찾기
while (v1 != v2) {
v1 = parent[v1];
v2 = parent[v2];
}
return v1;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringBuilder sb = new StringBuilder();
StringTokenizer st;
N = Integer.parseInt(br.readLine());
lst = new ArrayList[N + 1];
for (int i = 0; i < N + 1; i++) {
lst[i] = new ArrayList<>();
}
int v1, v2, d;
for (int i = 0; i < N - 1; i++) {
st = new StringTokenizer(br.readLine());
v1 = Integer.parseInt(st.nextToken());
v2 = Integer.parseInt(st.nextToken());
d = Integer.parseInt(st.nextToken());
lst[v1].add(new Node(v2, d));
lst[v2].add(new Node(v1, d));
}
parent = new int[N + 1];
for (int i = 0; i < N + 1; i++) {
parent[i] = i;
}
depth = new int[N + 1];
distFromRoot = new int[N + 1];
// 루트가 1인 트리 구성
dfs(1, 0, 0);
M = Integer.parseInt(br.readLine());
int lca, dist;
for (int i = 0; i < M; i++) { // 1만
st = new StringTokenizer(br.readLine());
v1 = Integer.parseInt(st.nextToken());
v2 = Integer.parseInt(st.nextToken());
lca = lca(v1, v2);
dist = (distFromRoot[v1] - distFromRoot[lca])
+ (distFromRoot[v2] - distFromRoot[lca]);
sb.append(dist).append("\n");
}
System.out.println(sb);
br.close();
}
}
알고리즘 문제) BOJ 17485. 진우의 달 여행 (Large)
문제 요약
- 지구와 우주사이는 N X M 행렬로 나타낼 수 있으며
- 각 원소의 값은 우주선이 그 공간을 지날 때 소모되는 연료의 양
- 우주선은 다음과 같은 특징이 존재
- 지구 → 달로 가기 위해서 우주선이 움직일 수 있는 방향은 아래와 같다.
- 우주선은 같은 방향으로 두 번 연속 움직일 수 없다.
- 지구 → 달로 가기 위해서 우주선이 움직일 수 있는 방향은 아래와 같다.
- 최대한 연료를 아끼며 달에 도착하기 위한 연료의 최소값은?
시간 제한
- 1초
입력
- 지구와 달 사이 공간을 나타내는 행렬의 크기를 나타내는 **N, M (2 ≤ N, M ≤ 1000)**이 주어진다
- 다음 N줄 동안 각 행렬의 원소 값이 주어진다
- 각 행렬의 원소값은 100 이하의 자연수
출력
- 달 여행에 필요한 최소 연료의 값을 출력
접근법
- 탑다운 dp로 접근
- 현재위치, cr, cc, 이전 방향 prvDir일 때 소모된 최소 연료 값을 dp배열에 저장
- 즉, 아래로 내려가면서 소모된 가장 작은 연료값을 return 하면서 최종적으로 출발지점 dp에 가장 최소 연료값을 저장함
코드
import java.io.*;
import java.util.*;
public class Main {
static int N, M;
static int[][] map;
static int[] delR = {0, 1, 1, 1};
static int[] delC = {0, -1, 0, 1};
static int[][][] dp;
static int answer = 1 << 30;
static int dfs(int cr, int cc, int prvDir) {
// 달에 도착
if (cr == N) return 0;
// 메모 확인
if (dp[cr][cc][prvDir] != -1) return dp[cr][cc][prvDir];
int ret = 1 << 30;
int mr, mc;
for (int i = 1; i <= 3; i++) {
// 연속 이동 방지
if (i == prvDir) continue;
mr = cr + delR[i];
mc = cc + delC[i];
if (mc < 0 || M <= mc) continue;
ret = Math.min(ret, dfs(mr, mc, i) + map[mr][mc]);
}
dp[cr][cc][prvDir] = ret;
return ret;
}
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st;
st = new StringTokenizer(br.readLine());
N = Integer.parseInt(st.nextToken());
M = Integer.parseInt(st.nextToken());
map = new int[N + 1][M];
for (int i = 1; i <= N; i++) {
st = new StringTokenizer(br.readLine());
for (int j = 0; j < M; j++) {
map[i][j] = Integer.parseInt(st.nextToken());
}
}
dp = new int[1010][1010][4];
for (int[][] d1 : dp) {
for (int[] d2 : d1) {
Arrays.fill(d2, -1);
}
}
for (int col = 0; col < M; col++) {
answer = Math.min(answer, dfs(0, col, 0));
}
System.out.println(answer);
br.close();
}
}