안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 스택 문제 히스토그램 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/1725

 

1725번: 히스토그램

첫 행에는 N (1 ≤ N ≤ 100,000) 이 주어진다. N은 히스토그램의 가로 칸의 수이다. 다음 N 행에 걸쳐 각 칸의 높이가 왼쪽에서부터 차례대로 주어진다. 각 칸의 높이는 1,000,000,000보다 작거나 같은

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

public class Main {

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

        n = parseInt(br.readLine());
        histogram = new int[n];

        for (int i = 0; i < n; i++) histogram[i] = parseInt(br.readLine());
        sb.append(getArea());
        System.out.println(sb);
    }

    static int getArea() { // 최대 20억을 넘지 않음
        Stack<Integer> stack = new Stack<>();
        int area = 0; // 20억을 넘지 않음

        for (int i = 0; i < n; i++) {

            // 스택이 비지 않고 인덱스의 값이 peek의 히스토그램 값보다 작거나 같다면
            while (!stack.isEmpty() && histogram[stack.peek()] >= histogram[i]) {
                int h = histogram[stack.pop()];

                // 스택이 비어 있다면 길이는 i, 스택이 있다면 현재 위치에서 stack의 인덱스
                int w = stack.isEmpty() ? i : i - 1 - stack.peek();

                area = Math.max(area, h * w);
            }
            stack.push(i);
        }

        // stack에 남아있는거 전부 제거하며 다시 업데이트
        while(!stack.isEmpty()) {
            int h = histogram[stack.pop()];

            int w = stack.isEmpty() ? n : n - 1 - stack.peek();
            area = Math.max(area, h * w);
        }

        return area;
    }
}

 

2. 풀이 중점 사항

 

해당 문제는 매우 어려웠던 문제로 하단의 블로그를 참고하였습니다.

stack에 값을 빼서, 면적을 구해야 하는 과정은 이해가 되었지만 도중에 높이가 0이 오는 경우에는 어떻게 해야 하는가?

라는 물음에 빠지면서, 많은 시간을 소비하였습니다.

 

// 스택이 비지 않고 인덱스의 값이 peek의 히스토그램 값보다 작거나 같다면
while (!stack.isEmpty() && histogram[stack.peek()] >= histogram[i]) {
    int h = histogram[stack.pop()];

    // 스택이 비어 있다면 길이는 i, 스택이 있다면 현재 위치에서 stack의 인덱스
    int w = stack.isEmpty() ? i : i - 1 - stack.peek();

    area = Math.max(area, h * w);
}
stack.push(i);

 

현재 있는 인덱스의 값보다 스택에 있는 높이가 높거나 같다면, 스택에서 빼면서 면적을 구하는 코드입니다.

위에 있는 코드는 다음과 같은데, 그림과 함께 설명하도록 하겠습니다.

 

 

예시는 7개의 입력과 4 2 1 2 0 2 1의 높이로 구성되어 있는 히스토그램입니다.

빨간색 화살표는 인덱스, 초록색 화살표는 스택의 peek입니다.

 

시작 인덱스 0은 stack.isEmpty()이므로 인덱스를 추가합니다.

 

 

스택에 있는 높이는 4인데, 인덱스 1에 있는 높이는 2입니다. 따라서 스택에 있는 0을 빼서 면적을 구해야 합니다.

 

 

stack이 비어 있으므로, 인덱스 길이만큼을 w로 설정합니다. 

Area = 4입니다.

스택에 인덱스 1을 추가합니다.

 

 

스택에 있는 peek는 1이고, 그것의 높이는 2입니다. 반면 포인터의 위치의 높이는 1이므로 

스택 1에 대한 면적을 구합니다.

 

 

스택이 비어있으므로, 인덱스 길이만큼 너비를 설정합니다. (스택의 너비를 인덱스 길이만큼 구하는 이유는 다음과 같이 연속된 같은 높이를 가지는 블록의 면적을 모두 고려하기 위함입니다.)

 

 

stack에 있는 인덱스 2의 높이보다 인덱스 3의 높이가 더 크므로 진행합니다.

 

 

같은 방법으로, 인덱스 4 이전에 쌓였던 스택을 모두 제거하며 면적을 구합니다.

 

 

여기서 중요한 부분은, 현재 포인터 위치 - 1 - 스택의. peek() 값만큼 너비를 선택해야 합니다.

그 이유는 stack.peek()는 경계를 설정하는 부분이고, 다음 스택에 값이 있으니 그 이전까지 면적을 구해야 합니다.

포인터의 길이는 항상 스택에 있는 값보다 +1 이상이 더 큽니다.

즉 point - 1은 너비의 가장 오른쪽의 끝점을 의미하고, stack.peek()는 시작하는 위치라고 생각하면,

(2, 3)의 넓이이므로 1이 도출되게 되는 것입니다. 이것을 한 번에 계산하기 위해 3 - 2 = point - 1 - stack.peek()가 도출됩니다.

 

이어서, 스택이 비어있으므로 point의 인덱스 값을 너비로 설정하여 면적을 구합니다.

 

 

 

제가 어려웠던 부분은 이처럼 0이 도중에 나오는 경우 어떻게 해결해야 하는가? 였습니다.

하지만 이전에 설정한 범위를 생각해 보면, while루프는 stack.peek()의 값이 히스토그램의 값보다 크거나 같을 때 

면적을 계산합니다. 즉, stack.peek()이 0이 된다면, 다시 0이 나오기 전까지 그 상태가 유지되는 것입니다.

(ex 0 2 1 3 4 --> 이렇게 유지될 경우 0보다 같거나 작은 수는 없으니 stack에 0은 그대로 유지)

 

따라서, 이처럼 0을 가진 h의 높이가 stack.peek()로 경계를 만들어주고 있는 것을 볼 수 있습니다.

 

 

 

마지막은 이처럼 stack에 0이 쌓여 있는 경우, 혹은 계속 상승하는 상태여서 stack에서 나오지 않는 경우가 있을 수 있습니다.

따라서, 스택에서 값을 빼며 면적을 다시 구합니다.

 

 

 

이를 통해 가장 큰 면적 4를 구할 수 있습니다.

 

이상으로 히스토그램 자바 풀이를 마치도록 하겠습니다.! 감사합니다.!

 

참조 블로그: https://st-lab.tistory.com/255

 

[백준] 6549번 : 히스토그램에서 가장 큰 직사각형 - JAVA [자바]

https://www.acmicpc.net/problem/6549 6549번: 히스토그램에서 가장 큰 직사각형 입력은 테스트 케이스 여러 개로 이루어져 있다. 각 테스트 케이스는 한 줄로 이루어져 있고, 직사각형의 수 n이 가장 처음

