문제 링크
접근(1)
int cctvCases[5][4] =
{
{ 0, 0, 0, 1 },
{ 0, 0, 1, 1 },
{ 0, 0, 1, 1 },
{ 0, 1, 1, 1 },
{ 1, 1, 1, 1 },
};
{위, 아래, 왼, 오른}순(바라보는 방향)으로 boolean 배열 규칙을 임의로 지정하고 CCTV의 모든 방향을 손으로 적어 봤더니 규칙을 찾을 수 있었다.
예를 들면 CCTV 1은 {1, 0, 0, 0}, {0, 1, 0, 0}, {0, 0, 1, 0}, {0, 0, 0, 1}으로 4C1의 경우의 수와 같다고 생각했다.
마찬가지로 CCTV2, CCTV3, CCTV4, CCTV5도 4Cn이라고 가정하고 문제를 풀이하였다.
이 과정에 next_permutation을 사용하였다. (하지만 이게 독이 됐다...)
따라서 알고리즘을 다음과 같이 짤 수 있었다.
- CCTV의 위치를 따로 저장해 놓는다.
- k번째 CCTV를 순서대로 순회하며 백트래킹한다.
- k+1, k+2 ... CCTV까지 회전할 수 있는 모든 경우의 수를 조합한다.
- 이후 과정을 다 마치면 bfs를 통해 사각지대의 넓이를 구한다.
CCTV2와 3은 내가 가정했던 대로 4C2의 형태로 값을 구하지만 각각 구별해야 하므로 예외 케이스를 두어 2와 3 케이스를 구분했다.
// 2와 3 구별 위해
// 2의 경우의 수를 따로 지정
int exceptCase[2][4] =
{
{1, 1, 0, 0},
{0, 0, 1, 1},
};
코드
#include <iostream>
#include <algorithm>
#include <queue>
#include <cstring>
using namespace std;
int map[10][10];
vector<pair<int, int>> cctvs;
queue<pair<int, int>> q;
bool visited[10][10];
bool isUsed[10];
int N;
int M;
int ans = 987654321;
int dx[4] = { 0, 0, -1, 1 };
int dy[4] = { -1, 1, 0, 0 };
int cctvCases[5][4] =
{
{ 0, 0, 0, 1 },
{ 0, 0, 1, 1 },
{ 0, 0, 1, 1 },
{ 0, 1, 1, 1 },
{ 1, 1, 1, 1 },
};
// 2와 3 구별 위해
int exceptCase[2][4] =
{
{1, 1, 0, 0},
{0, 0, 1, 1},
};
void Capture(int k, int x, int y)
{
switch (k)
{
// Up
case 0:
for (int i = x - 1; i >= 0; i--)
{
if (map[i][y] == 6) break;
map[i][y] = '#';
}
break;
// Down
case 1:
for (int i = x + 1; i < N; i++)
{
if (map[i][y] == 6) break;
map[i][y] = '#';
}
break;
// Left
case 2:
for (int i = y - 1; i >= 0; i--)
{
if (map[x][i] == 6) break;
map[x][i] = '#';
}
break;
// Right
case 3:
for (int i = y + 1; i < M; i++)
{
if (map[x][i] == 6) break;
map[x][i] = '#';
}
}
}
int bfs(int x, int y)
{
int cnt = 1;
q.push({ x, y });
visited[x][y] = true;
while (!q.empty())
{
int cx = q.front().first;
int cy = q.front().second;
q.pop();
for (int i = 0; i < 4; i++)
{
int nx = cx + dx[i];
int ny = cy + dy[i];
if (nx >= N || nx < 0 || ny >= M || ny < 0) continue;
if (visited[nx][ny] || map[nx][ny] != 0) continue;
q.push({ nx, ny });
visited[nx][ny] = true;
cnt++;
}
}
return cnt;
}
int Solve()
{
int ret = 0;
for (int i = 0; i < N; i++)
{
for (int j = 0; j < M; j++)
{
if (visited[i][j] || map[i][j] != 0)
continue;
ret += bfs(i, j);
}
}
memset(visited, 0, sizeof(visited));
return ret;
}
// k번째 cctv 탐색
void SearchAndRotate(int n)
{
if (n == cctvs.size())
{
ans = min(Solve(), ans);
return;
}
int x = cctvs[n].first;
int y = cctvs[n].second;
int curCctvCase[4];
memcpy(curCctvCase, cctvCases[map[x][y] - 1], sizeof(curCctvCase));
int curMap[10][10];
memcpy(curMap, map, sizeof(map));
do
{
bool flag = (equal(curCctvCase, curCctvCase + 4, exceptCase[0])
|| equal(curCctvCase, curCctvCase + 4, exceptCase[1]));
if (map[x][y] == 2 && !flag)
continue;
if (map[x][y] == 3 && flag)
continue;
for (int k = 0; k < 4; k++)
{
if (curCctvCase[k] == 0) continue;
Capture(k, x, y);
}
SearchAndRotate(n + 1);
memcpy(map, curMap, sizeof(map));
} while (next_permutation(curCctvCase, curCctvCase + 4));
}
int main()
{
ios::sync_with_stdio(false);
std::cin.tie(0);
std::cout.tie(0);
cin >> N >> M;
for (int i = 0; i < N; i++)
{
for (int j = 0; j < M; j++)
{
cin >> map[i][j];
if (map[i][j] > 0 && map[i][j] < 6)
cctvs.push_back({ i, j });
}
}
SearchAndRotate(0);
cout << ans;
return 0;
}
결과
두둥탁...
시간 초과가 나왔다.
질문 게시판을 찾아봤지만 나와 비슷한 풀이가 보이지 않아 gpt에게 물어보며 코드를 수정했다.
문제점(1) - memcpy의 남발이 문제인가?
memcpy는 전체 배열을 복사하는 함수이다.
일반적으로 NxM 크기의 배열을 복사하는 연산은 O(N*M)의 시간 복잡도를 가진다.
N, M은 8 이하로 매우 작지만 백트래킹이 계속 이어질 때, memcpy도 같이 남발하면 시간 복잡도에 영향을 줄 것 같아 이 부분을 수정해 보았다.
수정된 코드(1)
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>
#include <cstring>
using namespace std;
int map[10][10];
bool isUsed[10];
int N, M;
int ans = 987654321;
vector<pair<int, int>> cctvs;
queue<pair<int, int>> q;
bool visited[10][10];
int dx[4] = { 0, 0, -1, 1 };
int dy[4] = { -1, 1, 0, 0 };
int cctvCases[5][4] =
{
{ 0, 0, 0, 1 },
{ 0, 0, 1, 1 },
{ 0, 0, 1, 1 },
{ 0, 1, 1, 1 },
{ 1, 1, 1, 1 },
};
int exceptCase[2][4] =
{
{1, 1, 0, 0},
{0, 0, 1, 1},
};
int bfs(int x, int y)
{
int cnt = 1;
q.push({ x, y });
visited[x][y] = true;
while (!q.empty())
{
int cx = q.front().first;
int cy = q.front().second;
q.pop();
for (int i = 0; i < 4; i++)
{
int nx = cx + dx[i];
int ny = cy + dy[i];
if (nx >= N || nx < 0 || ny >= M || ny < 0) continue;
if (visited[nx][ny] || map[nx][ny] != 0) continue;
q.push({ nx, ny });
visited[nx][ny] = true;
cnt++;
}
}
return cnt;
}
int Solve()
{
int ret = 0;
for (int i = 0; i < N; i++)
{
for (int j = 0; j < M; j++)
{
if (visited[i][j] || map[i][j] != 0)
continue;
ret += bfs(i, j);
}
}
memset(visited, 0, sizeof(visited));
return ret;
}
// 변경된 셀만 기록하여 복원할 구조체
struct Change {
int x, y;
int prev;
};
// 변경된 좌표와 기존 값을 기록
vector<Change> CaptureWithRecord(int dir, int x, int y)
{
vector<Change> changes;
switch (dir)
{
case 0: // Up
for (int i = x - 1; i >= 0; i--)
{
if (map[i][y] == 6) break;
if (map[i][y] == 0)
{
changes.push_back({ i, y, map[i][y] });
map[i][y] = '#';
}
}
break;
case 1: // Down
for (int i = x + 1; i < N; i++)
{
if (map[i][y] == 6) break;
if (map[i][y] == 0)
{
changes.push_back({ i, y, map[i][y] });
map[i][y] = '#';
}
}
break;
case 2: // Left
for (int j = y - 1; j >= 0; j--)
{
if (map[x][j] == 6) break;
if (map[x][j] == 0)
{
changes.push_back({ x, j, map[x][j] });
map[x][j] = '#';
}
}
break;
case 3: // Right
for (int j = y + 1; j < M; j++)
{
if (map[x][j] == 6) break;
if (map[x][j] == 0)
{
changes.push_back({ x, j, map[x][j] });
map[x][j] = '#';
}
}
break;
}
return changes;
}
// 변경 기록을 되돌림
void UndoChanges(const vector<Change>& changes)
{
for (const auto& ch : changes)
map[ch.x][ch.y] = ch.prev;
}
// n번째 CCTV를 처리
void SearchAndRotate(int n)
{
if (n == cctvs.size())
{
ans = min(Solve(), ans);
return;
}
int x = cctvs[n].first;
int y = cctvs[n].second;
int curCctvCase[4];
memcpy(curCctvCase, cctvCases[map[x][y] - 1], sizeof(curCctvCase));
do
{
bool flag = (equal(curCctvCase, curCctvCase + 4, exceptCase[0])
|| equal(curCctvCase, curCctvCase + 4, exceptCase[1]));
if (map[x][y] == 2 && !flag)
continue;
if (map[x][y] == 3 && flag)
continue;
vector<Change> totalChanges;
for (int d = 0; d < 4; d++)
{
if (curCctvCase[d] == 0) continue;
vector<Change> changes = CaptureWithRecord(d, x, y);
totalChanges.insert(totalChanges.end(), changes.begin(), changes.end());
}
SearchAndRotate(n + 1);
UndoChanges(totalChanges);
} while (next_permutation(curCctvCase, curCctvCase + 4));
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> N >> M;
for (int i = 0; i < N; i++)
{
for (int j = 0; j < M; j++)
{
cin >> map[i][j];
if (map[i][j] > 0 && map[i][j] < 6)
cctvs.push_back({ i, j });
}
}
SearchAndRotate(0);
cout << ans;
return 0;
}
수정된 부분
// 변경된 셀만 기록하여 복원할 구조체
struct Change {
int x, y;
int prev;
};
// 변경된 좌표와 기존 값을 기록
vector<Change> CaptureWithRecord(int dir, int x, int y)
{
vector<Change> changes;
switch (dir)
{
case 0: // Up
for (int i = x - 1; i >= 0; i--)
{
if (map[i][y] == 6) break;
if (map[i][y] == 0)
{
changes.push_back({ i, y, map[i][y] });
map[i][y] = '#';
}
}
break;
case 1: // Down
for (int i = x + 1; i < N; i++)
{
if (map[i][y] == 6) break;
if (map[i][y] == 0)
{
changes.push_back({ i, y, map[i][y] });
map[i][y] = '#';
}
}
break;
case 2: // Left
for (int j = y - 1; j >= 0; j--)
{
if (map[x][j] == 6) break;
if (map[x][j] == 0)
{
changes.push_back({ x, j, map[x][j] });
map[x][j] = '#';
}
}
break;
case 3: // Right
for (int j = y + 1; j < M; j++)
{
if (map[x][j] == 6) break;
if (map[x][j] == 0)
{
changes.push_back({ x, j, map[x][j] });
map[x][j] = '#';
}
}
break;
}
return changes;
}
// 변경 기록을 되돌림
void UndoChanges(const vector<Change>& changes)
{
for (const auto& ch : changes)
map[ch.x][ch.y] = ch.prev;
}
기존 memcpy를 사용해서 배열을 통째로 복사하여 복구하는 것보다, 바뀐 좌표만 다른 곳에 원래 값과 같이 저장하고, 나중에 이를 이용하여 복구하는 것이 조금 더 낫다 판단하여 위와 같이 수정하였다.
결과
맞긴 했다. 하지만 채점 현황에서 다른 분들(C++ user)의 걸린 시간과 비교해 봤더니... 내 풀이의 걸린 시간이 여전히 마음에 들지 않았다.
또 시간 복잡도를 줄일 수 있는 방법은 뭐가 있을까?
문제점(2) - next_permutation이 과연 이 문제에서 효율적인가?
사실 얼마 전에 next_permutation에 대한 효율적인 문제 풀이 방법을 보아 괜히 써먹고 싶어 이 문제에 적용했다.
그러나 next_permutation은 이 문제에서 효율적이지 못했다.
이유는 다음과 같다.
위 문제에서 next_permutation은 4개의 원소에 대해 최대 24가지의 순열을 생성한다.
하지만 실제로 각 CCTV 유형마다 필요한 회전 경우의 수는 종류에 따라 1, 2 또는 4가지로 제한된다.
즉, 불필요한 중복 순열을 생성해 불필요한 분기를 늘리고, 그에 따른 상태 복사와 복원 비용도 함께 증가시킨다.
내가 아무리 예외 처리를 해서 중복된 순열의 일부를 건너뛰더라도, next_permutation은 여전히 4개의 원소에 대해 가능한 모든 순열(최악의 경우 24가지)을 모두 생성한다.
결론적으로,
- next_permutation은 모든 순열을 반복한다.
- 각 순열마다 백트래킹 호출과 맵 복사 혹은 상태 복원과 같은 추가 연산이 이루어지므로 전체 CCTV의 수가 많아지면 분기수가 지수적으로 늘어나게 되어 시간 초과가 발생할 수 있다.
- 중복 순열을 걸러내더라도 그 필터링은 상수 정도의 감소만 가져올 뿐, 근본적인 부분을 해결하지는 못한다.
따라서 next_permutation을 활용하지 않는 풀이법을 생각해 보았다.
수정된 코드(2)
#include <iostream>
#include <algorithm>
#include <queue>
#include <vector>
#include <cstring>
using namespace std;
int map[10][10];
vector<pair<int, int>> cctvs;
int N, M;
int ans = 987654321;
queue<pair<int, int>> q;
bool visited[10][10];
int dx[4] = { 0, 0, -1, 1 };
int dy[4] = { -1, 1, 0, 0 };
struct Change {
int x, y;
int prev;
};
vector<Change> CaptureWithRecord(int dir, int x, int y) {
vector<Change> changes;
switch (dir) {
case 0: // Up
for (int i = x - 1; i >= 0; i--) {
if (map[i][y] == 6) break;
if (map[i][y] == 0) {
changes.push_back({ i, y, map[i][y] });
map[i][y] = '#';
}
}
break;
case 1: // Down
for (int i = x + 1; i < N; i++) {
if (map[i][y] == 6) break;
if (map[i][y] == 0) {
changes.push_back({ i, y, map[i][y] });
map[i][y] = '#';
}
}
break;
case 2: // Left
for (int j = y - 1; j >= 0; j--) {
if (map[x][j] == 6) break;
if (map[x][j] == 0) {
changes.push_back({ x, j, map[x][j] });
map[x][j] = '#';
}
}
break;
case 3: // Right
for (int j = y + 1; j < M; j++) {
if (map[x][j] == 6) break;
if (map[x][j] == 0) {
changes.push_back({ x, j, map[x][j] });
map[x][j] = '#';
}
}
break;
}
return changes;
}
void UndoChanges(const vector<Change>& changes) {
for (const auto& ch : changes)
map[ch.x][ch.y] = ch.prev;
}
// 계산은 0의 개수를 세는 방식으로 간단히 처리 (0이 많을수록 블라인드 스팟)
int Solve() {
int cnt = 0;
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
if (map[i][j] == 0)
cnt++;
return cnt;
}
// 각 CCTV 유형별 회전 경우 (방향은 0: Up, 1: Down, 2: Left, 3: Right)
vector<vector<vector<int>>> rotations(6);
void initRotations() {
rotations[1] = { {0}, {1}, {2}, {3} };
rotations[2] = { {0,1}, {2,3} };
rotations[3] = { {0,3}, {3,1}, {1,2}, {2,0} };
rotations[4] = { {2,0,3}, {0,3,1}, {3,1,2}, {1,2,0} };
rotations[5] = { {0,1,2,3} };
}
// n번째 CCTV의 회전 방향을 선택하는 백트래킹 함수
void SearchAndRotate(int n) {
if (n == cctvs.size()) {
ans = min(ans, Solve());
return;
}
int x = cctvs[n].first;
int y = cctvs[n].second;
int type = map[x][y]; // CCTV의 종류 (1~5)
// 미리 정의한 회전 경우들을 순회
for (auto& dirs : rotations[type]) {
vector<Change> totalChanges;
// 각 방향에 대해 변경된 셀만 기록하며 적용
for (int d : dirs) {
vector<Change> changes = CaptureWithRecord(d, x, y);
totalChanges.insert(totalChanges.end(), changes.begin(), changes.end());
}
SearchAndRotate(n + 1);
UndoChanges(totalChanges);
}
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin >> N >> M;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
cin >> map[i][j];
if (map[i][j] > 0 && map[i][j] < 6)
cctvs.push_back({ i, j });
}
}
rotations.resize(6);
initRotations();
SearchAndRotate(0);
cout << ans;
return 0;
}
수정된 부분
각 CCTV가 회전할 수 있는 경우의 수를 그냥 배열로 지정해 두었다.
// 각 CCTV 유형별 회전 경우 (방향은 0: Up, 1: Down, 2: Left, 3: Right)
vector<vector<vector<int>>> rotations(6);
void initRotations() {
rotations[1] = { {0}, {1}, {2}, {3} };
rotations[2] = { {0,1}, {2,3} };
rotations[3] = { {0,3}, {3,1}, {1,2}, {2,0} };
rotations[4] = { {2,0,3}, {0,3,1}, {3,1,2}, {1,2,0} };
rotations[5] = { {0,1,2,3} };
}
사각지대 계산도 bfs가 아닌 그냥 0인 곳을 세어나가는 식으로 간단하게 변경하였다.
// 계산은 0의 개수를 세는 방식으로 간단히 처리 (0이 많을수록 블라인드 스팟)
int Solve() {
int cnt = 0;
for (int i = 0; i < N; i++)
for (int j = 0; j < M; j++)
if (map[i][j] == 0)
cnt++;
return cnt;
}
결과
나름 시간 복잡도가 괜찮게 줄어든 모습이다.
바킹독 님의 풀이
[실전 알고리즘] 0x0D강 - 시뮬레이션
안녕하세요, 이번 차시에서는 시뮬레이션을 다룹니다. 사실 코딩테스트에서 시뮬레이션 유형이라는 표현을 많이 쓰긴 하는데 이 유형의 문제들이 공통적인 특징을 가지고 있지는 않습니다. BFS
blog.encrypted.gg
위에선 이 문제를 백트래킹을 사용하여 풀이하였다.
이 문제와 같이 변수들이 가질 수 있는 값이 여러개이고, 모든 조합을 다 확인해 보고 싶은데 변수들끼리는 서로 독립적일 땐 백트래킹 대신 더 쉬운 방법이 있다고 한다.
바로 진법을 이용한 방법이다.
진법으로 풀어보기
CCTV의 가능한 방향 종류가 4개이니 4진법을 쓰면 되고, 1번 카메라가 3개일 때, 0부터 63(4^3)까지의 수를 4진법으로 나타내면 000, 001, ... 332, 333까지 간다.
그리고 각각의 자리를 방향 3개에 대응시킨다.
중간의 39로 예를 들어보면 39는 4진수로 213이니까 방향을 (2, 1, 3)으로 둘 수 있다.
이렇게 하면 총 64가지의 모든 조합을 다 확인해 볼 수 있다.
그럼 0부터 63까지의 수를 4진수로 바꾸고 어떻게 각 자릿수를 얻을 수 있을까?
이건 10진수에서의 각 자릿수 문제를 얻는 것과 같은 방식으로 하면 된다. 이번에는 10진수가 아니라 4진수일 뿐이다.
의문
1, 3, 4번 CCTV는 가능한 방향이 4개인 건 맞는데, 2번 CCTV는 좌우 혹은 상하로 2개이고 5번 CCTV는 가능한 방향이 1개이다.
Q.
그래서 만약 2번 CCTV가 2개이고 5번 CCTV가 1개라면 가능한 방향 조합의 수는 더 적은데 왜 무조건 4^k를 계산하는 걸까?
A.
물론 위 상황 때문에 중복되는 계산이 발생한다. 하지만 중복된 계산이 있어도 어차피 시간 내로 여유롭게 통과되기 때문에 예외 처리를 줄여 편하게 짜기 위해 CCTV의 종류를 가리지 않고 4^k개를 다 볼 것이다.
앞에서 4진법을 이용해 각 CCTV의 방향을 잡았다면 그 상황에서 사각 지대의 개수를 세면 된다.
코드
// http://boj.kr/c961e6bf6107428caf200c11c964f9e1
#include <bits/stdc++.h>
using namespace std;
#define X first
#define Y second
int dx[4] = {1,0,-1,0};
int dy[4] = {0,1,0,-1}; // 남쪽, 동쪽, 북쪽, 서쪽 순서
int n, m;
int board1[10][10]; // 최초에 입력받은 board를 저장할 변수
int board2[10][10]; // 사각 지대의 개수를 세기 위해 사용할 변수
vector<pair<int,int> > cctv; // cctv의 좌표를 저장할 변수
bool OOB(int a, int b){ // Out Of Bounds 확인
return a < 0 || a >= n || b < 0 || b >= m;
}
// (x,y)에서 dir 방향으로 진행하면서 벽을 만날 때 까지 지나치는 모든 빈칸을 7로 바꿔버림
void upd(int x, int y, int dir){
dir %= 4;
while(1){
x += dx[dir];
y += dy[dir];
if(OOB(x,y) || board2[x][y] == 6) return; // 범위를 벗어났거나 벽을 만나면 함수를 탈출
if(board2[x][y] != 0) continue; // 해당 칸이 빈칸이 아닐 경우(=cctv가 있을 경우) 넘어감
board2[x][y] = 7; // 빈칸을 7로 덮음
}
}
int main(void) {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
int mn = 0; // 사각 지대의 최소 크기 (=답)
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
cin >> board1[i][j];
if(board1[i][j] != 0 && board1[i][j] != 6)
cctv.push_back({i,j});
if(board1[i][j] == 0) mn++;
}
}
// 1 << (2*cctv.size())는 4의 cctv.size()승을 의미.
for(int tmp = 0; tmp < (1<<(2*cctv.size())); tmp++){ // tmp를 4진법으로 뒀을 때 각 자리수를 cctv의 방향으로 생각할 것이다.
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
board2[i][j] = board1[i][j];
int brute = tmp;
for(int i = 0; i < cctv.size(); i++){
int dir = brute % 4;
brute /= 4;
int x = cctv[i].X;
int y = cctv[i].Y; // tie(x, y) = cctv[i];로 쓰면 1줄로 줄일 수 있음
if(board1[x][y] == 1){
upd(x,y,dir);
}
else if(board1[x][y] == 2){
upd(x,y,dir);
upd(x,y,dir+2);
}
else if(board1[x][y] == 3){
upd(x,y,dir);
upd(x,y,dir+1);
}
else if(board1[x][y] == 4){
upd(x,y,dir);
upd(x,y,dir+1);
upd(x,y,dir+2);
}
else{ // board1[x][y] == 5
upd(x,y,dir);
upd(x,y,dir+1);
upd(x,y,dir+2);
upd(x,y,dir+3);
}
}
int val = 0;
for(int i = 0; i < n; i++)
for(int j = 0; j < m; j++)
val += (board2[i][j]==0);
mn = min(mn, val);
}
cout << mn;
}
주의할 점 - 정수의 정수 승을 계산할 때 pow 쓰지 말기
cmath 헤더에 있는 pow 함수는 인자로 실수를 받고, 또 실수를 반환하는 함수이기 때문에 오차가 생길 수도 있다.
차라리 for문을 도는 게 낫다.
오차가 생긴 예
위 코드의 시간 복잡도
느낀 점
진법을 활용한 풀이는 생각조차 못했는데, 내 힘으로 다시 구현해서 내 것으로 만들어야겠다!
'알고리즘' 카테고리의 다른 글
[C++] Boj 18808 스티커 붙이기 (0) | 2025.03.05 |
---|---|
[C++] Boj 16987 계란으로 계란치기 (0) | 2025.02.17 |
[C++] Boj 1941 소문난 칠공주 (1) | 2025.02.16 |
[C++] Boj 1759 암호 만들기 (1) | 2025.02.07 |
[C++] Boj 15666 N과 M (12) (0) | 2025.02.06 |