https://www.acmicpc.net/problem/17780

 

17780번: 새로운 게임

재현이는 주변을 살펴보던 중 체스판과 말을 이용해서 새로운 게임을 만들기로 했다. 새로운 게임은 크기가 N×N인 체스판에서 진행되고, 사용하는 말의 개수는 K개이다. 말은 원판모양이고, 하

www.acmicpc.net

#include <cstdio>
#include <vector>

using namespace std;

typedef struct Horse
{
	int row, col, dir;
	bool isBottom;
}Horse;

const int SIZE = 12;

vector<int> over[11];
Horse horse[11];

int map[SIZE][SIZE];
int unit[SIZE][SIZE];

int dirR[4] = { 0, 0, -1, 1 };
int dirC[4] = { 1, -1, 0, 0 };
int n;

int ReverseOrder(int index)
{
	vector<int> v = over[index];
	int size = v.size();

	if (size <= 1) //위에 쌓인게 없으면 그대로 둔다.
	{
		return index;
	}

	horse[index].isBottom = false;
	int row = horse[index].row;
	int col = horse[index].col;

	for (int i = 0; i < size / 2; ++i)
	{
		int temp = v[i];
		v[i] = v[size - 1 - i];
		v[size - 1 - i] = temp;
	}

	int newIndex = v[0]; //새로운 맨 밑이 탄생하였다.

	over[newIndex] = v;  //새로운 밑으로 말들 배치를 저장해주고,
	over[index].clear(); //기존 것은 제거해준다.

	horse[newIndex].isBottom = true; //새로운 말이 맨 밑이 되었고
	unit[row][col] = v[newIndex];    //현재 unit도 새로운 말 인덱스가 저장된다.

	return newIndex;   //기준 인덱스가 바뀌어야 된다.
}

void TurnAroundDirection(int index)
{
	int dir = horse[index].dir;
	dir = dir / 2 * 2 + (dir + 1) % 2;
	horse[index].dir = dir;
}

bool IsOutOfBound(int row, int col)
{
	return row < 0 || n <= row || col < 0 || n <= col;
}

bool Move(int index, int preRow, int preCol, int newRow, int newCol)
{
	int size = over[index].size();
	int bottom = unit[newRow][newCol];

	unit[preRow][preCol] = 0; //이동 전 기존 좌표는 비워준다.

	for (int i = 0; i < size; ++i) //위에 있는 말들도 함께 이동시킨다.
	{
		int idx = over[index][i];
		horse[idx].row = newRow;
		horse[idx].col = newCol;
	}

	if (bottom != 0) //이동하는 곳에 말이 있다면?
	{
		horse[index].isBottom = false;

		//bottom 벡터 리스트 맨 뒤에 이동 된 index 벡터 리스트를 추가하는 작업이다.
		over[bottom].insert(over[bottom].end(), over[index].begin(), over[index].end());
		over[index].clear();

		if (over[bottom].size() >= 4) //이동 후 말이 4마리 이상이면 게임 오버
		{
			return true;
		}
	}
	else //말이 없으면 그냥 점령한다.
	{
		unit[newRow][newCol] = index;
	}

	return false;
}

bool ProcessFromColor(int index, int step)
{
	Horse h = horse[index];
	
	int d = h.dir;
	int newRow = h.row + dirR[d];
	int newCol = h.col + dirC[d];

	if (IsOutOfBound(newRow, newCol))
	{
		if (step == 0)
		{
			TurnAroundDirection(index);
			return ProcessFromColor(index, step + 1);
		}
		else
		{
			return false;
		}
	}

	switch (map[newRow][newCol])
	{
		case 1:

			index = ReverseOrder(index);
			return Move(index, h.row, h.col, newRow, newCol);

		case 2:

			if (step == 0)
			{
				TurnAroundDirection(index);
				return ProcessFromColor(index, step + 1);
			}
			
			return false;
		default:

			return Move(index, h.row, h.col, newRow, newCol);
	}

	return false;
}