st-lab.tistory.com

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 문자열 문제 - 무한 문자열 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/12871

 

12871번: 무한 문자열

첫째 줄에 s, 둘째 줄에 t가 주어진다. 두 문자열 s와 t의 길이는 50보다 작거나 같은 자연수이고, 알파벳 소문자로만 이루어져 있다. 

www.acmicpc.net

 

1. 풀이 소스 

 

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringBuilder sb = new StringBuilder();
        
        String str1 = br.readLine();
        String str2 = br.readLine();
        
        int gcd = getGCD(str1.length(), str2.length()); // 최대 공약수 찾기
        if (gcd == 1) gcd = str1.length() * str2.length(); // 1이라면 최소 공배수는 두 문자열 길이의 곱
        else gcd = str1.length() * (str2.length() / gcd); // 1이 아니라면, 문자열 길이 * 문자열 길이 / 최대 공약수

        str1 = repeat(str1, gcd); // 문자열 반복
        str2 = repeat(str2, gcd);

        if (str1.equals(str2)) sb.append(1);
        else sb.append(0);
        
        System.out.print(sb);
    }

    // 유클리드 호재법
    static int getGCD(int x, int y) {
        if (y == 0) return x;
        return getGCD(y, x % y);
    }

    // 최소 공배수 개수만큼 문자열 증가
    static String repeat(String str, int lcm) {
        String repeat = str;
        while (str.length() < lcm) {
            str += repeat;
        }
        return str;
    }
}

 

2. 풀이 중점 사항

 

최소 공배수만큼 문자열을 반복한 후, 두 문자열을 비교하는 방법을 사용하였습니다.

중점 사항은 최대 공약수를 구하는 알고리즘입니다. 유클리드 호재법을 활용하여 재귀로 문제를 해결할 수 있습니다.

 

static int getGCD(int x, int y) {
    if (y == 0) return x;
    return getGCD(y, x % y);
}

 

나머지 풀이는 주석으로 대체하였습니다 감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 문자열 문제 - 여우는 어떻게 울지? 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/9536

 

9536번: 여우는 어떻게 울지?

각 테스트케이스마다 여우의 울음소리를 한 줄씩, 녹음된 순서대로 출력한다. 여우의 울음소리가 녹음되어 있음이 보장된다. (알려진 것과는 달리, 여우는 모스 부호로 의사소통하지 않는다.)

www.acmicpc.net

 

1. 풀이 소스

 

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

public class Main {
    public static void main(String[] args) throws IOException {
        
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int T = Integer.parseInt(br.readLine());
        StringBuilder sb = new StringBuilder();
        
        for (int i = 0; i < T; i++) {
            HashMap<String, ArrayList<Integer>> map = new HashMap<>();
            String[] saying = br.readLine().split(" "); // 울음소리 저장

            for (int c = 0; c < saying.length; c++) {
                if (!map.containsKey(saying[c])) map.put(saying[c], new ArrayList<>(List.of(c))); //키 파악
                else map.get(saying[c]).add(c); // 키 있다면 list에 저장
            }

            String str;
            while((str = br.readLine()) != null && !str.equals("what does the fox say?")) {
                String[] strArr = str.split(" ");
                String sound = strArr[strArr.length - 1];
                List<Integer> list = map.get(sound);
                for (Integer num : list) saying[num] = ""; // 울음 소리가 저장된 인덱스 찾아서 "" 만들기
            }

            for (String say : saying) {
                if (say != null && !say.equals("")) sb.append(say).append(" "); // null이 아니고 ""가 아니라면 울음 소리 저장
            }
            sb.append("\n");
        }
        
        System.out.print(sb);
    }
}

 

2. 풀이 중점 사항

 

HashMap<String, ArrayList<Integer>> map = new HashMap<>();
String[] saying = br.readLine().split(" "); // 울음소리 저장

for (int c = 0; c < saying.length; c++) {
    if (!map.containsKey(saying[c])) map.put(saying[c], new ArrayList<>(List.of(c))); //키 파악
    else map.get(saying[c]).add(c); // 키 있다면 list에 저장
}

 

울음소리는 계속 반복되므로, HashMap에 울음소리와 울음소리의 인덱스를 저장할 ArrayList<>을 key - value로 설정하여 값을 저장하였습니다.

 

정해진 개수만큼 입력을 받는 것이 아닌, EOF 혹은 특정 문자열 입력을 받으면 종료하는 로직을 작성해야할 때가 있습니다.

String str;
while((str = br.readLine()) != null && !str.equals("what does the fox say?")) {
}

 

이 경우, str에 br.readLine() 값을 할당했을 때, 그 값이 null이 아니고, 필요로 하는 문자열이 아닌 경우에만 입력을 계속 받을 수 있도록 처리하였습니다.

 

이상으로 여우는 어떻게 울지? 풀이를 마치도록 하겠습니다. 감사합니다.!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다. 

이번 포스팅은 백준 친구 네트워크 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/4195

 

4195번: 친구 네트워크

첫째 줄에 테스트 케이스의 개수가 주어진다. 각 테스트 케이스의 첫째 줄에는 친구 관계의 수 F가 주어지며, 이 값은 100,000을 넘지 않는다. 다음 F개의 줄에는 친구 관계가 생긴 순서대로 주어진

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.HashMap;
import java.util.Map;

import static java.lang.Integer.parseInt;

public class Main {

    static int[] parent; // 부모 노드 설정
    static int[] cnt; // 부모에 있는 노드의 자식 노드 개수

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

        int t = parseInt(br.readLine());
        for (int i = 0; i < t; i++) {

            Map<String, Integer> map = new HashMap<>(); // 각 이름과 인덱스를 저장할 map
            int n = parseInt(br.readLine()); // 배열을 초기화할 개수

            // n개의 연결관계가 있으므로 n * 2가 최대
            parent = new int[n * 2];
            cnt = new int[n * 2];

            for (int j = 0; j < parent.length; j++) {
                parent[j] = j; // 부모 노드 자신으로 초기화
                cnt[j] = 1; // 개수는 모두 1로 초기화
            }

            int idx = 0; // 인덱스 0 설정
            for (int j = 0; j < n; j++) {
                String[] str = br.readLine().split(" ");

                if (!map.containsKey(str[0])) map.put(str[0], idx++);
                if (!map.containsKey(str[1])) map.put(str[1], idx++);

                union(map.get(str[0]), map.get(str[1])); // 두 친구 연결하기
                sb.append(cnt[find(map.get(str[1]))]).append("\n"); // 두번째 인원의 부모를 찾아서 네트워크 연결된 수 찾기
            }
        }

