# 삼성전자 DS 코딩테스트 종합 해결 전략 가이드
## 목차
1. 전체 코드 구조
2. 기본 알고리즘
2.1 BFS (너비 우선 탐색)
2.2 DFS (깊이 우선 탐색)
2.3 다익스트라 알고리즘
2.4 브루트포스 (완전탐색)
2.5 그리디 알고리즘
2.6 우선순위 큐
2.7 백트래킹
3. 특수 알고리즘
3.1 투 포인터
3.2 비트마스킹
3.3 분리 집합 (Union-Find)
3.4 회전 알고리즘
4. 시뮬레이션 로직
5. 자주 사용되는 테크닉
5.1 회전
5.2 순열, 중복 순열, 조합, 중복 조합
5.3 중력
5.4 달팽이(나선형) 배열
5.5 이동 중 벽에 부딪혀서 방향이 전환되는 경우
5.6 배열 한 칸씩 돌리기
5.7 부분 회전
5.8 토네이도 (나선형) 이동
5.9 격자 밖으로 나가는 경우 처리
5.10 2차원 누적 합 (2D Prefix Sum)
5.11 C++에서 배열을 함수 매개변수로 사용하기
## 1. 전체 코드 구조
삼성 DS 코딩테스트 문제를 해결하기 위한 전체적인 코드 구조는 다음과 같습니다:
```cpp
#include <iostream>
#include <vector>
#include <queue>
#include <stack>
#include <algorithm>
#include <cstring>
#include <climits>
#include <map>
#include <set>
#include <bitset>
using namespace std;
const int MAX_N = 100;
const int INF = INT_MAX;
const int dx[] = {-1, 1, 0, 0, -1, -1, 1, 1};
const int dy[] = {0, 0, -1, 1, -1, 1, -1, 1};
int N, M, K;
vector<vector<int>> board;
vector<vector<bool>> visited;
struct Entity {
int x, y, id, hp, attack;
// 추가 속성
};
vector<Entity> entities;
// 각 알고리즘 함수 선언
void simulate();
vector<vector<int>> bfs(int startX, int startY);
void dfs(int x, int y);
vector<int> dijkstra(int start);
void bruteForce();
void greedyAlgorithm();
void usePriorityQueue();
void backtracking(int depth);
void twoPointers();
void bitmaskingExample();
void initUnionFind();
int find(int x);
void unite(int a, int b);
void rotateMatrix(vector<vector<int>>& matrix);
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
// 입력 처리
cin >> N >> M >> K;
board.resize(N, vector<int>(M));
visited.resize(N, vector<bool>(M, false));
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
cin >> board[i][j];
}
}
// 엔티티 정보 입력 (예시)
for (int i = 0; i < K; i++) {
int x, y, hp, attack;
cin >> x >> y >> hp >> attack;
entities.push_back({x, y, i, hp, attack});
}
// 시뮬레이션 실행
simulate();
// 결과 출력
// ...
return 0;
}
// 각 알고리즘 함수 구현
// ...
```
이 코드 구조의 특징:
1. 필요한 모든 헤더 파일을 포함합니다.
2. 자주 사용되는 상수와 방향 배열을 정의합니다.
3. 문제에서 주어지는 주요 변수들을 전역 변수로 선언합니다.
4. 2D 벡터를 사용하여 게임 보드와 방문 여부를 표현합니다.
5. `Entity` 구조체를 정의하여 게임 내 객체를 표현합니다.
6. 각 알고리즘에 대한 함수를 선언합니다.
7. `main` 함수에서 입력 처리, 시뮬레이션 실행, 결과 출력의 기본 흐름을 제공합니다.
## 2. 기본 알고리즘
### 2.1 BFS (너비 우선 탐색)
BFS는 그래프나 트리 구조에서 노드를 탐색하는 알고리즘으로, 시작 노드에서 가까운 노드부터 차례대로 탐색합니다.
#### BFS의 핵심 로직:
1. 시작 노드를 큐에 넣고 방문 처리합니다.
2. 큐가 빌 때까지 다음 과정을 반복합니다:
a. 큐에서 노드를 꺼냅니다.
b. 꺼낸 노드의 인접한 노드 중 방문하지 않은 노드를 모두 큐에 넣고 방문 처리합니다.
#### BFS의 특징:
- 최단 경로 문제를 해결할 수 있습니다.
- 두 노드 사이의 최단 거리나 경로를 찾을 수 있습니다.
- 큐를 사용하여 구현합니다.
- 메모리를 많이 사용할 수 있습니다.
#### 예시 코드 (미로 탈출 최단 경로):
다음은 "미로 탈출" 문제를 BFS로 해결하는 예시 코드입니다:
```cpp
struct Point {
int x, y, dist;
};
vector<vector<int>> bfs(int startX, int startY, int exitX, int exitY) {
queue<Point> q;
vector<vector<int>> dist(N, vector<int>(M, -1));
q.push({startX, startY, 0});
dist[startX][startY] = 0;
while (!q.empty()) {
Point current = q.front();
q.pop();
if (current.x == exitX && current.y == exitY) {
return dist; // 출구에 도달
}
for (int i = 0; i < 4; i++) {
int nx = current.x + dx[i];
int ny = current.y + dy[i];
if (nx >= 0 && nx < N && ny >= 0 && ny < M &&
board[nx][ny] == 0 && dist[nx][ny] == -1) {
dist[nx][ny] = current.dist + 1;
q.push({nx, ny, current.dist + 1});
}
}
}
return dist; // 출구에 도달하지 못한 경우
}
```
#### 기출 문제 예시 (정령 이동):
"정령 이동" 문제에서 BFS를 활용하여 정령의 이동 경로를 찾는 코드:
```cpp
struct Spirit {
int x, y, direction;
};
vector<vector<int>> findSpiritPath(int startX, int startY) {
queue<Spirit> q;
vector<vector<int>> visited(N, vector<int>(N, -1));
q.push({startX, startY, 2}); // 2: 남쪽
visited[startX][startY] = 0;
while (!q.empty()) {
Spirit current = q.front();
q.pop();
// 남쪽으로 이동 시도
if (canMoveDown(current)) {
int nx = current.x + 1;
if (visited[nx][current.y] == -1) {
visited[nx][current.y] = visited[current.x][current.y] + 1;
q.push({nx, current.y, 2});
}
}
// 서쪽으로 회전 시도
else if (canRotateWest(current)) {
int ny = current.y - 1;
if (visited[current.x][ny] == -1) {
visited[current.x][ny] = visited[current.x][current.y] + 1;
q.push({current.x, ny, 3});
}
}
// 동쪽으로 회전 시도
else if (canRotateEast(current)) {
int ny = current.y + 1;
if (visited[current.x][ny] == -1) {
visited[current.x][ny] = visited[current.x][current.y] + 1;
q.push({current.x, ny, 1});
}
}
}
return visited;
}
```
이 코드는 정령의 이동 규칙을 BFS에 적용하여 모든 가능한 이동 경로를 탐색합니다.
### 2.2 DFS (깊이 우선 탐색)
DFS는 그래프의 깊은 부분을 우선적으로 탐색하는 알고리즘입니다. 시작 정점에서 다음 분기로 넘어가기 전에, 해당 분기를 완벽하게 탐색하는 방법입니다.
#### DFS의 핵심 로직:
1. 시작 노드를 스택에 넣고 방문 처리합니다.
2. 스택의 최상단 노드에 방문하지 않은 인접 노드가 있으면 그 노드를 스택에 넣고 방문 처리합니다. 방문하지 않은 인접 노드가 없으면 스택에서 최상단 노드를 꺼냅니다.
3. 2번 과정을 스택이 빌 때까지 반복합니다.
#### DFS의 특징:
- 모든 노드를 방문하고자 할 때 사용합니다.
- 백트래킹에 사용됩니다.
- 스택 또는 재귀 함수를 이용하여 구현합니다.
- 메모리를 적게 사용하지만, 해가 없는 경로를 탐색할 때 시간이 오래 걸릴 수 있습니다.
#### 예시 코드 (연결된 구역 찾기):
```cpp
int dfs(int x, int y, int& size) {
visited[x][y] = true;
size++;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 0 && nx < N && ny >= 0 && ny < M &&
!visited[nx][ny] && board[nx][ny] == board[x][y]) {
dfs(nx, ny, size);
}
}
return size;
}
int findLargestConnectedArea() {
int maxSize = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (!visited[i][j]) {
int size = 0;
maxSize = max(maxSize, dfs(i, j, size));
}
}
}
return maxSize;
}
```
#### 기출 문제 예시 (바닥 먼지 청소):
"바닥 먼지 청소" 문제에서 DFS를 활용하여 특정 구역의 먼지를 청소하는 코드:
```cpp
int cleanDust(int x, int y) {
if (x < 0 || x >= N || y < 0 || y >= N || board[x][y] == 0 || visited[x][y]) {
return 0;
}
visited[x][y] = true;
int dustAmount = board[x][y];
board[x][y] = 0;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
dustAmount += cleanDust(nx, ny);
}
return dustAmount;
}
int cleanAllDust() {
int totalDust = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (!visited[i][j] && board[i][j] > 0) {
totalDust += cleanDust(i, j);
}
}
}
return totalDust;
}
```
이 코드는 DFS를 사용하여 연결된 먼지 영역을 찾고 청소하며, 청소한 먼지의 총량을 계산합니다.
### 2.3 다익스트라 알고리즘
다익스트라 알고리즘은 그래프에서 한 정점에서 다른 모든 정점까지의 최단 경로를 찾는 알고리즘입니다. 음의 가중치가 없는 그래프에서 사용할 수 있습니다.
#### 다익스트라 알고리즘의 핵심 로직:
1. 시작 정점을 선택하고, 시작 정점에서 다른 모든 정점까지의 거리를 무한대로 초기화합니다.
2. 시작 정점의 거리를 0으로 설정하고, 방문하지 않은 정점 중 가장 거리가 짧은 정점을 선택합니다.
3. 선택한 정점을 방문 처리하고, 이 정점을 거쳐 다른 정점으로 가는 거리가 기존에 알려진 거리보다 짧으면 거리를 갱신합니다.
4. 모든 정점을 방문할 때까지 2-3 과정을 반복합니다.
#### 다익스트라 알고리즘의 특징:
- 그리디 알고리즘의 일종입니다.
- 최단 경로를 구하는 데 사용됩니다.
- 우선순위 큐를 사용하여 효율적으로 구현할 수 있습니다.
- 음의 가중치가 있는 그래프에서는 사용할 수 없습니다.
#### 예시 코드 (최소 비용 경로 찾기):
```cpp
struct Node {
int x, y, cost;
Node(int _x, int _y, int _cost) : x(_x), y(_y), cost(_cost) {}
};
struct compare {
bool operator()(const Node& a, const Node& b) {
return a.cost > b.cost;
}
};
int dijkstra() {
vector<vector<int>> dist(N, vector<int>(M, INF));
priority_queue<Node, vector<Node>, compare> pq;
dist[0][0] = board[0][0];
pq.push(Node(0, 0, board[0][0]));
while (!pq.empty()) {
int x = pq.top().x;
int y = pq.top().y;
int cost = pq.top().cost;
pq.pop();
if (x == N-1 && y == M-1) return cost;
if (cost > dist[x][y]) continue;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 0 && nx < N && ny >= 0 && ny < M) {
int newCost = cost + board[nx][ny];
if (newCost < dist[nx][ny]) {
dist[nx][ny] = newCost;
pq.push(Node(nx, ny, newCost));
}
}
}
}
return -1; // 도달할 수 없는 경우
}
```
#### 기출 문제 예시 (마법사 상어와 파이어스톰):
"마법사 상어와 파이어스톰" 문제에서 다익스트라 알고리즘을 활용하여 얼음이 녹는 최소 시간을 계산하는 코드:
```cpp
struct Ice {
int x, y, time;
Ice(int _x, int _y, int _t) : x(_x), y(_y), time(_t) {}
};
struct compare {
bool operator()(const Ice& a, const Ice& b) {
return a.time > b.time;
}
};
int meltIce() {
vector<vector<int>> meltTime(N, vector<int>(N, INF));
priority_queue<Ice, vector<Ice>, compare> pq;
// 초기 얼음 위치 추가
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (board[i][j] > 0) {
pq.push(Ice(i, j, 0));
meltTime[i][j] = 0;
}
}
}
int lastMeltTime = 0;
while (!pq.empty()) {
int x = pq.top().x;
int y = pq.top().y;
int time = pq.top().time;
pq.pop();
if (time > meltTime[x][y]) continue;
lastMeltTime = max(lastMeltTime, time);
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx >= 0 && nx < N && ny >= 0 && ny < N) {
int newTime = time + 1;
if (newTime < meltTime[nx][ny]) {
meltTime[nx][ny] = newTime;
pq.push(Ice(nx, ny, newTime));
}
}
}
}
return lastMeltTime;
}
```
이 코드는 다익스트라 알고리즘을 사용하여 각 얼음이 녹는 최소 시간을 계산하고, 모든 얼음이 녹는 데 걸리는 총 시간을 반환합니다.
### 2.4 브루트포스 (완전탐색)
브루트포스(Brute Force) 또는 완전탐색은 가능한 모든 경우의 수를 탐색하여 문제의 해를 찾는 방법입니다. 단순하지만 확실한 방법으로, 문제의 제약 조건이 작을 때 유용합니다.
#### 브루트포스의 핵심 로직:
1. 문제의 가능한 모든 경우의 수를 나열합니다.
2. 각 경우에 대해 문제의 조건을 만족하는지 확인합니다.
3. 조건을 만족하는 경우 중 가장 좋은 해를 선택합니다.
#### 브루트포스의 특징:
- 모든 경우를 고려하므로 항상 정확한 해를 찾을 수 있습니다.
- 문제의 크기가 작을 때 효과적입니다.
- 구현이 비교적 간단합니다.
- 문제의 크기가 커지면 시간 복잡도가 급격히 증가합니다.
#### 예시 코드 (최적의 포탑 위치 찾기):
```cpp
int calculateScore(int x, int y) {
int score = 0;
for (int dir = 0; dir < 8; dir++) {
int nx = x, ny = y;
int dist = 0;
while (true) {
nx += dx[dir];
ny += dy[dir];
dist++;
if (nx < 0 || nx >= N || ny < 0 || ny >= M) break;
if (board[nx][ny] == 1) {
score += dist;
break;
}
}
}
return score;
}
int findOptimalTurretPosition() {
int maxScore = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (board[i][j] == 0) { // 빈 칸인 경우
int score = calculateScore(i, j);
maxScore = max(maxScore, score);
}
}
}
return maxScore;
}
```
#### 기출 문제 예시 (마법사 상어와 복제):
"마법사 상어와 복제" 문제에서 브루트포스를 활용하여 상어의 최적 이동 경로를 찾는 코드:
```cpp
int maxFishCaught = 0;
vector<int> bestPath;
void findBestPath(int x, int y, int moves, int fishCaught, vector<int>& path) {
if (moves == 3) {
if (fishCaught > maxFishCaught) {
maxFishCaught = fishCaught;
bestPath = path;
}
return;
}
for (int dir = 0; dir < 4; dir++) {
int nx = x + dx[dir];
int ny = y + dy[dir];
if (nx >= 0 && nx < 4 && ny >= 0 && ny < 4) {
int caught = board[nx][ny];
board[nx][ny] = 0; // 물고기 잡기
path.push_back(dir);
findBestPath(nx, ny, moves + 1, fishCaught + caught, path);
board[nx][ny] = caught; // 물고기 되돌리기
path.pop_back();
}
}
}
vector<int> findOptimalSharkPath(int startX, int startY) {
vector<int> path;
findBestPath(startX, startY, 0, 0, path);
return bestPath;
}
```
이 코드는 상어의 모든 가능한 이동 경로를 탐색하여 가장 많은 물고기를 잡을 수 있는 경로를 찾습니다.
### 2.5 그리디 알고리즘
그리디 알고리즘은 각 단계에서 지역적으로 최적인 선택을 하는 방식으로 전체 문제의 최적해를 구하는 알고리즘입니다. 항상 최적해를 보장하지는 않지만, 계산 속도가 빠르고 구현이 간단합니다.
#### 그리디 알고리즘의 핵심 로직:
1. 현재 상황에서 가장 좋아 보이는 선택을 합니다.
2. 선택한 결과를 기반으로 문제를 축소합니다.
3. 축소된 문제에 대해 1-2 과정을 반복합니다.
#### 그리디 알고리즘의 특징:
- 각 단계에서 지역적으로 최적인 선택을 합니다.
- 일반적으로 빠른 실행 시간을 가집니다.
- 항상 최적해를 보장하지는 않습니다.
- 최적해를 구할 수 있는 경우, 증명이 필요합니다.
#### 예시 코드 (동전 거스름돈 문제):
```cpp
int greedyCoinChange(int amount, vector<int>& coins) {
sort(coins.rbegin(), coins.rend()); // 내림차순 정렬
int coinCount = 0;
for (int coin : coins) {
while (amount >= coin) {
amount -= coin;
coinCount++;
}
}
return coinCount;
}
```
#### 기출 문제 예시 (상어 중학교):
"상어 중학교" 문제에서 그리디 알고리즘을 활용하여 블록 그룹을 찾는 코드:
```cpp
struct BlockGroup {
int size;
int rainbowCount;
int standardX;
int standardY;
};
BlockGroup findLargestBlockGroup() {
BlockGroup largest = {0, 0, -1, -1};
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (board[i][j] > 0) { // 일반 블록
BlockGroup current = findGroup(i, j);
if (isLarger(current, largest)) {
largest = current;
}
}
}
}
return largest;
}
bool isLarger(const BlockGroup& a, const BlockGroup& b) {
if (a.size != b.size) return a.size > b.size;
if (a.rainbowCount != b.rainbowCount) return a.rainbowCount > b.rainbowCount;
if (a.standardX != b.standardX) return a.standardX > b.standardX;
return a.standardY > b.standardY;
}
BlockGroup findGroup(int x, int y) {
int color = board[x][y];
vector<vector<bool>> visited(N, vector<bool>(N, false));
queue<pair<int, int>> q;
BlockGroup group = {1, 0, x, y};
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 >= 0 && nx < N && ny >= 0 && ny < N && !visited[nx][ny]) {
if (board[nx][ny] == color || board[nx][ny] == 0) {
visited[nx][ny] = true;
q.push({nx, ny});
group.size++;
if (board[nx][ny] == 0) group.rainbowCount++;
else if (nx < group.standardX || (nx == group.standardX && ny < group.standardY)) {
group.standardX = nx;
group.standardY = ny;
}
}
}
}
}
return group;
}
```
이 코드는 그리디 방식으로 가장 큰 블록 그룹을 찾습니다. 각 단계에서 현재 가장 큰 그룹을 선택하는 방식으로 동작합니다.
### 2.6 우선순위 큐
우선순위 큐는 일반적인 큐와 달리 우선순위가 가장 높은 데이터가 먼저 나오는 자료구조입니다. 힙(Heap)을 이용하여 구현되며, 다익스트라 알고리즘, 프림 알고리즘 등 다양한 알고리즘에서 활용됩니다.
#### 우선순위 큐의 특징:
- 삽입과 삭제 연산의 시간 복잡도는 O(log N)입니다.
- 최대 힙 또는 최소 힙으로 구현할 수 있습니다.
- STL의 priority_queue를 사용하거나 직접 구현할 수 있습니다.
#### 예시 코드 (최소 힙):
```cpp
#include <queue>
#include <vector>
#include <functional>
// 최소 힙
priority_queue<int, vector<int>, greater<int>> minHeap;
// 삽입
minHeap.push(5);
minHeap.push(2);
minHeap.push(8);
// 최솟값 추출
while (!minHeap.empty()) {
cout << minHeap.top() << " ";
minHeap.pop();
}
// 출력: 2 5 8
```
#### 기출 문제 예시 (마법사 상어와 파이어볼):
"마법사 상어와 파이어볼" 문제에서 우선순위 큐를 활용하여 파이어볼을 관리하는 코드:
```cpp
struct Fireball {
int r, c, m, s, d;
Fireball(int r, int c, int m, int s, int d) : r(r), c(c), m(m), s(s), d(d) {}
};
struct CompareFireball {
bool operator()(const Fireball& a, const Fireball& b) {
if (a.r != b.r) return a.r > b.r;
if (a.c != b.c) return a.c > b.c;
return a.m < b.m;
}
};
priority_queue<Fireball, vector<Fireball>, CompareFireball> pq;
void moveFireballs() {
priority_queue<Fireball, vector<Fireball>, CompareFireball> nextPq;
while (!pq.empty()) {
Fireball fb = pq.top();
pq.pop();
int nr = (fb.r + dr[fb.d] * fb.s) % N;
int nc = (fb.c + dc[fb.d] * fb.s) % N;
if (nr < 0) nr += N;
if (nc < 0) nc += N;
nextPq.push(Fireball(nr, nc, fb.m, fb.s, fb.d));
}
pq = nextPq;
}
void combineFireballs() {
vector<vector<vector<Fireball>>> grid(N, vector<vector<Fireball>>(N));
while (!pq.empty()) {
Fireball fb = pq.top();
pq.pop();
grid[fb.r][fb.c].push_back(fb);
}
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (grid[i][j].size() >= 2) {
int totalM = 0, totalS = 0;
bool allEven = true, allOdd = true;
for (Fireball& fb : grid[i][j]) {
totalM += fb.m;
totalS += fb.s;
if (fb.d % 2 == 0) allOdd = false;
else allEven = false;
}
int newM = totalM / 5;
int newS = totalS / grid[i][j].size();
if (newM > 0) {
for (int d = 0; d < 4; d++) {
int newD = (allEven || allOdd) ? d * 2 : d * 2 + 1;
pq.push(Fireball(i, j, newM, newS, newD));
}
}
} else if (grid[i][j].size() == 1) {
pq.push(grid[i][j][0]);
}
}
}
}
```
이 코드는 우선순위 큐를 사용하여 파이어볼을 위치와 질량에 따라 정렬하고 관리합니다. 파이어볼의 이동과 합체를 효율적으로 처리할 수 있습니다.
### 2.7 백트래킹
백트래킹은 해를 찾는 도중 해가 아니어서 막히면, 되돌아가서 다시 해를 찾아가는 기법입니다. 주로 최적화 문제와 결정 문제를 해결하는 데 사용됩니다.
#### 백트래킹의 핵심 로직:
1. 현재 상태에서 가능한 모든 후보군을 생성합니다.
2. 각 후보를 체크하여 해결책으로 가능한지 검사합니다.
3. 가능하다면 해당 후보를 선택하고 다음 단계로 진행합니다.
4. 해결책이 아니라면 이전 단계로 돌아갑니다.
#### 백트래킹의 특징:
- 가지치기(Pruning)를 통해 불필요한 경우를 제거합니다.
- DFS를 기반으로 하지만, 가지치기를 통해 효율성을 높입니다.
- 주로 재귀 함수로 구현합니다.
#### 예시 코드 (N-Queens 문제):
```cpp
bool isSafe(vector<int>& board, int row, int col, int N) {
for (int i = 0; i < row; i++) {
if (board[i] == col ||
abs(board[i] - col) == abs(i - row)) {
return false;
}
}
return true;
}
bool solveNQueens(vector<int>& board, int row, int N) {
if (row == N) {
return true;
}
for (int col = 0; col < N; col++) {
if (isSafe(board, row, col, N)) {
board[row] = col;
if (solveNQueens(board, row + 1, N)) {
return true;
}
board[row] = -1; // Backtrack
}
}
return false;
}
```
#### 기출 문제 예시 (게리맨더링 2):
"게리맨더링 2" 문제에서 백트래킹을 활용하여 선거구를 나누는 코드:
```cpp
int minDifference = INT_MAX;
void divideDistricts(int x, int y, int d1, int d2) {
if (x + d1 + d2 >= N || y - d1 < 0 || y + d2 >= N) return;
vector<vector<int>> districts(N, vector<int>(N, 5));
vector<int> population(6, 0);
// 경계선 그리기
for (int i = 0; i <= d1; i++) {
districts[x + i][y - i] = 5;
districts[x + d2 + i][y + d2 - i] = 5;
}
for (int i = 0; i <= d2; i++) {
districts[x + i][y + i] = 5;
districts[x + d1 + i][y - d1 + i] = 5;
}
// 1번 선거구
for (int r = 0; r < x + d1; r++) {
for (int c = 0; c <= y; c++) {
if (districts[r][c] == 5) break;
districts[r][c] = 1;
}
}
// 2번 선거구
for (int r = 0; r <= x + d2; r++) {
for (int c = N - 1; c > y; c--) {
if (districts[r][c] == 5) break;
districts[r][c] = 2;
}
}
// 3번 선거구
for (int r = x + d1; r < N; r++) {
for (int c = 0; c < y - d1 + d2; c++) {
if (districts[r][c] == 5) break;
districts[r][c] = 3;
}
}
// 4번 선거구
for (int r = x + d2 + 1; r < N; r++) {
for (int c = N - 1; c >= y - d1 + d2; c--) {
if (districts[r][c] == 5) break;
districts[r][c] = 4;
}
}
// 인구 계산
for (int r = 0; r < N; r++) {
for (int c = 0; c < N; c++) {
population[districts[r][c]] += board[r][c];
}
}
int maxPop = *max_element(population.begin() + 1, population.end());
int minPop = *min_element(population.begin() + 1, population.end());
minDifference = min(minDifference, maxPop - minPop);
}
void backtrack() {
for (int x = 0; x < N - 2; x++) {
for (int y = 1; y < N - 1; y++) {
for (int d1 = 1; y - d1 >= 0; d1++) {
for (int d2 = 1; y + d2 < N && x + d1 + d2 < N; d2++) {
divideDistricts(x, y, d1, d2);
}
}
}
}
}
```
이 코드는 백트래킹을 사용하여 모든 가능한 선거구 분할을 탐색하고, 인구 차이가 가장 작은 경우를 찾습니다.
## 3. 특수 알고리즘
### 3.1 투 포인터
투 포인터 알고리즘은 두 개의 포인터를 이용해 배열이나 리스트를 순차적으로 탐색하는 알고리즘입니다. 주로 연속된 구간에 대한 문제를 해결할 때 사용됩니다.
#### 투 포인터의 핵심 로직:
1. 시작점과 끝점을 가리키는 두 개의 포인터를 사용합니다.
2. 문제의 조건에 따라 포인터를 이동시킵니다.
3. 포인터가 배열의 끝에 도달하면 알고리즘을 종료합니다.
#### 예시 코드 (부분합이 S인 연속된 수열 찾기):
```cpp
int twoPointers(vector<int>& arr, int S) {
int n = arr.size();
int start = 0, end = 0;
int sum = 0, minLen = INT_MAX;
while (end < n) {
while (sum < S && end < n) {
sum += arr[end++];
}
while (sum >= S && start < n) {
minLen = min(minLen, end - start);
sum -= arr[start++];
}
}
return minLen == INT_MAX ? 0 : minLen;
}
```
#### 기출 문제 예시 (어항 정리):
"어항 정리" 문제에서 투 포인터를 활용하여 물고기를 재분배하는 코드:
```cpp
void redistributeFish() {
int left = 0, right = N - 1;
while (left < right) {
int diff = abs(fishbowls[left] - fishbowls[right]);
int move = diff / 5;
if (fishbowls[left] < fishbowls[right]) {
fishbowls[left] += move;
fishbowls[right] -= move;
} else {
fishbowls[left] -= move;
fishbowls[right] += move;
}
left++;
right--;
}
}
```
이 코드는 두 개의 포인터를 사용하여 양 끝의 어항부터 물고기를 재분배합니다.
### 3.2 비트마스킹
비트마스킹은 정수의 이진수 표현을 자료구조로 사용하는 기법입니다. 집합을 표현하거나 상태를 나타내는 데 효율적으로 사용될 수 있습니다.
#### 비트마스킹의 특징:
- 메모리 사용량을 줄일 수 있습니다.
- 빠른 집합 연산이 가능합니다.
- 코드가 간결해질 수 있습니다.
#### 예시 코드 (부분집합 생성):
```cpp
void generateSubsets(int n) {
for (int i = 0; i < (1 << n); i++) {
cout << "Subset " << i << ": ";
for (int j = 0; j < n; j++) {
if (i & (1 << j)) {
cout << j << " ";
}
}
cout << endl;
}
}
```
#### 기출 문제 예시 (마법사 상어와 비바라기):
"마법사 상어와 비바라기" 문제에서 비트마스킹을 활용하여 구름의 상태를 관리하는 코드:
```cpp
const int MAX_N = 50;
int N, M;
int board[MAX_N][MAX_N];
int clouds = 0; // 비트마스크로 구름 상태 표현
void moveAndRain(int d, int s) {
int newClouds = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (clouds & (1 << (i * N + j))) {
int ni = (i + dy[d] * s + N * 50) % N;
int nj = (j + dx[d] * s + N * 50) % N;
newClouds |= (1 << (ni * N + nj));
board[ni][nj]++;
}
}
}
clouds = newClouds;
}
void waterCopy() {
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (clouds & (1 << (i * N + j))) {
int count = 0;
for (int k = 1; k < 8; k += 2) {
int ni = i + dy[k];
int nj = j + dx[k];
if (ni >= 0 && ni < N && nj >= 0 && nj < N && board[ni][nj] > 0) {
count++;
}
}
board[i][j] += count;
}
}
}
}
void createNewClouds() {
int newClouds = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
if (!(clouds & (1 << (i * N + j))) && board[i][j] >= 2) {
newClouds |= (1 << (i * N + j));
board[i][j] -= 2;
}
}
}
clouds = newClouds;
}
```
이 코드는 비트마스킹을 사용하여 구름의 상태를 효율적으로 관리합니다. 각 비트는 해당 위치에 구름이 있는지 여부를 나타냅니다.
### 3.3 분리 집합 (Union-Find)
분리 집합(Disjoint Set)은 서로 중복되지 않는 부분 집합들로 나눠진 원소들에 대한 정보를 저장하고 조작하는 자료구조입니다. Union-Find 알고리즘을 통해 구현됩니다.
#### 분리 집합의 핵심 연산:
1. Find: 어떤 원소가 주어졌을 때, 이 원소가 속한 집합을 찾습니다.
2. Union: 두 개의 집합을 하나로 합칩니다.
#### 예시 코드:
```cpp
vector<int> parent;
int find(int x) {
if (parent[x] == x) return x;
return parent[x] = find(parent[x]);
}
void unite(int a, int b) {
a = find(a);
b = find(b);
if (a != b) parent[b] = a;
}
// 초기화
void init(int n) {
parent.resize(n);
for (int i = 0; i < n; i++) parent[i] = i;
}
```
#### 기출 문제 예시 (나무 재테크):
"나무 재테크" 문제에서 분리 집합을 활용하여 나무들의 그룹을 관리하는 코드:
```cpp
struct Tree {
int x, y, age;
Tree(int x, int y, int age) : x(x), y(y), age(age) {}
};
vector<int> parent;
vector<Tree> trees;
int find(int x) {
if (parent[x] == x) return x;
return parent[x] = find(parent[x]);
}
void unite(int a, int b) {
a = find(a);
b = find(b);
if (a != b) parent[b] = a;
}
void springAndSummer() {
vector<int> deadTrees;
for (int i = 0; i < trees.size(); i++) {
int x = trees[i].x, y = trees[i].y;
if (nutrients[x][y] >= trees[i].age) {
nutrients[x][y] -= trees[i].age;
trees[i].age++;
// 나무가 자란 후 인접한 나무들과 그룹화
for (int d = 0; d < 8; d++) {
int nx = x + dx[d], ny = y + dy[d];
if (nx >= 0 && nx < N && ny >= 0 && ny < N) {
for (int j = 0; j < trees.size(); j++) {
if (trees[j].x == nx && trees[j].y == ny) {
unite(i, j);
break;
}
}
}
}
} else {
deadTrees.push_back(i);
}
}
// 여름: 죽은 나무 처리
for (int i : deadTrees) {
int x = trees[i].x, y = trees[i].y;
nutrients[x][y] += trees[i].age / 2;
}
// 죽은 나무 제거
for (int i = deadTrees.size() - 1; i >= 0; i--) {
trees.erase(trees.begin() + deadTrees[i]);
}
}
```
이 코드는 분리 집합을 사용하여 인접한 나무들을 그룹화하고, 각 그룹별로 나무의 성장과 번식을 관리합니다.
### 3.4 회전 알고리즘
회전 알고리즘은 2차원 배열이나 행렬을 회전시키는 알고리즘입니다. 주로 90도, 180도, 270도 회전이 많이 사용됩니다.
#### 회전 알고리즘의 핵심 로직:
1. 새로운 2차원 배열을 생성합니다.
2. 원본 배열의 각 요소를 새 배열의 적절한 위치로 이동시킵니다.
3. 필요한 경우 원본 배열을 새 배열로 교체합니다.
#### 예시 코드 (90도 시계 방향 회전):
```cpp
vector<vector<int>> rotate90Clockwise(vector<vector<int>>& matrix) {
int n = matrix.size();
vector<vector<int>> rotated(n, vector<int>(n));
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
rotated[j][n-1-i] = matrix[i][j];
}
}
return rotated;
}
```
#### 기출 문제 예시 (모노미노도미노):
"모노미노도미노" 문제에서 블록을 회전시키는 코드:
```cpp
void rotateBlock(vector<pair<int, int>>& block) {
int maxX = 0, maxY = 0;
for (auto& p : block) {
maxX = max(maxX, p.first);
maxY = max(maxY, p.second);
}
for (auto& p : block) {
int temp = p.first;
p.first = p.second;
p.second = maxX - temp;
}
}
void placeBlock(vector<pair<int, int>>& block, int board[10][4]) {
int lowestY = 10;
for (auto& p : block) {
for (int y = 0; y < 10; y++) {
if (y == 9 || board[y + 1][p.first] != 0) {
lowestY = min(lowestY, y - p.second);
break;
}
}
}
for (auto& p : block) {
board[lowestY + p.second][p.first] = 1;
}
}
int playGame(vector<vector<pair<int, int>>>& blocks) {
int score = 0;
int board[10][4] = {0};
for (auto& block : blocks) {
placeBlock(block, board);
// 행이 가득 찼는지 확인
for (int y = 9; y >= 0; y--) {
if (board[y][0] && board[y][1] && board[y][2] && board[y][3]) {
score++;
for (int ny = y; ny > 0; ny--) {
for (int x = 0; x < 4; x++) {
board[ny][x] = board[ny-1][x];
}
}
for (int x = 0; x < 4; x++) board[0][x] = 0;
y++; // 한 행을 제거했으므로 같은 y를 다시 검사
}
}
// 상위 두 행 확인
int overflow = 0;
for (int y = 0; y < 2; y++) {
for (int x = 0; x < 4; x++) {
if (board[y][x]) {
overflow++;
break;
}
}
}
// 넘친 만큼 아래로 밀기
for (int y = 9; y >= 2; y--) {
for (int x = 0; x < 4; x++) {
board[y][x] = board[y - overflow][x];
}
}
for (int y = 0; y < 2; y++) {
for (int x = 0; x < 4; x++) {
board[y][x] = 0;
}
}
}
return score;
}
```
이 코드는 블록을 회전시키고 게임 보드에 배치하는 과정을 구현합니다. 회전 알고리즘을 사용하여 블록의 방향을 변경하고, 행이 가득 찼을 때 점수를 얻고 행을 제거하는 로직을 포함합니다.
## 4. 시뮬레이션 로직
시뮬레이션은 주어진 규칙에 따라 상태를 변화시키며 문제를 해결하는 방법입니다. 삼성 DS 코딩테스트에서 매우 중요한 부분입니다.
#### 시뮬레이션의 핵심 로직:
1. 초기 상태를 설정합니다.
2. 주어진 규칙에 따라 상태를 변화시킵니다.
3. 종료 조건을 확인합니다.
4. 필요한 경우 결과를 계산하거나 출력합니다.
#### 예시 코드 (간단한 생명 게임 시뮬레이션):
```cpp
vector<vector<int>> simulateLifeGame(vector<vector<int>>& board) {
int n = board.size(), m = board[0].size();
vector<vector<int>> newBoard(n, vector<int>(m));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
int liveNeighbors = countLiveNeighbors(board, i, j);
if (board[i][j] == 1) {
newBoard[i][j] = (liveNeighbors == 2 || liveNeighbors == 3) ? 1 : 0;
} else {
newBoard[i][j] = (liveNeighbors == 3) ? 1 : 0;
}
}
}
return newBoard;
}
int countLiveNeighbors(vector<vector<int>>& board, int x, int y) {
int count = 0;
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
if (i == 0 && j == 0) continue;
int nx = x + i, ny = y + j;
if (nx >= 0 && nx < board.size() && ny >= 0 && ny < board[0].size()) {
count += board[nx][ny];
}
}
}
return count;
}
```
#### 기출 문제 예시 (아기 상어):
"아기 상어" 문제에서 시뮬레이션을 활용한 코드:
```cpp
struct Shark {
int x, y, size, eat;
};
int N;
vector<vector<int>> board;
Shark shark;
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, -1, 0, 1};
int bfs() {
vector<vector<int>> dist(N, vector<int>(N, -1));
queue<pair<int, int>> q;
vector<pair<int, int>> fishes;
int minDist = INT_MAX;
q.push({shark.x, shark.y});
dist[shark.x][shark.y] = 0;
while (!q.empty()) {
int x = q.front().first;
int y = q.front().second;
q.pop();
if (dist[x][y] > minDist) break;
for (int i = 0; i < 4; i++) {
int nx = x + dx[i];
int ny = y + dy[i];
if (nx < 0 || nx >= N || ny < 0 || ny >= N) continue;
if (dist[nx][ny] != -1 || board[nx][ny] > shark.size) continue;
dist[nx][ny] = dist[x][y] + 1;
q.push({nx, ny});
if (board[nx][ny] > 0 && board[nx][ny] < shark.size) {
if (dist[nx][ny] < minDist) {
minDist = dist[nx][ny];
fishes.clear();
fishes.push_back({nx, ny});
} else if (dist[nx][ny] == minDist) {
fishes.push_back({nx, ny});
}
}
}
}
if (fishes.empty()) return 0;
sort(fishes.begin(), fishes.end());
int fx = fishes[0].first, fy = fishes[0].second;
shark.x = fx;
shark.y = fy;
shark.eat++;
if (shark.eat == shark.size) {
shark.size++;
shark.eat = 0;
}
board[fx][fy] = 0;
return minDist;
}
int simulate() {
int time = 0;
while (true) {
int moveTime = bfs();
if (moveTime == 0) break;
time += moveTime;
}
return time;
}
```
이 코드는 아기 상어의 움직임을 시뮬레이션합니다. BFS를 사용하여 가장 가까운 먹을 수 있는 물고기를 찾고, 상어를 이동시키며 크기를 키우는 과정을 구현합니다.
## 5. 자주 사용되는 테크닉
### 5.1 회전
회전은 2차원 배열을 다룰 때 자주 사용되는 테크닉입니다. ZIP 함수를 활용하거나 인덱스 규칙을 찾아 구현할 수 있습니다.
#### 5.1.1 ZIP을 활용한 회전 (Python):
```python
arr = [[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]]
# 시계 방향 90도 (= 반시계 방향 270도)
arr_90 = list(map(list, zip(*arr[::-1])))
print(arr_90)
# 시계 방향 180도 (= 반시계 방향 180도)
arr_180 = [a[::-1] for a in arr[::-1]]
print(arr_180)
# 시계 방향 270도 (= 반시계 방향 90도)
arr_270 = [x[::-1] for x in list(map(list, zip(*arr[::-1])))[::-1]]
print(arr_270)
```
이 방법의 장점:
- 생각하기 쉽고 빠르게 구현할 수 있습니다.
- 정사각형이 아닌 직사각형의 경우에도 배열의 인덱스 고민 없이 빠르게 처리할 수 있습니다.
단점:
- 메모리나 시간 복잡도 면에서는 인덱스 규칙을 사용한 방법보다 좋지 않을 수 있습니다.
#### 5.1.2 인덱스 규칙을 이용한 회전 (C++):
```cpp
vector<vector<int>> rotate90(vector<vector<int>>& arr) {
int n = arr.size();
int m = arr[0].size();
vector<vector<int>> result(m, vector<int>(n));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
result[j][n-1-i] = arr[i][j];
}
}
return result;
}
vector<vector<int>> rotate180(vector<vector<int>>& arr) {
int n = arr.size();
int m = arr[0].size();
vector<vector<int>> result(n, vector<int>(m));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
result[n-1-i][m-1-j] = arr[i][j];
}
}
return result;
}
vector<vector<int>> rotate270(vector<vector<int>>& arr) {
int n = arr.size();
int m = arr[0].size();
vector<vector<int>> result(m, vector<int>(n));
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
result[m-1-j][i] = arr[i][j];
}
}
return result;
}
```
이 방법의 장점:
- 메모리와 시간 복잡도 면에서 효율적입니다.
- 직사각형 배열에 대해서도 쉽게 적용할 수 있습니다.
주의할 점:
- 회전 후 배열의 크기가 변할 수 있으므로, 결과 배열의 크기를 올바르게 설정해야 합니다.
### 5.2 순열, 중복 순열, 조합, 중복 조합
순열과 조합은 백트래킹을 이용해 구현할 수 있습니다. 삼성 코딩테스트에서는 itertools 라이브러리 사용이 불가능하므로, 직접 구현해야 합니다.
#### 5.2.1 순열 (Permutations):
```cpp
vector<int> arr = {1, 2, 3, 4};
vector<bool> visited(arr.size(), false);
void permutations(int n, vector<int>& current) {
if (current.size() == n) {
// 순열 완성, 처리 로직
return;
}
for (int i = 0; i < arr.size(); i++) {
if (!visited[i]) {
visited[i] = true;
current.push_back(arr[i]);
permutations(n, current);
current.pop_back();
visited[i] = false;
}
}
}
// 사용 예: vector<int> current; permutations(2, current);
```
특징:
- 순서를 고려하며, 중복을 허용하지 않습니다.
- visited 배열을 사용하여 중복 선택을 방지합니다.
#### 5.2.2 중복 순열 (Product):
```cpp
void product(int n, vector<int>& current) {
if (current.size() == n) {
// 중복 순열 완성, 처리 로직
return;
}
for (int i = 0; i < arr.size(); i++) {
current.push_back(arr[i]);
product(n, current);
current.pop_back();
}
}
// 사용 예: vector<int> current; product(2, current);
```
특징:
- 순서를 고려하며, 중복을 허용합니다.
- visited 배열을 사용하지 않아 같은 원소를 여러 번 선택할 수 있습니다.
#### 5.2.3 조합 (Combinations):
```cpp
void combinations(int n, vector<int>& current, int start) {
if (current.size() == n) {
// 조합 완성, 처리 로직
return;
}
for (int i = start; i < arr.size(); i++) {
current.push_back(arr[i]);
combinations(n, current, i + 1);
current.pop_back();
}
}
// 사용 예: vector<int> current; combinations(2, current, 0);
```
특징:
- 순서를 고려하지 않으며, 중복을 허용하지 않습니다.
- start 인덱스를 사용하여 이전에 선택한 원소 이후의 원소만 고려합니다.
#### 5.2.4 중복 조합 (Combinations with replacement):
```cpp
void combinationsWithReplacement(int n, vector<int>& current, int start) {
if (current.size() == n) {
// 중복 조합 완성, 처리 로직
return;
}
for (int i = start; i < arr.size(); i++) {
current.push_back(arr[i]);
combinationsWithReplacement(n, current, i); // 다음 재귀에서도 현재 인덱스부터 시작
current.pop_back();
}
}
// 사용 예: vector<int> current; combinationsWithReplacement(2, current, 0);
```
특징:
- 순서를 고려하지 않으며, 중복을 허용합니다.
- 현재 인덱스부터 다시 시작하여 같은 원소를 여러 번 선택할 수 있습니다.
### 5.3 중력
중력 효과를 구현할 때는 아래에서부터 위로 올라가며 처리하는 것이 효율적입니다.
```cpp
void applyGravity(vector<vector<int>>& board) {
int n = board.size();
int m = board[0].size();
for (int j = 0; j < m; j++) {
int bottom = n - 1;
for (int i = n - 1; i >= 0; i--) {
if (board[i][j] != 0) {
swap(board[bottom][j], board[i][j]);
bottom--;
}
}
}
}
```
특징:
- 각 열에 대해 아래에서부터 위로 올라가며 처리합니다.
- 0이 아닌 값을 만나면 가장 아래쪽으로 이동시킵니다.
- 이 방식은 "떨어지는" 효과를 효율적으로 구현합니다.
### 5.4 달팽이(나선형) 배열
달팽이 배열은 2차원 배열을 나선형으로 순회하는 패턴입니다.
```cpp
vector<vector<int>> createSpiralArray(int n) {
vector<vector<int>> arr(n, vector<int>(n, 0));
int value = 1;
int top = 0, bottom = n - 1, left = 0, right = n - 1;
while (value <= n * n) {
for (int i = left; i <= right; i++)
arr[top][i] = value++;
top++;
for (int i = top; i <= bottom; i++)
arr[i][right] = value++;
right--;
if (top <= bottom) {
for (int i = right; i >= left; i--)
arr[bottom][i] = value++;
bottom--;
}
if (left <= right) {
for (int i = bottom; i >= top; i--)
arr[i][left] = value++;
left++;
}
}
return arr;
}
```
특징:
- 4개의 변수(top, bottom, left, right)를 사용하여 현재 채워야 할 영역의 경계를 관리합니다.
- 각 반복에서 한 변을 채우고 경계를 조정합니다.
- 안쪽으로 좁혀가며 모든 칸을 채웁니다.
### 5.5 이동 중 벽에 부딪혀서 방향이 전환되는 경우
물체가 이동하다 벽에 부딪혀 방향을 바꾸는 경우, 다음과 같이 구현할 수 있습니다:
```cpp
void move(int& x, int& y, int& dx, int& dy, int n, int m) {
x += dx;
y += dy;
if (x < 0 || x >= n || y < 0 || y >= m) {
// 벽에 부딪힌 경우
x -= dx;
y -= dy;
dx = -dx;
dy = -dy;
}
}
// 사용 예
int x = 0, y = 0;
int dx = 1, dy = 1; // 초기 이동 방향
int n = 10, m = 10; // 보드의 크기
for (int i = 0; i < 20; i++) {
move(x, y, dx, dy, n, m);
cout << "Position: (" << x << ", " << y << ")" << endl;
}
```
특징:
- 물체의 새 위치를 계산한 후, 벽과의 충돌 여부를 확인합니다.
- 충돌 시 이전 위치로 되돌리고 방향을 반대로 바꿉니다.
- 이 방식은 "튕기는" 효과를 간단히 구현할 수 있습니다.
### 5.6 배열 한 칸씩 돌리기
배열을 한 칸씩 돌리는 경우, 다음과 같이 구현할 수 있습니다:
```cpp
void rotateArrayOneStep(vector<int>& arr, bool clockwise = true) {
if (clockwise) {
int temp = arr.back();
for (int i = arr.size() - 1; i > 0; i--) {
arr[i] = arr[i-1];
}
arr[0] = temp;
} else {
int temp = arr[0];
for (int i = 0; i < arr.size() - 1; i++) {
arr[i] = arr[i+1];
}
arr.back() = temp;
}
}
// 2D 배열의 특정 행 또는 열 회전
void rotate2DArrayPart(vector<vector<int>>& arr, int index, bool isRow, bool clockwise = true) {
if (isRow) {
rotateArrayOneStep(arr[index], clockwise);
} else {
vector<int> column;
for (int i = 0; i < arr.size(); i++) {
column.push_back(arr[i][index]);
}
rotateArrayOneStep(column, clockwise);
for (int i = 0; i < arr.size(); i++) {
arr[i][index] = column[i];
}
}
}
```
특징:
- 1차원 배열 회전은 마지막(또는 첫) 요소를 임시 저장하고 나머지를 이동시킨 후 저장된 요소를 넣는 방식으로 구현합니다.
- 2D 배열의 특정 행이나 열 회전은 해당 부분을 1차원 배열로 추출하여 회전한 후 다시 넣는 방식으로 구현합니다.
- clockwise 매개변수를 통해 시계 방향 또는 반시계 방향 회전을 선택할 수 있습니다.
- 이 방법은 배열의 일부분만 회전해야 하는 경우에 유용합니다.
### 5.7 부분 회전
부분 회전은 2차원 배열의 특정 부분만 회전시키는 기법입니다. 이 기법은 단계별로 구현하는 것이 중요합니다.
```cpp
void rotatePartial90(vector<vector<int>>& arr, int sy, int sx, int size) {
vector<vector<int>> temp(size, vector<int>(size));
// 1단계: 부분 배열을 임시 배열로 복사
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
temp[i][j] = arr[sy+i][sx+j];
}
}
// 2단계: 90도 회전
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
arr[sy+j][sx+size-1-i] = temp[i][j];
}
}
}
// 사용 예
vector<vector<int>> board = {{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12},
{13, 14, 15, 16}};
rotatePartial90(board, 1, 1, 2); // (1,1)부터 2x2 크기의 부분을 90도 회전
```
특징:
- 회전할 부분을 임시 배열로 복사한 후 회전시킵니다.
- 이 방법은 배열의 일부분만 회전해야 하는 경우에 유용합니다.
- 회전의 중심점과 크기를 지정할 수 있어 유연하게 사용할 수 있습니다.
### 5.8 토네이도 (나선형) 이동
토네이도 패턴은 중앙에서 시작하여 나선형으로 이동하는 패턴입니다. 이는 "마법사 상어와 토네이도" 같은 문제에서 자주 사용됩니다.
```cpp
vector<pair<int, int>> tornadoPattern(int n) {
vector<pair<int, int>> pattern;
int x = n/2, y = n/2;
int dx[] = {0, 1, 0, -1}; // 좌, 하, 우, 상 순서
int dy[] = {-1, 0, 1, 0};
int dir = 0;
int moveCount = 0;
int moveDistance = 1;
pattern.push_back({x, y});
while (true) {
for (int i = 0; i < moveDistance; i++) {
x += dx[dir];
y += dy[dir];
if (x < 0 || y < 0) return pattern;
pattern.push_back({x, y});
}
dir = (dir + 1) % 4;
moveCount++;
if (moveCount == 2) {
moveDistance++;
moveCount = 0;
}
}
}
// 사용 예
vector<pair<int, int>> tornado = tornadoPattern(5);
for (auto& p : tornado) {
cout << "(" << p.first << ", " << p.second << ") ";
}
```
특징:
- 중앙에서 시작하여 반시계 방향으로 나선형 이동 패턴을 생성합니다.
- 이동 거리가 2번마다 1씩 증가합니다.
- 격자 밖으로 나가면 패턴 생성을 종료합니다.
### 5.9 격자 밖으로 나가는 경우 처리
격자 기반 문제에서 객체가 격자 밖으로 나가는 경우, 반대편으로 이동하거나 방향을 바꾸는 등의 처리가 필요할 수 있습니다.
```cpp
pair<int, int> wrapAround(int x, int y, int n) {
x = (x + n) % n;
y = (y + n) % n;
return {x, y};
}
pair<int, int> bounce(int x, int y, int& dx, int& dy, int n) {
if (x < 0 || x >= n) {
dx = -dx;
x = max(0, min(x, n-1));
}
if (y < 0 || y >= n) {
dy = -dy;
y = max(0, min(y, n-1));
}
return {x, y};
}
// 사용 예
int n = 5;
int x = 4, y = 4, dx = 1, dy = 1;
// 반대편으로 이동
auto [nx, ny] = wrapAround(x + dx, y + dy, n);
cout << "Wrap around: (" << nx << ", " << ny << ")" << endl;
// 방향 전환
tie(x, y) = bounce(x + dx, y + dy, dx, dy, n);
cout << "Bounce: (" << x << ", " << y << "), New direction: (" << dx << ", " << dy << ")" << endl;
```
특징:
- `wrapAround` 함수는 객체가 격자 밖으로 나갔을 때 반대편으로 이동시킵니다.
- `bounce` 함수는 객체가 격자 경계에 부딪혔을 때 방향을 바꾸고 위치를 조정합니다.
- 이러한 함수들은 다양한 시뮬레이션 문제에서 유용하게 사용될 수 있습니다.
### 5.10 2차원 누적 합 (2D Prefix Sum)
2차원 배열에서 특정 직사각형 영역의 합을 빠르게 계산해야 하는 경우, 2차원 누적 합을 사용할 수 있습니다.
```cpp
vector<vector<int>> calculatePrefixSum2D(const vector<vector<int>>& arr) {
int n = arr.size();
int m = arr[0].size();
vector<vector<int>> prefixSum(n + 1, vector<int>(m + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
prefixSum[i][j] = arr[i-1][j-1] + prefixSum[i-1][j] + prefixSum[i][j-1] - prefixSum[i-1][j-1];
}
}
return prefixSum;
}
int getSum(const vector<vector<int>>& prefixSum, int x1, int y1, int x2, int y2) {
return prefixSum[x2+1][y2+1] - prefixSum[x1][y2+1] - prefixSum[x2+1][y1] + prefixSum[x1][y1];
}
// 사용 예
vector<vector<int>> arr = {{1, 2, 3},
{4, 5, 6},
{7, 8, 9}};
auto prefixSum = calculatePrefixSum2D(arr);
cout << "Sum of rectangle (0,0) to (1,1): " << getSum(prefixSum, 0, 0, 1, 1) << endl;
```
특징:
- 전처리를 통해 누적 합 배열을 계산합니다.
- 이후 어떤 직사각형 영역의 합도 O(1) 시간에 계산할 수 있습니다.
- 특정 영역의 합을 자주 계산해야 하는 문제에서 유용합니다.
네, 이 내용을 전략서에 추가하기 위해 다음과 같이 정리할 수 있습니다:
### 5.11 C++에서 배열을 함수 매개변수로 사용하기
C++에서 배열을 함수의 매개변수로 사용할 때는 몇 가지 주의해야 할 점이 있습니다. 이를 이해하고 올바르게 사용하면 코딩 테스트에서 더 효율적이고 안전한 코드를 작성할 수 있습니다.
#### 5.11.1 배열 전달 메커니즘
C++에서 배열을 함수에 전달할 때, 배열의 첫 번째 요소의 메모리 주소가 전달됩니다. 이는 다음과 같은 특성을 가집니다:
- 배열 전체가 복사되지 않아 메모리 효율적입니다.
- 함수 내에서 원본 배열을 직접 수정할 수 있습니다.
```cpp
void processArray(int arr[]) {
// arr은 배열의 첫 번째 요소를 가리키는 포인터입니다.
}
```
#### 5.11.2 배열 크기 전달의 필요성
배열을 함수에 전달할 때는 배열의 크기도 함께 전달해야 합니다. 이는 다음과 같은 이유 때문입니다:
- C++에서 배열은 자체 크기 정보를 가지고 있지 않습니다.
- 함수 내에서 배열의 크기를 알아야 올바른 처리가 가능합니다.
```cpp
void printArray(int arr[], size_t size) {
for (size_t i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
}
// 사용 예
int myArray[] = {1, 2, 3, 4, 5};
printArray(myArray, 5);
```
배열의 크기를 계산하는 일반적인 방법:
```cpp
int size = sizeof(arr) / sizeof(arr[0]);
```
#### 5.11.3 원본 배열 보호
함수 내에서 원본 배열을 수정하지 않도록 하려면 `const` 키워드를 사용합니다:
```cpp
void printArray(const int arr[], size_t size) {
for (size_t i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
// arr[0] = 0; // 컴파일 에러: const 배열은 수정할 수 없습니다.
}
```
#### 5.11.4 주요 포인트 요약
1. 배열을 함수에 전달할 때는 배열의 첫 번째 요소의 주소가 전달됩니다.
2. 배열의 크기를 함께 전달하여 함수 내에서 올바르게 처리할 수 있도록 합니다.
3. 함수 내에서 배열을 직접 수정할 수 있으므로, 원본 보호가 필요한 경우 `const` 키워드를 사용합니다.
4. 배열 크기 계산 시 `sizeof(arr) / sizeof(arr[0])` 공식을 활용할 수 있습니다.
이러한 배열 처리 기법을 잘 활용하면 코딩 테스트에서 더 효율적이고 안전한 코드를 작성할 수 있습니다.