Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1-InSange #3

Merged
merged 1 commit into from
Mar 14, 2024
Merged

1-InSange #3

merged 1 commit into from
Mar 14, 2024

Conversation

InSange
Copy link
Collaborator

@InSange InSange commented Mar 11, 2024

🔗 문제 링크


숨바꼭질 4

✔️ 소요된 시간


3시간

✨ 수도 코드


BFS문제입니다.

수빈이가 동생의 위치까지 도착하는데 걸리는 이동 수와 거기까지의 경로를 출력해야 합니다.
이동할 수 있는 크기는 현재 수빈이의 위치 N에서 -1, +1 또는 *2 만큼 이동이 가능합니다.

첫번째 풀이

const int MAX_INT = 9999999;

int N, K;
pair<int, int> visited[100001];
queue<int> q;
stack<int> st;

bool Check(int index)
{
	if (index < 0 || index > 100000) return false;
	return true;
}

void Init()
{	// 수빈이(N : 출발지)와 동생(K : 목적지) 값 설정
	cin >> N >> K;
	// 0 번째 인덱스도 범위에 속하기 때문에 MAX_INT로 초기화 해준다.
	// 첫번째 값은 해당 번호를 방문하기 이전의 번호를 저장하고, 두번째 값은 해당 번호로 오기까지의 횟수를 저장한다.
	for (int i = 0; i < 100001; i++)
	{
		visited[i] = { MAX_INT, MAX_INT };
	}
	visited[N] = { N, 0 };
}

void Solve()
{	// 수빈이(N: 출발지) 값을 넣어준다.
	q.push(N);

	while (!q.empty())
	{
		int cur_num = q.front();
		q.pop();

		if(cur_num == K) break;

		int pre_num, cur_cnt, next_num;
		cur_cnt = visited[cur_num].second;

		next_num = cur_num - 1;
		if (Check(next_num) && visited[next_num].second > cur_cnt + 1)
		{
			q.push(next_num);
			visited[next_num].first = cur_num;
			visited[next_num].second = cur_cnt + 1;
		}
		next_num = cur_num + 1;
		if (visited[next_num].second > cur_cnt + 1)
		{
			q.push(next_num);
			visited[next_num].first = cur_num;
			visited[next_num].second = cur_cnt + 1;
		}
		next_num = 2*cur_num;
		if (visited[next_num].second > cur_cnt + 1)
		{
			q.push(next_num);
			visited[next_num].first = cur_num;
			visited[next_num].second = cur_cnt + 1;
		}
	}

	cout << visited[K].second << "\n";
	do {
		st.push(K);
		K = visited[K].first;
	} while (K != N);
	st.push(N);

	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
}

출발지에서 도착지까지 탐색하는 과정은 BFS를 사용하여 현재 위치 N에서 +1, -1, *2 를 곱해 다음 위치 값이 0 <= x <= 100000 사이에 들어있다면 큐에 계속 집어 넣는 식으로 하였습니다.
하지만 이럴 경우 이전에 방문했던 위치에 다시 재방문하는 경우가 발생해 중복이 발생하게 됩니다.

pair<int, int> visited[100001];
queue<int> q;		// 현재 이동해야할 칸들을 BFS로 방문하기 위해서 큐를 선언
stack<int> st;	// 도착지(K)에서 백트래킹을 통해 출발지(N)까지 방문했던 점들을 넣어 순서대로 출력하기 위해 스택을 선언

중복 방문을 피하기 위해서 visited 배열을 선언을 해주었고 해당 배열 안에 현재 위치까지 오는데 걸린 이동 횟수를 집어 넣어주었습니다.

pair<int, int>로 선언을 한 이유는 { 이전에 방문했던 노드 인덱스 값, 현재 노드까지 이동하는데 걸린 수 }로 저장하기 위하여 선언을 해 주었습니다.

image

두번째 풀이

void Solve()
{	
	if (K == N)
	{
		cout << visited[N].first << "\n" << N;
		return;
	}
	// 수빈이(N: 출발지) 값을 넣어준다.
	q.push(N);

	while (!q.empty())
	{
		int cur_num = q.front();
		q.pop();

		if(cur_num == K) break;

		int pre_num, cur_cnt, next_num;
		cur_cnt = visited[cur_num].second;

		next_num = cur_num - 1;
		if (Check(next_num) && visited[next_num].second > cur_cnt + 1)
		{
			q.push(next_num);
			visited[next_num].first = cur_num;
			visited[next_num].second = cur_cnt + 1;
		}
		next_num = cur_num + 1;
		if (visited[next_num].second > cur_cnt + 1)
		{
			q.push(next_num);
			visited[next_num].first = cur_num;
			visited[next_num].second = cur_cnt + 1;
		}
		next_num = 2*cur_num;
		if (visited[next_num].second > cur_cnt + 1)
		{
			q.push(next_num);
			visited[next_num].first = cur_num;
			visited[next_num].second = cur_cnt + 1;
		}
	}

	cout << visited[K].second << "\n";
	do {
		st.push(K);
		K = visited[K].first;
	} while (K != N);
	st.push(N);

	while (!st.empty())
	{
		cout << st.top() << " ";
		st.pop();
	}
}