        System.out.print(sb);
    }

    static int find(int child) { // find
        if (parent[child] == child) return child;
        return parent[child] = find(parent[child]); // 경로 압축
    }

    static void union(int c1, int c2) { // 합치기
        int p1 = find(c1); // c1의 부모 노드 
        int p2 = find(c2); // c2의 부모 노드

        if (p1 == p2) return;
        parent[p2] = p1;
        cnt[p1] += cnt[p2]; // p1의 자식 노드 수 p2의 자식 노드 수 합치기
        cnt[p2] = 0; // p2 인원 초기화
    }
}

 

2. 풀이 중점 사항

 

친구 네트워크는, 서로 다른 집합의 네트워크가 분산되어 있을 때, 하나로 연결하는 작업을 수행해야 합니다.

이렇게 서로 다른 집합을 서로 연결해야 하는 문제가 주어졌을 때, 적용할 수 있는 알고리즘이 유니온 파인드입니다.

 

static int find(int child) { // find
    if (parent[child] == child) return child;
    return parent[child] = find(parent[child]); // 경로 압축
}

static void union(int c1, int c2) { // 합치기
    int p1 = find(c1); // c1의 부모 노드 
    int p2 = find(c2); // c2의 부모 노드

    if (p1 == p2) return;
    parent[p2] = p1;
}

 

부모 노드에 속한 자식 노드 개수 구하기

 

p1, p2의 노드를 유니온 파인드 진행할 때, p1의 부모 노드의 자식 노드의 개수와 p2의 부모 노드의 자식 노드 개수를 모두 더해야 합니다. 이를 구현하기 위해 cnt 배열을 선언하였습니다.

 

자식 노드의 개수를 늘리는 로직은 union을 수행할 때 적용할 수 있습니다.

만약 두 부모 노드가 다르다면, 부모 노드 중 합쳐서 집합이 더 커진 대상을 기준으로 합쳐진(합쳐져서 없어진 집합)의 자식 노드 수를 더해줍니다. 

 

parent[p2] = p1;
cnt[p1] += cnt[p2]; // p1의 자식 노드 수 p2의 자식 노드 수 합치기
cnt[p2] = 0; // p2 인원 초기화

 

이상으로 자바 친구 네트워크 풀이를 마치도록 하겠습니다. 감사합니다.

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 문제는 백준 플로이드-와샬(워샬) 문제 플로이드(11404) 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/11404

 

11404번: 플로이드

첫째 줄에 도시의 개수 n이 주어지고 둘째 줄에는 버스의 개수 m이 주어진다. 그리고 셋째 줄부터 m+2줄까지 다음과 같은 버스의 정보가 주어진다. 먼저 처음에는 그 버스의 출발 도시의 번호가

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

public class Main {
    static final int INF = 100_000_001; // 100 * 100만 범위 이므로, +1 큰 것은 넘을 수 없음
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        int n = parseInt(br.readLine());
        int m = parseInt(br.readLine());     
        int[][] graph = new int[n + 1][n + 1]; // dp 그래프를 위한 배열 초기화

        for (int row = 1; row <= n; row++) {
            for (int col = 1; col <= n; col++) {
                if (row == col) continue; // 행과 열이 같다면, 노드에서 노드로 이동하므로 0
                graph[row][col] = INF; // 출발지와 도착지가 다른 노드는 INF로 초기화
            }
        }

        for (int i = 0; i < m; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            int e = parseInt(st.nextToken()); // 출발지 노드
            int v = parseInt(st.nextToken()); // 도착지 노드
            int w = parseInt(st.nextToken()); // 비용
            graph[e][v] = Math.min(graph[e][v], w); // 간선이 하나가 아닐 수 있다는 정보가 있으므로
        }

        
        for (int way = 1; way <= n; way++) { // 경유하려는 노드
            for (int row = 1; row <= n; row++) { // 출발지 노드
                if (row == way) continue; // 만약 출발지 노드와 경유하려는 노드가 같다면 패스

                for (int col = 1; col <= n; col++) { // 도착지 노드
                    // 출발지 노드와 도착지 노드가 같다면 패스
                    // 도착지 노드와 경유하려는 노드가 같다면 패스
                    if (row == col || col == way) continue;
                    graph[row][col] = Math.min(graph[row][way] + graph[way][col], graph[row][col]); // 출발 -> 경유 + 경유 -> 출발 , 출발 -> 도착 비교 
                }
            }
        }

        StringBuilder sb = new StringBuilder();
        for (int row = 1; row <= n; row++) {
            for (int col = 1; col <= n; col++) {
                if (graph[row][col] == INF) sb.append(0).append(" ");
                else sb.append(graph[row][col]).append(" ");
            }
            sb.append("\n");
        }
        
        System.out.print(sb);
    }
}

 

2. 플로이드-와샬(워샬) 알고리즘 

 

플로이드-와샬 알고리즘은 그래프의 모든 노드쌍에 대한 최단 거리를 구하는 알고리즘입니다. 다익스트라 알고리즘은 한 노드에서 다른 모든 노드로의 최단 거리를 구하는 알고리즘이지만, 플로이드 와샬은 모든 노드 쌍 사이의 최단 거리를 구할 수 있습니다.

 

여기서 활용되는 원리가 DP입니다.

출발지 -> 도착지 노드로 가는 거리보다, 경유지를 들려서 가는 거리가 더 짧을수도 있습니다.

이러한 경우를 효율적으로 계산하기 위해 특정 노드에서 특정 노드로 가는 거리를 dp에 저장하고,

만약 경유지로 해당 거리를 미리 저장해놓았다면, dp 테이블에서 이를 활용하여 경유지 거리를 계산하는 것입니다.

 

dp를 활용하여, 1 -> 2까지의 거리를 저장해 놓았다면 1 -> 2 + 2 -> 3의 거리와 1 -> 3까지의 거리를 비교하여 최소 거리를 찾을 수 있습니다.

 

for (int way = 1; way <= n; way++) { // 경유하려는 노드
    for (int row = 1; row <= n; row++) { // 출발지 노드
        if (row == way) continue; // 만약 출발지 노드와 경유하려는 노드가 같다면 패스

        for (int col = 1; col <= n; col++) { // 도착지 노드
            // 출발지 노드와 도착지 노드가 같다면 패스
            // 도착지 노드와 경유하려는 노드가 같다면 패스
            if (row == col || col == way) continue;
            graph[row][col] = Math.min(graph[row][way] + graph[way][col], graph[row][col]); // 출발 -> 경유 + 경유 -> 출발 , 출발 -> 도착 비교 
        }
    }
}

 

n개의 노드를 경유하고, 각 출발지 노드 n개, 도착지 노드 n를 설정해야하므로 시간복잡도는 O(n^3)을 가지게 됩니다.

 

