제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
SWEA 1812 수정이의 타일자르기입니다.

포인트

개인적으로 해당 문제가 조금 어려웠습니다.

타일을 자를때 하나의 타일에서 여러 타일을 만들 수도 있고, 못만드는 경우를 고려해야합니다.

그래서 저는 잘랐을 때의 타일을 저장하기로 하였습니다.

PriorityQueue를 활용하여 가장 큰 길이를 가진 타일을 나오도록 만들었습니다.

소스코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.Collections;
import java.util.PriorityQueue;
import java.util.StringTokenizer;

public class Q1812 {
    static class Tile implements Comparable<Tile> {

        int min, max;

        public Tile(int a, int b) {
            this.min = Math.min(a, b);
            this.max = Math.max(a, b);
        }

        @Override
        public int compareTo(Tile o) {
            return Integer.compare(o.min, this.min);
        }
    }
    private static int tc, N, M;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        tc = Integer.parseInt(br.readLine().trim());
        StringBuilder sb = new StringBuilder();
        StringTokenizer st;
        for (int t = 1; t <= tc; t++) {
            st = new StringTokenizer(br.readLine());
            N = Integer.parseInt(st.nextToken());
            M = Integer.parseInt(st.nextToken());

            st = new StringTokenizer(br.readLine());
            Integer[] length = new Integer[N];
            for (int i = 0; i < N; i++) {
                int b = Integer.parseInt(st.nextToken());
                length[i] = b;
            }
            Arrays.sort(length, Collections.reverseOrder());
            PriorityQueue<Tile> pq = new PriorityQueue<>();
            int ans = 1;
            pq.add(new Tile(M, M));
            for (int i = 0; i < N; i++) {
                Tile tile = pq.poll();
                int len = (1 << length[i]);
                if (tile.min >= len) {
                    pq.add(new Tile(tile.max - len, tile.min));
                    pq.add(new Tile(tile.min - len, len));
                } else {
                    pq.add(tile);
                    pq.add(new Tile(M - len, len));
                    pq.add(new Tile(M, M - len));
                    ans++;
                }
            }

            sb.append("#").append(t).append(" ").append(ans).append("\n");
        }
        System.out.println(sb);
    }
}

코드 설명

static class Tile implements Comparable<Tile> {
    int min, max;

    public Tile(int a, int b) {
        this.min = Math.min(a, b);
        this.max = Math.max(a, b);
    }

    @Override
    public int compareTo(Tile o) {
        return Integer.compare(o.min, this.min); // 큰 min부터 우선
    }
}
  • Tile 클래스 정의
    • 분할된 타일의 가로 세로를 담고, min 기준으로 내림차순 정렬되게 Comparable을 구성받아 구현하였습니다.
    • PriorityQueue에서 가장 적절한 위치에 타일을 배치하기 위한 기준입니다.
Arrays.sort(length, Collections.reverseOrder()); // 큰 타일부터 먼저 배치
PriorityQueue<Tile> pq = new PriorityQueue<>();
int ans = 1;
pq.add(new Tile(M, M));
  • 입력받은 length[i]는 타일의 크기를 2^k으로 표현합니다
  • 큰 타일부터 배치하기 위해 내림차순 정렬합니다.
  • 하나의 큰 정사각형(M×M)을 처음으로 넣어줍니다.
for (int i = 0; i < N; i++) {
    Tile tile = pq.poll(); // 현재 가장 적절한 빈 공간
    int len = (1 << length[i]); // 실제 타일 길이 (2^k)

    if (tile.min >= len) {
        // 현재 타일이 현재 빈 공간에 들어갈 수 있으면
        pq.add(new Tile(tile.max - len, tile.min)); // 오른쪽 빈 공간
        pq.add(new Tile(tile.min - len, len));      // 아래쪽 빈 공간
    } else {
        // 현재 타일이 안 들어가면 새 정사각형 추가
        pq.add(tile); // 원래 꺼낸 공간 다시 넣기
        pq.add(new Tile(M - len, len)); // 새 공간에서 남은 오른쪽
        pq.add(new Tile(M, M - len));   // 새 공간에서 남은 아래쪽
        ans++;
    }
}
  • 타일을 배치하고 남은 공간을 다시 Tile 객체로 분할하여 pq에 저장
  • 만약 현재 가장 큰 빈 공간에도 타일이 안 들어가면, 새로운 정사각형이 필요하다는 것으로 
    • 이 때 ans++ 해서 필요한 정사각형 개수를 증가합니다.