int main()
{
	int row, col, dir;
	int k;
	int moved = 0;
	
	scanf("%d %d", &n, &k);

	for (int i = 0; i < n; ++i)
	{
		for (int j = 0; j < n; ++j)
		{
			scanf("%d", &map[i][j]);
		}
	}

	for (int i = 1; i <= k; ++i) //말 번호는 1~k까지 쓸거지만
	{
		//row, col은 0, 0부터, dir는 0,1,2,3 으로 쓸거라 감소 시켜준다.
		scanf("%d %d %d", &row, &col, &dir);
		row--;
		col--;
		dir--;

		Horse h;
		h.row = row;
		h.col = col;
		h.dir = dir;
		h.isBottom = true;

		horse[i] = h; //말 정보를 등록하고
		unit[row][col] = i; //맵에도 말을 등록해준다.
		over[i].push_back(i); //리스트에는 자신을 먼저 등록해준다.
	}

	while (moved < 1000)
	{
		moved++;

		for (int i = 1; i <= k; ++i)
		{
			if (horse[i].isBottom == false)
			{
				continue;
			}

			bool isFinish = ProcessFromColor(i, 0);
			if (isFinish)
			{
				printf("%d\n", moved);
				return 0;
			}
		}
	}

	printf("-1\n");
	return 0;
}

오랜만에 문제를 하나 풀어봤는데, 머리속으론 로직이 정리되었는데 막상 짜니깐 오류가 생기고..

문제를 잘 못 이해한 부분때문에 이상한 답이 나오며 고생한 문제였다.

 

문제를 정리하면

n x n 맵에 하얀색(0), 빨간색(1), 파란색(2) 의 칸들이 존재하고

k개의 말들이 놓여있으며 말들은 방향을 갖고 있고, 해당 방향으로만 움직일 수 있다.

말들은 작은 숫자 순서대로 움직이고, 다른 말을 만나면 이동한 말(들)은 그 위에 쌓이게 되며

"가장 밑에 있는 말만 움직일 수 있게 된다."

 

하얀색 칸은 그냥 이동, (이동 시 해당 칸에 이미 말이 존재하면 해당 말 위에 올린다.)

빨간색 칸은 이동하기 전에 1,2,3 순서라면 3,2,1 순서로 변경하여 이동,

파란색 칸은 방향을 뒤로 바꾼 후 갈 수 있다면 이동, 불가능하다면 이동하지 않는다.

 

위의 빨간색 조건 때문에 이상한 답이 나왔었는데, 1턴에 모든 말들이 이동할 수 있는지 계속 체크해줘야 한다.

만약 (맨 밑)1, 2로 이루어진 말이 있는데 이 말이 빨간 칸에 가게 되면

2, 1로 바뀌게 된다. 그리고 2번 말이 이동할 수 있게 되며 이것이 1턴에 이루어지는 일들이다. 아래는 예제..

 

그렇게 이 조건을 몰라서 왜 안돼지?? 뻘짓을 하고 말았다고 한다..

 

코드를 설명해보면

우선 맵의 상태를 저장할 map[size][size] 배열과

말들의 위치를 저장할 unit[size][size] 로 따로 관리해주고 있다.

unit은 말의 번호를 저장해두고,

말들에 대해선 horse 배열을 통해 현재 위치 (row, col), 방향, 맨 밑에 있는지를 알아 볼 수 있다.

(사실 체스 말인데.. 그냥 영어 말로함;)

 

unit[1][1] = 1 이라면

horse[1] = { row = 1, col = 1, dir = 어떤 방향, isBottom = true }

(벡터 리스트) over[1] = { 1, 위에 있는 말들 없으면 끝 } 

식으로 해당 인덱스를 대입해서 판별하게 해두었다.

 

vector<int> over[11] 는 위에 쌓이는 말들을 저장하기 위한 리스트로 초기 상태는 자신의 번호만 들어가있다.

ReverseOrder 또는 Move 시에 다른 말위에 올라탈 수 있다면 해당 리스트가 복사되는 형태이다.

 

ProcessFromColor 로직은 길긴한데.. 이렇게 생겼다.

 

1. 방향에 맞게 새로운 좌표를 지정 (d, newRow, newCol 설정)

 

2. 해당 좌표가 IsOutOfBound 라면 방향을 전환 후 이동할 수 있는지 다시 해본다.

(재귀 함수를 이용했는데 step이 0이 아니라면 이미 한번 방향을 돌린 것이고, 그래도 이동이 안되면 가만있는다.)

 