이상으로 백준 플로이드-와샬 문제 플로이드 풀이를 마치도록 하겠습니다.! 감사합니다.!!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 벨만포드 문제 - 타임머신 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/11657

 

11657번: 타임머신

첫째 줄에 도시의 개수 N (1 ≤ N ≤ 500), 버스 노선의 개수 M (1 ≤ M ≤ 6,000)이 주어진다. 둘째 줄부터 M개의 줄에는 버스 노선의 정보 A, B, C (1 ≤ A, B ≤ N, -10,000 ≤ C ≤ 10,000)가 주어진다. 

www.acmicpc.net

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;
import static java.lang.Integer.parseInt;

public class Main {
    static ArrayList<ArrayList<Edge>> graph = new ArrayList<>();
    static final int INF = Integer.MAX_VALUE;
    static long[] dist;
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        StringTokenizer st = new StringTokenizer(br.readLine());
        
        int n = parseInt(st.nextToken()); // 정점 개수
        int m = parseInt(st.nextToken()); // 간선 개수
        for (int i = 0; i <= n; i++) graph.add(new ArrayList<>());
        for (int i = 0; i < m; i++) {
            st = new StringTokenizer(br.readLine());
            int e = parseInt(st.nextToken());
            int v = parseInt(st.nextToken());
            int w = parseInt(st.nextToken());
            
            graph.get(e).add(new Edge(v, w)); // 그래프에 엣지 추가하기
        }
        
        dist = new long[n + 1]; // 정점 개수로 dist 초기화 노드 1번에서 각 노드 출발
        Arrays.fill(dist, INF);
        dist[1] = 0; // 시작노드인 1번은 = 0

        for (int k = 0; k < n - 1; k++) { // 벨만 포드 알고리즘은 k - 1번 반복
            for (int i = 1; i <= n; i++) { // 정점 개수
                ArrayList<Edge> edges = graph.get(i);

                for (Edge edge : edges) {
                    if (dist[i] != INF && dist[edge.v] > dist[i] + edge.w) {
                        dist[edge.v] = dist[i] + edge.w; // 출발 정점에서 도착 정점까지의 거리가, 이전의 값보다 작다면 업데이트
                    }
                }
            }
        }

        StringBuilder sb = new StringBuilder();

        for (int i = 1; i <= n; i++) { // 음수 사이클이 존재하는지 판단
            ArrayList<Edge> edges = graph.get(i);

            for (Edge edge : edges) {
                if (dist[i] != INF && dist[edge.v] > dist[i] + edge.w) { // 만약 음수 사이클로 인해 이전 값이 줄어든다면 무한 사이클 가능
                    sb.append(-1).append("\n");
                    System.out.print(sb);
                    return;
                }
            }
        }

        for (int i = 2; i <= n; i++) {
            if (dist[i] == INF) sb.append(-1).append("\n"); // 갈 수 없는 곳이라면
            else sb.append(dist[i]).append("\n");
        }

        System.out.print(sb);
    }
    
    static class Edge {
        int v; // 도착 정점
        int w; // 가중치
        
        Edge (int v, int w) {
            this.v = v;
            this.w = w;
        }
    }
}

 

2. 벨만포드 알고리즘

 

벨만포드 알고리즘은 그래프 최단 경로를 구하는 알고리즘으로, 하나의 정점에서 출발하는 최단 거리를 구하는 문제입니다. 

음수 가중치를 허용하는 것이 특징입니다.

 

벨만 포드 알고리즘은 다음의 절차로 이루어집니다.

  • 간선 m개를 하나씩 모두 탐색
  • dist[도착 정점] = min(dist[도착 정점], dist[출발지 정점] + cost(도착지 정점으로 가는 간선의 가중치))
  • 노드 개수의 n - 1개 만큼 이를 반복하여 특정 정점에서 각 노드로의 이동거리를 업데이트 진행

 

이를 그림의 예시로 설명하면 다음과 같습니다.

 

 

해당 그래프와 간선은 다음과 같이 주어졌다고 가정하겠습니다.

 

초기 dist배열은 1번 노드에서, 각 노드로의 최단 거리를 담고 있습니다. 1번 노드에서 1번 노드의 이동은 0으로 초기화하고, 나머지 노드로의 이동은 아직 간선으로 이동하지 않았으므로 무한대로 초기화합니다.

 

1번 노드에서 3번 노드로 연결된 간선을 통해 dist가 업데이트될 수 있습니다. 이때 조건은 다음과 같습니다.

 

if (dist[i] != INF && dist[edge.v] > dist[i] + edge.w) {
    dist[edge.v] = dist[i] + edge.w; // 출발 정점에서 도착 정점까지의 거리가, 이전의 값보다 작다면 업데이트
}

 

i는 이동을 시작하는 곳의 노드 번호이고, edge.v는 도착하려는 곳의 정점입니다.

dist[i], 즉 1번 노드의 dist[1] = 0 이고, 이동하려는 3번 노드의 거리는 무한대인데, dist[i] + edge.w가 1이므로 3번 노드의 거리를 업데이트합니다.

수식: dist[edge.v] > dist[i] + edge.w          =>           dist[3] > dist[1] + 1                  =>        ∞ > 1      =>    dist[3] = 1

 

다음은 2번 노드에서 4번 노드로 이동하는 간선입니다.

이때, dist[2] = ∞입니다. 따라서, 해당 간선은 dist[i] != INF라는 조건에 위배되어 실행되지 않고 넘어갑니다.

 

 

다음은 3번 노드에서 2번 노드로 이동하는 간선의 차례입니다.

dist[3] = 1이므로 dist[3] != INF를 만족합니다.

    dist[2] > dist[3] + 1 

 = ∞ > 2

=> dist[2] = 2를 만족하게 됩니다.

 

이렇게 종료된다면, 2 -> 4로 이동하는 것은 여전히 무한대로 남아있습니다.

앞 서, n - 1번을 반복하는 이유가 이러한 반례가 존재하기 때문입니다.

따라서, n - 1번 반복을 하게 된다면, 2번 노드가 현재 1로 업데이트 되어 있으므로 4번 노드까지의 거리를 업데이트할 수 있습니다.

 

 

3. 벨만포드 알고리즘 음수 간선

 

앞서 양수로 움직이는 벨만포드 알고리즘을 확인할 수 있었습니다. 벨만포드 알고리즘은 음수 간선을 적용할 수 있다는 점에서 다익스트라 알고리즘과 비교할 수 있습니다.

 

음수 간선이 적용된다면, 벨만포드 알고리즘은 어떻게 처리가 될까요?

 

