300x250
300x250

ML Agents로 더욱 매력적인 게임 개발 | Unity 

 

간단한 플래피 버드 게임에 ML-Agents 2.0를 적용해봤어요.

 

using UnityEngine;
using UnityEngine.Events;
using Unity.MLAgents;
using Unity.MLAgents.Sensors;
using Unity.MLAgents.Actuators;
using System;

public class BirdAgentTest : Agent
{
    private Rigidbody2D rBody;
    public float jumpForce;
    public UnityAction onDie;
    public UnityAction<int> onSetLevel;
    
    EnvironmentParameters m_ResetParams;
    private int isJump;
    void Start()
    {
        this.rBody = this.GetComponent<Rigidbody2D>();
    }

    public void Init()
    {
        //this.jumpCoolTimeRoutine = null;
        int level = Convert.ToInt32(this.m_ResetParams.GetWithDefault("wall_height", 0));
        this.onSetLevel(level);

    }

    public override void OnEpisodeBegin()
    {
        Init();
    }
    public override void Initialize()
    {
        this.m_ResetParams = Academy.Instance.EnvironmentParameters;
    }

    public override void CollectObservations(VectorSensor sensor)
    {
        // 관찰
        sensor.AddObservation(this.transform.localPosition.y);    // 내위치 1
    }

    public override void OnActionReceived(ActionBuffers actions)
    {
        //행동, 보상 
        var discreteActions = actions.DiscreteActions;

        int isJump = discreteActions[0];


        if (this.transform.localPosition.y <= -5.0f  || this.transform.localPosition.y >= 5.6f)
        {
            this.Die();
        }

        if (isJump == 1)
        {
            this.Jump();
            isJump = 0;
        }

        AddReward(0.1f);
    }


    public override void Heuristic(in ActionBuffers actionsOut)
    {
        var discreteActions = actionsOut.DiscreteActions;

        if (Input.GetKey(KeyCode.Space))
        {
            discreteActions[0] = 1;
        }
        else
        {
            discreteActions[0] = 0;
        }
        this.onSetLevel(0);
    }

    private void Jump()
    {
        this.rBody.velocity = Vector3.up * this.jumpForce; 
    }
    private void Die()
    {
        AddReward(-1f);
        this.onDie();
        EndEpisode();
    }


    private void OnTriggerEnter2D(Collider2D collision)
    {
        this.Die();        
    }


}

 

에이전트에겐 this.transform.localPosition.y 즉 자신의 y값과 

 

움직이는 파이프 들에 레이를 통해서 감지할수있는 박스콜라이더를 달아 주었고 태그를 Pipe로 입력해줘서 최대한 파이프에 충돌을 안하며 생존할 수 있도록 학습시켰어요.

에이전트는 Jump 메소드만을 사용할 수 있어서 점프타이밍만 조절하면서 생존하는 기능이기 때문에 비교적 쉽게 학습이 된 거 같아요.

 

아래는 총 100만 회 학습시켰고 단계별로 난이도를 조절해가며 학습시켜 봤어요.

난이도를 조정하는 부분인데 처음에는 위아래 파이프 간격이 넓다가 점점 간격이 좁아지는 형식으로 학습시키는 코드에요.

 

 

BirdAgentTest.yaml

behaviors:
  BirdAgentTest:
    trainer_type: ppo
    hyperparameters:
      batch_size: 128
      buffer_size: 2048
      learning_rate: 0.0003
      beta: 0.005
      epsilon: 0.2
      lambd: 0.95
      num_epoch: 3
      learning_rate_schedule: linear
    network_settings:
      normalize: false
      hidden_units: 256
      num_layers: 2
      vis_encode_type: simple
    reward_signals:
      extrinsic:
        gamma: 0.99
        strength: 1.0
    keep_checkpoints: 5
    max_steps: 1000000
    time_horizon: 128
    summary_freq: 20000
environment_parameters:
  wall_height:
    curriculum:
      - name: Lesson0 # The '-' is important as this is a list
        completion_criteria:
          measure: progress
          behavior: BirdAgentTest
          signal_smoothing: true
          min_lesson_length: 100
          threshold: 0.1
        value: 0.0

      - name: Lesson1 # This is the start of the second lesson
        completion_criteria:
          measure: progress
          behavior: BirdAgentTest
          signal_smoothing: true
          min_lesson_length: 100
          threshold: 0.3
        value: 1.0

      - name: Lesson2
        completion_criteria:
          measure: progress
          behavior: BirdAgentTest
          signal_smoothing: true
          min_lesson_length: 100
          threshold: 0.5
        value: 2.0

BirdAgentTest.onnx
0.38MB
BirdAgentTest.yaml
0.00MB

300x250
300x250

1. 제목

- 백준 2583 영역 구하기

- BOJ 2583 영역 구하기

문제 링크 : 2583번: 영역 구하기 (acmicpc.net)


2. 문제 설명

영역 구하기 

 
시간 제한메모리 제한제출정답맞힌 사람정답 비율
1 초 128 MB 30995 17384 13764 56.567%

문제

눈금의 간격이 1인 M×N(M,N≤100)크기의 모눈종이가 있다. 이 모눈종이 위에 눈금에 맞추어 K개의 직사각형을 그릴 때, 이들 K개의 직사각형의 내부를 제외한 나머지 부분이 몇 개의 분리된 영역으로 나누어진다.

예를 들어 M=5, N=7 인 모눈종이 위에 <그림 1>과 같이 직사각형 3개를 그렸다면, 그 나머지 영역은 <그림 2>와 같이 3개의 분리된 영역으로 나누어지게 된다.

<그림 2>와 같이 분리된 세 영역의 넓이는 각각 1, 7, 13이 된다.

M, N과 K 그리고 K개의 직사각형의 좌표가 주어질 때, K개의 직사각형 내부를 제외한 나머지 부분이 몇 개의 분리된 영역으로 나누어지는지, 그리고 분리된 각 영역의 넓이가 얼마인지를 구하여 이를 출력하는 프로그램을 작성하시오.

입력

첫째 줄에 M과 N, 그리고 K가 빈칸을 사이에 두고 차례로 주어진다. M, N, K는 모두 100 이하의 자연수이다. 둘째 줄부터 K개의 줄에는 한 줄에 하나씩 직사각형의 왼쪽 아래 꼭짓점의 x, y좌표값과 오른쪽 위 꼭짓점의 x, y좌표값이 빈칸을 사이에 두고 차례로 주어진다. 모눈종이의 왼쪽 아래 꼭짓점의 좌표는 (0,0)이고, 오른쪽 위 꼭짓점의 좌표는(N,M)이다. 입력되는 K개의 직사각형들이 모눈종이 전체를 채우는 경우는 없다.

출력

첫째 줄에 분리되어 나누어지는 영역의 개수를 출력한다. 둘째 줄에는 각 영역의 넓이를 오름차순으로 정렬하여 빈칸을 사이에 두고 출력한다.

예제 입력 1 복사

5 7 3
0 2 4 4
1 1 2 5
4 0 6 2

예제 출력 1 복사

3
1 7 13

출처

Olympiad > 한국정보올림피아드 > 한국정보올림피아드시․도지역본선 > 지역본선 2006 > 고등부 2번


3. 풀이 과정

입력에서 주어지는 좌표를 기준으로 맵에 영역을 표시하고 최종적으로 맵에 영역이 표시가 안 된 구역의 수와 각 구역의 넓이를 구하는 문제였어요. 일단 주어진 좌표를 기준으로 맵에만 제대로 표시한다면 이후에는 DFS, BFS 등을 이용하여 탐색을 하면 문제가 요구하는 각 영역의 개수와 넓이를 구할 수 있게 돼요.

 

입력 예시가 보면 0 2 4 4 이런 식으로 주어지는데 이건 0,2 좌표부터 4,4 좌표까지 맵에 그리라는 건데 이걸 그대로 코드에 바로 사용할 순 없었어요.

맵에서 주어지는 좌표는 이렇게 각 꼭 짓점의 좌표를 주는 건데 제가 맵 배열을 관리할 때 각 좌표는 

이렇게 되기 때문이에요.

만약 입력에서 주어지는 좌표를 기준으로  x2, y2 좌표에서 단순히 -1 만 해서 맵에 그려주게 되면 원하는 그림이 나와요.

 

 

그래서 입력으로 주어지는 꼭짓점 좌표를 이용해서 맵에 표시를 해주기 위해서 다음과 같은 코드를 사용했어요.

 


4. 코드

#include <iostream>
#include <stack>
#include <vector>
#include <algorithm>

using namespace std;
#define X first
#define Y second
#define MAX 100 + 1

int N, M, K;

// 상하좌우
int dx[4] = {0, 0, -1, 1};
int dy[4] = {1, -1, 0, 0};
int map[MAX][MAX];
int visit[MAX][MAX];

int Dfs(int x, int y)
{
	int cnt = 1;
	stack<pair<int, int>> s;
	s.push({x , y});
	visit[x][y] = 1;
	while (!s.empty())
	{
		pair<int, int> cur = s.top();
		s.pop();
		if (visit[cur.X][cur.Y] == 0)
			cnt++;
		visit[cur.X][cur.Y] = 1;
		

		for (int dir = 0; dir < 4; dir++)
		{
			int nx = cur.X + dx[dir];
			int ny = cur.Y + dy[dir];

			if (nx < 0 || ny < 0 || nx >= N || ny >= M)
				continue;

			if (map[nx][ny] == 1 || visit[nx][ny] == 1)
				continue;

			s.push({ nx, ny });
		}
	}
	return cnt;
}

int main()
{
	vector<int> v;
	// M 세로 N 가로
	cin >> M >> N >> K;

	for (int i = 0; i < K; i++)
	{
		int x1, y1, x2, y2;
		cin >> x1 >> y1 >> x2 >> y2;

		for (int y = y1; y < y2; y++)
		{
			for (int x = x1; x < x2; x++)
			{
				map[x][y] = 1;
			}			
		}
	}

	for (int y = M - 1; y >= 0; y--)
	{
		for (int x = 0; x < N; x++)
		{
			if (map[x][y] == 0 && visit[x][y] == 0)
			{
				v.push_back(Dfs(x, y));
			}
		}
	}

	sort(v.begin(), v.end());

	cout << v.size() << endl;
	for (int i = 0; i < v.size(); i++)
	{
		cout << v[i] << " ";
	}

	return 0;
}

 

 

300x250
300x250

1. 제목

- 백준 1743 음식물 피하기

- BOJ 1743 음식물 피하기

문제 링크 : 1743번: 음식물 피하기 (acmicpc.net)


2. 문제 설명

음식물 피하기 


시간 제한 메모리 제한 제출 정답 맞힌 사람 정답 비율
2 초 128 MB 11539 5382 4316 46.933%

문제

코레스코 콘도미니엄 8층은 학생들이 3끼의 식사를 해결하는 공간이다. 그러나 몇몇 비양심적인 학생들의 만행으로 음식물이 통로 중간 중간에 떨어져 있다. 이러한 음식물들은 근처에 있는 것끼리 뭉치게 돼서 큰 음식물 쓰레기가 된다. 

이 문제를 출제한 선생님은 개인적으로 이러한 음식물을 실내화에 묻히는 것을 정말 진정으로 싫어한다. 참고로 우리가 구해야 할 답은 이 문제를 낸 조교를 맞추는 것이 아니다. 

통로에 떨어진 음식물을 피해가기란 쉬운 일이 아니다. 따라서 선생님은 떨어진 음식물 중에 제일 큰 음식물만은 피해 가려고 한다. 

선생님을 도와 제일 큰 음식물의 크기를 구해서 “10ra"를 외치지 않게 도와주자.

입력

첫째 줄에 통로의 세로 길이 N(1 ≤ N ≤ 100)과 가로 길이 M(1 ≤ M ≤ 100) 그리고 음식물 쓰레기의 개수 K(1 ≤ K ≤ N×M)이 주어진다.  그리고 다음 K개의 줄에 음식물이 떨어진 좌표 (r, c)가 주어진다.

좌표 (r, c)의 r은 위에서부터, c는 왼쪽에서부터가 기준이다. 입력으로 주어지는 좌표는 중복되지 않는다.

출력

첫째 줄에 음식물 중 가장 큰 음식물의 크기를 출력하라.

예제 입력 1 복사

3 4 5
3 2
2 2
3 1
2 3
1 1