제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
SWEA 3503 초보자를 위한 점프대 설치하기입니다.

포인트

해당 문제에서 구하라는 부분은 두 점프대의 높이차 중에서 가장 큰 값을 최소화하는 것입니다.

그래서 오름차순(내림차순)으로 정렬하면 차이가 적지 않나 라고 생각했지만, 점프대는 원형으로 설치하는 것이 문제입니다.

따라서 배열을 원형이라고 생각하여
배열의 0번의 왼쪽 인덱스는 N-1, 오른쪽 인덱스는 1로 생각하여 정렬된 기준으로 점프대의 높이를 설치합니다.

소스코드

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

public class Q3503 {
    private static int tc, N;
    private static Integer[] heights;

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

        StringBuilder sb = new StringBuilder();
        StringTokenizer st;
        for (int t = 1; t <= tc; t++) {
            N = Integer.parseInt(br.readLine());
            heights = new Integer[N];
            st = new StringTokenizer(br.readLine());
            for (int i = 0; i < N; i++) {
                heights[i] = Integer.parseInt(st.nextToken());
            }
            Arrays.sort(heights, Collections.reverseOrder());

            int idx = 0;
            int[] ans = new int[N];
            Arrays.fill(ans, -1);
            ans[idx] = heights[idx];
            idx++;
            for (int i = 1; i < N; i++) {
                int num = heights[i];
                if (ans[N - idx] == -1 && ans[idx] == -1) {
                    ans[N - idx] = num;
                } else if (ans[idx] == -1) {
                    ans[idx] = num;
                    idx++;
                }
            }

            int maxDiff = 0;
            for (int i = 0; i < N; i++) {
                int diff = Math.abs(ans[i] - ans[(i + 1) % N]);
                maxDiff = Math.max(maxDiff, diff);
            }
            sb.append("#").append(t).append(" ").append(maxDiff).append("\n");
        }
        System.out.println(sb);
    }
}

/*
3
7
13 10 12 11 10 11 12
5
2 4 5 7 9
8
6 6 6 6 6 6 6 6
 */

코드 설명

  • Arrays.sort(heights, Collections.reverseOrder());
    프대 높이를 내림차순으로 정렬 (높은 점프대부터 배치하기 위해).
for (int i = 1; i < N; i++) {
    int num = heights[i];
    if (ans[N - idx] == -1 && ans[idx] == -1) {
        ans[N - idx] = num;
    } else if (ans[idx] == -1) {
        ans[idx] = num;
        idx++;
    }
}
  • 두 번째 점프대부터는 idx를 기준으로 좌우 교차 배치합니다.(N-idx는 왼쪽 인덱스, idx는 오른쪽 인덱스)
  • 이렇게 하면 큰 높이차가 발생할 수 있는 인접 점프대를 최대한 멀리 배치할 수 있습니다.
int maxDiff = 0;
for (int i = 0; i < N; i++) {
    int diff = Math.abs(ans[i] - ans[(i + 1) % N]);
    maxDiff = Math.max(maxDiff, diff);
}
  • 원형 구조이므로 마지막과 첫 번째 점프대도 인접합니다.
  • 인접한 점프대들 사이의 높이 차이를 모두 계산하고, 그 중 최댓값을 구합니다.
제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
SWEA 5521 상원이의 생일파티입니다.

포인트

이 문제는 그래프 탐색 문제입니다.

특정 인물(상원이, 번호 1번)을 기준으로 자신의 친구와 친구의 친구까지 초대할 수 있는 사람 수를 구하는 것이 목적입니다.
즉, 깊이 2까지 탐색하는 문제로, 단순한 BFS 또는 DFS가 아니라 범위를 제한한 탐색이 핵심입니다.

소스코드

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

public class Q5521 {
    private static int tc, N, M;
    private static List<List<Integer>> friends;

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