두번째 풀이에서는 만약 수빈이와 동생의 위치가 같을 때의 예외 사항을 추가해줬습니다.

if (K == N)
	{
		cout << visited[N].first << "\n" << N;
		return;
	}

image

마지막 풀이

https://forward-gradually.tistory.com/72
위의 링크에서 반례들을 참고하게 되었고

수빈이의 위치가 동생의 위치보다 큰 경우와 수빈이의 위치가 동생의 위치보다 작은 경우로 나눠주는 예외를 처리해주었습니다.

void Solve()
{	// 3가지의 경우의 수
	if (N == K)	// 1. 수빈(N)과 동생(K)가 같은 위치에 있을 경우 이동하는 칸은 0이고 해당 위치를 바로 출력해준다.
	{
		cout << visited[N].second << "\n" << N;
		return;
	}
	else if (N > K)	// 2. 수빈(N)이 동생(K)보다 큰 경우 K로 감소하는 접근은 -1 밖에 없기 때문에 -1로만 계산한 값들을 출력해준다.
	{
		cout << N-K << "\n";
		while (N != K)
		{
			cout << N << " ";
			N--;
		}
		cout << N << "\n";
		return;
	}
	else// if(N < K)		// 3. 수빈(N)이 동생(K)보다 작은 경우 +1, *2 뿐만 아니라 -1 * 2로 빠른 접근을 노려 볼수도 있다. ex) 5에서 8로 갈 경우 (1) 5 -> 4 -> 8 (2) 5 -> 10 -> 9 -> 4
	{	
		// 수빈이(N: 출발지) 값을 넣어준다.
		q.push(N);

		while (!q.empty())
		{	// 현재 수빈이의 위치를 큐에서 꺼내준다.
			int cur_num = q.front();
			q.pop();
			// 현재 이동한 칸의 수와 다음 이동할 점에 대한 위치 값을 저장할 변수들을 선언해준다.
			int cur_cnt, next_num;
			cur_cnt = visited[cur_num].second;
			// 현재 위치에서 -1만큼 이동했을 경우.
			next_num = cur_num - 1;
			if (Check(next_num) && visited[next_num].second > cur_cnt + 1)
			{
				q.push(next_num);
				visited[next_num].first = cur_num;
				visited[next_num].second = cur_cnt + 1;
			}
			if (next_num == K) break;
			// 현재 위치에서 +1만큼 이동했을 경우.
			next_num = cur_num + 1;
			if (Check(next_num) && visited[next_num].second > cur_cnt + 1)
			{
				q.push(next_num);
				visited[next_num].first = cur_num;
				visited[next_num].second = cur_cnt + 1;
			}
			if (next_num == K) break;
			// 현재 위치에서 *2만큼 이동했을 경우.
			next_num = 2 * cur_num;
			if (Check(next_num) && visited[next_num].second > cur_cnt + 1)
			{
				q.push(next_num);
				visited[next_num].first = cur_num;
				visited[next_num].second = cur_cnt + 1;
			}
			if (next_num == K) break;
		}
		// 출발지에서 도착지까지 가는데 걸린 칸의 수
		cout << visited[K].second << "\n";
		// 도착지에서 출발지까지 방문했던 점들을 저장하여 출력 저장할때는 K->N이지만 출력할때는 N->K로 출력이 됨!
		do {
			st.push(K);
			K = visited[K].first;
		} while (K != N);
		st.push(N);

		while (!st.empty())
		{
			cout << st.top() << " ";
			st.pop();
		}
	}
}

image

📚 새롭게 알게된 내용

반례 참고 : https://forward-gradually.tistory.com/72

@InSange InSange self-assigned this Mar 11, 2024
@tgyuuAn tgyuuAn changed the title 2024-03-11 숨바꼭질 4 1-InSange Mar 12, 2024
@yuyu0830
Copy link
Collaborator

별도 solve 함수랑 return 문을 사용하는 경우 else if, else는 없어도 될 것 같아요!
둘의 위치가 같은 경우랑 수빈의 위치가 큰 경우도 우선순위 큐와 벡터를 잘 이용하면 경우의 수를 분기하지 않고 한 탐색 루프 안에서 해결할 수도 있을 것 같네요
마지막 pair<int, int>로 경로 찾는게 신기하네요. 저라면 vector를 포함한 구조체 노드로 큐를 구성했을 것 같은데 하나 배워갑니다 :)

