# 삼성전자 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])` 공식을 활용할 수 있습니다. 이러한 배열 처리 기법을 잘 활용하면 코딩 테스트에서 더 효율적이고 안전한 코드를 작성할 수 있습니다.