예제 출력 1 복사

4

힌트

# . . .
. # # .
# # . .

위와 같이 음식물이 떨어져있고 제일큰 음식물의 크기는 4가 된다. (인접한 것은 붙어서 크게 된다고 나와 있음. 대각선으로는 음식물 끼리 붙을수 없고 상하좌우로만 붙을수 있다.)

출처

Olympiad > USA Computing Olympiad > 2007-2008 Season > USACO November 2007 Contest > Bronze 3번

  • 문제를 번역한 사람: author9
  • 문제의 오타를 찾은 사람: eric00513
  • 데이터를 추가한 사람: wbcho0504

 

 

3. 풀이 과정

탐색알고리즘만 알고 있다면 쉽게 풀 수 있는 문제였어요.
저 같은 경우는 맵 0, 0 좌표부터 탐색을 시작하여 쓰레기가 발견되면 그 지점부터 DFS 탐색을 시작해 주변 지역에 있는 쓰레기 범위를 구하여 result 변수에 저장하여 모든맵 탐색을 끝냈을 때 가장 최댓값을 구하는 방식으로 해결했어요.


4. 코드

#include <iostream>
#include <stack>

#define X first
#define Y second
#define MAX 100 + 2

using namespace std;

int map[MAX][MAX];
int visit[MAX][MAX];
//			상 하 좌 우
int dx[4] = {0, 0, -1, 1};
int dy[4] = {1, -1, 0, 0};

// N : 세로 길이, M : 가로 길이, K : 쓰레기의 개수
int N, M, K;
int result;

void Dfs(int x, int y)
{
	int cnt = 1;
	stack<pair<int, int>> s;
	s.push({x, y});
	visit[x][y] = 1;

	while (!s.empty())
	{
		pair<int, int> cur = s.top();
		s.pop();

		for (int dir = 0; dir < 4; dir++)
		{
			int nx = cur.X + dx[dir];
			int ny = cur.Y + dy[dir];

			if (nx < 1 || ny < 1 || nx > M || ny > N)
				continue;

			if (visit[nx][ny] == 1 || map[nx][ny] == 0)
				continue;

			s.push({nx, ny});
			visit[nx][ny] = 1;
			cnt++;
		}
	}
	if (result < cnt)
		result = cnt;
}

int main()
{
	cin >> N >> M >> K;

	// 맵 정보 입력
	// 쓰레기가 있는곳 좌표만 1로 설정
	for (int i = 0; i < K; i++)
	{
		int x, y;
		// 입력을 y , x로 줌
		cin >> y >> x;
		map[x][y] = 1;
	}

	for (int y = 1; y <= N; y++)
	{
		for (int x = 1; x <= M; x++)
		{
 			if (visit[x][y] == 0 && map[x][y] == 1)
			{
				Dfs(x, y);
			}
		}
	}

	cout << result;
	return 0;
}
300x250
300x250

협업을 하는 과정에서 의도치않은 파일 삭제가 있었어요.

