191017 백준 알고리즘 문제풀기

|

[15683] 감시

  • 난이도: 극상상!
    • 문제 이해하는데는 10분도 걸리지 않았지만, 구현+디버깅하는데 3~4시간이 넘게 소요된듯 하다.
      • 처음에 전체 맵에 대해 모든 경우의 수를 구하도록 했더니 worst case 기분 연산까지 1분이 넘게 소모된다.
    • DFS의 이용에 대해 익숙하지 않아서 이렇게 오래 걸린듯 하다.
    • DFS를 조금 더 응용할 수 있는 능력을 길러야 할 듯 하다.
      • DFS는 모든 경우의 수를 찾을 때(완전탐색) 굉장히 유용한 알고리즘이다.
      • 함수를 재귀적으로 구성하고, 어떤 조건을 만족했을 때 해당 재귀를 끝내고 연산을 시작하게 만들 수 있다.
    • 개인적으로 풀어본 문제 중 가장 공부가 많이 되었던 문제인것 같다.
      • 나중에 복습해야할듯.
    • 특히, 모든 카메라의 경우의 수를 고려할 때 2차원 배열로 해서 각각 위치에 가능한 카메라들을 할당하고 접근하여 풀게 했더니 무조건 시간초과가 나는 결과를 초래했다.
      • Worst case 기준 1분이 넘어가도 경우의 수가 계산되지 않음..
    • 연구소 문제같은 경우는 2차원 배열 안에서 벽을 선택하면 되지만, 이런 문제는 그런식으로 접근하면 무조건 시간초과가 발생한다.
    • 이런식으로 어떠한 큰 배열(Map) 안에 특정 점의 소수의 상태(Camera)가 주어지는 경우, 맵에서 모든 경우의 수를 찾는게 아니라 Camera에서 모든 경우의 수를 찾아야 한다.
    • 중요한 타입의 문제 같으니 꼭 다시한번 봐야할듯..
  • 문제
    • https://www.acmicpc.net/problem/15683
    • 입력으로 N, M, N열 M행의 맵의 주어질 때, 카메라의 사각지대가 최소가 될 때의 사각지대 크기를 구한다.
    • 맵이 입력되고, 카메라는 1~5, 벽은 6으로 주어진다.
    • 카메라끼리 보는 영역은 중첩될 수 있다. 단, 카메라는 통과 할 수 있지만 벽은 통과하지 못한다.
    • 카메라는 각각 90도씩 회전 가능하며, 아래 영역을 한번에 본다.
      1. 한 방향
      2. 좌, 우
      3. 상, 우
      4. 좌, 상, 우
      5. 네 방향
    • 모든 경우의 수를 구해 사각지대를 계산한다.
  • 풀이
    • 문제를 나누면
      • 입력을 받고
      • 모든 경우의 수를 찾은다음에
      • 찾아진 각 경우의 수에 대해 카메라가 보는 영역을 만들고
      • 사각지대를 계산해서
      • 최솟값을 저장한다.
    • 가장 중요한 부분은 모든 경우의 수를 찾는 부분이다.
      • DFS를 이용해서 모든 경우의 수를 찾는다.
      • 각 카메라는 1, 3, 4는 4가지의 경우의 수, 2는 2가지, 5는 1가지의 경우의 수가 있다.
      • 이에 맞게 각각 최대 경우의 수를 설정하고(각각 숫자는 방향을 의미하도록!)
      • 해당 카메라가 고를 수 있는 방향의 경우의 수 중 방향을 정하고
      • 다음 카메라로 넘어가 또 방향의 경우의 수 중 방향을 정한다.
      • 만약 모든 카메라를 다 정했으면 경우의 수를 구한것이므로 연산을 시작한다.
      • 자세한건 밑에 코드에…
    • 카메라 방향을 정했으면, 카메라가 보는 영역을 만든다.
      • 간단하게 while문으로 카메라의 어떤 방향으로만 참조하는 함수를 만들고 호출해 완성
    • 사각지대를 계산
      • 전체 맵에서 카메라, 벽은 세지 않는다.
      • 이에 근거해 빈 칸(0)을 세면 된다.