@9kyo-hwang
Copy link

별도 solve 함수랑 return 문을 사용하는 경우 else if, else는 없어도 될 것 같아요! 둘의 위치가 같은 경우랑 수빈의 위치가 큰 경우도 우선순위 큐와 벡터를 잘 이용하면 경우의 수를 분기하지 않고 한 탐색 루프 안에서 해결할 수도 있을 것 같네요 마지막 pair<int, int>로 경로 찾는게 신기하네요. 저라면 vector를 포함한 구조체 노드로 큐를 구성했을 것 같은데 하나 배워갑니다 :)

햄 리뷰 달 때 "add a single comment" 말고 "approve"로 해야 리뷰어즈 리퀘스트가 체크돼요

int N, K;
pair<int, int> visited[100001];
queue<int> q; // ���� �̵��ؾ��� ĭ���� BFS�� �湮�ϱ� ���ؼ� ť�� ����
stack<int> st; // ������(K)���� ��Ʈ��ŷ�� ���� �����(N)���� �湮�ߴ� ������ �־� ������� ����ϱ� ���� ������ ����

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 stack의 역할은 vector로 완벽하게 대체 가능합니다. push()는 push_back(), pop()은 pop_back()에 대응되죠.
vector를 쓰게 되면 배열 중간에 위치한 요소도 접근이 가능해서 좋습니다. stack은 그게 불가능하거든요 :)