가장 최근 커밋에 보면 원하지않은 파일 삭제가 있었는데 해당 파일을 살리기 위하여 일단 해당 커밋을 [Revert changes in commit...버튼을 눌렀어요. 해당 버튼을 누르니까 바로 아래 커밋했을떄로 돌아가더라고요.

 

그럼 이 커밋을 Undo 할 수 있는 버튼이 생겨요.

이걸 Undo를 하면 이런 상태가 돼요

그럼 당연하게도 커밋한 내용 중 추가한 건 삭제 처리가 될 거고 삭제한 건 추가 처리가 될 거예요

저는 삭제 됐던 파일들은 복구하고 새로 추가한 파일들은 그대로 추가를 해야 하므로 추가된 파일을 삭제한 기록을 [Discard Changes] 해줬어요.

 

푸쉬까지 하면 실수로 삭제한 파일이 복구된 게 서버에 반영된걸 볼 수 있어요.

 

 

지금 같은 경우는 가장 최근에 커밋에서 문제가 발생해서 비교적 쉽게 해결했지만 커밋이 쌓인 상태에서 파일을 복구할 때는 해당 시점으로 간 후 복구할 파일을 따로 파일로 저장해놓은 다음 최신 버전으로 돌아와서 추가하는 식으로 해야 할 거 같아요. 깃을 항상 최신 상태로 유지해야 할 것 같아요.

 

아래 링크는 위방법을 Git Bash를 사용해서 할수있는 방법이 있어서 참고했어요.

[Git] 이전 commit 불러오기/파일 복구하기 :: 무르르릉 (tistory.com)

300x250
300x250

1. 제목

- 백준 11403 경로 찾기

- BOJ 11403 경로 찾기

문제 링크 : 11403번: 경로 찾기 (acmicpc.net)


2. 문제 설명

 

경로 찾기 

 
시간 제한 메모리 제한 제출 정답 맞힌 사람 정답 비율
1 초 256 MB 34968 20419 14875 58.035%

문제

가중치 없는 방향 그래프 G가 주어졌을 때, 모든 정점 (i, j)에 대해서, i에서 j로 가는 경로가 있는지 없는지 구하는 프로그램을 작성하시오.

입력

첫째 줄에 정점의 개수 N (1 ≤ N ≤ 100)이 주어진다. 둘째 줄부터 N개 줄에는 그래프의 인접 행렬이 주어진다. i번째 줄의 j번째 숫자가 1인 경우에는 i에서 j로 가는 간선이 존재한다는 뜻이고, 0인 경우는 없다는 뜻이다. i번째 줄의 i번째 숫자는 항상 0이다.

출력

총 N개의 줄에 걸쳐서 문제의 정답을 인접행렬 형식으로 출력한다. 정점 i에서 j로 가는 경로가 있으면 i번째 줄의 j번째 숫자를 1로, 없으면 0으로 출력해야 한다.

예제 입력 1 복사

3
0 1 0
0 0 1
1 0 0

예제 출력 1 복사

1 1 1
1 1 1
1 1 1

예제 입력 2 복사

7
0 0 0 1 0 0 0
0 0 0 0 0 0 1
0 0 0 0 0 0 0
0 0 0 0 1 1 0
1 0 0 0 0 0 0
0 0 0 0 0 0 1
0 0 1 0 0 0 0

예제 출력 2 복사

1 0 1 1 1 1 1
0 0 1 0 0 0 1
0 0 0 0 0 0 0
1 0 1 1 1 1 1
1 0 1 1 1 1 1
0 0 1 0 0 0 1
0 0 1 0 0 0 0

출처

  • 문제를 만든 사람: baekjoon
  • 데이터를 추가한 사람: degurii

 


3. 풀이 과정

플로이드 워셜을 이용하면 쉽게 풀 수 있는 문제인데 DFS로도 풀 수 있는 문제였어요. 0번째 노드부터 DFS 탐색을 시작하는데 해당 노드에 방문 했을 때 갈 수 있는 경로가 있다면 계속 DFS 탐색을 하며 최종적으로 갈 수 있는 모든 경로를 찾을 수 있게 돼요. 첫번째 예제로 설명을 드리면 탐색 순서가 0 => 1 => 2 순서로 탐색을 하는것을 할수있어요.

예제1

 

예제 2에 3번노드의 탐색을 시각화 해볼게요.

 

 

이런 식으로 0번 노드부터 N-1번 노드까지 탐색을 하면 연결 여부를 알 수 있게 돼요.


4. 함수 설명

void reset() 잠기는 높이가 바뀔때마다 bfs 탐색을 다시 하기 위해서 visited을 초기화 해줘요.


5. 코드

#include <iostream>
#include <vector>
#include <stack>
#include <cstring>
#define MAX 100 + 1
using namespace std;
int N;

vector<int>graph[MAX];
int visit[MAX];

void Reset()
{
    // memset(visit, 0, sizeof(visit)); 
    // 위코드와 같은 역할
    for (int i = 0; i < N; i++)
    {
        visit[i] = 0;
    }
}

void Dfs(int node) 
{
    stack<int> s;    
    s.push(node);

    while (!s.empty())
    {
        int cur = s.top();
        s.pop();

        // 해당 노드가 가지고 있는 경로만큼 탐색
        for (int i = 0; i < graph[cur].size(); i++)
        {
            // 이미 방문을 했다면 스킵하기
            if (visit[graph[cur][i]] == 1)
                continue;

            visit[graph[cur][i]] = 1;
            s.push(graph[cur][i]);
        }
    }
}
int main() 
{
    cin >> N;
    for (int i = 0; i < N; i++) 
    {
        for (int j = 0; j < N; j++) 
        {
            int path;
            cin >> path;
            // 해당 위치로 가는 경로가 있다면
            if (path == 1)
                graph[i].push_back(j);
        }
    }

    for (int i = 0; i < N; i++) 
    {
        // 각 노드마다 방문여부 초기화 해주고
        Reset();
        // 순서대로 방문가능한곳부터 dfs 탐색 시작
        Dfs(i);
        for (int j = 0; j < N; j++)
            cout << visit[j] << " ";
        cout << "\n";
    }
}
300x250
300x250

1. 제목

- 백준 2468 안전 영역

- BOJ 2468 안전 영역

문제 링크 : 2468번: 안전 영역 (acmicpc.net)

 


2. 문제 설명

안전 영역 

 

시간 제한 메모리 제한 제출 정답 맞힌 사람 정답 비율
1 초 128 MB 64782 24290 16366 34.237%

문제

재난방재청에서는 많은 비가 내리는 장마철에 대비해서 다음과 같은 일을 계획하고 있다. 먼저 어떤 지역의 높이 정보를 파악한다. 그 다음에 그 지역에 많은 비가 내렸을 때 물에 잠기지 않는 안전한 영역이 최대로 몇 개가 만들어 지는 지를 조사하려고 한다. 이때, 문제를 간단하게 하기 위하여, 장마철에 내리는 비의 양에 따라 일정한 높이 이하의 모든 지점은 물에 잠긴다고 가정한다.

어떤 지역의 높이 정보는 행과 열의 크기가 각각 N인 2차원 배열 형태로 주어지며 배열의 각 원소는 해당 지점의 높이를 표시하는 자연수이다. 예를 들어, 다음은 N=5인 지역의 높이 정보이다.

이제 위와 같은 지역에 많은 비가 내려서 높이가 4 이하인 모든 지점이 물에 잠겼다고 하자. 이 경우에 물에 잠기는 지점을 회색으로 표시하면 다음과 같다. 

물에 잠기지 않는 안전한 영역이라 함은 물에 잠기지 않는 지점들이 위, 아래, 오른쪽 혹은 왼쪽으로 인접해 있으며 그 크기가 최대인 영역을 말한다. 위의 경우에서 물에 잠기지 않는 안전한 영역은 5개가 된다(꼭짓점으로만 붙어 있는 두 지점은 인접하지 않는다고 취급한다). 

또한 위와 같은 지역에서 높이가 6이하인 지점을 모두 잠기게 만드는 많은 비가 내리면 물에 잠기지 않는 안전한 영역은 아래 그림에서와 같이 네 개가 됨을 확인할 수 있다. 

이와 같이 장마철에 내리는 비의 양에 따라서 물에 잠기지 않는 안전한 영역의 개수는 다르게 된다. 위의 예와 같은 지역에서 내리는 비의 양에 따른 모든 경우를 다 조사해 보면 물에 잠기지 않는 안전한 영역의 개수 중에서 최대인 경우는 5임을 알 수 있다. 

어떤 지역의 높이 정보가 주어졌을 때, 장마철에 물에 잠기지 않는 안전한 영역의 최대 개수를 계산하는 프로그램을 작성하시오. 

입력

첫째 줄에는 어떤 지역을 나타내는 2차원 배열의 행과 열의 개수를 나타내는 수 N이 입력된다. N은 2 이상 100 이하의 정수이다. 둘째 줄부터 N개의 각 줄에는 2차원 배열의 첫 번째 행부터 N번째 행까지 순서대로 한 행씩 높이 정보가 입력된다. 각 줄에는 각 행의 첫 번째 열부터 N번째 열까지 N개의 높이 정보를 나타내는 자연수가 빈 칸을 사이에 두고 입력된다. 높이는 1이상 100 이하의 정수이다.

출력

첫째 줄에 장마철에 물에 잠기지 않는 안전한 영역의 최대 개수를 출력한다.

예제 입력 1 복사

5
6 8 2 6 2
3 2 3 4 6
6 7 3 3 2
7 2 5 3 6
8 9 5 2 7

예제 출력 1 복사

5

예제 입력 2 복사

7
9 9 9 9 9 9 9
9 2 1 2 1 2 9
9 1 8 7 8 1 9
9 2 7 9 7 2 9
9 1 8 7 8 1 9
9 2 1 2 1 2 9
9 9 9 9 9 9 9

예제 출력 2 복사

6

 


3. 풀이 과정

 

잠기는 영역이 바뀔 때마다 안전 구역이 달라지기 때문에 잠기는 영역을 1부터 모두 검사하면서 찾아야 하는 문제라고 생각이 됐어요. 그래서 최대 높이 변수를 입력받을 때 저장한 다음 최대 높이부터 1씩 내려가며 모든 경우를 탐색해서 최대로 나올 수 있는 안전 구역을 구했어요.

 

탐색 과정을 눈으로 보여드리기 위해 첫번째 테스트케이스에 잠긴높이가 5일때 기준으로 가시화 해봤어요.

0,0 부터 탐색을 시작하며 잠긴높이보다 높은 6부터 탐색을 시작해서 안전 영역을 구해여.


4. 함수 설명

void reset() 잠기는 높이가 바뀔때마다 bfs 탐색을 다시 하기 위해서 visited을 초기화 해줘요.


5. 코드

#define _CRT_SECURE_NO_WARNINGS
#define X first
#define Y second
#define MAX 100+1

#include <iostream>
#include <queue>

using namespace std;

int visited[MAX][MAX];
int map[MAX][MAX];

// 동서남북
int dx[] = { 0, 0, -1, 1 };
int dy[] = { -1, 1, 0, 0 };

// N : 맵 크기
int N;
int safetyArea;
queue<pair<int, int>> q;


void bfs(int height)
{
    while (!q.empty())
    {
        pair<int, int> cur = q.front();
        q.pop();
        visited[cur.X][cur.Y] = 1;

        for (int dir = 0; dir < 4; dir++)
        {
            int nx = cur.X + dx[dir];
            int ny = cur.Y + dy[dir];
            // 배열 범위 벗어나는지 체크
            if (nx < 0 || nx >= N || ny < 0 || ny >= N)
                continue;

            // 방문할 지역의 높이가 잠긴 구역보다 낮다면 안전 구역이 아니므로 생략
            if (map[nx][ny] <= height || visited[nx][ny] == 1)
                continue;
            visited[nx][ny] = 1;

            q.push({ nx, ny });
        }
    }
}

void reset()
{
    safetyArea = 0;
    for (int x = 0; x < N; x++)
    {
        for (int y = 0; y < N; y++)
        {
            visited[x][y] = 0;
        }
    }
}

int main()
{
    int height = 0, result = 0;
    ios::sync_with_stdio(0);
    cin.tie(0);

    scanf("%d", &N);
    // M : 가로길이, N : 세로 길이
    // 맵 정보 입력
    for (int x = 0; x < N; x++)
    {
        for (int y = 0; y < N; y++)
        {
            scanf("%d", &map[x][y]);
            if (height < map[x][y])
                height = map[x][y];
        }
    }

    while (height >= 0)
    {
        for (int x = 0; x < N; x++)
        {
            for (int y = 0; y < N; y++)
            {
                // 잠긴 높이보다 지역 높이가 크다면 안전 구역이므로 탐색 시작
                if (map[x][y] > height && visited[x][y] == 0)
                {
                    q.push({ x, y });
                    bfs(height);
                    safetyArea++;
                }
            }
        }
        if (result < safetyArea)
            result = safetyArea;

        reset();
        height--;
    }

    printf("%d", result);
    return 0;
}
300x250
300x250

Player.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;

public class Player : MonoBehaviour
{
    public float moveSpeed;
    private Coroutine moveRoutine;

    public UnityAction<Vector2Int, Vector2Int> onDecideTargetTile;


    private void Update()
    {
        // 마우스 왼쪽버튼 클릭시
        if (Input.GetMouseButtonDown(0))
        {
            // 마우스 클릭시 좌표를 인게임 좌표로 변환하여 mousePos 변수에 저장
            Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            // z값은 사용을 안하므로 x, y 값만 저장후 Move
            int targetPosX = (int)Math.Truncate(mousePos.x);
            int targetPosY = (int)Math.Truncate(mousePos.y);

            int currentPosX = (int)Math.Round(this.transform.position.x);
            int currentPosY = (int)Math.Round(this.transform.position.y);
            Vector2Int curPos = new Vector2Int(currentPosX, currentPosY);
            Vector2Int targetPos = new Vector2Int(targetPosX, targetPosY);
            Debug.LogFormat("curPos : {0}, targetPos : {1}", curPos, targetPos);
            this.onDecideTargetTile(curPos, targetPos);
        }
    }

    // 플레이어 이동 스크립트
    // 매개변수 pathList를 입력받아 경로 단위로 움직입니다.
    public void Move(List<Vector3> pathList)
    {
        if (this.moveRoutine != null)
            this.StopCoroutine(moveRoutine);
        moveRoutine = this.StartCoroutine(this.MoveRoutine(pathList));
    }

    private IEnumerator MoveRoutine(List<Vector3> pathList)
    {
        int pathCount = pathList.Count;

        for (int index = 1; index < pathList.Count; index++)
        {
            if (pathCount < 1)
                break;

            while (true)
            {
                // dir(방향) = 타겟방향 - 플레이어 현재 위치
                var dir = pathList[index] - this.transform.position;
                this.transform.Translate(dir.normalized * this.moveSpeed * Time.deltaTime);

                // 타겟위치와 현재위치의 거리차이가 0.1이하가 될시 while문 빠져나옵니다
                var distance = dir.magnitude;
                if (distance <= 0.1f)
                    break;
                yield return null;
            }
        }
        // move루틴 끝났으므로 null로 초기화
        this.moveRoutine = null;
    }
}

Main.cs

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;   
public class Main : MonoBehaviour
{
    private MapManager mapManager;
    private Player player;

    void Start()
    {
        this.player = GameObject.FindObjectOfType<Player>();
        this.mapManager = GameObject.FindObjectOfType<MapManager>();
        this.player.onDecideTargetTile = (startPos, targetPos) =>
        {
            this.mapManager.PathFinding(startPos, targetPos);
            this.player.Move(this.mapManager.PathList);
        };

    }
}

 

MapManager.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MapManager : MonoBehaviour
{
    public class Node
    {
        public Node(bool _isWall, int _x, int _y) { isWall = _isWall; x = _x; y = _y; }

        public bool isWall;
        public Node ParentNode;

        // G : 시작으로부터 이동했던 거리, H : |가로|+|세로| 장애물 무시하여 목표까지의 거리, F : G + H
        public int x, y, G, H;
        public int F { get { return G + H; } }
    }

    private GridLayout gridMap;


    int sizeX, sizeY;
    Node[,] NodeArray;
    Node StartNode, TargetNode, CurNode;
    List<Node> OpenList, ClosedList;
    public Vector2Int bottomLeft, topRight;
    private List<Node> FinalNodeList;

    public bool allowDiagonal, dontCrossCorner;
    public List<Vector3> PathList
    {
        get; private set;
    }

    private void Start()
    {
        this.gridMap = GameObject.Find("GridMap").GetComponent<GridLayout>();
        this.PathList = new List<Vector3>();
    }


    public void PathFinding(Vector2Int startPos, Vector2Int targetPos)
    {
        this.PathList.Clear();
        // 캡크기 설정
        // NodeArray의 크기 정해주고, isWall, x, y 대입
        sizeX = topRight.x - bottomLeft.x + 1;
        sizeY = topRight.y - bottomLeft.y + 1;
        NodeArray = new Node[sizeX, sizeY];


        // 장애물 감지
        // 맵에 Layer가 Wall인 태그를 가진 오브젝트가 있는지 검사하고 있을시 지나갈수없는 통로로 설정한다.
        for (int i = 0; i < sizeX; i++)
        {
            for (int j = 0; j < sizeY; j++)
            {
                bool isWall = false;
                foreach (Collider2D col in Physics2D.OverlapCircleAll(new Vector2(i + bottomLeft.x + 0.5f, j + bottomLeft.y + 0.5f), 0.4f))
                {
                    if (col.gameObject.layer == LayerMask.NameToLayer("Wall")) isWall = true;
                }

                NodeArray[i, j] = new Node(isWall, i + bottomLeft.x, j + bottomLeft.y);
            }
        }
        // 시작과 끝 노드, 열린리스트와 닫힌리스트, 마지막리스트 초기화
        StartNode = NodeArray[startPos.x - bottomLeft.x, startPos.y - bottomLeft.y];
        TargetNode = NodeArray[targetPos.x - bottomLeft.x, targetPos.y - bottomLeft.y];

        OpenList = new List<Node>() { StartNode };
        ClosedList = new List<Node>();
        FinalNodeList = new List<Node>();

        while (OpenList.Count > 0)
        {
            // 열린리스트 중 가장 F가 작고 F가 같다면 H가 작은 걸 현재노드로 하고 열린리스트에서 닫힌리스트로 옮기기
            CurNode = OpenList[0];
            for (int i = 1; i < OpenList.Count; i++)
                if (OpenList[i].F <= CurNode.F && OpenList[i].H < CurNode.H) CurNode = OpenList[i];

            OpenList.Remove(CurNode);
            ClosedList.Add(CurNode);


            // 마지막
            if (CurNode == TargetNode)
            {
                Node TargetCurNode = TargetNode;
                while (TargetCurNode != StartNode)
                {
                    FinalNodeList.Add(TargetCurNode);
                    TargetCurNode = TargetCurNode.ParentNode;
                }
                FinalNodeList.Add(StartNode);
                FinalNodeList.Reverse();

                for (int i = 0; i < FinalNodeList.Count; i++)
                {
                    //print(i + "번째는 " + FinalNodeList[i].x + ", " + FinalNodeList[i].y);
                    Vector3 path = new Vector3(FinalNodeList[i].x, FinalNodeList[i].y, 0);
                    this.PathList.Add(path);
                }
                return;
            }


            // ↗↖↙↘
            if (allowDiagonal)
            {
                OpenListAdd(CurNode.x + 1, CurNode.y + 1);
                OpenListAdd(CurNode.x - 1, CurNode.y + 1);
                OpenListAdd(CurNode.x - 1, CurNode.y - 1);
                OpenListAdd(CurNode.x + 1, CurNode.y - 1);
            }

            // ↑ → ↓ ←
            OpenListAdd(CurNode.x, CurNode.y + 1);
            OpenListAdd(CurNode.x + 1, CurNode.y);
            OpenListAdd(CurNode.x, CurNode.y - 1);
            OpenListAdd(CurNode.x - 1, CurNode.y);
        }
    }

    void OpenListAdd(int checkX, int checkY)
    {
        // 상하좌우 범위를 벗어나지 않고, 벽이 아니면서, 닫힌리스트에 없다면
        if (checkX >= bottomLeft.x && checkX < topRight.x + 1 && checkY >= bottomLeft.y && checkY < topRight.y + 1 && !NodeArray[checkX - bottomLeft.x, checkY - bottomLeft.y].isWall && !ClosedList.Contains(NodeArray[checkX - bottomLeft.x, checkY - bottomLeft.y]))
        {
            // 대각선 허용시, 벽 사이로 통과 안됨
            if (allowDiagonal) if (NodeArray[CurNode.x - bottomLeft.x, checkY - bottomLeft.y].isWall && NodeArray[checkX - bottomLeft.x, CurNode.y - bottomLeft.y].isWall) return;

            // 코너를 가로질러 가지 않을시, 이동 중에 수직수평 장애물이 있으면 안됨
            if (dontCrossCorner) if (NodeArray[CurNode.x - bottomLeft.x, checkY - bottomLeft.y].isWall || NodeArray[checkX - bottomLeft.x, CurNode.y - bottomLeft.y].isWall) return;


            // 이웃노드에 넣고, 직선은 10, 대각선은 14비용
            Node NeighborNode = NodeArray[checkX - bottomLeft.x, checkY - bottomLeft.y];
            int MoveCost = CurNode.G + (CurNode.x - checkX == 0 || CurNode.y - checkY == 0 ? 10 : 14);


            // 이동비용이 이웃노드G보다 작거나 또는 열린리스트에 이웃노드가 없다면 G, H, ParentNode를 설정 후 열린리스트에 추가
            if (MoveCost < NeighborNode.G || !OpenList.Contains(NeighborNode))
            {
                NeighborNode.G = MoveCost;
                NeighborNode.H = (Mathf.Abs(NeighborNode.x - TargetNode.x) + Mathf.Abs(NeighborNode.y - TargetNode.y)) * 10;
                NeighborNode.ParentNode = CurNode;

                OpenList.Add(NeighborNode);
            }
        }
    }

    void OnDrawGizmos()
    {
        if (FinalNodeList == null)
            return;
        if (FinalNodeList.Count != 0)
        {
            for (int i = 0; i < FinalNodeList.Count - 1; i++)
            {
                Gizmos.DrawLine(new Vector2(FinalNodeList[i].x + 0.5f, FinalNodeList[i].y + 0.5f), new Vector2(FinalNodeList[i + 1].x + 0.5f, FinalNodeList[i + 1].y + 0.5f));
            }
        }
    }
}

 

 

 

 

출처 

고라니 유니티2D: 2D 타일맵을 위한 A* 길찾기 알고리즘 (goraniunity2d.blogspot.com)

300x250
300x250

Unity 2021.3.5f1 버전에서는 기본적으로 2D Tilemap Extras가 패키지안에 포함되어 있어요.

근데 프리팹브러쉬가 없어서 Unity-Technologies/2d-extras: Fun 2D Stuff that we'd like to share! (github.com)

 

GitHub - Unity-Technologies/2d-extras: Fun 2D Stuff that we'd like to share!

Fun 2D Stuff that we'd like to share! Contribute to Unity-Technologies/2d-extras development by creating an account on GitHub.

github.com

해당사이트에 방문해서 프로젝트를 받은후 프리팹 브러쉬폴더만 따로 프로젝트에 추가 해줬어요.

근데 이상태에선 에러가 바로나요.

 

 

Assets\PrefabBrushes\BrushMenuItem.cs(7,77): error CS0122: 'EBrushMenuItemOrder' is inaccessible due to its protection level

문제가 되고 있는 EBrushMenuItemOrder는 

여기에 있어요.

 

 

네임스페이스는 같은데도 왜인지 보호수준떄문에 접근을 할수없다는 오류가 나서 EBrushMenuItemOrder를 public으로 풀어버리는 방법도 있겠지만 패키지 스크립트는 안건드리는쪽으로 하고싶어서 

 internal enum EBrushMenuItemOrder
    {
        RandomBrush = 3,
        PrefabBrush,
        PrefabRandomBrush
    }

해당코드를 BrushMenuItem.cs 스크립트에 넣어줬어요.

 

잘 작동하네요!

 

참고자료

Prefab brush is not showing or working both are error · Issue #339 · Unity-Technologies/2d-extras (github.com)

300x250
300x250

1. 영역을 잡아줍니다.

2. 프레임을 잡아줍니다.

3. 체력을 표시해줄 Fill을 넣어줍니다.

체력 슬라이더바 셋팅은 끝났습니다.

 

4. 게임 오브젝트로 플레이어를 생성해주고 체력바를 위치시킬 포인트를 잡아줍니다.

 

빈오브젝트 이기때문에 눈에 안보여서 아이콘을 달아주면 확인하기 편해요.

 

Hero.cs

using System.Collections;
using UnityEngine;
using UnityEngine.Events;

namespace MonsterHpExam
{
    public class Hero : MonoBehaviour
    {
        private Transform hpGaugePoint;
        public float speed;
        private Coroutine moveRoutine;

        public UIHpGauge uiHpGauge;
        public UnityAction<float> onHit;
        private int maxHp = 100;
        private int currentHp;

        private void Start()
        {
            this.hpGaugePoint = transform.Find("HpGaugePoint").transform;
            this.uiHpGauge.Init(this.hpGaugePoint);
            this.currentHp = maxHp;
        }

        private void Update()
        {
            // 마우스 왼쪽버튼 클릭시
            if (Input.GetMouseButtonDown(0))
            {
                // 마우스 클릭시 좌표를 인게임 좌표로 변환하여 mousePos 변수에 저장
                Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
                // z값은 사용을 안하므로 x, y 값만 저장후 Move
                Vector3 tpos = new Vector3(mousePos.x, mousePos.y, 0);
                this.Move(tpos);
            }
            else if (Input.GetMouseButtonDown(1))
            {
                Hit(10);
            }
        }

        public void Move(Vector3 tpos)
        {
            if (this.moveRoutine != null)
                this.StopCoroutine(moveRoutine);
            moveRoutine = this.StartCoroutine(this.MoveRoutine(tpos));
        }

        private IEnumerator MoveRoutine(Vector3 tpos)
        {
            while (true)
            {
                var dir = tpos - this.transform.position;
                this.transform.Translate(dir.normalized * this.speed * Time.deltaTime);

                var distance = dir.magnitude;
                if (distance <= 0.1f)
                    break;
                yield return null;
            }
            this.moveRoutine = null;
        }

        private void Hit(int damage)
        {            
            currentHp -= damage;
            this.onHit((float)currentHp / maxHp);
        }
    }
}

GameMain.cs

using UnityEngine;

namespace MonsterHpExam
{
    public class GameMain : MonoBehaviour
    {
        private Hero hero;
        private UIHpGauge uiHpGauge;
        // Start is called before the first frame update
        void Start()
        {
            this.hero = GameObject.FindObjectOfType<Hero>();
            this.uiHpGauge = GameObject.FindObjectOfType<UIHpGauge>();

            this.hero.onHit = (currentHpPer) =>
            {
                this.uiHpGauge.SetHpUI(currentHpPer);
            };
        }

    }
}

UIHpGauge.cs

using System.Collections;
using UnityEngine;
using UnityEngine.UI;

namespace MonsterHpExam
{
    public class UIHpGauge : MonoBehaviour
    {
        private Image fillImg;
        private Transform targetTransform;
        private RectTransform rectTrans;

        public void Init(Transform targetTransform)
        {
            this.targetTransform = targetTransform;
            this.rectTrans = this.GetComponent<RectTransform>();
            this.fillImg = transform.Find("Fill").GetComponent<Image>();
            this.Move();
        }

        private void Move()
        {
            StartCoroutine(this.MoveRoutine());
        }

        private IEnumerator MoveRoutine()
        {
            while(true)
            {
                Vector3 screenPosition = Camera.main.WorldToScreenPoint(targetTransform.position);
                rectTrans.position = screenPosition;

                yield return null;
            }
        }
        public void SetHpUI(float currentHpPer)
        {
            if (currentHpPer > 1 || currentHpPer < 0)
                return;
            this.fillImg.fillAmount = currentHpPer;
        }
    }
}

마우스 왼쪽버튼을 클릭하면 플레이어가 움직이고 오른쪽 버튼을 클릭하면 HP가 10%씩 떨어지게 하고 테스트를 해봤습니다.

 

 

 

300x250
300x250

1. 영역을 잡아줘요.

 

 

2. 백그라운드 이미지를 넣어줘요.

3. 프레임을 넣어줘요.

4. 스킬 아이콘을 넣어줘요

5. 스킬 쿨타임을 표시해줄때 어두운 이미지를 연출할 그림자를 넣어줘요.

6. 스킬 쿨타임을 연출할 텍스트를 넣어줘요.

7. 처음 영역을 잡은 오브젝트에 작성한 스크립트와 Image, Button컴포넌트를 추가해줘요.

Image는 Raycast Target을 이용해 클릭이벤트를 가져오기위해서 넣는것이기 때문에 컬러 알파값을0으로 빼줘요.

 

UISkillBtn.cs

using System.Collections;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

namespace UISkillBtnExam
{
    public class UISkillBtn : MonoBehaviour
    {
        public Button btn;
        public float coolTime = 10f;
        public TMP_Text textCoolTime;

        private Coroutine coolTimeRoutine;

        public Image imgFill;

        public void Init()
        {
            this.textCoolTime.gameObject.SetActive(false);
            this.imgFill.fillAmount = 0;
        }
        // Start is called before the first frame update
        void Start()
        {
            this.btn = this.GetComponent<Button>();
            this.btn.onClick.AddListener(() =>
            {
                if (this.coolTimeRoutine != null)
                {
                    Debug.Log("쿨타임 중입니다...");
                }
                else
                {
                    this.coolTimeRoutine = this.StartCoroutine(this.CoolTimeRoutine());
                }
            });

            Init();
        }

        private IEnumerator CoolTimeRoutine()
        {
            Debug.Log(textCoolTime);
            this.textCoolTime.gameObject.SetActive(true);
            var time = this.coolTime;

            while (true)
            {
                time -= Time.deltaTime;
                this.textCoolTime.text = time.ToString("F1");

                var per = time / this.coolTime;
                //Debug.Log(per);
                this.imgFill.fillAmount = per;

                if (time <= 0)
                {
                    this.textCoolTime.gameObject.SetActive(false);
                    break;
                }
                yield return null;
            }

            this.coolTimeRoutine = null;
        }
    }
}

 

 

 

300x250
300x250

1. 제목

- 백준 1991 트리 순회

- BOJ 1991 트리 순회

문제 링크 : 1991번: 트리 순회 (acmicpc.net)

 


2. 문제 설명

 

트리 순회 

 
시간 제한 메모리 제한 제출 정답 맞힌 사람 정답 비율
2 초 128 MB 35985 23266 17704 65.893%

문제

이진 트리를 입력받아 전위 순회(preorder traversal), 중위 순회(inorder traversal), 후위 순회(postorder traversal)한 결과를 출력하는 프로그램을 작성하시오.

예를 들어 위와 같은 이진 트리가 입력되면,

  • 전위 순회한 결과 : ABDCEFG // (루트) (왼쪽 자식) (오른쪽 자식)
  • 중위 순회한 결과 : DBAECFG // (왼쪽 자식) (루트) (오른쪽 자식)
  • 후위 순회한 결과 : DBEGFCA // (왼쪽 자식) (오른쪽 자식) (루트)

가 된다.

입력

첫째 줄에는 이진 트리의 노드의 개수 N(1 ≤ N ≤ 26)이 주어진다. 둘째 줄부터 N개의 줄에 걸쳐 각 노드와 그의 왼쪽 자식 노드, 오른쪽 자식 노드가 주어진다. 노드의 이름은 A부터 차례대로 알파벳 대문자로 매겨지며, 항상 A가 루트 노드가 된다. 자식 노드가 없는 경우에는 .으로 표현한다.

출력

첫째 줄에 전위 순회, 둘째 줄에 중위 순회, 셋째 줄에 후위 순회한 결과를 출력한다. 각 줄에 N개의 알파벳을 공백 없이 출력하면 된다.

예제 입력 1 복사

7
A B C
B D .
C E F
E . .
F . G
D . .
G . .

예제 출력 1 복사

ABDCEFG
DBAECFG
DBEGFCA

3. 풀이 과정

 

C++에서 제공하는 map(key, value)형태의 자료구조로 트리를 구현해서 풀어봤어요.

 


4. 함수 설명

preOreder(), inOrder, postOreder() 세함수 모두 재귀호출로 작동하며 출력순서만 차이가 있어요.

출력순서 = 데이터를 처리하는 순서


5. 코드

 

#pragma warning(disable:4996)
#include <iostream>
#include <map>

using namespace std;

struct Node
{
	char left;
	char right;
};


// 맵으로 트리 구현
map<char, Node> m;

// 전위 순회
void preOrder(char node) 
{ 
	// root - left - right
	if (node == '.') return;

	printf("%c", node);
	preOrder(m[node].left);
	preOrder(m[node].right);
}

// 중위 순회
void inOrder(char node) 
{ 
	// left - root - right
	if (node == '.') return;

	inOrder(m[node].left);
	printf("%c", node);
	inOrder(m[node].right);
}

// 후위 순회
void postOrder(char node) 
{ 
	// left - right - root
	if (node == '.') return;

	postOrder(m[node].left);
	postOrder(m[node].right);
	printf("%c", node);
}

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

	char node, left, right;
	// 트리 입력받기
	for (int i = 0; i < n; i++) 
	{
		cin >> node >> left >> right;
		m[node].left = left;
		m[node].right = right;
		// m.insert({ node, {left, right} }); 위와 같다.
	}

	preOrder('A');
	printf("\n");

	inOrder('A');
	printf("\n");

	postOrder('A');

	return 0;
}
300x250
300x250