이번 예시는

1 -> 2 : 1

2 -> 3 : 1

3 -> 1 : -1

로 처리되는 그래프를 예시로 확인해 보겠습니다.

 

이 그래프는 1 -> 2 -> 3 -> 1로 이어지는 사이클을 형성하고 있습니다.

벨만포드 알고리즘으로 각 간선을 적용하여 n - 1번 반복하며 값을 업데이트하면 다음과 같습니다.

1 -> 2 이동 거리 업데이트 

 

2 -> 3 이동 거리 업데이트

 

 

3 -> 1 거리 업데이트.

 

 

이렇게까지 종료를 하면, 올바른 답을 구하지 못할 수 있습니다.

이 예시에서는 이렇게 종료하더라도, 문제가 발생하지 않습니다.

하지만, 만약 음수 사이클이 존재하여 값이 무한대로 감소하게 된다면, 최단 거리를 구할 수 없게 됩니다.

 

4. 벨만포드로 음수 사이클 판단하기

 

예시를 조금 수정하여, 음수 사이클이 적용되는 예시를 만들어 보았습니다.

 

간선은

1 -> 2 : 1

2 -> 3 : 1

3 -> 1: -10으로 구성됩니다.

 

마찬가지로 각 간선으로 거리를 구하면 다음의 절차를 따릅니다.

 

 

1 -> 2 거리 업데이트 1

 

 

2 -> 3 거리 업데이트 2

 

3 -> 1 거리 업데이트 -8 

 

여기서 벨만포드 알고리즘은, 간선을 업데이트하는 방식을 한 번 더 적용하여, 해당 간선이 음수 사이클을 형성하고 있는지 판단할 수 있습니다.

지금 1 -> 2 -> 3 -> 1까지 거치는 최종 거리는 -8입니다.

만약 한 번 더 이동을 하게 된다면 1- > 2 -> 3 -> 1 -> 2 -> 3 -> 1 는 -16이 되게 됩니다.

즉, 무한번 사이클을 회전하게 된다면 그 값이 무한정 감소하게 되는 것입니다.

 

따라서 벨만포드 알고리즘은 다음의 절차로 정리할 수 있습니다

 

  • 모든 노드의 거리를 무한대로 설정하고 시작 노드의 거리만 0으로 설정합니다.
  • 모든 간선에 대해, 만약 시작 노드에서 해당 간선의 시작점까지의 현재 거리와 간선의 가중치의 합이 간선의 끝점까지의 현재 거리보다 작다면, 간선의 끝점까지의 거리를 업데이트합니다. 이 단계를 노드 수 - 1번 반복합니다.
  • 모든 간선을 다시 한 번 검사하여, 만약 위의 업데이트를 계속할 수 있다면 이는 음의 사이클이 존재함을 의미합니다.

 

5. 코드로 정리하기

 

for (int i = 1; i <= n; i++) { // 음수 사이클이 존재하는지 판단
    ArrayList<Edge> edges = graph.get(i);

    for (Edge edge : edges) {
        if (dist[i] != INF && dist[edge.v] > dist[i] + edge.w) { // 만약 음수 사이클로 인해 이전 값이 줄어든다면 무한 사이클 가능
            sb.append(-1).append("\n");
            System.out.print(sb);
            return;
        }
    }
}
  •  

앞 서, 음의 사이클이 존재하는지 여부는 이처럼 간선을 한 번더 탐색하여, 값이 업데이트되는 경우가 존재하는지 판단합니다.

dist[edge.v] > dist[i] + edge.w 가 실행된다는 것은, 이전에 업데이트한 값보다 더 작은 값이 생성된다는 의미이므로, 음의 사이클이 발생했다고 판단할 수 있습니다.

 

이상으로 벨만포드 알고리즘으로 해결할 수 있는 타임머신 문제 풀이를 마치도록 하겠습니다. 감사합니다!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 LCS 문제 풀이를 진행하고자 합니다.

문제 출처: https://www.acmicpc.net/problem/9251 

 

9251번: LCS

LCS(Longest Common Subsequence, 최장 공통 부분 수열)문제는 두 수열이 주어졌을 때, 모두의 부분 수열이 되는 수열 중 가장 긴 것을 찾는 문제이다. 예를 들어, ACAYKP와 CAPCAK의 LCS는 ACAK가 된다.

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String str1 = br.readLine();
        String str2 = br.readLine();
        
        int[][] dp = new int[str1.length() + 1][str2.length() + 1];
        
        for (int i = 1; i <= str1.length(); i++) {
            for (int j = 1; j <= str2.length(); j++) {
                if (str1.charAt(i - 1) == str2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;
                else dp[i][j] = Math.max(dp[i- 1][j], dp[i][j - 1]);
            }
        }
        System.out.println(dp[str1.length()][str2.length()]);
    }
}

 

 

2. LCS

 

LCS는 Longest Common Subsequence로 두 문자열에서 가장 긴 공통부분 문자열을 찾는 문제를 의미합니다. 부분 문자열은 ABC와 ABC 두 문자열이 동일해야만 하는 것이 아니라, ABDC와 ABC 문자처럼 연속되지는 않지만 하나의 부분 문자열로 ABC를 뽑아낼 수 있는 문자열을 의미합니다.

 

해당 풀이는 다른 소스와 달리 주석을 설정하지 않았는데, 공식처럼 사용되는 부분이 있기 때문입니다. 이 부분을 그림으로 설명하도록 하겠습니다.

 

CAPCAK 문자열과 ACAYKP 두 문자열의 LCS를 구하면 다음과 같이 2차원 배열을 설정할 수 있습니다.

1행에 있는 문자 C와 각 열에 있는 문자 ACAYKP를 하나씩 검사하며, 같은 문자가 있는지 파악합니다.

1행의 C와 2열의 C가 같기 때문에 +1을 진행합니다.

 

1행의 C와 3열의 A는 같지 않습니다. 하지만, 최장 공통 부분 문자열이란, 공통의 긴 부분 문자열 쉽게 말해, 최대 몇 개까지 공통된 문자로 수열을 만들 수 있는가? 를 의미합니다.

따라서, 비록 C와 A는 같지 않지만, 이전 2열에서 C와 C가 동일하기 때문에 1을 그대로 이어서 가져오게 됩니다.

 

 

2행의 A는 2열의 A와 동일합니다. 이 경우는 +1 을 진행해야 하지만, 이전 행과 이전 열의 값에서 +1을 해야 하는 것을 그림에서 파악할 수 있습니다. 왜냐하면, 이전 행과 열을 파악한 결과 같아서 + 1을 했는데, 그 다음행과 그다음열이 또 같기 때문에 이전에 계산한 결과를 그대로 활용해야 하기 때문입니다. 따라서, 하나의 공식을 도출할 수 있습니다.

 