        StringBuilder sb = new StringBuilder();
        StringTokenizer st;
        for (int t = 1; t <= tc; t++) {
            st = new StringTokenizer(br.readLine());
            N = Integer.parseInt(st.nextToken());
            M = Integer.parseInt(st.nextToken());

            friends = new ArrayList<>();
            for (int i = 0; i <= N; i++) {
                friends.add(new ArrayList<>());
            }

            for (int i = 0; i < M; i++) {
                st = new StringTokenizer(br.readLine());
                int a = Integer.parseInt(st.nextToken());
                int b = Integer.parseInt(st.nextToken());
                friends.get(a).add(b);
                friends.get(b).add(a);
            }

            int sum = 0;
            boolean[] already = new boolean[N + 1];
            already[1] = true;
            for (int num : friends.get(1)) {
                if (!already[num]) {
                    sum++;
                    already[num] = true;
                }
                for (int next : friends.get(num)) {
                    if (!already[next]) {
                        sum++;
                        already[next] = true;
                    }
                }
            }

            sb.append("#").append(t).append(" ").append(sum).append("\n");
        }
        System.out.println(sb);
    }
}
/*
2
6 5
2 3
3 4
4 5
5 6
2 5
6 5
1 2
1 3
3 4
2 3
4 5
 */

코드 설명

for (int i = 0; i < M; i++) {
    st = new StringTokenizer(br.readLine());
    int a = Integer.parseInt(st.nextToken());
    int b = Integer.parseInt(st.nextToken());
    friends.get(a).add(b);
    friends.get(b).add(a);
}
  • 인접 리스트 방식으로 친구 관계를 저장할 friends 리스트를 초기화합니다.
  • 사람 번호는 1번부터 시작하므로 0번은 사용하지 않습니다.
  • 양방향 그래프 형태로 친구 관계를 저장합니다.
for (int num : friends.get(1)) {
    if (!already[num]) {
        sum++;
        already[num] = true;
    }
    for (int next : friends.get(num)) {
        if (!already[next]) {
            sum++;
            already[next] = true;
        }
    }
}
  • 상원이(1번)의 친구(num)와 친구의 친구(next)를 순회하며 초대 가능한 사람을 탐색합니다.
  • 이미 초대한 사람은 already 배열로 체크하여 중복을 방지합니다.
제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
SWEA 3421 수제 버거 장인입니다.

포인트

중·고등학교 때 배운 부분집합 개념을 떠올려 보면, 원소의 개수가 N개일 때 가능한 모든 부분집합의 수는 2ⁿ임을 알 수 있습니다.

이 문제에서도 모든 음식 조합(부분집합)을 고려하되, 서로 같이 먹으면 안 되는 음식 쌍(충돌 쌍)이 포함된 조합은 제외해야 합니다.

초기에는 DFS를 이용해 완전탐색을 시도했지만, 시간 초과가 발생하였고, 더 빠른 탐색 방식이 필요했습니다.

 

그 대안으로 선택한 것이 비트마스킹(bit masking)입니다.

비트마스킹은 집합의 포함 여부를 비트(0 또는 1)로 표현할 수 있기 때문에, 모든 조합을 효율적으로 순회할 수 있으며,
AND(&) 연산을 통해 충돌 여부도 매우 빠르게 판단할 수 있습니다.

이처럼 비트 연산은 속도가 빠르고, 메모리 효율도 뛰어나기 때문에,
이번 문제와 같이 부분집합을 다루는 경우에 매우 적합한 방법입니다.

소스코드

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

public class Solution {
    private static int tc, N, M;
    private static boolean[][] disabled;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        tc = Integer.parseInt(br.readLine());
        StringBuilder sb = new StringBuilder();
        StringTokenizer st;
        for (int t = 1; t <= tc; t++) {
            st = new StringTokenizer(br.readLine());
            int N = Integer.parseInt(st.nextToken());
            int M = Integer.parseInt(st.nextToken());
            long[][] conflict = new long[M][2];

            for (int i = 0; i < M; i++) {
                st = new StringTokenizer(br.readLine());
                long a = 1 << (Integer.parseInt(st.nextToken()) - 1);
                long b = 1 << (Integer.parseInt(st.nextToken()) - 1);
                conflict[i][0] = a;
                conflict[i][1] = b;
            }

            long count = 0;
            for (int i = 0; i < (1 << N); i++) {
                boolean check = false;
                for (int j = 0; j < M; j++) {
                    long foodA = conflict[j][0];
                    long foodB = conflict[j][1];
                    if ((i & foodA) != 0 && (i & foodB) != 0) {
                        check = true;
                        break;
                    }
                }
                if (!check) {
                    count++;
                }
            }
            sb.append("#").append(t).append(" ").append(count).append("\n");
        }
        System.out.println(sb);
    }
}
/*
3
3 2
1 2
2 3
3 0
3 3
1 2
1 3
2 3
 */