1. 제목

- 백준 7576 토마토

- BOJ 7576 토마토

문제 링크 : 7576번: 토마토 (acmicpc.net)


2. 문제 설명

토마토 


시간 제한 메모리 제한 제출 정답 맞힌 사람 정답 비율
1 초 256 MB 128233 47215 29782 34.899%

문제

 

철수의 토마토 농장에서는 토마토를 보관하는 큰 창고를 가지고 있다. 토마토는 아래의 그림과 같이 격자 모양 상자의 칸에 하나씩 넣어서 창고에 보관한다. 

창고에 보관되는 토마토들 중에는 잘 익은 것도 있지만, 아직 익지 않은 토마토들도 있을 수 있다. 보관 후 하루가 지나면, 익은 토마토들의 인접한 곳에 있는 익지 않은 토마토들은 익은 토마토의 영향을 받아 익게 된다. 하나의 토마토의 인접한 곳은 왼쪽, 오른쪽, 앞, 뒤 네 방향에 있는 토마토를 의미한다. 대각선 방향에 있는 토마토들에게는 영향을 주지 못하며, 토마토가 혼자 저절로 익는 경우는 없다고 가정한다. 철수는 창고에 보관된 토마토들이 며칠이 지나면 다 익게 되는지, 그 최소 일수를 알고 싶어 한다.