str1.charAt(i) == str2.charAt(j) :

          dp[i][j] = d[i - 1][j - 1] + 1

 

 

 

4행의 C와 6열의 P는 같지 않습니다. 그렇다면 그대로 유지를 해야 합니다. 이때 두 가지를 선택할 수 있습니다.

CAPC / ACAYK, CAPC / ACAYKP의 부분 문자열

먼저 전자는 CA로 2개가 동일하고, 후자는 CAP 3개가 동일합니다.

LCS는 가장 긴 공통 부분 문자열을 구해야 하므로, CAP 동일한 문자열 3개를 선택하게 됩니다.

 

여기서 하나의 공식을 도출할 수 있습니다.

 

str1.charAt(i) != str2.charAt(j) :

dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);

 

즉, 행과 열 중 1이 더 작은 값을 가지는 값에서 가장 큰 값을 가지는 것을 그대로 유지하는 것입니다.

이러한 원리로 모든 문자를 다 채우게 되면 다음과 같습니다.

 

 

for (int i = 1; i <= str1.length(); i++) {
    for (int j = 1; j <= str2.length(); j++) {
        if (str1.charAt(i - 1) == str2.charAt(j - 1)) dp[i][j] = dp[i - 1][j - 1] + 1;
        else dp[i][j] = Math.max(dp[i- 1][j], dp[i][j - 1]);
    }
}
System.out.println(dp[str1.length()][str2.length()]);

 

이를 토대로 dp 마지막에 위치한 값을 각 dp 테이블에서 최대값을 따르게 되는 것을 확인할 수 있습니다.

 

이상으로 LCS 문제 풀이를 마치도록 하겠습니다. 감사합니다.!!!

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 이분탐색 문제 랜선 자르기 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/1654

 

1654번: 랜선 자르기

첫째 줄에는 오영식이 이미 가지고 있는 랜선의 개수 K, 그리고 필요한 랜선의 개수 N이 입력된다. K는 1이상 10,000이하의 정수이고, N은 1이상 1,000,000이하의 정수이다. 그리고 항상 K ≦ N 이다. 그

www.acmicpc.net

 

1. 풀이 소스

 

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

import static java.lang.Integer.parseInt;

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

        int k = parseInt(st.nextToken());
        int n = parseInt(st.nextToken());

        int[] arr = new int[k]; // k개의 값을 저장할 배열
        long max = 0; // 최대값 저장 (가장 긴 랜선을 줄여 나가야 가장 큰 길이를 구할 수 있음)

        for (int i = 0; i < k; i++) {
            arr[i] = parseInt(br.readLine());
            if (max < arr[i]) max = arr[i]; // 최대값 업데이트
        }

        max++; //
        long min = 0;
        long mid = 0;

        while (min < max) { // 이분탐색으로 min < max인 조건으로 루프
            mid = (max + min) / 2; // 중간 값 설정하기

            long count = 0; // 카운트 개수 0 초기화

            for (int j : arr) {
                count += (j / mid); // count를 업데이트
                if (count >= n) break; // 개수가 n 보다 크면 종료
            }

            if (count < n) max = mid; // count
            else min = mid + 1; // upper bound로 min 값 증가
        }

        System.out.println(min - 1);
    }
}

 

2. 풀이 중점 사항

 

이번 문제는 이분 탐색의 upper bound를 활용하는 것이 중점 사항이었습니다.

도중에 47%에서 메모리 초과가 발생하여, 하단의 블로그를 참고하였습니다.

 

upper bound 방법은 이분 탐색의 한 종류로, 주어진 값보다 처음으로 큰 원소를 찾는 알고리즘입니다. 이분탐색의 차이점은 이분 탐색은 정확하게 특정 값을 찾는다면, upper bound는 이름처럼 가장 근접하게 큰 원소를 찾는 것이라고 설명할 수 있습니다.

예를 들어, min = 199, max = 201이 있다면, mid = (min + max) / 2 = 200이 됩니다. 

count < n이 성립하지 않으므로 min을 mid + 1 추가합니다. 이 과정에서, min < max 조건이 위배되게 되므로,

min = 201 상태에서 종료됩니다. 

upper bound는 앞 서 정의한 것처럼 가장 근접하게 큰 원소를 찾는 것이므로, 우리의 목표인 200보다 1 큰 값을 찾게 됩니다.

따라서, min - 1로 문제를 해결할 수 있었습니다.

 

이상으로 백준 랜선 자르기 풀이를 마치도록 하겠습니다. 감사합니다!!! 

 

 

참고 블로그: https://st-lab.tistory.com/269

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 누적합 문제 - 인간 컴퓨터 상호작용 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/16139

 

16139번: 인간-컴퓨터 상호작용

첫 줄에 문자열 $S$가 주어진다. 문자열의 길이는 $200,000$자 이하이며 알파벳 소문자로만 구성되었다. 두 번째 줄에는 질문의 수 $q$가 주어지며, 문제의 수는 $1\leq q\leq 200,000$을 만족한다. 세 번째

www.acmicpc.net

 

1. 풀이 소스

 

import java.io.*;
import java.util.*;
import static java.lang.Integer.parseInt;

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        List<Question> qs = new ArrayList<>(); // question을 저장할 리스트
        
        String str = br.readLine();
        int n = str.length();
        int q = parseInt(br.readLine());
        for (int i = 0; i < q; i++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            qs.add(new Question(st.nextToken().charAt(0), parseInt(st.nextToken()), parseInt(st.nextToken())));
        }
        
        int[][] dp = new int[n + 1][26]; // dp 2차원 배열 (누적합)

        for (int i = 1; i <= n; i++) { // 인덱스 에러를 방지하기 위해 1부터 시작
            int num = str.charAt(i - 1) - 'a'; // char - 'a'로 정수로 변환
            for (int j = 0; j < 26; j++) {
                if (j == num) dp[i][num] = dp[i - 1][num] + 1; // 만약 해당 문자에 해당하는 정수라면 +1
                else dp[i][j] = dp[i - 1][j]; // 아니라면 그대로
            }
        }

        StringBuilder sb = new StringBuilder();
        for (Question question : qs) {
            int num = question.c - 'a';
            sb.append(dp[question.e][num] - dp[question.s][num]).append("\n"); //양 끝을 포함해야하므로 e는 1을 더해주고, s는 그대로
        }
        
        System.out.print(sb);
    }
    
    static class Question {
        char c;
        int s;
        int e;
        
        Question (char c, int s, int e) {
            this.c = c;
            this.s = s;
            this.e = e + 1; // 끝점 포함 
        }
    }
}

 