코드 설명

  • long[][] conflict
    서로 함께 먹을 수 없는 음식 쌍을 저장하는 배열
    • 1 << (번호 - 1) 형태로 비트 마스크로 변환하여 저장합니다.
      (예: 음식 1 → 0001, 음식 3 → 0100)
for (int i = 0; i < (1 << N); i++) {
    boolean check = false;
    for (int j = 0; j < M; j++) {
        long foodA = conflict[j][0];
        long foodB = conflict[j][1];
        if ((i & foodA) != 0 && (i & foodB) != 0) {
            check = true;
            break;
        }
    }
    if (!check) {
        count++;
    }
}
  • i는 0부터 2^N - 1까지의 모든 정수를 나타내며, 각 숫자가 하나의 부분집합을 의미합니다.
    • 예: i = 5 (101₂) → 음식 1과 음식 3을 선택한 경우
  • conflict에 저장된 음식 쌍이 동시에 부분집합에 존재하면 해당 조합은 제외
    • (i & foodA) != 0 && (i & foodB) != 0 → 두 음식 모두 포함되었는지 확인
  • 조건을 만족하는 조합(check == false)만 count++
제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
SWEA 6782 현주가 좋아하는 제곱근 놀이입니다.

포인트

해당 문제의 가장 포인트는 Math.sqrt()의 반환값입니다.

먼저 자바의 Math.sqrt() 스펙을 살펴보겠습니다.

/**
 * Returns the correctly rounded positive square root of a
 * {@code double} value.
 * Special cases:
 * <ul><li>If the argument is NaN or less than zero, then the result
 * is NaN.
 * <li>If the argument is positive infinity, then the result is positive
 * infinity.
 * <li>If the argument is positive zero or negative zero, then the
 * result is the same as the argument.</ul>
 * Otherwise, the result is the {@code double} value closest to
 * the true mathematical square root of the argument value.
 *
 * @param   a   a value.
 * @return  the positive square root of {@code a}.
 */
@IntrinsicCandidate
public static native double sqrt(double a);

스펙을 살펴보면 반올림된 양의 제곱근을 반환한다고 되어 있습니다.

예를 들어서 Math.sqrt(9) = 3이지만, Math.sqrt(8) = 2.xx 값으로 나오는 것입니다.

더보기

※ 추가로

  • @IntrinsicCandidate 어노테이션은 JVM HotSpot 컴파일러가 이 메서드를 자동 최적화할 수 있도록 힌트를 주는 표시입니다.
    • 목적은 자바 바이트코드를 native CPU 명령어로 치환하기 위함입니다.(CPU 수준의 명령어로 대체해서 실행)
    • 사용 이유는 빠른 처리를 위한 함수들(Math.log10() 등)에 사용됩니다.
  • native 키워드는 자바가 아닌 다른 언어(C/C++ 등..)으로 작성된 것을 나타내는 키워드입니다.
    • Math.sqrt()는 성능이 매우 중요한 함수이기 때문에,
      Java로 직접 구현하지 않고 플랫폼에 최적화된 C/C++ 코드로 OS 레벨에서 구현해 둔 것을 JNI(Java Native Interface)를 통해 호출하는 구조입니다.

이제 문제를 살펴보면

 

값이 2일때는 0번의 연산

연산을 한번만 하는 것은 4(루트 4)이므로 3은 4로 이동 후 연산을 진행하면 2번의 연산이 진행됩니다.(n+1, 루트 N)

다음 제곱수를 생각했을 때 9는 루트 처리 후 3의 값에 +1하면 됩니다.

 