토마토를 창고에 보관하는 격자모양의 상자들의 크기와 익은 토마토들과 익지 않은 토마토들의 정보가 주어졌을 때, 며칠이 지나면 토마토들이 모두 익는지, 그 최소 일수를 구하는 프로그램을 작성하라. 단, 상자의 일부 칸에는 토마토가 들어있지 않을 수도 있다.

입력

첫 줄에는 상자의 크기를 나타내는 두 정수 M,N이 주어진다. M은 상자의 가로 칸의 수, N은 상자의 세로 칸의 수를 나타낸다. 단, 2 ≤ M,N ≤ 1,000 이다. 둘째 줄부터는 하나의 상자에 저장된 토마토들의 정보가 주어진다. 즉, 둘째 줄부터 N개의 줄에는 상자에 담긴 토마토의 정보가 주어진다. 하나의 줄에는 상자 가로줄에 들어있는 토마토의 상태가 M개의 정수로 주어진다. 정수 1은 익은 토마토, 정수 0은 익지 않은 토마토, 정수 -1은 토마토가 들어있지 않은 칸을 나타낸다.

토마토가 하나 이상 있는 경우만 입력으로 주어진다.

출력

여러분은 토마토가 모두 익을 때까지의 최소 날짜를 출력해야 한다. 만약, 저장될 때부터 모든 토마토가 익어있는 상태이면 0을 출력해야 하고, 토마토가 모두 익지는 못하는 상황이면 -1을 출력해야 한다.

예제 입력 1 복사

6 4
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 1

예제 출력 1 복사

8

예제 입력 2 복사

6 4
0 -1 0 0 0 0
-1 0 0 0 0 0
0 0 0 0 0 0
0 0 0 0 0 1

예제 출력 2 복사

-1

예제 입력 3 복사

6 4
1 -1 0 0 0 0
0 -1 0 0 0 0
0 0 0 0 -1 0
0 0 0 0 -1 1

예제 출력 3 복사

6

예제 입력 4 복사

5 5
-1 1 0 0 0
0 -1 -1 -1 0
0 -1 -1 -1 0
0 -1 -1 -1 0
0 0 0 0 0

예제 출력 4 복사

14

예제 입력 5 복사

2 2
1 -1
-1 1

예제 출력 5 복사

0

출처

Olympiad > 한국정보올림피아드 > 한국정보올림피아드시․도지역본선 > 지역본선 2013 > 고등부 1번

 

 

 

 


3. 풀이 과정

BFS를 활용하면 쉽게 풀수 있는 문제 였어요.

BFS는 현재 노드에서 인접한 범위부터 탐색을 하는 특성을 가지고 있어요.

 

그래서 이러한 성질을 이용해서 익은 토마토가 있는 시점부터 1씩 해당 지역에 값을 1씩 증가시켜주면 해당 지역에 있는 토마토가 익는 데까지 걸린 소요일-1의 값을 구할 수 있어요. 문제에서는 모든 토마토가 익는 데까지 수요일을 구하라고 하였으니 탐색을 할 때마다 해당 지역에 있는 토마토가 익는 데까지 걸린 수요일을 비교하며 최댓값을 찾아 -1을 해주면 문제에서 요구하는 답을 구할 수 있어요.

 


4. 함수 설명

check() : BFS 탐색을 끝낸 후에도 안 익은 토마토가 있는지 확인하며 안 익은 토마토가 있으면 result 값을 0으로 초기화 해줘요.


5. 코드

#define _CRT_SECURE_NO_WARNINGS
#define X first
#define Y second
#define MAX 1000+1

#include <iostream>
#include <queue>

using namespace std;

int visited[MAX][MAX];
int map[MAX][MAX];

// 동서남북
int dx[] = { 0, 0, -1, 1 };
int dy[] = { -1, 1, 0, 0 };

// M : 가로길이, N : 세로 길이
int N, M;
int result;
queue<pair<int, int>> q;

// 맵에 안익은 토마토가 있는지 체크
void check()
{
    for (int x = 0; x < N; x++)
    {
        for (int y = 0; y < M; y++)
        {           
            if (map[x][y] == 0)
            {
                // 마지막에 --를 해주므로 0으로 초기화해서 안익은 토마토가 있을경우 -1값 유도
                result = 0;
                return;
            }
        }
    }
}

void bfs()
{    
    while (!q.empty())
    {
        // q에 들어있는 토마토를 꺼내서 cur에 할당
        pair<int, int> cur = q.front();
        q.pop();

        // 각 맵에는 익는데 까지 걸린 소요일-1 값을 갖고있으므로 q에서 꺼낸 토마토의 값을 비교하여 최대값 구하기  
        if (result < map[cur.X][cur.Y])
            result = map[cur.X][cur.Y];

        for (int dir = 0; dir < 4; dir++)
        {                  
            int nx = cur.X + dx[dir];
            int ny = cur.Y + dy[dir];
            // 동서남북 탐색중 배추밭을 벗어나거나 
            if (nx < 0 || nx >= N || ny < 0 || ny >= M)
                continue;
            // 이미 방문을 했던곳이거나, 해당지역에 안익은 토마토가 없을경우 방문할 필요가 없으므로 방문처리 X
            if (visited[nx][ny] == 1 || map[nx][ny] != 0)
                continue;
            visited[nx][ny] = 1;
            // 방문할때마다 익게만든 토마토가 갖고있는값에서 1씩 더해줌
            map[nx][ny] = map[cur.X][cur.Y] + 1;

            // 방문한지역에도 익은 토마토가 새롭게 생겼으므로 q에 넣어주기
            q.push({ nx, ny });
        }
    }
}

int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);

    scanf("%d %d", &M, &N);
    // M : 가로길이, N : 세로 길이
    // 맵 정보 입력
    for (int x = 0; x < N; x++) 
    {
        for (int y = 0; y < M; y++) 
        {
            scanf("%d", &map[x][y]);
            // 맵 정보 입력중 토마토가 있을경우 q에 미리 넣어둔다.
            if (map[x][y] == 1)
            {
                q.push({ x, y });
                // q에 있는 지역은 반드시 방문을 할예정이므로 미리 방문체크
                visited[x][y] = 1;
            }
        }
    }
    bfs();
    check();

    result--;
    printf("%d", result);
    return 0;
}
300x250
300x250

1. 제목

- 백준 1012 유기농 배추

- BOJ 1012 유기농 배추

문제 링크 : 1012번: 유기농 배추 (acmicpc.net)


2. 문제 설명

유기농 배추 

 

시간 제한, 메모리 제한, 제출, 정답, 맞힌 사람정답 비율

 

1 초 512 MB 116022 45115 30516 36.918%

문제

차세대 영농인 한나는 강원도 고랭지에서 유기농 배추를 재배하기로 하였다. 농약을 쓰지 않고 배추를 재배하려면 배추를 해충으로부터 보호하는 것이 중요하기 때문에, 한나는 해충 방지에 효과적인 배추흰지렁이를 구입하기로 결심한다. 이 지렁이는 배추근처에 서식하며 해충을 잡아 먹음으로써 배추를 보호한다. 특히, 어떤 배추에 배추흰지렁이가 한 마리라도 살고 있으면 이 지렁이는 인접한 다른 배추로 이동할 수 있어, 그 배추들 역시 해충으로부터 보호받을 수 있다. 한 배추의 상하좌우 네 방향에 다른 배추가 위치한 경우에 서로 인접해있는 것이다.

한나가 배추를 재배하는 땅은 고르지 못해서 배추를 군데군데 심어 놓았다. 배추들이 모여있는 곳에는 배추흰지렁이가 한 마리만 있으면 되므로 서로 인접해있는 배추들이 몇 군데에 퍼져있는지 조사하면 총 몇 마리의 지렁이가 필요한지 알 수 있다. 예를 들어 배추밭이 아래와 같이 구성되어 있으면 최소 5마리의 배추흰지렁이가 필요하다. 0은 배추가 심어져 있지 않은 땅이고, 1은 배추가 심어져 있는 땅을 나타낸다.

1 1 0 0 0 0 0 0 0 0
0 1 0 0 0 0 0 0 0 0
0 0 0 0 1 0 0 0 0 0
0 0 0 0 1 0 0 0 0 0
0 0 1 1 0 0 0 1 1 1
0 0 0 0 1 0 0 1 1 1

입력

입력의 첫 줄에는 테스트 케이스의 개수 T가 주어진다. 그 다음 줄부터 각각의 테스트 케이스에 대해 첫째 줄에는 배추를 심은 배추밭의 가로길이 M(1 ≤ M ≤ 50)과 세로길이 N(1 ≤ N ≤ 50), 그리고 배추가 심어져 있는 위치의 개수 K(1 ≤ K ≤ 2500)이 주어진다. 그 다음 K줄에는 배추의 위치 X(0 ≤ X ≤ M-1), Y(0 ≤ Y ≤ N-1)가 주어진다. 두 배추의 위치가 같은 경우는 없다.

출력

각 테스트 케이스에 대해 필요한 최소의 배추흰지렁이 마리 수를 출력한다.

예제 입력 1 

2
10 8 17
0 0
1 0
1 1
4 2
4 3
4 5
2 4
3 4
7 4
8 4
9 4
7 5
8 5
9 5
7 6
8 6
9 6
10 10 1
5 5

예제 출력 1 

5
1

예제 입력 2 

1
5 3 6
0 2
1 2
2 2
3 2
4 2
4 0

예제 출력 2 

2

3. 풀이 과정

DFS나 BFS를 활용하면 쉽게 풀수 있는 문제 였어요.

두가지 모두 사용해서 풀어 보았어요. 기본적인 BFS, DFS코드는 바킹독님의 코드를 사용 했습니다.

DFS : basic-algo-lecture/DFS.cpp at master · encrypted-def/basic-algo-lecture (github.com)

 

GitHub - encrypted-def/basic-algo-lecture: 바킹독의 실전 알고리즘 강의 자료

바킹독의 실전 알고리즘 강의 자료. Contribute to encrypted-def/basic-algo-lecture development by creating an account on GitHub.

github.com

BFS : basic-algo-lecture/BFS.cpp at master · encrypted-def/basic-algo-lecture (github.com)

 

GitHub - encrypted-def/basic-algo-lecture: 바킹독의 실전 알고리즘 강의 자료

바킹독의 실전 알고리즘 강의 자료. Contribute to encrypted-def/basic-algo-lecture development by creating an account on GitHub.

github.com

 

동서남북으로 탐색을 진행하며 배추가 서로 인접되어있는 배추를 찾을때마다 방문처리를 해서 해당 지역을 다시 방문 하지 않음으로써 배추들의 그룹을 찾을수 있어요. BFS, DFS탐색을 한번씩 할때마다 한개의 그룹이 있는것 이므로 탐색을 한번씩 완료 할떄마다 배추흰지렁이수를 1씩 증가시켜주면 풀수 있는 문제였어요.


4. 함수 설명

reset() : 테스트 케이스가 여러개 이다 보니 테스트 케이스를 진행할때마다 배추밭 map배열과 방문자 처리용 visited배열을 초기화 해주는 함수에요.


5. 코드

#define _CRT_SECURE_NO_WARNINGS
#define X first
#define Y second
#define MAX 50+1

#include <iostream>
#include <stack>
#include <queue>

using namespace std;

int visited[MAX][MAX];
int map[MAX][MAX];


// T : 테스트 케이스 갯수, M : 배추밭의 가로 길이
// N : 배추밭의 세로 길이, K : 배추 갯수
int T, M, N, K;
// wormCnt : 배추흰지렁이
int wormCnt;
// 동서남북
int dx[] = { 0, 0, -1, 1 };
int dy[] = { -1, 1, 0, 0 };


// 테스트 케이스마다 배추밭맵, 방문처리맵 초기화
void reset()
{
    wormCnt = 0;
    for (int x = 0; x < N; x++)
    {
        for (int y = 0; y < M; y++)
        {
            map[x][y] = 0;
            visited[x][y] = 0;
        }
    }
}

void dfs_stack(int x, int y)
{
    stack<pair<int, int>> s;
    // 출발 지점 방문처리
    visited[x][y] = 1;
    s.push({ x,y });

    while (!s.empty())
    {
        pair<int, int> cur = s.top();
        s.pop();

        for (int dir = 0; dir < 4; dir++)
        {
            // nx = nextX, ny = nextY             
            int nx = cur.X + dx[dir];
            int ny = cur.Y + dy[dir];
            // 동서남북 탐색중 배추밭을 벗어나거나 
            if (nx < 0 || nx >= N || ny < 0 || ny >= M)
                continue;
            // 이미 방문을 했던곳이거나, 해당지역에 배추가 없을경우 방문할 필요가 없으므로 방문처리 X
            if (visited[nx][ny] == 1 || map[nx][ny] != 1)
                continue;
            visited[nx][ny] = 1;
            s.push({nx, ny});
        }
    }
}

void bfs_queue(int x, int y)
{
    queue<pair<int, int>> q;
    // 출발 지점 방문처리
    visited[x][y] = 1;
    q.push({x,y});
    while (!q.empty())
    {
        pair<int, int> cur = q.front();
        q.pop();
        for (int dir = 0; dir < 4; dir++)
        {
            // nx = nextX, ny = nextY             
            int nx = cur.X + dx[dir];
            int ny = cur.Y + dy[dir];
            // 동서남북 탐색중 배추밭을 벗어나거나 
            if (nx < 0 || nx >= N || ny < 0 || ny >= M)
                continue;
            // 이미 방문을 했던곳이거나, 해당지역에 배추가 없을경우 방문할 필요가 없으므로 방문처리 X
            if (visited[nx][ny] == 1 || map[nx][ny] != 1)
                continue;
            visited[nx][ny] = 1;
            q.push({ nx, ny });
        }
    }
}
int main()
{
    ios::sync_with_stdio(0);
    cin.tie(0);

    cin >> T;    
    
    for (int t = 0; t < T; t++)
    {        
        cin >> M >> N >> K;
        reset();
        // 배추밭 데이터 입력
        for (int k = 0; k < K; k++)
        {
            int x, y;
            cin >> x >> y;
            // 입력 데이터와 맵 좌표를 맞추기위해 y x 바꿔주기
            // ex 1 1 0
            //    0 1 0
            //    0 0 0
            // (0,0), (0,1), (1,1)
            map[y][x] = 1;
        }
        for (int x = 0; x < N; x++)
        {
            for (int y = 0; y < M; y++)
            {
                if (map[x][y] == 1 && visited[x][y] == 0)
                {
                    //dfs_stack(x,y);
                    bfs_queue(x,y);
                    // 탐색이 끝날떄마다 배추흰지렁이 한마리씩 늘려주기
                    wormCnt++;
                }
            }
        }
        cout << wormCnt << endl;
    }

    return 0;
}
300x250
300x250

 

 

#pragma warning(disable:4996)

적어주기

300x250

'[개인공부] > C++' 카테고리의 다른 글

[C++ STL] vector 에 2개의 값 넣기  (0) 2020.10.14
300x250

이전에 C#으로 Json데이터를 불러와서 Datamanager 스크립트를 작성해서 관리 하게 한게 있어요.