2. 풀이 중점 사항

 

문제 해결 방법은 문자열 길이 n를 탐색하며 26개의 알파벳을 누적합으로 업데이트하는 방법입니다.

시간 복잡도는 O(n * 26 + question.size()) 으로 계산할 수 있습니다. 시간제한이 1초이므로 n, quesion.size()가 최대 20만이므로 약 O(540만)입니다. 1초에 약 1억 번 연산이 가능하므로 문제를 해결할 수 있을 것이라고 판단하였습니다.

 

for (int i = 1; i <= n; i++) { // 인덱스 에러를 방지하기 위해 1부터 시작
    int num = str.charAt(i - 1) - 'a'; // char - 'a'로 정수로 변환
    for (int j = 0; j < 26; j++) {
        if (j == num) dp[i][num] = dp[i - 1][num] + 1; // 만약 해당 문자에 해당하는 정수라면 +1
        else dp[i][j] = dp[i - 1][j]; // 아니라면 그대로
    }
}

 

26개의 문자를 배열에 저장하기 위해 char - 'a'를 하여 0 ~ 26의 정수형을 만들었습니다.

이후, num의 값을 가지는 2차원 배열은 +1을 하고 나머지는 그대로 유지하도록 설정하였습니다.

안녕하세요. 회사와 함께 성장하고 싶은 KOSE입니다.

이번 포스팅은 백준 백트래킹 문제 스도쿠 자바 풀이를 진행하도록 하겠습니다.

문제 출처: https://www.acmicpc.net/problem/2580

 

2580번: 스도쿠

스도쿠는 18세기 스위스 수학자가 만든 '라틴 사각형'이랑 퍼즐에서 유래한 것으로 현재 많은 인기를 누리고 있다. 이 게임은 아래 그림과 같이 가로, 세로 각각 9개씩 총 81개의 작은 칸으로 이루

www.acmicpc.net

 

1. 풀이 소스

 

import java.util.*;
import java.io.*;
import static java.lang.Integer.parseInt;

public class Main {
    static boolean sudokuBreak; // 스도쿠를 찾으면 종료
    static int[][] map; // 값을 저장할 맵
    static boolean[] check = new boolean[10]; // row, col, block에서 값이 없는 숫자를 확인
    static List<Point> zeros = new ArrayList<>(); // zero가 저장된 위치 저장
    static Map<Integer, Set<Integer>> hashMap = new HashMap<>(); // hashMap에 스도쿠 row, col, block에 숫자 저장

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        
        map = new int[10][10]; // 맵 초기화
        zeros = new ArrayList<>(); // zero 초기화

        for (int row = 1; row <= 9; row++) {
            hashMap.put(row, new HashSet<>());
            hashMap.put(row * -1, new HashSet<>()); // col은 겹치지 않도록 음수로 저장
            hashMap.put(row + 100, new HashSet<>()); // block은 겹치지 않도록 + 100
        }

        for (int row = 1; row <= 9; row++) {
            StringTokenizer st = new StringTokenizer(br.readLine());
            for (int col = 1; col <= 9; col++) {
                int num = parseInt(st.nextToken());
                map[row][col] = num;
                if (num == 0) zeros.add(new Point(row, col)); // 값이 zero라면 zeros에 추가
            }
        }

        for (int row = 1; row <= 9; row++) {
            for (int col = 1; col <= 9; col++) {
                int num = map[row][col];
                if (num != 0) { // 값이 0이 아니라면, rowSet, colSet, blockSet에 추가
                    Set<Integer> rowSet = hashMap.get(row);
                    Set<Integer> colSet = hashMap.get(col * -1);

                    rowSet.add(num);
                    colSet.add(num);

                    Set<Integer> blockSet = getBlockSet(row, col);
                    blockSet.add(num);
                }
            }
        }

        for (Point p : zeros) setPossible(p); // zero 위치에서 가능한 숫자 저장

        StringBuilder sb = new StringBuilder();
        if (zeros.size() == 0) { // 만약 zeros가 0이라면 그대로 출력
            for (int i = 1; i <= 9; i++) {
                for (int j = 1; j <= 9; j++)
                    sb.append(map[i][j]).append(" ");
                sb.append("\n");
            }
            System.out.print(sb);
            return;
        }

        dfs(0); // 재귀

        for (int i = 1; i <= 9; i++) {
            for (int j = 1; j <= 9; j++)
                sb.append(map[i][j]).append(" ");
            sb.append("\n");
        }
        System.out.print(sb);
    }

    static void dfs(int count) {
        // 검사영역, row, col, 3 by 3
        if (count == zeros.size()) { // 만약 모두 만족했다면 종료
            sudokuBreak = true;
            return;
        }

        Point p = zeros.get(count); // 포인트 가져오기
        for (Integer i : p.possible) { // p 위치에서 가능한 숫자

            if (!isValid(p.row, p.col, i))  continue; // 불가능한 경우라면 패스

            map[p.row][p.col] = i; // 값 설정
            Set<Integer> rowSet = hashMap.get(p.row);
            rowSet.add(i); // rowSet에 추가

            Set<Integer> colSet = hashMap.get(p.col * -1);
            colSet.add(i); // colSet에 추가

            Set<Integer> blockSet = getBlockSet(p.row, p.col);
            blockSet.add(i); // 현재 row, col이 속한 block에 추가

            dfs(count + 1);

            if (sudokuBreak) return;
            map[p.row][p.col] = 0; // 백트래킹
            rowSet.remove(i);
            colSet.remove(i);
            blockSet.remove(i);
        }
        
    }
        
    static boolean isValid(int row, int col, int num) { // value가 있다면 종료
        Set<Integer> rowSet = hashMap.get(row);
        if (rowSet.contains(num)) return false;

        Set<Integer> colSet = hashMap.get(col * -1);
        if (colSet.contains(num)) return false;

        Set<Integer> blockSet = getBlockSet(row, col);
        return !blockSet.contains(num);
    }

    static Set<Integer> getBlockSet(int row, int col) { //block 설정하기
        Set<Integer> blockSet;
        if (row <= 3 && col <= 3) blockSet = hashMap.get(101);
        else if (row <= 6 && col <= 3) blockSet = hashMap.get(102);
        else if (col <= 3) blockSet = hashMap.get(103);
        else if (row <= 3 && col <= 6) blockSet = hashMap.get(104);
        else if (row <= 6 && col <= 6) blockSet = hashMap.get(105);
        else if (col <= 6) blockSet = hashMap.get(106);
        else if (row <= 3) blockSet = hashMap.get(107);
        else if (row <= 6) blockSet = hashMap.get(108);
        else blockSet = hashMap.get(109);
        return blockSet;
    }


    static void setPossible(Point p) { // 가능한 숫자 찾기
        int blockRow, blockCol;
        Set<Integer> possibles = new HashSet<>();
        
        if (p.row <= 3) blockRow = 1;
        else if (p.row <= 6) blockRow = 4;
        else blockRow = 7;
        
        if (p.col <= 3) blockCol = 1;
        else if (p.col <= 6) blockCol = 4;
        else blockCol = 7;

        Arrays.fill(check, false);
        for (int i = blockRow; i < blockRow + 3; i++) { // block에 없는 숫자 찾기
            for (int j = blockCol; j < blockCol + 3; j++) check[map[i][j]] = true;
        }        
        for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);
        
        Arrays.fill(check, false);
        for (int i = 1; i <= 9; i++) check[map[p.row][i]] = true; // row에 없는 숫자 찾기
        for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);
        
        Arrays.fill(check, false);
        for (int i = 1; i <= 9; i++) check[map[i][p.col]] = true; // col에 없는 숫자 찾기 
        for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);

        p.possible.addAll(possibles);
    }
    
    static class Point {
        int row;
        int col;
        List<Integer> possible = new ArrayList<>();
        
        Point (int row, int col) {
            this.row = row;
            this.col = col;
        }
    }
}

 