즉 일반화를 진행하면

  1. 현재 수가 완전제곱수라면: √N 연산 (연산 1회)
  2. 그 외의 경우: +1 연산을 반복해서 완전제곱수로 만든 뒤, √ 연산
  • 완전제곱수가 아니면, +1 연산을 통해 다음 완전제곱수로 이동한 뒤 √ 연산을 합니다.
  • 이 과정을 반복하면서 N이 2가 될 때까지 연산 횟수를 누적합니다.
  • 즉, N → 다음 제곱수 → √ → 반복, 이 과정을 통해 DP 없이도 최적의 연산 횟수를 구할 수 있습니다.

소스코드

public class Q6782 {
    private static int tc;
    private static Long N;

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

        StringBuilder sb = new StringBuilder();
        for (int t = 1; t <= tc; t++) {
            N = Long.parseLong(br.readLine());
            sb.append("#").append(t).append(" ").append(solve(N)).append("\n");
        }
        System.out.println(sb);
    }

    private static long solve(long n) {
        long count = 0;

        while (n != 2) {
            long sqrt = (long) Math.sqrt(n);
            if (sqrt * sqrt == n) {
                n = sqrt;
                count++; // 한 번의 연산
            } else {
                long nextSquare = (sqrt + 1) * (sqrt + 1);
                count += nextSquare - n;
                n = nextSquare;
            }
        }

        return count;
    }
}

코드 설명

  • solve(long n)
    n의 2로 만드는 최소 연산 값을 구하는 함수
    • n이 2가 아닐동안
      • Math.sqrt(n)을 통해 sqrt 값을 구해줍니다.(여기서 기본 스펙이 double이기 때문에 long으로 타입 캐스팅을 진행해줍니다.)
      • n이 제곱수인지를 판단해주기 위해 sqrt * sqrt = n인지 파악합니다
        • 제곱수라면
          • n = sqrt로 치환 후 count를 1 증가합니다.
        • 제곱수가 아니라면
          • 다음 제곱수까지 count를 증가시키고
          • n을 다음 제곱수로 치환합니다.
제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
SWEA 7793 오! 나의 여신님입니다.

포인트

해당 문제에서는 악마의 손아귀가 먼저 진행되어야 합니다.

만약 악마의 손아귀를 통해서 전파된 곳에 수연이가 이미 있는 곳이라도 사실 크게 의미가 없습니다.

왜냐하면 이미 수연이가 있는 위치는 수연이가 도달한 공간이기 때문에 다음 시간에는 수연이가 이동하기에 부식된 공간과는 수연이의 위치와 무관합니다. 수연이가 고려할 것은 다음 위치에 악마의 손아귀가 있는지 없는지 입니다.

따라서 BFS 탐색을 통해서 문제를 진행합니다.

소스코드


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

public class Q7793 {
    static class Pair {
        int x, y;

        public Pair(int x, int y) {
            this.x = x;
            this.y = y;
        }
    }
    private static int tc, N, M, ans;
    private static char[][] map;
    private static int[][] step;
    private static boolean[][] visited;
    private static int[] dx = {-1, 0, 1, 0};
    private static int[] dy = {0, 1, 0, -1};
    private static Pair angle, su;
    private static List<Pair> demons;

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