[C#] Json 데이터 불러와서 활용하기 :: 별빛상자 (tistory.com)

 

[C#] Json 데이터 불러와서 활용하기

studen_data.xlsx => student_data.json [ { "id": 5000, "name": "길동이", "korean": 100, "math": 90, "english": 80 }, { "id": 5001, "name": "철수", "korean": 70, "math": 60, "english": 50 }, { "id":..

starlightbox.tistory.com

 

    public sealed class DataManager
    {
        // 외부에서 접근 읽기 상태로만 접근할 수 있어 데이터 수정을 막고 데이터를 읽기만 할 수 있는 싱글톤 DataManager 선언
        public static readonly DataManager instance = new DataManager();

        // 딕셔너리로 데이터관리
        private Dictionary<int, RawData> dicDatas = new Dictionary<int, RawData>();

        private DataManager()
        {
        }

        // json 데이터를 불러와서 딕셔너리에 저장하는 메소드
        public void LoadData<T>(string path) where T : RawData
        {
            var json = File.ReadAllText(path);
            T[] arr = JsonConvert.DeserializeObject<T[]>(json);
            arr.ToDictionary(x => x.id).ToList().ForEach(x => dicDatas.Add(x.Key, x.Value));
        }

        // id 값으로 딕셔너리에서 검색하는 메소드
        public T GetData<T>(int id) where T : RawData
        {
            var collection = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
            return (T)collection.ToList().Find(x => x.id == id);
        }

        // 특정 데이터 그룹을 검색하고싶을대 해당 데이터 타입을 호출하면 해당 데이터타입 객체만 반환한다.
        // ex(GetDatas<Student>()) = 딕셔너리에 저장된 데이터들중 Student타입을 가진 객체들만 반환
        public IEnumerable<T> GetDatas<T>() where T : RawData
        {
            IEnumerable<RawData> col = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
            return col.Select(x => (T)Convert.ChangeType(x, typeof(T)));
        }

이때 당시에는 Datamanager에서 Json 데이터를 로드화는 과정에서 넣어야 하는 데이터의 타입과 Json데이터 파일 위치를 받아서 로드하는 과정을 가졌는데 이렇게 하면 데이터를 로드하는 과정이 아래처럼 나와요.

 public class DataTest
 {
        public DataTest()
        {
            // json데이터를 불러와서 딕셔너리에 저장
            DataManager.instance.LoadData<StudentData>("./student_data.json");
            DataManager.instance.LoadData<StudyData>("./study_data.json");
            DataManager.instance.LoadData<UserData>("./user_data.json");
        }
 }

 

이코드를 처음 사용할때는 문제점을 못느꼈는데 Unity에서 게임을 만들때 아래처럼 데이터가 로드되는 과정을 연출하고싶을때 문제가 되더라구요.

 

먼저 이런 연출을 하기 위해선 데이터파일의 총 갯수를 알아야 해요.

 

그래서 Datamanager에 데이터파일의 경로를 관리하는 리스트를 선언하고 데이터들의 경로가 담겨있는 데이터파일을 작성한다음 Init함수 에서 dataPathList에 데이터를 추가 해줬어요.

 

DatapathData.cs

public class DatapathData
{
    public int id;
    public string res_name;
    public string type;
}

 

 

datapath_data.json

[
  {
    "id": 0,
    "res_name": "basket_data",
    "type": "BasketData"
  },
  {
    "id": 1,
    "res_name": "item_data",
    "type": "ItemData"
  },
  {
    "id": 2,
    "res_name": "item1_data",
    "type": "ItemData"
  },
  {
    "id": 3,
    "res_name": "item2_data",
    "type": "ItemData"
  },
  {
    "id": 4,
    "res_name": "item3_data",
    "type": "ItemData"
  }
]

id는 중복값을 피하기 위함이고, res_name에는 데이터파일의 경로가 들어가요. type은 해당 json데이터들의 클래스를 적어줘요.

    private IEnumerator LoadAllDataRoutine()
    {
        int idx = 0;
        foreach (var data in this.dataPathList)
        {
            var path = string.Format("Datas/{0}", data.res_name);
            ResourceRequest req = Resources.LoadAsync<TextAsset>(path);
            yield return req;
            float progress = (float)(idx + 1) / this.dataPathList.Count;
            TextAsset asset = (TextAsset)req.asset;

            LoadData<DataType>(asset.text);

            yield return new WaitForSeconds(0.3f);
            idx++;
        }
        yield return null;
    }

자 이제 위에서 Init 함수를 정상적으로 작동 했다면 dataPathList에는 json 파일들의 경로가 저장되어 있어요.

리소스폴더 경로 + json파일의 경로를 해서 데이터의 Full 경로를 가져왔다면 이 경로로 Resource.LoadAsync<TextAsset>(풀경로)를 이용해서 ResourceRequest 데이터를 받아올수 있어요.

asset.text = json 데이터파일

ResourceRequest.asset은 바로 Newtonsoft의 JsonConvert로 역직렬화를 할수 없기 때문에 string형식을 지원하는 TextAsset.text를 사용하기 위해서 TextAsset형식으로 바꿔준다음 LoadData에 Json문자열을 넣고 를 호출해주면 되요

근데 이때 기존 LoadData를 사용하면 문제가 발생해요.

    // json 데이터를 불러와서 딕셔너리에 저장하는 메소드
    public void LoadData<T>(string json) where T : RawData
    {
        var datas = JsonConvert.DeserializeObject<T[]>(json);
        datas.ToDictionary(x => x.id).ToList().ForEach(x => dicDatas.Add(x.Key, x.Value));
    }

Json 문자열을 역직렬화한후에 딕셔너리에 넣는 과정에 데이터 타입을 알아야해서 <T> 동적으로 데이터타입을 받아야하는데 지금 코드로는 LoadData를 호출할때 데이터타입을 넣어줄수가 없어요. 동적으로 데이터타입을 받는 방법은 없을까? 알아보던중 해결방법이 담긴 링크를 찾았는데 자세히 이해는 못해서 링크만 남겨요.

c# - Using a string value as a generic type parameter - Stack Overflow

 

Using a string value as a generic type parameter

Let's say I have a method that looks like this: public bool Execute<T>() { } ...and I have a string variable that describes the class name of the type I need to pass in, like string type...

stackoverflow.com

직역 하면 문자열 값을 제네릭 형식 매개 변수로 사용하는 방법에 대한 방법이에요.

그래서 LoadAllDataRoutine를 조금 수정 해줬어요.

    private IEnumerator LoadAllDataRoutine()
    {
        int idx = 0;
        foreach (var data in this.dataPathList)
        {
            var path = string.Format("Datas/{0}", data.res_name);
            ResourceRequest req = Resources.LoadAsync<TextAsset>(path);
            yield return req;
            float progress = (float)(idx + 1) / this.dataPathList.Count;
            this.onDataLoadComplete.Invoke(data.res_name, progress);
            TextAsset asset = (TextAsset)req.asset;

            var typeDef = Type.GetType(data.type);
            this.GetType().GetMethod(nameof(LoadData))
                .MakeGenericMethod(typeDef).Invoke(this, new string[] { asset.text });

            idx++;
        }
        yield return null;
        this.onDataLoadFinished.Invoke();
    }

여기서 봐야하는부분이 LoadData를 호출하는 방법이 바꼈는데 Json 파일 데이터 경로를 넘기는 방법에서 직접 Json문자열을 넘기고 이때 데이터타입을 넘겨 줘야하는데 기존에는 

 public class DataTest
 {
        public DataTest()
        {
            // json데이터를 불러와서 딕셔너리에 저장
            DataManager.instance.LoadData<StudentData>("./student_data.json");
            DataManager.instance.LoadData<StudyData>("./study_data.json");
            DataManager.instance.LoadData<UserData>("./user_data.json");
        }
 }

이런식으로 데이터 타입을 넘겨주었지만 이방법을 지금은 쓸수 없으니 

            var typeDef = Type.GetType(data.type);
            this.GetType().GetMethod(nameof(LoadData))
                .MakeGenericMethod(typeDef).Invoke(this, new string[] { asset.text });

데이터타입을 문자열로 먼저 저장해놓은 다음 Type.GetType으로 데이터타입을 받아온다음에 해당형식을 써주면

    public void LoadData<T>(string json) where T : RawData
    {
        var datas = JsonConvert.DeserializeObject<T[]>(json);
        datas.ToDictionary(x => x.id).ToList().ForEach(x => dicDatas.Add(x.Key, x.Value));
    }

이렇게 데이터타입을 받을수 있게 됬어요.

이때 고민점은 DataManager가 아닌 외부에서는 

 

    public void LoadAllData()
    {
        App.instance.StartCoroutine(LoadAllDataRoutine());
    }

이런식으로 LoadAllData 메소드를 통해서 LoadLoadAllDataRoutine 코루틴만 실행하면 정상적으로 모든데이터를 불러올수있어서 사실상 LoadData<T>를 외부에서 호출할 이유는 없어요. 그래서 private 으로 잠굴려고 했으나

외부에서는 처음에 Json데이터들의 타입과 주소를 저장하는 InIt메소드와 LoadAllData메소드만 호출함에도 불구하고 에러가 나오고 있는데 해결방법이 있으신분은 알려주세요.

 

DataManager.cs

using Newtonsoft.Json;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using System;

public class DataManager
{
    public UnityEvent<string, float> onDataLoadComplete = new UnityEvent<string, float>();
    public UnityEvent onDataLoadFinished = new UnityEvent();

    public static readonly DataManager instance = new DataManager();
    private Dictionary<int, RawData> dicDatas = new Dictionary<int, RawData>();
    private List<DatapathData> dataPathList;

    public void Init()
    {
        var datapath = "Datas/datapath_data";
        var asset = Resources.Load<TextAsset>(datapath);
        var json = asset.text;
        var datas = JsonConvert.DeserializeObject<DatapathData[]>(json);

        this.dataPathList = datas.ToList();
    }
    public void LoadData<T>(string json) where T : RawData
    {
        var datas = JsonConvert.DeserializeObject<T[]>(json);
        datas.ToDictionary(x => x.id).ToList().ForEach(x => dicDatas.Add(x.Key, x.Value));
    }

    public void LoadAllData()
    {
        App.instance.StartCoroutine(LoadAllDataRoutine());
    }
    private IEnumerator LoadAllDataRoutine()
    {
        int idx = 0;
        foreach (var data in this.dataPathList)
        {
            var path = string.Format("Datas/{0}", data.res_name);
            ResourceRequest req = Resources.LoadAsync<TextAsset>(path);
            yield return req;
            float progress = (float)(idx + 1) / this.dataPathList.Count;
            this.onDataLoadComplete.Invoke(data.res_name, progress);
            TextAsset asset = (TextAsset)req.asset;
            Debug.Log(req.asset);
            var typeDef = Type.GetType(data.type);
            this.GetType().GetMethod(nameof(LoadData))
                .MakeGenericMethod(typeDef).Invoke(this, new string[] { asset.text });

            yield return new WaitForSeconds(0.3f);
            idx++;
        }
        yield return null;
        this.onDataLoadFinished.Invoke();
    }


    // id 값으로 딕셔너리에서 검색하는 메소드
    public T GetData<T>(int id) where T : RawData
    {
        var collection = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
        return (T)collection.ToList().Find(x => x.id == id);
    }

    // 특정 데이터 그룹을 검색하고싶을대 해당 데이터 타입을 호출하면 해당 데이터타입 객체만 반환한다.
    // ex(GetDatas<Student>()) = 딕셔너리에 저장된 데이터들중 Student타입을 가진 객체들만 반환
    public IEnumerable<T> GetDataList<T>() where T : RawData
    {
        IEnumerable<RawData> col = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
        return col.Select(x => (T)Convert.ChangeType(x, typeof(T)));
    }

}

UnityEvent를 이용해서 데이터가 로드될때마다 이벤트를 호출하여 현재진행상황을 알려주어서 로딩씬 연출을 했어요.

지금은 데이터가 너무 적어서 너무 빠르게 진행되서 올라가는게 잘안보여서 중간에

yield return new WaitForSeconds(0.3f); 를써서 올라가는게 눈으로 보이게 테스트를 한 모습이에요.

 

감사합니다.

300x250
300x250

총알이 생성될 지점 설정해주기

 

PlayerAction.cs

using UnityEngine;

public class PlayerAction : MonoBehaviour
{
    private Transform bulletSpawnPoint;
    public GameObject bulletPrefab;
    // Start is called before the first frame update
    void Start()
    {
        // 플레이어 오브젝트에 자식오브젝트로 있는 BulletSpawnPoint 찾아와서 위치 설정 해주기
        bulletSpawnPoint = this.transform.Find("BulletSpawnPoint");
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            //총알을 만든다 
            var bulletGo = Instantiate<GameObject>(this.bulletPrefab);
            bulletGo.transform.position = this.bulletSpawnPoint.position;
        }
    }
}

Bullet.cs

using UnityEngine;

public class Bullet : MonoBehaviour
{
    public float speed = 5f;

    void Update()
    {
        // 총알이 많이 날라가면 삭제 해주기
        if(this.transform.position.y > 5)
        {
            Destroy(this.gameObject);
        }
        // Vector3.up = new Vector(0, 1, 0)
        this.transform.Translate(Vector3.up * this.speed * Time.deltaTime);
    }
}

총알 프리팹 설정, Bullet 스크립트 넣어주기

플레이어한테 넣어주기

 

300x250
300x250
using UnityEngine;

public class PlayerMove : MonoBehaviour
{
    public float speed = 1f;

    void Update()
    {
        // 가로 이동 반환값 : LeftArrow = -1 RightArrow = 1
        var h = Input.GetAxisRaw("Horizontal");
        // 세로 이동 반환값 : DownArrow = -1 UpArrow = 1        
        var v = Input.GetAxisRaw("Vertical");

        //단위 벡터 (크기가 1인 벡터)
        var dir = new Vector3(h, v, 0).normalized;

        this.transform.Translate(dir * this.speed * Time.deltaTime);
    }
}

300x250
300x250

1. 제목

- 프로그래머스 기능개발(42586)

문제 링크 : 코딩테스트 연습 - 기능개발 | 프로그래머스 (programmers.co.kr)


2. 문제 설명

프로그래머스 팀에서는 기능 개선 작업을 수행 중입니다. 각 기능은 진도가 100%일 때 서비스에 반영할 수 있습니다.

또, 각 기능의 개발속도는 모두 다르기 때문에 뒤에 있는 기능이 앞에 있는 기능보다 먼저 개발될 수 있고, 이때 뒤에 있는 기능은 앞에 있는 기능이 배포될 때 함께 배포됩니다.

먼저 배포되어야 하는 순서대로 작업의 진도가 적힌 정수 배열 progresses와 각 작업의 개발 속도가 적힌 정수 배열 speeds가 주어질 때 각 배포마다 몇 개의 기능이 배포되는지를 return 하도록 solution 함수를 완성하세요.

제한 사항

  • 작업의 개수(progresses, speeds배열의 길이)는 100개 이하입니다.
  • 작업 진도는 100 미만의 자연수입니다.
  • 작업 속도는 100 이하의 자연수입니다.
  • 배포는 하루에 한 번만 할 수 있으며, 하루의 끝에 이루어진다고 가정합니다. 예를 들어 진도율이 95%인 작업의 개발 속도가 하루에 4%라면 배포는 2일 뒤에 이루어집니다.

입출력 예

[93, 30, 55] [1, 30, 5] [2, 1]
[95, 90, 99, 99, 80, 99] [1, 1, 1, 1, 1, 1] [1, 3, 2]

입출력 예 설명

입출력 예 #1
첫 번째 기능은 93% 완료되어 있고 하루에 1%씩 작업이 가능하므로 7일간 작업 후 배포가 가능합니다.
두 번째 기능은 30%가 완료되어 있고 하루에 30%씩 작업이 가능하므로 3일간 작업 후 배포가 가능합니다. 하지만 이전 첫 번째 기능이 아직 완성된 상태가 아니기 때문에 첫 번째 기능이 배포되는 7일째 배포됩니다.
세 번째 기능은 55%가 완료되어 있고 하루에 5%씩 작업이 가능하므로 9일간 작업 후 배포가 가능합니다.

따라서 7일째에 2개의 기능, 9일째에 1개의 기능이 배포됩니다.

입출력 예 #2
모든 기능이 하루에 1%씩 작업이 가능하므로, 작업이 끝나기까지 남은 일수는 각각 5일, 10일, 1일, 1일, 20일, 1일입니다. 어떤 기능이 먼저 완성되었더라도 앞에 있는 모든 기능이 완성되지 않으면 배포가 불가능합니다.

따라서 5일째에 1개의 기능, 10일째에 3개의 기능, 20일째에 2개의 기능이 배포됩니다.


3. 풀이 과정

생각보다 문제 이해하는데 시간이 좀 걸렸어요. 그래서 중요한 포인트만 짚어서 요약을 해보자면

1. 모든 작업은 먼저 입력된 작업이 100%가 되어야 뒤에 있는 작업도 완료 처리가 된다.

2. 모든 작업은 각각의 속도로 병렬처리 된다.

이 두가지가 핵심 이라고 생각했고 이두가지를 구현할려고 생각 했어요.

먼저 들어온 작업부터 처리를 해야하는게 Queue의 속성을 이용하면 좋을거같네요.


4. 함수 설명

첫번쨰 포문에서는 벡터로 입력받은 값들을 큐에 넣어주는 작업이에요.

그후에 while문에서 큐에 데이터가 없을때까지 time변수를 증가 시키며 time * speeds (작업속도) 100이 넘는지를 검사하고 100이 넘을때 큐에서 데이터를 삭제한후 바로 그다음 작업현황이 100이 넘었는지를 확인 하며 answer벡터에 기록했어요.


5. 코드

#include <iostream>
#include <vector>
#include <queue>

using namespace std;

vector<int> solution(vector<int> progresses, vector<int> speeds)
{
    queue<int> qProg;
    queue<int> qSpeeds;
    vector<int> answer;

    int progressesSize = progresses.size();

    for (int index = 0; index < progressesSize; index++)
    {
        qProg.push(progresses[index]);
        qSpeeds.push(speeds[index]);
    }
    
    int time = 0;
    int successCount = 0;
    while (!qProg.empty())
    {
        // 모든 작업은 병렬로 처리되기 떄문에 time변수를 공통적으로 사용
        time++;
        // 앞에 있는 작업이 100%가 되었다면 그뒤에 작업들도 100%가 넘는것이 있는지 검사하여 queue에서 빼주기
        while (qProg.front() + (qSpeeds.front() * time) >= 100)
        {
            qProg.pop();
            qSpeeds.pop();
            // 완료된 작업수 세기
            successCount++;
            if (qProg.empty())
            {
                break;
            }
        }
        // 완료된 작업이 있을때만 완료된 작업수를 answer벡터에 넣어주기
        if (successCount > 0)
        {
            answer.push_back(successCount);
            successCount = 0;
        }
    }
    return answer;
}
300x250
300x250

studen_data.xlsx  => student_data.json

[
  {
    "id": 5000,
    "name": "길동이",
    "korean": 100,
    "math": 90,
    "english": 80
  },
  {
    "id": 5001,
    "name": "철수",
    "korean": 70,
    "math": 60,
    "english": 50
  },
  {
    "id": 5002,
    "name": "유리",
    "korean": 40,
    "math": 30,
    "english": 20
  },
  {
    "id": 5003,
    "name": "훈이",
    "korean": 10,
    "math": 0,
    "english": 10
  }
]

study_data.xlsx => study_data.json

[
  {
    "id": 200,
    "professor": "수학왕",
    "class_subject": "수학",
    "phone": "010-1234-5678"
  },
  {
    "id": 201,
    "professor": "영어킹",
    "class_subject": "영어",
    "phone": "010-9876-5432"
  },
  {
    "id": 202,
    "professor": "국어왕",
    "class_subject": "국어",
    "phone": "010-2222-3333"
  },
  {
    "id": 203,
    "professor": "코딩왕",
    "class_subject": "프로그래밍",
    "phone": "010-1111-2222"
  }
]

user_data.xlsx => user_data.json

[
  {
    "id": 1000,
    "name": "길동이",
    "weight": 40.3,
    "age": 8
  },
  {
    "id": 1001,
    "name": "철수",
    "weight": 41.3,
    "age": 9
  },
  {
    "id": 1002,
    "name": "유리",
    "weight": 43.5,
    "age": 10
  },
  {
    "id": 1003,
    "name": "훈이",
    "weight": 45.7,
    "age": 6
  }
]

 

위와같은 엑셀 데이터(xlsx) 파일을 json화 해서 C# 프로젝트에서 적용하는 방법을 정리 해봤어요.

데이터 파일을 json화를 했다면 (프로젝트에 도구 => NuGet 패키지 관리자(N) => 솔루션용 Nuget 패키지 관리)로 들어가줍니다.

 

 

들어가서 찾아보기탭을 누른다음 검색창에 newtonsoft.json을 검색한후에 우측에 현재 프로젝트를 선택하고 설치를 눌러줘요.

 

설치가 완료되면 솔루션 탐색기에서 참조 버튼을 눌렀을때 Newtonsoft.Json이 추가가 되있다면 준비는 끝난거에요.

 

저는 student_data.json과 매칭되는 클래스파일을 선언 해준 다음 데이터들을 관리해줄 싱글톤 클래스를 싱글톤으로 선언 해줬어요.

 

StudentData.cs

namespace SLG_CS_JsonExample
{   
    public class StudentData
    {        
        public int id;        
        public string name;        
        public int korean;        
        public int math;        
        public int english;
    }
}

 

DataManager.cs

using Newtonsoft.Json;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace SLG_CS_JsonExample
{
    public sealed class DataManager
    {
        // 외부에서 접근 읽기 상태로만 접근할 수 있어 데이터 수정을 막고 데이터를 읽기만 할 수 있는 싱글톤 DataManager 선언
        public static readonly DataManager instance = new DataManager();

        // 딕셔너리로 데이터관리
        private Dictionary<int, StudentData> dicStudents = new Dictionary<int, StudentData>();

        private DataManager()
        {
        }

        // student_data.json 데이터를 불러와서 딕셔너리에 값을 저장하는 메소드
        public void LoadStudentDatas()
        {
            string json = File.ReadAllText("./student_data.json");
            // 역직렬화  : 문자열 -> 객체 
            StudentData[] studentDatas = JsonConvert.DeserializeObject<StudentData[]>(json);

            // 내부 람다의 의미는 배열에 담긴 객체의 속성중 키를 어떤것으로 할것인가?
            this.dicStudents = studentDatas.ToDictionary(x => x.id);    
        }

        // dicStudents에서 id값으로 데이터를 검색한 후 해당하는 데이터를 가져오는 메소드
        public StudentData GetStudentData(int id)
        {
            return this.dicStudents[id];
        }

        // dicStudents에 딕셔너리를 통째로 가져옴으로써 전체 데이터를 받아오는 메소드
        public Dictionary<int, StudentData> GetStudentDatas()
        {
            return this.dicStudents;
        }
    }
}

이렇게 해놓은 다음

 

DataTest.cs

using System;
using System.Collections.Generic;

namespace SLG_CS_JsonExample
{
    public class DataTest
    {
        public DataTest()
        {
            // json데이터를 불러와서 딕셔너리에 저장
            DataManager.instance.LoadStudentDatas();

            // 딕셔너리에 저장된 StudentData데이터딕셔너리를 통째로 DataManager 인스턴스에 접근하여 받아오기
            Dictionary<int, StudentData> studentDatas = DataManager.instance.GetStudentDatas();

            foreach (var studentData in studentDatas)
            {
                var id = studentData.Value.id;
                var name = studentData.Value.name;
                var korean = studentData.Value.korean;
                var math = studentData.Value.math;
                var english = studentData.Value.english;
                Console.WriteLine("id : {0}, name : {1}, korean : {2}, math : {3}, english : {4}", id, name, korean, math, english);
            }
            Console.WriteLine();
            Console.WriteLine();
            Console.WriteLine();
            Console.WriteLine("Search Data id : 5002");
            // 딕셔너리에 특정 데이터만 검색하여 접근하기
            StudentData searchData = DataManager.instance.GetStudentData(5002);

            Console.WriteLine("id : {0}, name : {1}, korean : {2}, math : {3}, english : {4}", 
                searchData.id, searchData.name, searchData.korean, searchData.math, searchData.english);
        }
    }
}

 

이런 식으로 사용할 수 있어요.

근데 이런 방식으로 하면 데이터파일이 증가할때마다

 

private Dictionary<int, StudentData> dicStudents = new Dictionary<int, StudentData>();

변수와 LoadStudentDatas(), GetStudentData(int id), GetStudentDatas() 메소드를 매번 정의 해줘야 해요.

모든 경우엔 할 수 없지만, 위에서 3개의 데이터엔 id라는 공통 필드가 있어요. 이 경우엔 이필드를 이용하여 일반화를 하면 좀 더 쉽게 데이터를 관리 할 수 있어요.

 

먼저 id값을 갖는 부모 클래스변수를 선언 해줘요.

RawData.cs

namespace SLG_CS_JsonExample
{
    public class RawData
    {
        public int id;
    }
}

그리고 데이터 클래스변수들은 이 클래스를 상속받아요.

StudentData.cs

namespace SLG_CS_JsonExample
{    
    public class UserData : RawData
    {            
        public string name;        
        public float weight;        
        public int age;
    }
}

Study.cs

namespace SLG_CS_JsonExample
{    
    public class StudyData : RawData
    {         
        public string professor;        
        public string class_subject;        
        public string phone;
    }
}

UserData.cs

namespace SLG_CS_JsonExample
{    
    public class UserData : RawData
    {            
        public string name;        
        public float weight;        
        public int age;
    }
}

이렇게 클래스들을 작성을 해준후 DataManager 에서 json데이터를 불러와서 저장하고 제가 직접 사용할수 있는 코드를 새로 작성해주면 되요.

DataManager.cs

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

namespace SLG_CS_JsonExample
{
    public sealed class DataManager
    {
        // 외부에서 접근 읽기 상태로만 접근할 수 있어 데이터 수정을 막고 데이터를 읽기만 할 수 있는 싱글톤 DataManager 선언
        public static readonly DataManager instance = new DataManager();

        // 딕셔너리로 데이터관리
        private Dictionary<int, RawData> dicDatas = new Dictionary<int, RawData>();

        private DataManager()
        {
        }

        // json 데이터를 불러와서 딕셔너리에 저장하는 메소드
        public void LoadData<T>(string path) where T : RawData
        {
            var json = File.ReadAllText(path);
            T[] arr = JsonConvert.DeserializeObject<T[]>(json);
            arr.ToDictionary(x => x.id).ToList().ForEach(x => dicDatas.Add(x.Key, x.Value));
        }

        // id 값으로 딕셔너리에서 검색하는 메소드
        public T GetData<T>(int id) where T : RawData
        {
            var collection = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
            return (T)collection.ToList().Find(x => x.id == id);
        }

        // 특정 데이터 그룹을 검색하고싶을대 해당 데이터 타입을 호출하면 해당 데이터타입 객체만 반환한다.
        // ex(GetDatas<Student>()) = 딕셔너리에 저장된 데이터들중 Student타입을 가진 객체들만 반환
        public IEnumerable<T> GetDatas<T>() where T : RawData
        {
            IEnumerable<RawData> col = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
            return col.Select(x => (T)Convert.ChangeType(x, typeof(T)));
        }
    }
}

 

아래는 DataManager를 실제 사용하여 데이터를 불러오는 예제에요.

DataTest.cs

using System;
using System.Linq;
using System.Collections.Generic;

namespace SLG_CS_JsonExample
{
    public class DataTest
    {
        public DataTest()
        {
            // json데이터를 불러와서 딕셔너리에 저장
            DataManager.instance.LoadData<StudentData>("./student_data.json");
            DataManager.instance.LoadData<StudyData>("./study_data.json");
            DataManager.instance.LoadData<UserData>("./user_data.json");

            // 딕셔너리에 저장된 StudentData데이터딕셔너리를 통째로 DataManager 인스턴스에 접근하여 받아오기
            IEnumerable<StudentData> studentDatas = DataManager.instance.GetDatas<StudentData>();
            List<StudentData> studentDataList = studentDatas.ToList();
            Console.WriteLine("=============studentDataList=============");
            foreach (var studentData in studentDataList)
            {
                var id = studentData.id;
                var name = studentData.name;
                var korean = studentData.korean;
                var math = studentData.math;
                var english = studentData.english;
                Console.WriteLine("id : {0}, name : {1}, korean : {2}, math : {3}, english : {4}", id, name, korean, math, english);
            }

            // 딕셔너리에 저장된 StudyData데이터딕셔너리를 통째로 DataManager 인스턴스에 접근하여 받아오기
            IEnumerable<StudyData> studyDatas = DataManager.instance.GetDatas<StudyData>();
            List<StudyData> studyDataList = studyDatas.ToList();
            Console.WriteLine("=============studyDataList=============");
            foreach (var studyData in studyDataList)
            {
                var id = studyData.id;
                var professor = studyData.professor;
                var class_subject = studyData.class_subject;
                var phone = studyData.phone;
                Console.WriteLine("id : {0}, professor : {1}, class_subject : {2}, phone : {3}", id, professor, class_subject, phone);
            }

            // 딕셔너리에 저장된 UserData데이터딕셔너리를 통째로 DataManager 인스턴스에 접근하여 받아오기
            IEnumerable<UserData> userDatas = DataManager.instance.GetDatas<UserData>();
            List<UserData> userDataList = userDatas.ToList();
            Console.WriteLine("=============userDataList=============");
            foreach (var userData in userDataList)
            {
                var id = userData.id;
                var name = userData.name;
                var weight = userData.weight;
                var age = userData.age;
                Console.WriteLine("id : {0}, name : {1}, weight : {2}, age : {3}", id, name, weight, age);
            }
            Console.WriteLine();
            Console.WriteLine("Search Data id : 5002");
            // 딕셔너리에 특정 데이터만 검색하여 접근하기
            StudentData searchData = DataManager.instance.GetData<StudentData>(5002);

            Console.WriteLine("id : {0}, name : {1}, korean : {2}, math : {3}, english : {4}", 
                searchData.id, searchData.name, searchData.korean, searchData.math, searchData.english);
        }
    }
}

300x250
300x250

알고리즘 문제 풀이입니다.

1. 제목

- 백준 10828 스택

- BOJ 10828 스택


2. 문제설명

평범하게 스택을 구현하는 문제 입니다.


3. 풀이과정

사실 C++ STL 라이브러리에 stack이 이미 구현이 되어 있는데 그대로 쓰면 너무 양심에 찔려서 매우 유사한 vector로 구현 해봤습니다.


4. 함수설명

vector에 사실 size와 empty가 다 구현이 되어 있어서 저는 그냥 사용만 했습니다.

vector.size() // 벡터에 원소수 출력합니다.

vector.empty() // 벡터에 원소가 없으면 1 출력 있으면 0 출력합니다.

vector.push_back() // 벡터에 맨 끝에 원소 삽입합니다

vector.back() // 벡터에 맨 마지막 값 가리킴, 스택의 탑과 유사합니다.

vector.pop() // 벡터 맨 마지막값 삭제합니다.


5. 코드

#include <iostream>
#include <string>
#include <vector>

using namespace std;

int main()
{
	vector <int> vec_stack;
	int length;

	cin >> length;

	for (int i = 0; i < length; i++)
	{
		string command;
		cin >> command;

		if (command == "push")
		{
			int num;
			cin >> num;
			vec_stack.push_back(num);
		}
		else if (command == "pop")
		{
			if (vec_stack.empty())
			{
				cout << "-1" << endl;
			}
			else
			{
				cout << vec_stack.back() << endl;
				vec_stack.pop_back();
			}
		}
		else if (command == "size")
		{
			cout << vec_stack.size() << endl;
		}
		else if (command == "empty")
		{
			cout << vec_stack.empty() << endl;
		}
		else if (command == "top")
		{
			if (vec_stack.empty())
			{
				cout << "-1" << endl;
			}
			else
			{
				cout << vec_stack.back() << endl;
			}			
		}
		else
		{
			cout << "ERROR" << endl;
		}

	}
}

 

출처 : www.acmicpc.net/problem/10828

감사합니다.

300x250
300x250