2. 풀이 중점 사항

 

제가 푼 풀이 코드가 많이 복잡하여 아쉬웠던 문제였습니다. 다른 분들이 해결한 코드를 보면 평균 400ms 정도로 매우 효율적이었지만 제 풀이는 1900ms 정도가 소요되었습니다.

 

static void setPossible(Point p) { // 가능한 숫자 찾기
    int blockRow, blockCol;
    Set<Integer> possibles = new HashSet<>();
    
    if (p.row <= 3) blockRow = 1;
    else if (p.row <= 6) blockRow = 4;
    else blockRow = 7;
    
    if (p.col <= 3) blockCol = 1;
    else if (p.col <= 6) blockCol = 4;
    else blockCol = 7;

    Arrays.fill(check, false);
    for (int i = blockRow; i < blockRow + 3; i++) { // block에 없는 숫자 찾기
        for (int j = blockCol; j < blockCol + 3; j++) check[map[i][j]] = true;
    }        
    for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);
    
    Arrays.fill(check, false);
    for (int i = 1; i <= 9; i++) check[map[p.row][i]] = true; // row에 없는 숫자 찾기
    for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);
    
    Arrays.fill(check, false);
    for (int i = 1; i <= 9; i++) check[map[i][p.col]] = true; // col에 없는 숫자 찾기
    for (int i = 1; i <= 9; i++) if (!check[i]) possibles.add(i);

    p.possible.addAll(possibles);
}

 

제가 풀이한 방법은 zero가 위치한 곳에서 0가 나올 수 있는 숫자의 경우를 set에 추가합니다.

예를 들어 1 행 1 열에 행, 열 , 블록을 확인해 보니 숫자 3, 4, 5가 없다면 해당 포인트의 possible에 3, 4, 5를 추가합니다. 

이렇게 zeros의 Point 객체들이 가질 수 있는 경우를 모두 set 한 후,

dfs()를 호출하여 백트래킹을 진행하였습니다.

 

 

static boolean isValid(int row, int col, int num) { // value가 있다면 종료
    Set<Integer> rowSet = hashMap.get(row);
    if (rowSet.contains(num)) return false;

    Set<Integer> colSet = hashMap.get(col * -1);
    if (colSet.contains(num)) return false;

    Set<Integer> blockSet = getBlockSet(row, col);
    return !blockSet.contains(num);
}

 

만약, value가 같은 row, col, block에 속해있는 Set에 추가하려는 문자가 없는 경우 map의 값을 업데이트하고, set에 추가하였습니다.  그리고 count를 증가하여 재귀 호출하였고 백트래킹으로 다시 map의 값을 0, set에서 값을 제거하였습니다.

 

map[p.row][p.col] = i; // 값 설정
Set<Integer> rowSet = hashMap.get(p.row);
rowSet.add(i); // rowSet에 추가

Set<Integer> colSet = hashMap.get(p.col * -1);
colSet.add(i); // colSet에 추가

Set<Integer> blockSet = getBlockSet(p.row, p.col);
blockSet.add(i); // 현재 row, col이 속한 block에 추가

dfs(count + 1);

if (sudokuBreak) return;
map[p.row][p.col] = 0; // 백트래킹
rowSet.remove(i);
colSet.remove(i);
blockSet.remove(i);

 

최종적으로 스도쿠를 완성하면, map을 출력하였습니다.

 

for (int i = 1; i <= 9; i++) {
    for (int j = 1; j <= 9; j++)
        sb.append(map[i][j]).append(" ");
    sb.append("\n");
}
System.out.print(sb);

 

 

3. 더 좋은 코드 

 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.StringTokenizer;
import java.util.stream.Collectors;

public class Main {

	static int[][] board = new int[9][9];

	public static void main(String[] args) throws IOException {

		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

		for (int i = 0; i < 9; i++) {
			StringTokenizer st = new StringTokenizer(br.readLine());
			for (int j = 0; j < 9; j++) {
				board[i][j] = Integer.parseInt(st.nextToken());
			}
		}

		sudoku(0, 0);
	}

	private static boolean sudoku(int row, int col) {

		if (col == 9) {
			return sudoku(row + 1, 0);
		}

		if (row == 9) {
			printBoard();
			return true;
		}

		if (board[row][col] == 0) {
			for (int num = 1; num <= 9; num++) {
				if (isPossible(row, col, num)) {
					board[row][col] = num;
					if (sudoku(row, col + 1)) {
						return true;
					}
					board[row][col] = 0;
				}
			}
			return false;
		} else {
			return sudoku(row, col + 1);
		}
	}

	private static boolean isPossible(int row, int col, int num) {

		int rowStart = (row / 3) * 3;
		int colStart = (col / 3) * 3;

		for (int i = 0; i < 9; i++) {
			if (board[row][i] == num || board[i][col] == num || board[rowStart + (i / 3)][colStart + (i % 3)] == num) {
				return false;
			}
		}

		return true;
	}

	private static void printBoard() {
		StringBuilder sb = new StringBuilder();
		for (int i = 0; i < 9; i++) {
			for (int j = 0; j < 9; j++) {
				sb.append(board[i][j]).append(" ");
			}
			sb.append("\n");
		}
		System.out.println(sb.toString());
	}
}

 

 

이상으로 스도쿠 자바 풀이를 마치도록 하겠습니다. 감사합니다!

+ Recent posts