#pragma warning(disable:4996)
#include <iostream>
#include <cstdio>
#include <vector>
#include <tuple>

#define MAX 2147483647

using namespace std;

struct camera
{
	int x, y, type;
};

int map[9][9]; // 맵 저장
int chosen_dir[9][9]; // 선택된 방향
int calc_map[9][9]; // 계산용 맵
vector<camera> cams;

int dx[] = { 0, 1,0,-1,0 }; // 1, 2, 3, 4 index는
int dy[] = { 0, 0,1,0,-1 }; // 우, 하, 좌, 상 순서

int num_cams = 0;
int answer = MAX;

void init_calcmap(int h, int w)
{ // 계산용 맵을 초기화한다.
	for (int j = 0; j < h; j++)
	{
		for (int i = 0; i < w; i++)
		{
			calc_map[j][i] = chosen_dir[j][i];
		}
	}
}

void viewer(int h, int w, int nx, int ny, int dir)
{ // 해당 방향으로 끝날때까지 check한다.
	while (1)
	{
		nx += dx[dir];
		ny += dy[dir];
		if (nx < 0 || w <= nx || ny < 0 || h <= ny) break; // 범위 벗어나면 끝
		if (map[ny][nx] == 6) break; // 벽 나오면 끝
		calc_map[ny][nx] = 1;
	}
	return;
}

void view_map(int h, int w, int cx, int cy, int type, int dir)
{ // 카메라의 종류에 따라 맵을 완성시킨다.
	int nx = cx;
	int ny = cy;
	if (type == 1)
	{
		calc_map[cy][cx] = 1; // 현재위치 찍고
		viewer(h, w, nx, ny, dir); // 본다.
	}
	else if (type == 2)
	{// 2번 방향 좌우
		calc_map[cy][cx] = 1; // 현재위치 찍고
		if (dir == 1)
		{// 좌우 1, 3
			viewer(h, w, nx, ny, 1);
			viewer(h, w, nx, ny, 3);
		}
		if (dir == 2)
		{// 상하 4, 2
			viewer(h, w, nx, ny, 4);
			viewer(h, w, nx, ny, 2);
		}
	}
	else if (type == 3)
	{// 3번 방향 상우
		calc_map[cy][cx] = 1; // 현재위치 찍고
		if (dir == 1)
		{// 상우 4, 1
			viewer(h, w, nx, ny, 4);
			viewer(h, w, nx, ny, 1);
		}
		if (dir == 2)
		{// 우하 1, 2
			viewer(h, w, nx, ny, 1);
			viewer(h, w, nx, ny, 2);

		}
		if (dir == 3)
		{// 하좌 2, 3
			viewer(h, w, nx, ny, 2);
			viewer(h, w, nx, ny, 3);
		}
		if (dir == 4)
		{// 좌상 3, 4
			viewer(h, w, nx, ny, 3);
			viewer(h, w, nx, ny, 4);
		}
	}
	else if (type == 4)
	{// 4번 방향 좌상우
		calc_map[cy][cx] = 1; // 현재위치 찍고
		if (dir == 1)
		{// 좌상우 3, 4, 1
			viewer(h, w, nx, ny, 3);
			viewer(h, w, nx, ny, 4);
			viewer(h, w, nx, ny, 1);
		}
		if (dir == 2)
		{// 상우하 4, 1, 2
			viewer(h, w, nx, ny, 4);
			viewer(h, w, nx, ny, 1);
			viewer(h, w, nx, ny, 2);
		}
		if (dir == 3)
		{// 우하좌 1, 2, 3
			viewer(h, w, nx, ny, 1);
			viewer(h, w, nx, ny, 2);
			viewer(h, w, nx, ny, 3);
		}
		if (dir == 4)
		{// 하좌상 2, 3, 4
			viewer(h, w, nx, ny, 2);
			viewer(h, w, nx, ny, 3);
			viewer(h, w, nx, ny, 4);
		}
	}
	else //(type == 5)
	{
		calc_map[cy][cx] = 1; // 현재위치 찍고
		//4방향
		for (int i = 1; i < 5; i++)
		{
			viewer(h, w, nx, ny, i);
		}
	}
	return;
}

void calc(int h, int w)
{
	// 사각지대 만들기
	for (int i = 0; i < num_cams; i++)
	{//카메라들 동안
		int cx = cams[i].x;
		int cy = cams[i].y;
		int type = cams[i].type;
		int cd = chosen_dir[cy][cx];
		view_map(h, w, cx, cy, type, cd); // 현재 카메라가 보는 맵 만들기.
	}

	// 사각지대 세기
	int cnt = 0;
	for (int j = 0; j < h; j++)
	{
		for (int i = 0; i < w; i++)
		{
			if (map[j][i] == 0)
			{//카메라 감시 가능한 구간만
				if (calc_map[j][i] == 0)
				{//사각지대 세기
					cnt++;
				}
			}
		}
	}
	//정답 비교
	if (cnt < answer) answer = cnt;
}

void dfs(int h, int w, int cnt)
{
	if (cnt == num_cams)
	{// 마지막 카메라까지 방향을 골랐으면
		init_calcmap(h, w); // 계산용 맵 초기화하고
		calc(h, w); // 계산을 시작한다.
		return;
	}
	//가능한 카메라 기본 방향은 4방향으로 하고
	int dir_cnt = 4;
	if (map[cams[cnt].y][cams[cnt].x] == 2) dir_cnt = 2; // 2번카메라는 2방향
	if (map[cams[cnt].y][cams[cnt].x] == 5) dir_cnt = 1; // 5번카메라는 1방향
	for (int i = 1; i <= dir_cnt; i++)
	{//모든 카메라 방향동안
		chosen_dir[cams[cnt].y][cams[cnt].x] = i;//현재 순번 카메라 좌표의 방향을 찍고
		dfs(h, w, cnt + 1); //다음 카메라로 넘어간다.
	}

}

int main()
{
	int N, M;
	scanf("%d %d", &N, &M);
	for (int j = 0; j < N; j++)
	{
		for (int i = 0; i < M; i++)
		{
			scanf("%d", &map[j][i]);
			if (map[j][i] != 0 && map[j][i] != 6)
			{//카메라라면
				cams.push_back({ i,j,map[j][i] }); // 카메라들 저장
				//x, y, type, dir
			}
		}
	}
	//카메라 개수 세고
	num_cams = cams.size();
	//경우의 수 만들기 시작
	dfs(N, M, 0);

	//정답
	printf("%d\n", answer);

	//std::system("pause");
	return 0;
}

[17142] 연구소 3

  • 난이도: 극상!
    • 풀었던 문제를 실패서 다시 풀었다. 성공하도록 풀어봤다.
    • 전체 경우의 수를 구하는 부분을 공부하기 아주 좋은 문제인듯 하다.
    • 이미 풀었던 문제기에 처음부터 푸는데 1시간 반정도 걸린듯 하다.
  • 문제
    • https://www.acmicpc.net/problem/17142
    • 맵 가로세로 크기 N, 활성화 바이러스 개수 M개, 맵에 주어지고 M개의 바이러스를 활성화시켜 전체 연구소를 덮는 최소시간을 구하는 문제
    • 문제를 나누면
      • 바이러스 고르기
      • 바이러스 퍼치기
    • 이렇게 나뉘지만, 바이러스 고르는게 제일 관건인 문제다.
    • 특이한것은, 활성화 되지 않은 바이러스라도 바이러스가 존재하는것으로 간주해야 한다.
  • 풀이
    • 바이러스 고르기
      • DFS 알고리즘을 이용한다.
      • 바이러스를 고르는데는, 첫 번째 바이러스부터 전체 바이러스 동안 DFS를 이용해 활성화->DFS 재귀->비활성화 순을 따르면 된다.
      • 하지만 중요한것은
        • 다음 DFS 접근 시 해당 인덱스부터 시작하도록 해야 중복되는 경우를 방지할 수 있다.
        • 예를 들어 말하면
          • 5개 바이러스중 3개를 고르는 경우의 수는 5C3으로 5.4.3/3.2.1 로 총 10가지의 경우의 수다.
          • 만약 0번째 인덱스부터 매 번을 고르게 되면 고른 수를 나열하는 5P3이 되므로 60가지로 늘어난다.
          • 이를 처리하지 못해 시간초과가 발생했었다.
      • 자세한건 밑에 코드에서 설명..
    • 바이러스 퍼치기
      • 일반적인 BFS 알고리즘을 이용한다.
      • 다만, 비활성화 바이러스가 존재하는것은 이미 바이러스가 있는것이다.
      • 따라서 다음 위치가 비활성화 바이러스라면 다음위치에서 바이러스 퍼치기가 불가능할 경우 counting 하지 않아야 한다.
        • 이미 바이러스가 존재하므로 counting하면 중복 counting이 된다.
        • 문제 페이지의 첫 번째 test case를 해결하는 방법이다.
#pragma warning(disable:4996)
#include <iostream>
#include <cstdio>
#include <vector>
#include <queue>
#include <cstring>

#define MAX 2147483647

using namespace std;

struct virus
{
	int x, y, activate; // 각각 좌표와 활성화 여부를 저장한다.
};

int map[51][51]; // 맵 저장용
int calc_map[51][51]; // 계산맵 저장용

int dx[] = { -1,1,0,0 }; // 이동방향
int dy[] = { 0,0,-1,1 };

vector<virus> viruses; // 바이러스 정보 저장
int num_virus = 0; // 활성화 바이러스 개수

int answer = MAX;

void spread(int N)
{// 바이러스 퍼치기
	//맵 초기화
	memset(&calc_map, 0, sizeof(calc_map)); // 계산맵을 초기화하고
	
	queue<pair<int, int>> q;
	
	for (int i = 0; i<int(viruses.size()); i++)
	{//전체 바이러스 중
		//활성화 바이러스 우선 활성화해서 큐에 넣는다.
		if (viruses[i].activate == 0) continue;
		calc_map[viruses[i].y][viruses[i].x] = 1;
		q.push(make_pair(viruses[i].x, viruses[i].y));
	}
	// 활성화바이러스들부터 먼저 탐색을 시작해야 전체적으로 매 초마다 고르게 퍼져간다.
	// 그렇지 않으면 첫 번째 바이러스가 끝날때까지 무조건 다 돌게되어 올바르지 않게 동작한다!
	
	while (!q.empty())
	{
		int cx = q.front().first;
		int cy = q.front().second;
		q.pop();
		for (int k = 0; k < 4; k++)
		{
			int nx = cx + dx[k];
			int ny = cy + dy[k];
			if (nx < 0 || N <= nx || ny < 0 || N <= ny) continue; // 범위 벗어나면 지나감
			if (map[ny][nx] == 1) continue; // 벽 만나면 지나감
			if (calc_map[ny][nx] != 0) continue; // 방문한적 있음 지나감
			if (map[ny][nx] == 2)
			{//만약 다음이 비활성화 바이러스라면
				int cnt = 0;
				for (int t = 0; t < 4; t++)
				{
					int tx = nx + dx[t];
					int ty = ny + dy[t];
					if (tx < 0 || N <= tx || ty < 0 || N <= ty) continue; // 범위 벗어나면 지나감
					if (map[ty][tx] == 1) continue; // 벽 만나면 지나감
					if (calc_map[ty][tx] != 0) continue; // 방문한적 있음 지나감
					//갈 데가 있으면 count
					cnt++;
				}
				if (cnt == 0)
				{//갈 곳이 한군데도 없다면 check만 한다.
				// check만 하는게 비활성화를 활성화 상태로 만드는것이다.
				// 어차피 비활성화 상태 바이러스도 바이러스이므로 다음에 퍼지지 않는다면
				// 시간이 소요되면 안되므로 예외처리한다.
					calc_map[ny][nx] = calc_map[cy][cx];
				}
				else
				{//갈 곳이 있으면 다음 부분으로 퍼치기 위해 시간이 소요되며 퍼진다.
					calc_map[ny][nx] = calc_map[cy][cx] + 1;
					q.push(make_pair(nx, ny));
				}
			}
			else
			{//만약 다음이 비활성화 바이러스가 아니라면 그냥 퍼진다.
				calc_map[ny][nx] = calc_map[cy][cx] + 1;
				q.push(make_pair(nx, ny));
			}
				
		}
	}

	// 바이러스 최대 시간 찾기
	int maxval = -1;
	bool iszero = false;
	for (int j = 0; j < N; j++)
	{
		for (int i = 0; i < N; i++)
		{
			if (map[j][i] == 1) continue;//벽은 넘어가고
			if (calc_map[j][i] == 0)
			{// 0이 있다면 계산하나 마나니까 빠져나감
				iszero = true;
				break;
			}
			if (maxval < calc_map[j][i])
			{//맵에서 최댓값 찾고
				maxval = calc_map[j][i];
			}
		}
	}
	if (iszero == false && maxval < answer)
	{
		answer = maxval;
	}
}