Comment on lines +15 to +19
bool Check(int index)
{ // �̵��� �� �ִ� �� ����( 0 <= x <= 100,000 )�� ����� �ȵȴ�!
if (index < 0 || index > 100000) return false;
return true;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

함수명을 좀 더 직관적으로 알 수 있게 적으면 좋을 것 같네요.
개인적으로 저는 아래와 같이 작성합니다.

bool OutOfBound(int index)
{
    return index < 0 || index > 100000;
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

참고하겠습니다!
확실히 어디에 필요한지 좀 더 명시적이면 좋겠네용

Comment on lines +26 to +31
for (int i = 0; i < 100001; i++)
{
visited[i] = { MAX_INT, MAX_INT };
}
// ��� ������ 0�� Ƚ���� ä���ֱ�
visited[N] = { N, 0 };
Copy link

@9kyo-hwang 9kyo-hwang Mar 12, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pair<int, int> visited[100001]; 대신 vector<pair<int, int>> visited;로 쓰면 좀 더 편리할 것 같습니다. 이렇게 하면

visited.assign(100001, {MAX_INT, MAX_INT})

이런 식으로 간편하게 초기화할 수 있거든요 :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

동적으로 데이터를 추가할때를 제외하고는 vector를 잘 안쓰다보니 해당 방식의 초기화는 몰랐네요!
좋은 정보 하나 알아갑니다~

Comment on lines +41 to +52
else if (N > K) // 2. ����(N)�� ����(K)���� ū ��� K�� �����ϴ� ������ -1 �ۿ� ���� ������ -1�θ� ����� ������ ������ش�.
{
cout << N-K << "\n";
while (N != K)
{
cout << N << " ";
N--;
}
cout << N << "\n";
return;
}
else// if(N < K) // 3. ����(N)�� ����(K)���� ���� ��� +1, *2 �Ӹ� �ƴ϶� -1 * 2�� ���� ������ ��� ������ �ִ�. ex) 5���� 8�� �� ��� (1) 5 -> 4 -> 8 (2) 5 -> 10 -> 9 -> 4

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사실 return을 걸어줬으면 else if로 분기할 필요는 없죠.

if (N == K)
{
    ...
    return;
}

if(N > K)
{
    ...
    return;
}

...
return;

이런 식으로 하면 아래 else 분기의 indent를 줄일 수 있을 것 같네요.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

사람들에게 3가지 방법에 대해서 좀 더 직관적으로 보여줄 수 있는 방법이 뭐가 있을까 고려하다보니 깜빡했습니다!
지적 감사합니다~

Comment on lines +65 to +89
next_num = cur_num - 1;
if (Check(next_num) && visited[next_num].second > cur_cnt + 1)
{
q.push(next_num);
visited[next_num].first = cur_num;
visited[next_num].second = cur_cnt + 1;
}
if (next_num == K) break;
// ���� ��ġ���� +1��ŭ �̵����� ���.
next_num = cur_num + 1;
if (Check(next_num) && visited[next_num].second > cur_cnt + 1)
{
q.push(next_num);
visited[next_num].first = cur_num;
visited[next_num].second = cur_cnt + 1;
}
if (next_num == K) break;
// ���� ��ġ���� *2��ŭ �̵����� ���.
next_num = 2 * cur_num;
if (Check(next_num) && visited[next_num].second > cur_cnt + 1)
{
q.push(next_num);
visited[next_num].first = cur_num;
visited[next_num].second = cur_cnt + 1;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 반복문을 이용하면 코드 중복을 줄일 수 있습니다.

for(int offset_num : {-1, +1, cur_num})
{
    int next_num = cur_num + offset_num;
    if(!Check(next_num) || visited[next_num] <= cur_cnt + 1)
    {
        continue;
    }
    q.push(next_num);
    visited[next_num] = {cur_num, cur_cnt + 1};
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이거는 확실히 도움이되네요!
for문을 많이 사용하지만 가장 기본적으로만 활용하다보니 이렇게 다양하게 접근하는 방법은 익숙치 않은 것 같습니다.
확실히 좀 더 명확하고 짧게 확인할 수 있다는 점에서 좋네요.
습관을 들여보도록 노력하겠습니다.

@9kyo-hwang
Copy link

9kyo-hwang commented Mar 12, 2024

저는 요렇게 풀어봤습니다 :)

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

using namespace std;

int main()
{
    cin.tie(nullptr)->sync_with_stdio(false);
    
    int N, K; cin >> N >> K;
    vector<int> Distances(100001, -1);
    vector<int> Parents(100001, 0);
    iota(Parents.begin(), Parents.end(), 0);
    
    deque<int> Q;
    Q.emplace_back(N);
    Distances[N] = 0;
    
    while(!Q.empty())
    {
        const int X = Q.front();
        Q.pop_front();
        
        if(X == K)
        {
            cout << Distances[X] << "\n";
            break;
        }
        
        for(const int D : {X, -1, 1})
        {
            int NX = X + D;
            if(NX < 0 || NX > 100000 || Distances[NX] != -1 && Distances[NX] < Distances[X] + 1)
            {
                continue;
            }

            Distances[NX] = Distances[X] + 1;
            Parents[NX] = X;
            Q.emplace_back(NX);
        }
    }
    
    deque<int> Paths;
    
    int Pos = K;
    while(Pos != N)
    {
        Paths.emplace_front(Pos);
        Pos = Parents[Pos];
    }
    Paths.emplace_front(Pos);
    
    for(const int Path : Paths)
    {
        cout << Path << " ";
    }

    return 0;
}

@9kyo-hwang
Copy link

9kyo-hwang commented Mar 12, 2024

별도 solve 함수랑 return 문을 사용하는 경우 else if, else는 없어도 될 것 같아요! 둘의 위치가 같은 경우랑 수빈의 위치가 큰 경우도 우선순위 큐와 벡터를 잘 이용하면 경우의 수를 분기하지 않고 한 탐색 루프 안에서 해결할 수도 있을 것 같네요 마지막 pair<int, int>로 경로 찾는게 신기하네요. 저라면 vector를 포함한 구조체 노드로 큐를 구성했을 것 같은데 하나 배워갑니다 :)

우선순위큐...? 우선순위큐를 어떤 식으로 적용할 수 있을까요?

Copy link
Contributor

@dhlee777 dhlee777 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

범위 체크함수 와 초기화 함수 , solve 함수 를 분리하면서 코드가 깔끔해지는 것 같습니다
그리고 visited 배열을 pair<int,int>형으로 두는 방식이 색달랐던거 같습니다. 잘 배워갑니다..!

Copy link
Collaborator

@yuyu0830 yuyu0830 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저도 줄일 수 있는 부분은 줄이는 연습을 해야겠네요
문제 재밌어보이네요 저도 풀러 갑니다 :)

Copy link
Collaborator

@seongwon030 seongwon030 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

visited 배열로 경로를 찾는 게 신기했어요. 저도 중복을 피하는 방법에 대해 고민을 많이 했었는데 많이 배워갑니다. 그리고 1번째 풀이부터 마지막 풀이까지 잘 정리되어 있어서 읽기 정말 편했습니다.

@InSange
Copy link
Collaborator Author

InSange commented Mar 14, 2024

visited 배열로 경로를 찾는 게 신기했어요. 저도 중복을 피하는 방법에 대해 고민을 많이 했었는데 많이 배워갑니다. 그리고 1번째 풀이부터 마지막 풀이까지 잘 정리되어 있어서 읽기 정말 편했습니다.

그럼요~

@InSange InSange merged commit 982569f into main Mar 14, 2024
1 check passed
@InSange InSange deleted the 1-InSange branch March 14, 2024 04:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants