1. 문제 링크
https://www.acmicpc.net/problem/23289
2. 문제 풀이
이제 곧 기다리던 삼성 코딩 테스트죠? 구현만큼은 자신 있었던 (과거형) 저도 이번만큼은 긴장되네요. 그야 자소서 문항이 바뀌고 난 후 첫 시험이라 .. 코딩테스트도 변동이 있을 가능성도 없진 않을 테니까요. 그래서 예전 같았으면 그냥 배열 돌리기랑 bfs만 돌리고 갔을 텐데 이번엔 다릅니다. 몇 달 전에 큐빙을 풀긴 했지만 좀 제대로 된 플래티넘 구현 문제를 풀고 싶었습니다.

가장 중요한 건 설계. 코드를 구현하기 전에 꼼꼼하게 조건을 숙지한 후 구현을 해야 합니다. 그래도 나름 짧은 코드로 만들어보고 싶었으나 코드가 좀 길게 나왔습니다.
1. 모든 온풍기에서 바람 나옴
2. 온도 조절
3. 온도 1 이상인 가장 바깥쪽 칸의 온도가 1씩 감소
4. 초콜릿 먹기
5. 조사하는 모든 칸의 온도가 K 이상이 되었는지 체크. K 이상이면 테스트를 중단. 아니면 1번부터 다시 시작.
대충 절차만 보면은 간단하죠. 근데 문제는 벽이 있다는 겁니다. 그래서 이 벽의 위치를 어떻게 저장해서 활용할지를 제일 먼저 생각해야 합니다. 그다음은 온풍기에서 바람이 나올 때 온도가 올라가는 부분을 어떻게 탐색해야 할지를 고려해야 하고.. 이 두 가지 조건만 해결되면 나머지 구현은 쉽습니다.
2-1. Simulation
while(!check()) {
heating();
control();
chilling();
chocolate++;
if(100 < chocolate) break;
}
이 부분이 바로 시뮬레이션의 본체입니다. 저 메서드들을 구현하기만 하면 완성되는 겁니다.
2-2. Variable
static int R,C,K;
static int[][] map;
static class Node {
int x, y;
Node (int x, int y){
this.x = x;
this.y = y;
}
}
static class Heater {
int d, x, y;
Heater (int d, int x, int y){
this.d = d; this.x = x; this.y = y;
}
}
static HashMap<Integer, HashSet<Integer>> downBlock = new HashMap<>();
static HashMap<Integer, HashSet<Integer>> upBlock = new HashMap<>();
static HashMap<Integer, HashSet<Integer>> leftBlock = new HashMap<>();
static HashMap<Integer, HashSet<Integer>> rightBlock = new HashMap<>();
static int[] dx = {0, 0, 0, -1, 1};
static int[] dy = {0, 1, -1, 0, 0};
static List<Node> checkList = new ArrayList<>();
static List<Heater> heaterList = new ArrayList<>();
물론 그 이전에 문제를 푸는데 필요한 데이터와 입력 값을 받아야 하는 부분을 먼저 생각해야겠지만요. HashMap이 왜 저렇게 많냐-고 묻는다면, 제 알량한 구현력이 저지른 실수라 답하겠습니다. 삼차원 배열로 데이터를 관리하면 좀 더 편하긴 했겠지만, "엥 굳이?"라는 생각이 들었습니다. 아 물론 Null check를 신경 쓰지 않으면 소스가 터져버리기 때문에 저도 저러한 방식을 선호하지는 않지만 그렇게 했습니다.
2-3. Input
map = new int[R][C];
for(int i = 0; i < R; i++){
st = new StringTokenizer(br.readLine());
for(int j = 0; j < C; j++){
int tmp = Integer.parseInt(st.nextToken());
switch(tmp){
case 1: case 2: case 3: case 4:
heaterList.add(new Heater(tmp, i, j));
break;
case 5:
checkList.add(new Node(i, j));
break;
}
}
}
입력 값으로 주어지는 맵 데이터를 굳이 그대로 받아야 할까요? 어차피 문제가 요구하는 건 맵의 온도입니다. 히터의 위치니 체크하는 배열의 인덱스니 하는 것들은 따로 리스트로 관리하도록 합시다.
int wallCnt = Integer.parseInt(br.readLine());
for(int i = 0; i < wallCnt; i++){
st = new StringTokenizer(br.readLine());
int tx = Integer.parseInt(st.nextToken())-1;
int ty = Integer.parseInt(st.nextToken())-1;
int flag = Integer.parseInt(st.nextToken());
HashSet<Integer> set, set2;
switch (flag){
case 0:
if(downBlock.containsKey(tx)){
set = downBlock.get(tx);
set.add(ty);
downBlock.put(tx, set);
set2 = upBlock.get(tx-1);
set2.add(ty);
upBlock.put(tx-1, set2);
}
else{
set = new HashSet<>();
set2 = new HashSet<>();
set.add(ty);
set2.add(ty);
downBlock.put(tx, set);
upBlock.put(tx-1, set2);
}
break;
case 1:
if(leftBlock.containsKey(tx)){
set = leftBlock.get(tx);
set.add(ty);
leftBlock.put(tx, set);
set2 = rightBlock.get(tx);
set2.add(ty+1);
rightBlock.put(tx, set2);
}
else{
set = new HashSet<>();
set2 = new HashSet<>();
set.add(ty);
set2.add(ty+1);
leftBlock.put(tx, set);
rightBlock.put(tx, set2);
}
break;
}
}
이 문제에서 가장 중요한 벽 처리. 어쩌다 보니 다른 사람들보다 코드의 양이 많게 되었는데 이런 부분에서 미숙함이 묻어 나오는 듯합니다. 처음에는 HashMap을 2개만 만들어서 좌우/상하로 묶어서 좌표 값만 계산해서 처리하려고 했으나, 뒤에서 구현할 메서드들이 더 복잡해지는 겁니다. 그래서 각 방향에서 가지 못하는 인덱스를 저장할 HashMap 4개를 생성했습니다.
2-4. Heating
static void heating(){
for(Heater heater : heaterList){
int temperature = 5;
int hx = heater.x, hy = heater.y, hd = heater.d;
map[hx+dx[hd]][hy+dy[hd]] += temperature;
Queue<Node> q = new ArrayDeque<>();
q.add(new Node(hx+dx[hd], hy+dy[hd]));
boolean[][] visited = new boolean[R][C];
visited[hx+dx[hd]][hy+dy[hd]] = true;
while(!q.isEmpty()){
Node tmp = q.poll();
if(hd == 1 || hd == 2){
if(isRange(tmp.x, tmp.y+dy[hd])){
if(!visited[tmp.x][tmp.y+dy[hd]] && !isWall(tmp.x, tmp.y+dy[hd], hd)) {
visited[tmp.x][tmp.y+dy[hd]] = true;
if(0 < 4-Math.abs(hy-tmp.y+dy[hd])) {
map[tmp.x][tmp.y + dy[hd]] += 4-Math.abs(hy-tmp.y+dy[hd]);
q.add(new Node(tmp.x, tmp.y + dy[hd]));
}
}
}
if(isRange(tmp.x-1, tmp.y+dy[hd])){
if(!visited[tmp.x-1][tmp.y+dy[hd]] && !isWall(tmp.x-1, tmp.y, 3) && !isWall(tmp.x-1, tmp.y+dy[hd], hd)){
visited[tmp.x-1][tmp.y+dy[hd]] = true;
if(0 < 4-Math.abs(hy-tmp.y+dy[hd])) {
map[tmp.x - 1][tmp.y + dy[hd]] += 4-Math.abs(hy-tmp.y+dy[hd]);
q.add(new Node(tmp.x - 1, tmp.y + dy[hd]));
}
}
}
if(isRange(tmp.x+1, tmp.y+dy[hd])){
if(!visited[tmp.x+1][tmp.y+dy[hd]] && !isWall(tmp.x+1, tmp.y, 4) && !isWall(tmp.x+1, tmp.y+dy[hd], hd)){
visited[tmp.x+1][tmp.y+dy[hd]] = true;
if(0 < 4-Math.abs(hy-tmp.y+dy[hd])) {
map[tmp.x + 1][tmp.y + dy[hd]] += 4-Math.abs(hy-tmp.y+dy[hd]);
q.add(new Node(tmp.x + 1, tmp.y + dy[hd]));
}
}
}
}
else{
if(isRange(tmp.x+dx[hd], tmp.y)){
if(!visited[tmp.x+dx[hd]][tmp.y] && !isWall(tmp.x+dx[hd], tmp.y, hd)) {
visited[tmp.x+dx[hd]][tmp.y] = true;
if(0 < 4-Math.abs(hx-tmp.x+dx[hd])) {
map[tmp.x + dx[hd]][tmp.y] += 4-Math.abs(hx-tmp.x+dx[hd]);
q.add(new Node(tmp.x + dx[hd], tmp.y));
}
}
}
if(isRange(tmp.x+dx[hd], tmp.y-1)){
if(!visited[tmp.x+dx[hd]][tmp.y-1] && !isWall(tmp.x, tmp.y-1, 2) && !isWall(tmp.x+dx[hd], tmp.y-1, hd)) {
visited[tmp.x+dx[hd]][tmp.y-1] = true;
if(0 < 4-Math.abs(hx-tmp.x+dx[hd])) {
map[tmp.x + dx[hd]][tmp.y - 1] += 4-Math.abs(hx-tmp.x+dx[hd]);
q.add(new Node(tmp.x + dx[hd], tmp.y - 1));
}
}
}
if(isRange(tmp.x+dx[hd], tmp.y+1)){
if(!visited[tmp.x+dx[hd]][tmp.y+1] && !isWall(tmp.x, tmp.y+1, 1) && !isWall(tmp.x+dx[hd], tmp.y+1, hd)) {
visited[tmp.x+dx[hd]][tmp.y+1] = true;
if(0 < 4-Math.abs(hx-tmp.x+dx[hd])) {
map[tmp.x + dx[hd]][tmp.y + 1] += 4-Math.abs(hx-tmp.x+dx[hd]);
q.add(new Node(tmp.x + dx[hd], tmp.y + 1));
}
}
}
}
}
}
}
Heater 리스트를 순회하면서 온풍기를 하나씩 켜줍니다. 온풍기의 바로 앞은 벽이 없다는 제약 조건으로 초기 값을 세팅하고, 큐를 만들어서 벽이 막혀있는지를 체크하면서 바람을 보내주었습니다. 바람을 보내는 것도 좀 까다로운데 일단은 한 칸에서 바람을 보낼 수 있는 방법은 총 3 가지고, 3곳 모두 바람을 불어넣을 수 있습니다.

예를 들자면, (x, y)에서 바람의 진행 방향이 오른쪽일 때, (x, y+1), (x-1, y+1), (x+1, y+1) 칸으로 바람을 보낼 수가 있습니다. 물론 바람은 대각선으로는 움직일 수 없고, 무조건 직각으로만 움직입니다. 바람이 이동하는 경로 중간에 벽이 존재한다면 목적지까지 바람이 닿을 수 없겠죠? 그래서 이 부분을 정리하기 위해 바람의 이동 경로를 생각해 봤습니다.
우좌상하 순이므로 순서대로 1, 2, 3, 4라 쳐봅시다.
우(1)
3 -> 1
1
4 -> 1
좌(2)
3 -> 2
2
4 -> 2
상(3)
2 -> 3
3
1 -> 3
하(4)
2 -> 4
4
1 -> 4
현재의 인덱스에서 다음 인덱스로 갈 때의 방향이 온풍기의 방향과 같을 때 목적지에 도착했다고 판단하면 됩니다. 어쨌든 이렇게 시각적으로 표현했을 때 방향이 겹치는 부분을 확인할 수 있습니다. 우, 좌 / 상, 하로 나눠서 적절히 처리하면 되겠죠?
예로 들자면,
if(isRange(tmp.x+dx[hd], tmp.y-1)){
if(!visited[tmp.x+dx[hd]][tmp.y-1] && !isWall(tmp.x, tmp.y-1, 2) && !isWall(tmp.x+dx[hd], tmp.y-1, hd)) {
visited[tmp.x+dx[hd]][tmp.y-1] = true;
if(0 < 4-Math.abs(hx-tmp.x+dx[hd])) {
map[tmp.x + dx[hd]][tmp.y - 1] += 4-Math.abs(hx-tmp.x+dx[hd]);
q.add(new Node(tmp.x + dx[hd], tmp.y - 1));
}
}
}
1) 다음 인덱스가 맵 범위 안에 있고 2) 바람이 분 칸이 아닐 경우 3) 그리고 가는 길 사이에 벽이 없을 경우를 고려하면 됩니다. 그리고 온풍기에서 멀어지면 멀어질수록 온도가 낮아지니까 이 부분도 포함해서 구현했습니다.
관련 메서드들 (isRange, isWall)
static boolean isRange(int x, int y){
return 0 <= x && x < R && 0 <= y && y < C;
}
static boolean isWall(int x, int y, int d){
HashSet<Integer> set;
switch(d){
case 1:
if(rightBlock.containsKey(x)){
set = rightBlock.get(x);
if(set.contains(y)) return true;
}
break;
case 2:
if(leftBlock.containsKey(x)){
set = leftBlock.get(x);
if(set.contains(y)) return true;
}
break;
case 3:
if(upBlock.containsKey(x)){
set = upBlock.get(x);
if(set.contains(y)) return true;
}
break;
case 4:
if(downBlock.containsKey(x)){
set = downBlock.get(x);
if(set.contains(y)) return true;
}
break;
}
return false;
}
2-5. Control
static void control(){
int[][] tmpArr = new int[R][C];
for(int i = 0; i < R; i++){
for(int j = 0; j < C; j++){
for(int k = 1; k <= 4; k++){
int nx = i + dx[k];
int ny = j + dy[k];
if(isRange(nx, ny) && !isWall(nx, ny, k)){
if(map[i][j] < map[nx][ny]){
tmpArr[i][j] += (map[nx][ny] - map[i][j])/4;
tmpArr[nx][ny] -= (map[nx][ny] - map[i][j])/4;
}
}
}
}
}
for(int i = 0; i < R; i++)
for(int j = 0; j < C; j++)
map[i][j] += tmpArr[i][j];
}
자, 여기까지 왔으면 정말 별 거 없습니다. 필요한 메서드들은 앞에서 다 구현했거든요! 온도 조절 코드인 control 메서드는 따로 온도의 합과 차의 결과를 저장할 tmpArr를 추가로 생성합시다. 그런 후에 최종 온도 조절 결과를 원래의 배열에 더하면 됩니다!
2-6. Chilling
static void chilling(){
for(int i = 0; i < R; i++)
for(int j = 0; j < C; j++)
if(i == 0 || i == R-1 || j == 0 || j == C-1)
if(1 <= map[i][j]) map[i][j]-=1;
}
저는 사실 테두리 온도가 식는 부분을 어렵게 생각했거든요? 온도가 존재하는 칸의 테두리를 구해서 전부 -1 해주는 줄 알았는데, 문제를 다시 읽어보니 그냥 단순히 map의 가장 겉 테두리만 -1 해주는 문제였습니다. (그래서 좀 실망했었는데) 아무튼 날로 먹는 부분이니 단순하게 배열 순회로 -1 합시다.
2-7. Check
static boolean check(){
for(Node tmp : checkList)
if(map[tmp.x][tmp.y] < K) return false;
return true;
}
이제 마지막으로 온도를 조사해야 할 칸을 순회합니다. Simulation에서 while문의 조건에 넣을 메서드라, boolean 값으로 return 해주면 되겠죠?
(2-8. Chocolate)
음, 별로 중요하지 않은 초콜릿은 나중에 드세요.
3. 소스 코드
import java.io.*;
import java.util.*;
public class BOJ23289 {
static int R,C,K;
static int[][] map;
static class Node {
int x, y;
Node (int x, int y){
this.x = x;
this.y = y;
}
}
static class Heater {
int d, x, y;
Heater (int d, int x, int y){
this.d = d; this.x = x; this.y = y;
}
}
static HashMap<Integer, HashSet<Integer>> downBlock = new HashMap<>();
static HashMap<Integer, HashSet<Integer>> upBlock = new HashMap<>();
static HashMap<Integer, HashSet<Integer>> leftBlock = new HashMap<>();
static HashMap<Integer, HashSet<Integer>> rightBlock = new HashMap<>();
static int[] dx = {0, 0, 0, -1, 1};
static int[] dy = {0, 1, -1, 0, 0};
static List<Node> checkList = new ArrayList<>();
static List<Heater> heaterList = new ArrayList<>();
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st = new StringTokenizer(br.readLine());
R = Integer.parseInt(st.nextToken()); C = Integer.parseInt(st.nextToken());
K = Integer.parseInt(st.nextToken());
map = new int[R][C];
for(int i = 0; i < R; i++){
st = new StringTokenizer(br.readLine());
for(int j = 0; j < C; j++){
int tmp = Integer.parseInt(st.nextToken());
switch(tmp){
case 1: case 2: case 3: case 4:
heaterList.add(new Heater(tmp, i, j));
break;
case 5:
checkList.add(new Node(i, j));
break;
}
}
}
int wallCnt = Integer.parseInt(br.readLine());
for(int i = 0; i < wallCnt; i++){
st = new StringTokenizer(br.readLine());
int tx = Integer.parseInt(st.nextToken())-1;
int ty = Integer.parseInt(st.nextToken())-1;
int flag = Integer.parseInt(st.nextToken());
HashSet<Integer> set, set2;
switch (flag){
case 0:
if(downBlock.containsKey(tx)){
set = downBlock.get(tx);
set.add(ty);
downBlock.put(tx, set);
set2 = upBlock.get(tx-1);
set2.add(ty);
upBlock.put(tx-1, set2);
}
else{
set = new HashSet<>();
set2 = new HashSet<>();
set.add(ty);
set2.add(ty);
downBlock.put(tx, set);
upBlock.put(tx-1, set2);
}
break;
case 1:
if(leftBlock.containsKey(tx)){
set = leftBlock.get(tx);
set.add(ty);
leftBlock.put(tx, set);
set2 = rightBlock.get(tx);
set2.add(ty+1);
rightBlock.put(tx, set2);
}
else{
set = new HashSet<>();
set2 = new HashSet<>();
set.add(ty);
set2.add(ty+1);
leftBlock.put(tx, set);
rightBlock.put(tx, set2);
}
break;
}
}
int chocolate = 0;
while(!check()) {
heating();
control();
chilling();
chocolate++;
if(100 < chocolate) break;
}
if(100 < chocolate) System.out.println(101);
else System.out.println(chocolate);
}
static void heating(){
for(Heater heater : heaterList){
int temperature = 5;
int hx = heater.x, hy = heater.y, hd = heater.d;
map[hx+dx[hd]][hy+dy[hd]] += temperature;
Queue<Node> q = new ArrayDeque<>();
q.add(new Node(hx+dx[hd], hy+dy[hd]));
boolean[][] visited = new boolean[R][C];
visited[hx+dx[hd]][hy+dy[hd]] = true;
while(!q.isEmpty()){
Node tmp = q.poll();
if(hd == 1 || hd == 2){
if(isRange(tmp.x, tmp.y+dy[hd])){
if(!visited[tmp.x][tmp.y+dy[hd]] && !isWall(tmp.x, tmp.y+dy[hd], hd)) {
visited[tmp.x][tmp.y+dy[hd]] = true;
if(0 < 4-Math.abs(hy-tmp.y+dy[hd])) {
map[tmp.x][tmp.y + dy[hd]] += 4-Math.abs(hy-tmp.y+dy[hd]);
q.add(new Node(tmp.x, tmp.y + dy[hd]));
}
}
}
if(isRange(tmp.x-1, tmp.y+dy[hd])){
if(!visited[tmp.x-1][tmp.y+dy[hd]] && !isWall(tmp.x-1, tmp.y, 3) && !isWall(tmp.x-1, tmp.y+dy[hd], hd)){
visited[tmp.x-1][tmp.y+dy[hd]] = true;
if(0 < 4-Math.abs(hy-tmp.y+dy[hd])) {
map[tmp.x - 1][tmp.y + dy[hd]] += 4-Math.abs(hy-tmp.y+dy[hd]);
q.add(new Node(tmp.x - 1, tmp.y + dy[hd]));
}
}
}
if(isRange(tmp.x+1, tmp.y+dy[hd])){
if(!visited[tmp.x+1][tmp.y+dy[hd]] && !isWall(tmp.x+1, tmp.y, 4) && !isWall(tmp.x+1, tmp.y+dy[hd], hd)){
visited[tmp.x+1][tmp.y+dy[hd]] = true;
if(0 < 4-Math.abs(hy-tmp.y+dy[hd])) {
map[tmp.x + 1][tmp.y + dy[hd]] += 4-Math.abs(hy-tmp.y+dy[hd]);
q.add(new Node(tmp.x + 1, tmp.y + dy[hd]));
}
}
}
}
else{
if(isRange(tmp.x+dx[hd], tmp.y)){
if(!visited[tmp.x+dx[hd]][tmp.y] && !isWall(tmp.x+dx[hd], tmp.y, hd)) {
visited[tmp.x+dx[hd]][tmp.y] = true;
if(0 < 4-Math.abs(hx-tmp.x+dx[hd])) {
map[tmp.x + dx[hd]][tmp.y] += 4-Math.abs(hx-tmp.x+dx[hd]);
q.add(new Node(tmp.x + dx[hd], tmp.y));
}
}
}
if(isRange(tmp.x+dx[hd], tmp.y-1)){
if(!visited[tmp.x+dx[hd]][tmp.y-1] && !isWall(tmp.x, tmp.y-1, 2) && !isWall(tmp.x+dx[hd], tmp.y-1, hd)) {
visited[tmp.x+dx[hd]][tmp.y-1] = true;
if(0 < 4-Math.abs(hx-tmp.x+dx[hd])) {
map[tmp.x + dx[hd]][tmp.y - 1] += 4-Math.abs(hx-tmp.x+dx[hd]);
q.add(new Node(tmp.x + dx[hd], tmp.y - 1));
}
}
}
if(isRange(tmp.x+dx[hd], tmp.y+1)){
if(!visited[tmp.x+dx[hd]][tmp.y+1] && !isWall(tmp.x, tmp.y+1, 1) && !isWall(tmp.x+dx[hd], tmp.y+1, hd)) {
visited[tmp.x+dx[hd]][tmp.y+1] = true;
if(0 < 4-Math.abs(hx-tmp.x+dx[hd])) {
map[tmp.x + dx[hd]][tmp.y + 1] += 4-Math.abs(hx-tmp.x+dx[hd]);
q.add(new Node(tmp.x + dx[hd], tmp.y + 1));
}
}
}
}
}
}
}
static boolean isRange(int x, int y){
return 0 <= x && x < R && 0 <= y && y < C;
}
static boolean isWall(int x, int y, int d){
HashSet<Integer> set;
switch(d){
case 1:
if(rightBlock.containsKey(x)){
set = rightBlock.get(x);
if(set.contains(y)) return true;
}
break;
case 2:
if(leftBlock.containsKey(x)){
set = leftBlock.get(x);
if(set.contains(y)) return true;
}
break;
case 3:
if(upBlock.containsKey(x)){
set = upBlock.get(x);
if(set.contains(y)) return true;
}
break;
case 4:
if(downBlock.containsKey(x)){
set = downBlock.get(x);
if(set.contains(y)) return true;
}
break;
}
return false;
}
static void control(){
int[][] tmpArr = new int[R][C];
for(int i = 0; i < R; i++){
for(int j = 0; j < C; j++){
for(int k = 1; k <= 4; k++){
int nx = i + dx[k];
int ny = j + dy[k];
if(isRange(nx, ny) && !isWall(nx, ny, k)){
if(map[i][j] < map[nx][ny]){
tmpArr[i][j] += (map[nx][ny] - map[i][j])/4;
tmpArr[nx][ny] -= (map[nx][ny] - map[i][j])/4;
}
}
}
}
}
for(int i = 0; i < R; i++)
for(int j = 0; j < C; j++)
map[i][j] += tmpArr[i][j];
}
static void chilling(){
for(int i = 0; i < R; i++)
for(int j = 0; j < C; j++)
if(i == 0 || i == R-1 || j == 0 || j == C-1)
if(1 <= map[i][j]) map[i][j]-=1;
}
static boolean check(){
for(Node tmp : checkList)
if(map[tmp.x][tmp.y] < K) return false;
return true;
}
}
4. 한줄평
I pick myself up and get back in the race.
'etc. > BOJ' 카테고리의 다른 글
[BOJ] 9202 Boggle - Java (0) | 2024.12.10 |
---|---|
[BOJ] 14942 개미 🐜 - Java (1) | 2024.12.08 |
[BOJ] 5373 큐빙 - Java (0) | 2024.08.09 |
[BOJ] 2293 동전 1 - Java (0) | 2023.06.06 |
[BOJ] 21609 상어 중학교 - Java (0) | 2023.03.24 |