void dfs(int N, int idx, int cnt)
{ // index 번째 바이러스부터 시작해서 활성화 바이러스를 다 고른다음 퍼친다.
	if (cnt == num_virus)
	{//활성화 바이러스를 다 골랐으므로
		spread(N); // 퍼친다.
		return;
	}
	// 인덱스는 idx번째부터 시작한다.
	// 그래야 중복선택되는것을 막을 수 있다.
	// 즉, 해당 idx번째가 골라졌을때 계산 가능한 경우의 수는 모두 구해졌기에
	// 다음부터 볼 필요 없으므로 idx로 초기화한다.
	for (int i = idx; i < int(viruses.size()); i++)
	{//전체 바이러스들에 대해
		if (viruses[i].activate == 0)
		{// 활성화 되지 않았다면
			viruses[i].activate = 1; // 해당 바이러스 활성화 시키고
			dfs(N, i + 1,cnt + 1); // 여기가 핵심!!
			// 현재 인덱스 다음 바이러스부터 고려하면 된다.
			viruses[i].activate = 0; // 다시 비활성화 한다.
		}
	}
}

int main()
{
	int N, M;
	scanf("%d %d", &N, &M);
	int wall_cnt = 0;
	for (int j = 0; j < N; j++)
	{
		for (int i = 0; i < N; i++)
		{
			scanf("%d", &map[j][i]);
			if (map[j][i] == 2) viruses.push_back({ i,j,0 });
			if (map[j][i] == 1) wall_cnt++;
		}
	}
	num_virus = M;

	if ((wall_cnt + int(viruses.size())) == (N*N))
	{// 전체 맵에 비활성화 바이러스로 다 찬 경우, 이미 바이러스가 다 찬 상태이므로 0초 소요되는게 맞다.
		printf("%d\n", 0);
	}
	else
	{// 아닌 경우라면 연산을 수행
		//바이러스 선택하기
		dfs(N, 0, 0);

		if (answer == MAX) answer = -1;
		else answer -= 1;
		printf("%d\n", answer);
	}

	//std::system("pause");
	return 0;
}
  • DFS와 BFS를 능수능란하게 다뤄야 하는 어려운 문제다.
    • DFS를 이용한 모든 경우의 수를 고려하는 부분에 대한 연습이 많이 필요한듯 하다.

