이번에는 백준 17406번 배열 돌리기 4 문제를 풀어보았습니다.
이 문제는 주어진 회전 연산을 모두 한 번씩 수행하되, 연산 순서를 자유롭게 정할 수 있습니다.
회전 연산의 순서에 따라 최종 배열이 달라지므로, 가능한 모든 연산 순서를 확인한 뒤 배열의 값이 가장 작아지는 경우를 찾아야 합니다.
따라서 순열을 이용한 완전탐색과 배열 시뮬레이션으로 해결하였습니다.
N × M 크기의 배열 A가 주어집니다.
배열의 값은 각 행의 합 중 최솟값입니다.
회전 연산 (r, c, s)는 중심 (r, c)를 기준으로 크기가 다른 여러 개의 테두리를 각각 시계 방향으로 한 칸씩 이동시키는 연산입니다.
주어진 K개의 회전 연산은 모두 한 번씩 사용해야 하며, 수행 순서는 자유롭게 정할 수 있습니다.
가능한 모든 연산 순서 중 배열의 값을 최소로 만드는 경우를 구하는 문제입니다.
회전 연산의 개수 K는 최대 6입니다.
따라서 가능한 연산 순서의 수는 최대
6! = 720
개입니다.
next_permutation()을 사용하여 모든 연산 순서를 생성하고, 각 순서에 대해 실제로 배열을 회전시킵니다.
하나의 회전 연산 (r, c, s)는 중심에서 거리가 1인 테두리부터 s인 테두리까지 총 s개의 층으로 구성됩니다.
각 층마다 다음 순서로 테두리 좌표를 벡터에 저장합니다.
좌표를 시계 방향 순서로 저장한 뒤, 현재 좌표의 값을 다음 좌표로 이동시키면 테두리가 시계 방향으로 한 칸 회전합니다.
모든 연산을 수행한 뒤 각 행의 합을 구하고, 그중 최솟값으로 정답을 갱신합니다.
#include <bits/stdc++.h>
using namespace std;
int A[101][101];
int N,M,C;
int rotate_input[6][3];
int ret = INT_MAX;
void run_rotate(int y, int x, int n){
int temp1[101][101];
for (int i=1; i<=n; i++) {
memcpy(temp1, A, sizeof(A));
int top = y-i;
int bot = y+i;
int left = x-i;
int right = x+i;
vector<pair<int, int>> idx;
for (int j=left; j<=right; j++)
idx.push_back({top,j});
for (int j=top+1; j<=bot; j++)
idx.push_back({j,right});
for (int j=right-1; j>=left; j--)
idx.push_back({bot,j});
for (int j=bot-1; j>top; j--)
idx.push_back({j,left});
int idx_size = idx.size();
for (int j=0; j<idx_size; j++) {
int cy = idx[j].first;
int cx = idx[j].second;
int ny = idx[(j+1)%idx_size].first;
int nx = idx[(j+1)%idx_size].second;
temp1[ny][nx] = A[cy][cx];
}
}
memcpy(A, temp1, sizeof(A));
}
void get_min_val() {
for (int i=1; i<=N; i++) {
int sum=0;
for (int j=1; j<=M; j++) {
sum += A[i][j];
}
ret = min(ret, sum);
}
}
void solve(vector<int> order) {
int temp1[101][101];
memcpy(temp1, A, sizeof(A));
for (int i=0; i<order.size(); i++) {
run_rotate(rotate_input[order[i]][0], rotate_input[order[i]][1],rotate_input[order[i]][2]);
}
get_min_val();
memcpy(A, temp1, sizeof(A));
}
int main() {
ios_base::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
cin >> N >> M >> C;
for (int i=1; i<=N; i++) {
for (int j=1; j<=M; j++) {
cin >> A[i][j];
}
}
vector<int> permute_vec(C);
for (int i=0; i<C; i++) {
cin >> rotate_input[i][0];
cin >> rotate_input[i][1];
cin >> rotate_input[i][2];
permute_vec[i] = i;
}
do {
solve(permute_vec);
} while(next_permutation(permute_vec.begin(), permute_vec.end()));
cout << ret;
return 0;
}
배열과 회전 연산 정보를 입력받습니다.
각 회전 연산의 번호를 permute_vec에 저장합니다.
next_permutation()을 사용하여 가능한 모든 연산 순서를 생성합니다.
각 순열마다 현재 배열을 임시 배열에 저장합니다.
해당 순서대로 모든 회전 연산을 수행합니다.
회전이 끝난 배열에서 각 행의 합을 계산합니다.
행의 합 중 최솟값으로 ret을 갱신합니다.
다음 순열을 확인하기 위해 배열을 원래 상태로 복구합니다.
모든 순서를 확인한 뒤 ret을 출력합니다.
int rotate_input[6][3];
각 행에는 하나의 회전 연산 정보를 저장합니다.
rotate_input[i][0] = r
rotate_input[i][1] = c
rotate_input[i][2] = s
K가 최대 6이므로 크기를 6으로 선언하였습니다.
vector<int> permute_vec(C);
permute_vec에는 회전 연산의 인덱스를 저장합니다.
for (int i=0; i<C; i++) {
...
permute_vec[i] = i;
}
예를 들어 회전 연산이 3개라면 초기 상태는 다음과 같습니다.
0 1 2
이후 next_permutation()을 사용하여 모든 순서를 확인합니다.
do {
solve(permute_vec);
} while(next_permutation(permute_vec.begin(), permute_vec.end()));
회전 연산은 수행 순서에 따라 결과가 달라지므로 모든 순열을 탐색해야 합니다.
void solve(vector<int> order) {
int temp1[101][101];
memcpy(temp1, A, sizeof(A));
각 순열은 반드시 같은 초기 배열에서 시작해야 합니다.
따라서 회전 연산을 수행하기 전에 현재 배열을 temp1에 저장합니다.
for (int i=0; i<order.size(); i++) {
run_rotate(rotate_input[order[i]][0],
rotate_input[order[i]][1],
rotate_input[order[i]][2]);
}
전달받은 순서대로 모든 회전 연산을 수행합니다.
모든 연산과 최솟값 계산이 끝난 후에는 배열을 다시 복구합니다.
memcpy(A, temp1, sizeof(A));
이렇게 해야 다음 순열도 원본 배열에서 시작할 수 있습니다.
for (int i=1; i<=n; i++)
회전 연산 (y, x, n)은 중심을 기준으로 총 n개의 테두리를 회전시킵니다.
예를 들어 n = 2라면 다음 두 층을 각각 회전합니다.
중심에서 거리 1인 테두리
중심에서 거리 2인 테두리
현재 층의 거리를 i라고 했을 때 범위는 다음과 같습니다.
int top = y-i;
int bot = y+i;
int left = x-i;
int right = x+i;
이를 통해 현재 회전시킬 정사각형 테두리의 위, 아래, 왼쪽, 오른쪽 범위를 구합니다.
vector<pair<int, int>> idx;
현재 층의 테두리 좌표를 시계 방향 순서대로 저장합니다.
for (int j=left; j<=right; j++)
idx.push_back({top,j});
왼쪽 위에서 오른쪽 위까지 저장합니다.
for (int j=top+1; j<=bot; j++)
idx.push_back({j,right});
위쪽 모서리는 이미 저장했으므로 top + 1부터 시작합니다.
for (int j=right-1; j>=left; j--)
idx.push_back({bot,j});
오른쪽 아래 모서리는 이미 저장했으므로 right - 1부터 왼쪽 방향으로 저장합니다.
for (int j=bot-1; j>top; j--)
idx.push_back({j,left});
아래쪽과 위쪽 모서리는 이미 저장했으므로 두 모서리를 제외합니다.
이 과정을 거치면 테두리의 모든 좌표가 중복 없이 시계 방향 순서로 저장됩니다.
int idx_size = idx.size();
for (int j=0; j<idx_size; j++) {
현재 좌표의 값을 다음 좌표로 이동시킵니다.
int cy = idx[j].first;
int cx = idx[j].second;
현재 위치를 가져옵니다.
int ny = idx[(j+1)%idx_size].first;
int nx = idx[(j+1)%idx_size].second;
다음 위치는 j + 1번째 좌표입니다.
마지막 좌표의 다음 위치는 첫 번째 좌표가 되어야 하므로 % idx_size를 사용합니다.
temp1[ny][nx] = A[cy][cx];
현재 좌표의 값을 다음 좌표에 저장합니다.
테두리 좌표가 시계 방향 순서로 저장되어 있으므로, 모든 값이 시계 방향으로 한 칸씩 이동합니다.
회전할 때 값을 원본 배열에 바로 덮어쓰면 아직 이동하지 않은 원래 값이 사라질 수 있습니다.
따라서 각 층을 회전하기 전에 현재 배열을 임시 배열에 복사합니다.
memcpy(temp1, A, sizeof(A));
값을 이동할 때는 원본 배열 A에서 읽고, 임시 배열 temp1에 씁니다.
temp1[ny][nx] = A[cy][cx];
이렇게 하면 모든 좌표가 회전 전 상태의 값을 기준으로 이동합니다.
for (int i=1; i<=n; i++) {
memcpy(temp1, A, sizeof(A));
현재 코드에서는 안쪽 층부터 바깥쪽 층까지 순서대로 회전합니다.
각 층은 서로 겹치지 않는 테두리이므로 순서대로 처리해도 문제가 없습니다.
한 층의 회전 결과는 temp1에 저장됩니다.
다음 층을 처리할 때는 다시 현재 배열 상태를 기준으로 복사합니다.
모든 층의 회전이 끝난 후 최종 결과를 원본 배열에 반영합니다.
memcpy(A, temp1, sizeof(A));
void get_min_val() {
for (int i=1; i<=N; i++) {
int sum=0;
각 행의 합을 구합니다.
for (int j=1; j<=M; j++) {
sum += A[i][j];
}
현재 행의 합을 구한 뒤 정답과 비교합니다.
ret = min(ret, sum);
모든 행을 확인하면 현재 배열의 값인 행 합의 최솟값이 ret에 반영됩니다.
문제에서 주어지는 회전 연산의 좌표는 1부터 시작합니다.
코드에서도 배열을 다음과 같이 사용합니다.
for (int i=1; i<=N; i++) {
for (int j=1; j<=M; j++) {
따라서 입력 좌표를 별도로 0-based로 변환하지 않고 그대로 사용할 수 있습니다.
int top = y-i;
int bot = y+i;
int left = x-i;
int right = x+i;
문제의 (r, c, s) 값을 그대로 사용하여 회전 범위를 계산할 수 있다는 장점이 있습니다.
회전 연산의 개수는 최대 6입니다.
따라서 가능한 연산 순서의 수는 최대 다음과 같습니다.
6! = 720
각 순서에 대해 모든 회전 연산을 수행하고 배열의 값을 계산해도 경우의 수가 충분히 작습니다.
따라서 모든 순열을 확인하는 완전탐색으로 해결할 수 있습니다.
회전 연산의 순서 수는 K!개입니다.
하나의 순열마다 K개의 회전 연산을 수행합니다.
회전 연산 하나는 최악의 경우 배열의 테두리들을 순회하므로 O(N × M)으로 볼 수 있습니다.
모든 연산이 끝난 뒤 행의 합을 계산하는 데에도 O(N × M)이 필요합니다.
따라서 전체 시간복잡도는 다음과 같습니다.
O(K! × K × N × M)
K는 최대 6이므로 충분히 해결할 수 있습니다.