3. 이동한 곳의 map 정보 판별 ( switch (map[newRow][newCol] )

 

 3-1. (빨간 칸) ReverseOrder를 통해 뒤집어 주고, 새로운 맨 밑이 현재 인덱스로 설정하게 해주었다.

   이 함수에서 unit[row][col] 에 대한 말 번호, 맨 밑에 있던 말의 isBottom, 새로운 맨 밑이 된 말의 isBottom

   초기화가 굉장히 중요하다. 순서 바꾸는 건 for에 있음.

 

 3-2. (파란 칸) 방향 전환 후 이동인데, 이미 방향을 바꿨던 경우 (step > 0) 라면 그냥 가만있으면 된다.

 

 3-3. (하얀 칸) 이동한다. 이동 시 이동할 unit 배열을 통해 말이 있는지 없는지 판별한다.

                   기존 말이 없다면 이동한 말이 해당 좌표에 등록되면 되고,

                   기존 말이 있다면 그 말의 맨 위로 올려버리고 운명을 함께 하면 되겠다.

 

 

사실 부족한 코드지만 글을 쓰고, 정리하다보면 좀 더 나아지지 않을까 하는 생각으로 올려봅니다.

https://www.acmicpc.net/problem/17386

 

17386번: 선분 교차 1

첫째 줄에 L1의 양 끝 점 x1, y1, x2, y2가, 둘째 줄에 L2의 양 끝 점 x3, y3, x4, y4가 주어진다. 세 점이 일직선 위에 있는 경우는 없다.

www.acmicpc.net

#include <stdio.h>

bool CrossCheck(int ax1, int ay1, int ax2, int ay2, int bx1, int by1, int bx2, int by2)
{
    //중심점을 구하고 해당 점으로부터의 방향을 외적을 이용하여 찾아낸다.
    int baseX = ax1 - ax2;
    int baseY = ay1 - ay2;
    int x1 = bx1 - ax2;
    int x2 = bx2 - ax2;
    int y1 = by1 - ay2;
    int y2 = by2 - ay2;

    //백만 * 백만이 될 수 있어 정수형은 오버플로우 될 수 있다.
    long long cross1 = (long long)baseX * y1 - (long long)baseY * x1;
    long long cross2 = (long long)baseX * y2 - (long long)baseY * x2;

    if (cross1 == 0 || cross2 == 0) //같은 위치에 점이 있는 경우!
    {
        return 0; //교차로 할거면 1, 여기선 아니라고 해두었다.
    }

    if ((cross1 > 0 && cross2 > 0) ||
        (cross1 < 0 && cross2 < 0))   //중심점을 기준으로 양쪽이 같은 방향이면 교차x
    {
        return 0;
    }

    return 1;
}

int main()
{
    int ax1, ax2, ay1, ay2, bx1, bx2, by1, by2;
    int cross;
    
    scanf("%d %d %d %d", &ax1, &ay1, &ax2, &ay2);
    scanf("%d %d %d %d", &bx1, &by1, &bx2, &by2);

    cross = CrossCheck(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2);
    if (cross > 0) //성공하면 중심점을 바꿔서 한번 더 해본다.
    {
        cross = CrossCheck(bx1, by1, bx2, by2, ax1, ay1, ax2, ay2);
    }

    printf("%d\n", cross);
}

수학 관련된 문제..

벡터의 외적을 통해 방향을 알아내는 식으로 작성된 코드이다.

외적은 다음과 같은 행렬의 여인수(cofactor) 전개를 통해서 구할 수 있다.

 

아래처럼 일단 외적 계산을 위한 행렬을 배치해보자.

E1 = (1, 0, 0), E2 = (0, 1, 0), E3 = (0, 0, 1)

 

E1 E2 E3

x1 y1 z1

x2 y2 z2

 

2차원 공간이기 때문에 z1과 z2는 무조건 0이다!

그렇기 때문에 행렬식을 전개해보면 다음과 같이 된다.

 E1 * ( y1 * z2(0) - z1(0) * y2 ) = 0

-E2 * ( x1 * z2(0) - z1(0) * x2 ) = 0

 E3 * ( x1 * y2 - y1 * x2 ) = ??

 

즉 x1 * y2 - y1 * x2 계산을 통해 외적을 구할 수 있게 된다.

중심 선을 기준으로 오른쪽에 위치하면 마이너스가 나오게 되고,

왼쪽에 위치하면 플러스가 나오고 겹쳐 있다면 0이 나온다.

확인을 해보도록 하자!

위의 CrossCheck 코드를 보면

먼저 baseX, baseY 를 구하고 있다. 어느 지점을 기준으로 왼쪽 오른쪽을 판별할지를 결정하는 것이고,

특정한 점을 기준으로 좌표를 계산해주고 있다. (코드는 (3, 3) ax2, ay2 를 기준으로 삼고 있다.)

위의 그림의 경우는 (3, 3)이 중심이 된다고 가정하면

(1, 1) => (1, 1) - (3, 3) = (-2, -2)

(3, 5) => (3, 5) - (3, 3) = (0, 2)

(4, 3) => (4, 3) - (3, 3) = (1, 0)

이 된다. 이러한 그림이 되는 것이고, 중심점인 빨강 선을 기준으로 외적을 구한다.

 

0, 2에 대해서 외적을 구해보면

-2 -2

0  2    ==>  (-2 * 2) - (-2 * 0) = -4

음수값이 나온다. 즉 시계 방향(오른쪽)에 위치하고 있다.

0, 1에 대해서 외적을 구해보면

-2 -2

1  0     ==>  (-2 * 0) - (-2 * 1) = 2

양수값이 나오며 시계 반대방향(왼쪽)에 위치하고 있다고 판별이 된다.

 

우선 선분이 교차할 가능성이 생겨있게 된다. 교차할 수 없는 경우는

모두 양수 또는 음수가 나온 경우이다. (cross1 > 0 && cross2 > 0) || (cross1 < 0 && cross2 < 0)

 

빨간 선을 기준으로 판별한 결과 교차할 수 있다고 나왔다.

이번엔 파란 선을 기준으로 다시 판별을 해줘야 한다.

마찬가지로 기준점을 하나 잡아 준다. 위의 코드에선 a와 b에 대해서 순서가 바뀐 것을 확인할 수 있다.

(기준 점은 빨간 선, 파란 선을 구성하는 어느 점을 선택하든 결과는 똑같다!! <- 이걸 확인해보고 싶었다 ㅎㅎ)

 

귀찮아서 대충 이런 느낌..

파란선의 방향은 아래쪽이고.. 이러한 경우는 외적 결과가 오른쪽에 2개가 있다고 나올 것이므로

두 개의 선분은 교차하지 않는다. 파란 선을 기준으로도 왼쪽, 오른쪽이 나온다면 두 선분은

교차하게 된다. 설명을 잘 못해서 너무 길어졌네.. 어쨋든 나중에도 기억하도록!!

www.acmicpc.net/problem/17406

 

17406번: 배열 돌리기 4

크기가 N×M 크기인 배열 A가 있을때, 배열 A의 값은 각 행에 있는 모든 수의 합 중 최솟값을 의미한다. 배열 A가 아래와 같은 경우 1행의 합은 6, 2행의 합은 4, 3행의 합은 15이다. 따라서, 배열 A의

www.acmicpc.net

#include <vector>
#include <cstdio>

using namespace std;

typedef struct Oper
{
    int row, col, start;
}Oper;

int minValue = 987654321;
int n, m;

int min(int a, int b)
{
    return a < b ? a : b; //a가 b 보다 작으면 리턴 a, 아니면 리턴 b
}

//memset을 써도 된다.
void CopyArray(int copyArray[50][50], int arr[50][50])
{
    for (int i = 0; i < n; ++i)
    {
        for (int j = 0; j < m; ++j)
        {
            copyArray[i][j] = arr[i][j];
        }
    }
}

void RotateRight(int arr[50][50], int copyArray[50][50], int r, int c, int s)
{
    if (s == 0)
    {
        return;
    }
    
    int up = r - s;
    int down = r + s;
    int left = c - s;
    intt right = c + s;

    for (int i = left + 1; i <= right; ++i)
    {
        copyArray[up][i] = arr[up][i - 1];
    }

    for (int i = up + 1; i <= down; ++i)
    {
        copyArray[i][right] = arr[i - 1][right];
    }

    for (int i = right - 1; i >= left; --i)
    {
        copyArray[down][i] = arr[down][i + 1];
    }

    for (int i = down - 1; i >= up; --i)
    {
        copyArray[i][left] = arr[i + 1][left];
    }

    //s를 줄여 점점 줄여가며 안쪽도 회전시켜준다.
    RotateRight(arr, copyArray, r, c, s - 1);
}

void RotateArray(int arr[50][50], vector<Oper>& v, vector<bool> &usedCheck, int needUse)
{
    if (needUse == 0) //모든 회전을 다 했다면
    {
        //전체 행 각각 합을 구하고 정답을 갱신한다.
        for (int i = 0; i < n; ++i)
        {
            int sum = 0;
            for (int j = 0; j < m; ++j)
            {
                sum += arr[i][j];
            }
            minValue = min(minValue, sum);
        }
        return;
    }

    int copyArray[50][50];
    int size = usedCheck.size();
    for (int i = 0; i < size; ++i)
    {
        if (usedCheck[i]) //이미 쓴 회전은 넘어간다.
        {
            continue;
        }

        usedCheck[i] = true; //회전 체크
		
        int r = v[i].row;
        int c = v[i].col;
        int s = v[i].start;

        CopyArray(copyArray, arr); //회전을 위해 배열 복사
        RotateRight(arr, copyArray, r, c, s); //오른쪽 회전을 한다.
        RotateArray(copyArray, v, usedCheck, needUse - 1); //다음 회전을 하러 간다.
		
        usedCheck[i] = false; //회전 체크 해제
    }
}

int main()
{
    vector<Oper> v;
    vector<bool> usedCheck;
    int arr[50][50] = { 0 };
    int k, r, c, s;

    scanf("%d %d %d", &n, &m, &k);
    usedCheck.resize(k);

    for (int i = 0; i < n; ++i)
    {
        for (int j = 0; j < m; ++j)
        {
            scanf("%d", &arr[i][j]);
        }
    }

    for (int i = 0; i < k; ++i)
    {
        scanf("%d %d %d", &r, &c, &s);
        r--; //0부터 시작하는 배열을 쓸거기 때문에 1씩 줄여준다.
        c--; //위와 마찬가지

        v.push_back({ r, c, s });
    }

    RotateArray(arr, v, usedCheck, k); //돌린다.

    printf("%d\n", minValue);
}

 

문제를 간단히 정리해보면 n x m 배열을 행, 열 (r, c) 축을 기준으로 s 크기만큼의 영역만 돌리고,

각 행별로의 숫자들의 합이 최소가 되는 값을 찾는 문제이다.

이 때 r, c, s 는 k개까지 입력을 받고, 모든 회전을 다 해봐야된다.

만약 회전의 방법이 3가지라면

 

1 2 3

  3 2

2 1 3

  3 1

3 1 2

  2 3

 

( 3! = 6 ) 개 만큼의 모든 상황들을 시뮬레이션 해봐야 한다. 최대 ( 6! = 6x5x4x3x2x1 = 720 ) 가지 방법이 될 수 있다.

문제는 배열 돌리기 (해당 좌표에 대해서만) + 욕심쟁이 방법 (=브루트 포스), 두 가지를 통해 해결해야 된다.

 

가장 중요한 배열 돌리기에 대해서 살펴보면

              (시계 방향으로 회전)

1 2 3        4 1 2

4 5 6  =>  7 5 3

7 8 9        8 9 6

 

1칸씩 밀리는 방법이다.

배열의 주소를 이용하여 *( *(arr + i) + j) 와 같은 방법으로도 해결해 볼 수 있는 것 같지만, 복잡할거 같아 지금은 넘어가고, 직관적으로 풀어나가는 것이 좋을거라 생각 된다.

먼저 A 배열을 복사할 새로운 B 배열이 필요하고 

      1 2 3         x x x

A = 4 5 6    B = x x x

      7 8 9         x x x

 

다음과 같은 방법으로 배열을 돌릴 수 있다. 오른쪽, 아래, 왼쪽, 위 순서로 채워나갈 수 있다.

오른쪽으로 먼저 채운다. B는 아래와 같은 상태가 된다.

       x 1 2

B =  x x x

       x x x

 

for ( int i = left + 1; i <= right; ++i )

   B[ up ][ i ] = A[ up ][ i - 1 ];

 

즉 up = 0, left = 0, right = 2라면

  B[0][1] = A[0][0] (1)

  B[0][2] = A[0][1] (2)

가 된다. 다음으로 아래쪽으로 채우게 된다면?

 

       x 1 2

B =  x x 3

       x x 4

for ( int i = up + 1; i <= down; ++i )

   B[ i ][ right ] = A[ i - 1 ][ right];

 

왼쪽으로 채우면

       x 1 2

B =  x x 3

       6 5 4

(가장 오른쪽 - 1 에서 왼쪽까지 가기때문에 마이너스가 된다. 물론 플러스로 구현해도 상관없다.)

for ( int i = right - 1; i >= left; --i )

   B[ down ][ i ] = A[ down ][ i + 1]; 

 

마지막으로 위로 채우면

       8 1 2

B =  7 x 3

       6 5 4

for ( int i = down - 1; i >= up; --i )

   B[ i ][ left ] = A[ i + 1][ left ];

 

이런식으로 채워지게 된다. 먼저 이 회전에 대해서 이해하고, 나머지는 반복적인 재귀로 처리할 것!

분명히 나중에 또 까먹겠지.. ㅠㅠ

+ Recent posts