[15686] 치킨 배달

  • 난이도: 중상
    • 문제를 이해하고, 예제 손으로 풀어보고, 문제를 작게 나누고, 풀이를 떠올리는데 30분
    • 1차적으로 구현하는데 30분 해서 총 1시간만에 정답을 맞췄다.
    • 하지만 시간초과가 떴으며, 비효율적인 접근방법과 문제를 살짝 잘못이해해서 수정하는데 추가 30분정도 더 걸렸다.
      • 각 집과 치킨집까지의 거리를 처음엔 BFS로 찾았지만, 쓸데없는 짓이었다. 그냥 좌표끼리 빼서 절댓값을 씌우면 그게 최소거리다.
        • 중간에 장애물이 있는 경우엔 BFS로 푸는게 맞지만, 이렇게 장애물이 없는경우엔 그냥 절댓값 차를 이용하면 된다.
    • 앞에서 다뤘던 DFS로 모든 경우의 수를 찾는 문제를 연습 할 수 있어서 좋았다.
  • 문제
    • https://www.acmicpc.net/problem/15686
    • NxN 크기의 도시는 1크기의 칸으로 나뉘고, 각 자리는 빈칸(0), 집(1), 치킨집(2)로 나뉜다.
    • 각 집부터 치킨집까지의 최소거리(제일 가까운곳)를 치킨 거리라고 한다.
    • 모든 치킨 거리를 더했을 때 이를 도시의 치킨 거리라고 한다.
    • 입력으로 주어지는 M 숫자와 도시 맵에서 최대 M개의 치킨집 을 남겼을 때, 도시의 치킨 거리의 최솟값을 출력한다.
      • 즉, 1개부터 M개까지의 치킨집을 모두 test 해 봐야 한다.
    • 문제를 나누면
      • 입력을 받고
      • 정해진 개수의 치킨집을 고른다음에(1개부터 M개까지)
      • 골라진 치킨집을 이용해 각 집과의 치킨 거리를 계산하고
      • 도시의 치킨 거리를 구하고 최솟값을 저장한다.
  • 풀이
    • 입력을 받고
      • 입력은 배열을 이용했지만, 전체 맵에 대해 집 좌표와 치킨집 좌표를 저장해 활용한다.
      • 속도를 빠르게 할 수 있다.
    • 정해진 개수의 치킨집을 고른다.
      • 고르는 치킨집 개수는 for문을 이용해 1개부터 M개까지 고르도록 함수 인자로 넘긴다.
      • 앞의 문제들처럼 DFS를 이용해 푼다.
        • DFS의 인자로 현재 index와 카운팅 개수, 목적 개수(고르는 치킨집 개수)를 받는다.
        • 만약 카운팅 개수가 목적 개수와 같아진다면 치킨집을 다 골랐으므로 치킨거리를 계산한다.
        • 카운팅 개수가 부족하다면 현재 인덱스 다음 인덱스를 DFS에 집어넣어 재귀호출을 실행한다.
    • 골라진 치킨집을 이용해 각 집과의 치킨 거리 계산
      • 저장된 집 위치에서 골라진 치킨집 위치끼리의 거리를 모두 계산하고 최솟값을 받아 저장한다.
      • 거리는 각 좌표간 절댓값 차를 이용한다.
    • 도시의 치킨 거리를 구하고 최솟값을 저장
#pragma warning(disable:4996)
#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <queue>
#include <cmath>

#define MAX 2147483647

using namespace std;

struct info
{
	int x, y, chosen;
};

int map[51][51];

vector<info> chicken;
vector<info> houses;

int answer = MAX;

void calc_distance(int N)
{
	int distances = 0;

	for (int i = 0; i<int(houses.size()); i++)
	{
		int min_dist = MAX;
		for (int k = 0; k<int(chicken.size()); k++)
		{// 치킨집까지의 거리중 최솟값을 저장
			if (chicken[k].chosen != 1) continue;
			int dist = abs(houses[i].x - chicken[k].x) + abs(houses[i].y - chicken[k].y);
			if (dist < min_dist) min_dist = dist;
		}
		distances += min_dist;
	}
	if (distances < answer) answer = distances; // 최솟값 저장
}

void dfs(int idx, int cnt, int num_chicken, int N)
{
	if (cnt == num_chicken)
	{
		calc_distance(N);
		return;
	}

	for (int i = idx; i<int(chicken.size()); i++)
	{//전체 치킨집동안
		if (chicken[i].chosen == 1) continue; // 골라졌으면 넘어간다.
		chicken[i].chosen = 1; // 고르고
		dfs(i + 1, cnt + 1, num_chicken, N); // 다음 인덱스로 넘겨 들어간다.(중복 방지)
		chicken[i].chosen = 0; // 고르기 해제
	}
}


int main()
{
	int N, M;
	scanf("%d %d", &N, &M);

	//입력받기
	for (int j = 0; j < N; j++)
	{
		for (int i = 0; i < N; i++)
		{
			scanf("%d", &map[j][i]);
			if (map[j][i] == 2) chicken.push_back({ i,j,0 });
			if (map[j][i] == 1)	houses.push_back({ i,j,-1 });
		}
	}

	for (int idx = 1; idx <= M; idx++)
	{// idx = 골라지는 치킨집 갯수
		dfs(0, 0, idx, N);
	}

	printf("%d\n", answer);

	//std::system("pause");
	return 0;
}