        for (int t = 1; t <= tc; t++) {
            st = new StringTokenizer(br.readLine());
            N = Integer.parseInt(st.nextToken());
            M = Integer.parseInt(st.nextToken());
            map = new char[N][M];
            step = new int[N][M];
            visited = new boolean[N][M];
            demons = new ArrayList<>();
            ans = Integer.MAX_VALUE;

            for (int i = 0; i < N; i++) {
                String line = br.readLine();
                for (int j = 0; j < M; j++) {
                    map[i][j] = line.charAt(j);
                    if (map[i][j] == 'D') {
                        angle = new Pair(i, j);
                    } else if (map[i][j] == 'S') {
                        su = new Pair(i, j);
                    } else if (map[i][j] == '*') {
                        demons.add(new Pair(i, j));
                    }
                }
            }

            int answer = bfs();

            sb.append("#").append(t).append(" ")
                    .append(answer == -1 ? "GAME OVER" : answer)
                    .append("\n");
        }
        System.out.println(sb);
    }

    static int bfs() {
        Queue<Pair> suyeonQ = new LinkedList<>();
        Queue<Pair> demonQ = new LinkedList<>();
        suyeonQ.add(su);
        demonQ.addAll(demons);

        while (!suyeonQ.isEmpty()) {
            // 1. 악마 먼저 퍼짐
            int demonSize = demonQ.size();
            for (int i = 0; i < demonSize; i++) {
                Pair d = demonQ.poll();
                for (int dir = 0; dir < 4; dir++) {
                    int nx = d.x + dx[dir];
                    int ny = d.y + dy[dir];
                    if (canGo(nx, ny) && map[nx][ny] != 'D') {
                        map[nx][ny] = '*';
                        demonQ.offer(new Pair(nx, ny));
                    }
                }
            }

            // 2. 수연이 이동
            int suSize = suyeonQ.size();
            for (int i = 0; i < suSize; i++) {
                Pair s = suyeonQ.poll();
                for (int dir = 0; dir < 4; dir++) {
                    int nx = s.x + dx[dir];
                    int ny = s.y + dy[dir];
                    if (canGo(nx, ny)) {
                        if (map[nx][ny] == '*') {
                            continue;
                        } else if (map[nx][ny] == 'D') {
                            return step[s.x][s.y] + 1;
                        }
                        visited[nx][ny] = true;
                        step[nx][ny] = step[s.x][s.y] + 1;
                        suyeonQ.offer(new Pair(nx, ny));
                    }
                }
            }
        }
        return -1;
    }

    private static boolean canGo(int x, int y) {
        if (!inRange(x, y)) return false;
        if (visited[x][y] || map[x][y] == 'X' || map[x][y] == '*') return false;

        return true;
    }

    private static boolean inRange(int x, int y) {
        return 0 <= x && x < N && 0 <= y && y < M;
    }
}

/*
2
5 3
D*S
.X.
.X.
.X.
...
5 3
D*S
...
.X.
.X.
...
 */

코드 설명

  • inRange(int x, int y)
    x, y가 격자 내에 존재하는지 판단하는 함수
  • canGo(int x, int y)
    x, y가 갈 수 있는 위치인지 판단하는 함수
    • 격자 내에 존재하거나
    • 방문하지 않았던 점이거나
    • 이나 악마의 손아귀가 아닌 점일 시에 true를 반환합니다.
  • bfs()
    bfs 탐색을 하는 함수
    • suyeonQ, demonQ를 선언합니다.
      • suyeonQ: 수연이의 위치를 저장하는 Queue
      • demonQ: 악마의 손아귀의 위치를 저장하는 Queue
    • suyeonQ.isEmpty()가 아닐동안
      • 악마의 손아귀를 먼저 진행
        • 현재 시점의 큐 크기(demonSize)만큼만 반복하여, 같은 시간대의 악마의 손아귀만 처리하고 다음 단계는 다음 턴에 처리
        • 갈 수 있을 때(canGo(nx, ny) && map[nx][ny] != 여신)
          • demonQ에 저장
      • 수연이 이동
        • 현재 시점의 큐 크기(suSize)만큼만 반복하여, 같은 시간대의 수연이 이동만 처리하고 다음 단계는 다음 턴에 처리 
        • step[][] 배열을 통해 이전 위치 + 1을 저장
        • 만약 다음 canGo에 여신이면 step[s.x][s.y] + 1 반환.
제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁

스프링 정리를 위한 포스팅입니다.
해당 포스팅은 Spring Security DSL기반 필터 등록 과정입니다.

 

서론

문득 Security 작업을 진행하면서 커스텀 필터를 어느 곳에 위치하는 것이 좋은지 호기심이 생겼습니다.

처음 Security를 사용했을때 UsernamePasswordAuthenticationFilter를 앞에 문득 붙였었는데 

Spring Security는 다양한 필터를 체인 형태로 적용하는데, 내가 만든 필터가 정확히 어떤 순서에 들어가야 효율적인지,
이를 확인하기 위해 실제 Filter 등록과정을 분석하고, 내부 소스 코드를 따라가보았습니다.

 

 

 

본론

SecurityConfig 클래스는 다음과 같이 구성하였습니다.

@Bean
public SecurityFilterChain web(HttpSecurity http) throws Exception {

    http.formLogin()
            .loginPage("/login.html")            // 사용자 정의 로그인 페이지
            .defaultSuccessUrl("/home")          // 로그인 성공 후 이동 페이지
            .failureUrl("/login.html?error=true")          // 로그인 실패 후 이동 페이지
            .usernameParameter("username")       // 아이디 파라미터명 설정
            .passwordParameter("password")       // 패스워드 파라미터명 설정
            .loginProcessingUrl("/login");       // 로그인 Form Action Url

    return http
            .csrf(AbstractHttpConfigurer::disable)
            .sessionManagement((sessionManagement) ->
                    sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> {
                auth.requestMatchers(PERMIT_ALL_URLS)
                        .permitAll();
                auth.requestMatchers(PERMIT_ADMIN_URLS)
                        .hasRole("ADMIN");
                auth.anyRequest()
                        .authenticated();
            })
            .addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
//                .addFilterBefore(filter, LogoutFilter.class)
            .addFilter(webConfig.corsFilter())
            .build();
}

먼저 Security의 filter들이 어느 것이 있는지 파악하기 위해 다음과 같은 코드를 추가하였습니다.

import jakarta.servlet.Filter;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.FilterChainProxy;

import java.util.List;

@Configuration
public class FilterLoggingConfig {

    @Bean
    public ApplicationRunner logSecurityFilters(FilterChainProxy filterChainProxy) {
        return args -> {
            List<Filter> filters = filterChainProxy.getFilters("/");

            System.out.println("\n=== [Spring Security Filter Chain 순서] ===");
            for (int i = 0; i < filters.size(); i++) {
                System.out.printf("%2d. %s%n", i + 1, filters.get(i).getClass().getSimpleName());
            }
            System.out.println("==========================================\n");
        };
    }

}

스프링을 빌드하면 다음과 같은 출력이 나옵니다.

FilterChain 순서

여기서  7번째 JwtRequestFilter는 제가 작성한 커스텀 필터입니다.

UsernamePasswordAuthenticationFilter는 HttpSecurity.formLogin()시에 자동적으로 설정되는 필터입니다.
(Spring 6.1 이후 deprecated되었습니다. 예시를 위해 이해를 돕기 위한 구조로 설명을 진행하겠습니다.)

HttpSecurity 클래스의 formLogin

HttpSecurity 클래스에서는 formLogin() 메소드 호출시 FormLoginConfigurer 클래스를 생성 후 getOrApply 메소드의 인자 값으로 넘겨줍니다.

 

FormLoginConfigurer 생성자

 

FormLoginConfigurer는 AbstractAuthenticationFilterConfigurer를 상속받으며, 이 상위 클래스에서 기본 인증 필터인 UsernamePasswordAuthenticationFilter를 생성하고 등록하는 역할을 합니다.
이후 이 Configurer는 HttpSecurity 내부의 configurers 맵에 저장되어, 나중에 build() 과정에서 자동으로 configure() 메서드가 호출되며 필터가 SecurityFilterChain에 실제로 추가됩니다.

 

AbstractSecurityBuilder 추상클래스의 build() method
AbstractConfiguredSecurityBuilder 추상 클래스의 doBuild() method
HttpSecurity 클래스의 performBuild method

 

HttpSecurity.build()를 호출하면, 최종적으로 performBuild() 메서드가 실행됩니다. 이 메서드는 지금까지 설정된 모든 필터들을 종합하여 DefaultSecurityFilterChain 객체를 생성하는 핵심 단계입니다.
즉, 지금까지 Configurer들이 등록하고 추가한 필터들이 실제 하나의 FilterChain으로 묶여 Spring Security의 필터 체인으로 등록되는 마지막 절차입니다.

 

순서 정리

# 순서도

HttpSecurity
  └── formLogin()
       └── getOrApply()
            └── FormLoginConfigurer 생성
                 └── UsernamePasswordAuthenticationFilter 준비
HttpSecurity.build()
  └── doBuild()
       └── configure() 호출
            └── addFilter()
                 └── performBuild()
                      └── DefaultSecurityFilterChain 완성
  1. HttpSecurity.formLogin() 호출(HttpSecurity클래스)
    1. getOrApply(new FormLoginConfigurer<>()) 실행
      1. getConfigurer(configurer.getClass()) 실행
        1. getConfigurer(Class <C> clazz) 실행 (AbstractConfiguredSecurityBuilder 추상 클래스)
        2. 여기서 configures 정의
  2. FormLoginConfigurer(클래스 생성)
    1. AbstractAuthenticationFilterConfigurere에서 생성자 등록
      1. UsernamePasswordAuthenticationFilter 생성
  3. HttpSecurity.build() 호출 (SecurityBuilder 인터페이스)
    1. build() 호출 (AbstractSecurityBuilder 추상클래스)
      1. doBuild() 호출
        1. AbstractConfiguredSecurityBuilder 추상 클래스에서 doBuild() 실행
          1. configure() 실행
            1. AbstractConfiguredSecurityBuilder 클래스에서 호출. configurer.configure((B) this)
              1. HttpSecurity.addFilter()로 등록
          2. performBuild() 호출
            1. 여기서 filter가 등록된걸 DefaultSecurityFilterChain에 넣어주기(DefaultSecurityFilterChain)

결론

Spring Security에서 http.formLogin()을 호출하면, 내부적으로 FormLoginConfigurer가 HttpSecurity에 등록됩니다.

이 Configurer는 UsernamePasswordAuthenticationFilter를 생성한 뒤,

AbstractAuthenticationFilterConfigurer.configure()에서 http.addFilter()를 호출하여 필터 체인에 등록합니다.

이후 http.build() 호출 시 필터 리스트가 정렬되어 DefaultSecurityFilterChain이 생성되고,

최종적으로 FilterChainProxy에 포함되어 DispatcherServlet 앞에서 모든 요청을 처리하게 됩니다.

그래서 저는 UsernamePasswordAuthenticationFilter와 동일한 역할을 하는 Filter를 개발하여 해당 필터 순서에 넣었습니다.

 

용어정리

더보기

 

  • AbstractConfiguredSecurityBuilder: 여러 Configurer를 모아서 설정을 수행하는 빌더 클래스입니다.
  • FormLoginConfigurer: 로그인 처리를 위한 Configurer로, 기본적으로 UsernamePasswordAuthenticationFilter를 설정합니다.
  • DefaultSecurityFilterChain: 최종적으로 적용되는 필터 리스트를 가진 객체로, Spring Security가 실제 요청 처리 시 사용하는 체인입니다.
  • configures: HttpSecurity 설정시 다양한 설정 메소드 호출. 등록된 SecurityConfigurer들을 클래스 타입별로 그룹핑하여 저장하는 자료구조(formLogin(), csrf(), authorizeRequests() 등)

 

코드

 

제가 공부한 내용을 정리하는 블로그입니다.
아직 많이 부족하고 배울게 너무나도 많습니다. 틀린내용이 있으면 언제나 가감없이 말씀해주시면 감사하겠습니다😁
SWEA 1256 K번째 접미어입니다.

 

해당 문제도 솔직히 효율적인 코드인지는 잘 모르겠습니다만, 제가 활용한 알고리즘을 정리하겠습니다.

포인트

Java의 substring() method를 활용하여, 접미어를 구하고, Collections.sort를 진행합니다.

그 이후 k번째 접미어를 찾아줍니다.

소스코드

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Q1256 {
    private static int tc, K;
    private static List<String> postfix;

    public static void main(String[] args) throws IOException {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        tc = Integer.parseInt(br.readLine());
        StringBuilder sb = new StringBuilder();
        for (int t = 1; t <= tc; t++) {
            K = Integer.parseInt(br.readLine());
            String line = br.readLine();

            postfix = new ArrayList<>();
            for (int i = 0; i < line.length(); i++) {
                postfix.add(line.substring(i));
            }

            Collections.sort(postfix);

            sb.append("#").append(t).append(" ")
                    .append(postfix.size() >= K ? postfix.get(K-1) : "none").append("\n");
        }
        System.out.println(sb);
    }
}

코드 설명

postfix = new ArrayList<>();
for (int i = 0; i < line.length(); i++) {
    postfix.add(line.substring(i));
}

Collections.sort(postfix);
  • index가 i부터인 substring을 postfix에 넣어주고, 정렬을 진행해줍니다.
  • 그 이후 k번째 접미어를 찾고 만약 없으면 none을 반환해줍니다.

+ Recent posts