Even Idiots Can Make Game
백준 14500 G4 테트로미노
- Date/Lastmod
- Section
- PS
- Title
- 백준 14500 G4 테트로미노
- Ps-Tags
- Solved
- 03-14/25
#1 접근: 브루트 포스
테트로미노의 각 정사각형을 픽셀 좌표라고 생각하면 테트로미노 하나를 4개의 pair<int, int>
배열로 표현할 수 있다.
또한, 테트로미노 하나를 시계 방향으로 0, 90, 180, 270도 돌릴 수 있고, 좌 우로 한번 뒤집은 후 0, 90, 180, 270도 돌릴 수 있다. 그러니까, 테트로미노 하나로 총 8개의 모양을 만들어 낼 수 있다.
이 8개의 모양 중 중복되는 것이 분명 있을 것이고, “중복되는 것을 제외하면 어떨까?” 라고 생각해 보았지만, 문제가 너무 복잡해지는 것 같아서 다른 방향으로 접근했다.
그냥 무식하게 풀어도 시간 초과가 나는가 안 나는가에 대해 고민했고, 그 판단을 내린 과정은 다음과 같다:
무식하게 푼다는 것은 중복이 있건 없건 만들어질 수 있는 모든 테트로미노를 대상으로, 각 테트로미노의 4개 좌표 중 하나를 기준으로 삼아, 주어진 점수 보드의 각 칸에 하나씩 다 대입해서 점수를 계산하고 그 최댓값을 갱신하겠다는 것이다.
보드의 크기는 최대 500이고, 한 번 순회에 약 $10^4$의 연산이 필요하다.
그리고 보드의 좌표 하나당, 5개의 테트로미노를 대상으로 생성된 각 테트로미노별 8개 모양, 그리고 그 모양별로 4개의 좌표로 점수를 계산해 내야 하므로 보드 한 칸당 총 5 * 8 * 4, 약 $10^2$번의 연산이 필요하다.
그, 테트로미노 연산에 중복이 있건 없건 간에 $10^4 \cdot 10^2 = 10^6$ 밖에 연산이 안 들고 제한시간은 2초이므로 매우 넉넉하다. 중복에 대해 그리 생각할 필요가 없다!
그러니, 무식하게 풀자!
#1 알고리즘
- 각 테트로미노를 정의할 때, 4개의 두 정수의 쌍으로 정의하되, 기준 정수쌍인 $(0, 0)$을 항상 포함시킨다.
- 다른 모든 테트로미노의 픽셀 좌표는 $(0, 0)$에 상대적인 좌표로 표현한다.
- 다시 말해, $(0, 0)$은 테트로미노의 로컬 좌표계 의 기준이다.
- 5개의 테트로미노를 돌리고 뒤집고 반복해서 테트로미노의 모든 모양을 한 방에 계산하여 배열에 넣어둔다.
- 이 배열은 중복된 것이 포함되지만 무시하고, 총 갯수가 40개여야 한다.
- “뒤집고 반복해서” 에 회전 변환 과, 대칭 이동 변환 이 사용된다.
- 점수판의 각 픽셀에 대해서:
- 모든 테트로미노에 대해서:
- 테트로미노의 로컬 좌표를, 점수판의 좌표계로 이동시킨다. (그냥 더하면 된다.)
- 이동시켰을 때, 점수판의 경계를 넘어서면 반복문의 다음으로 넘어간다.
- 넘어서지 않으면 점수를 계산하고, 최대값이면 기록한다.
알고리즘 자체는 쉬우나 관리할 정보의 양이 많기 때문에 해당 능력을 테스트하는 문제가 아닌가 싶다.
#1 코드
|
|
#1 기타
#2 2차원 좌표의 회전 변환
2차원 공간에서 한 점 $(x, y)$을 원점 $(0, 0)$을 기준으로 반시계 방향으로 $\theta$만큼 회전시키는 공식은 다음과 같다.
$$
x^\prime = x\cos{\theta}-y\sin{\theta}
$$
$$
y^\prime = x\sin{\theta}+y\cos{\theta}
$$
이를 행렬 형태로 표현하면 아래와 같다.
$$ \begin{pmatrix} x^\prime \\ y^\prime \end{pmatrix} = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix} \begin{pmatrix} x \\ y \end{pmatrix} $$
다시한 번 주의할 점은 이게 원점을 기준으로 하는 회전 변환이라는 것이다. 이 문제에서도 그러므로 모든 테트로미노가 로컬 좌표계에서 원점에 붙어 있도록 의도하였다.
#2
범위 기반 for
문은 C 스타일 배열도 순회가 가능하다
T[]
형식으로 선언된 배열에만 해당된다. T*
로 Decay 되어버린 배열로는 순회가 안 된다. (이는 배열 형식 자체가 크기에 대한 정보를 가지고 있기 때문)
int[] arr{0, 1, 2};
int* aptr = new int[3];
for (int& e: arr) { /* works */ }
for (int& e: aptr) { /* not works */ }
#2 벡터 합치기
대중적으로 두 가지 방식이 있다. (둘다 캐퍼시티를 초과하면 재할당이 일어나므로 주의한다. 결과 벡터의 길이를 대충 알고있다면, 미리 예약해 두는 것도 좋다.)
첫 번째는:
std::vector<T> dst, src;
dst.insert(
dst.end(),
std::make_move_iterator(src.begin()),
std::make_move_iterator(std.end())
);
반복자 어답터인 std::make_move_iterator
를 이용하면 src
의 요소에 복사를 발생시키지 않고, 마치 각 요소에 대해 std::move(*it)
가 적용된 것처럼 작동한다.
두 번째는:
std::vector<T> dst, src;
std::move(
src.begin(),
src.end(),
std::back_inserter(dst)
)
둘 다 src
를 이동시키기 때문에 연산이 끝난 후 src
에 대한 접근을 조심해야 한다. 이동 후에 src.clear()
를 써서 가독성을 높여 실수를 방지할 수도 있다. 1
src.clear()
호출 없어도src
내용을 전부 이동시켰으면src
는 빈 상태가 된다. ↩︎