인프런 영문 브랜드 로고
인프런 영문 브랜드 로고

Inflearn Community Q&A

tangbole6065's profile image
tangbole6065

asked

10-Week C++ Coding Test | Algorithm Coding Test

7-N

7-N 네 방향 탐색, dp 적용 관련 질문드립니다.

Written on

·

55

0

안녕하세요! dfs랑 dp 구조에 대해 공부하려고 조금 다양한 시도를 해보다 궁금증이 생겨서 나름대로 스스로 이해를 해봤는데 제대로 이해한것인지 모르겠어서 질문 드립니다.

제가 이해한 내용이 맞는지 확인 부탁드립니다!

 

궁금증은

1) 네 방향으로 탐색하면 어떻게 되는지(순차탐색을 하는 이유)

2) dp 를 적용할 수 는 없는지

이 두가지입니다. 1)번은 생각보다 구현자체가 복잡해서 아이디어만 정리하고 포기를 했고, 2)번에 대해 구현한 코드는 아래와 같습니다.

https://www.acmicpc.net/source/89683214

그리고 1,2번에 대해 제가 나름대로 이해한 방식은 다음과 같습니다.

 

Q1. DFS 함수를 int형 반환값으로 구현하고 상하좌우 네 방향으로 탐색할 때, 단순 DFS 완전탐색과 DP 구조로는 해결이 어려운 이유는 무엇인가?

A. 네방향 탐색 시 모든 완전탐색의 경우의 수를 통해 최소값을 찾아야 한다. 이때 "하나의 경우"는 여러 분기에서 "하나의 가지"를 의미한다. 그러나 DFS로 상하좌우 탐색을 하면, 여러 방향으로 분기되며 반환값이 생기고, 남은 1의 개수가 0이 되는 것을 기저조건으로 한 함수 구조에서 매개변수가 하나로 전달되지 않는다. (ex. 1이 5개 남았을 때 왼쪽으로 탐색하여 3개 완료, 오른쪽으로 이동하여 2개 완료하는 완전탐색이 가능하나 실제로 매개변수가 0이 되는 경우가 없음)

이러한 방식으로 문제를 해결하려면 현재 탐색 방향 외의 방향에 존재할 수 있는 1이 있는 칸을 탐색하는 별도의 함수(a)가 필요하다. 이 함수를 현재 재귀함수의 네 방향 반복문이 종료된 후 호출되어야 완전한 탐색이 가능해진다.

 

Q2. 시간복잡도를 줄이기 위해 DP 적용가능 여부. 첫 풀이 시도가 틀린 이유는?

A. 이 문제에서 상태값은, "현재 y좌표, 현재 x좌표, 남은 1의 개수, 지금까지 붙여온 종이의 배치 구조" 가 필요하다. (이걸 다 저장한다면 부분 최적해가 보장되어 dp 사용가능하다고 이해했습니다.) 처음에는 순차탐색을 보장한다면 지금까지의 배치 구조를 저장하지 않아도 되지 않을까했다. 남은 색종이 종류별 개수는 별도 배열로 관리하기도 하고, 애초에 배치 구조를 저장하면 자동 결정되는 것이니 굳이 또 저장할 필요 없다.

 

감사합니다 ㅎㅎ

c++코딩-테스트

Answer 1

0

kundol님의 프로필 이미지
kundol
Instructor

안녕하세요 지민님 ㅎㅎ 깊게 잘 생각하셨네요

 

Q1. DFS 함수를 int형 반환값으로 구현하고 상하좌우 네 방향으로 탐색할 때, 단순 DFS 완전탐색과 DP 구조로는 해결이 어려운 이유는 무엇인가?

-> 단순히 4방향이라서 DP가 불가능하지 않습니다. DP가 힘든경우는 해당 배열을 선언하기가 어려워서 안하곤 합니다.

이 문제의 경우 DP정의가 매우 까다롭습니다.

    int& ret = dp[r][c][remOne]; 

지민님이 만든 DP의 의미가 좀 모호한게 r, c까지 채웠을 때 remOne이 2이라는 의미자체는 전체칸에서 어떤 칸이 색종이로 안뒤덮혀있는가? 에 대한 상태를 파악하기가 어려운 것 같습니다. 이 때문에 보드의 상태가 정확히 메모이제이션이 안될 것 같습니다.

 

Q2. 시간복잡도를 줄이기 위해 DP 적용가능 여부. 첫 풀이 시도가 틀린 이유는?

A. 이 문제에서 상태값은, "현재 y좌표, 현재 x좌표, 남은 1의 개수, 지금까지 붙여온 종이의 배치 구조" 가 필요하다. (이걸 다 저장한다면 부분 최적해가 보장되어 dp 사용가능하다고 이해했습니다.)

-> 비슷합니다. 만약 DP로 만들거면 보드의 상태, 각 색종이의 상태 이정도면 가능할 것 같습니다.

한번 만들어볼까요?

#include <bits/stdc++.h>
using namespace std;

const int INF = 1e9;
const int N = 10;

// dp 메모이제이션: key = (board 상태, p1, p2, p3, p4, p5)
// dp 함수: 현재 board 상태와 각 색종이 사용 횟수를 인자로 받아, 남은 '1'을 덮기 위한 최소 색종이 수를 반환
unordered_map<string, int> memo;

// 보드 + 색종이 상태값 인코딩
string encode(const string &board, int p1, int p2, int p3, int p4, int p5) {
    return board + "#" + to_string(p1) + "," + to_string(p2) + "," + to_string(p3) + "," + to_string(p4) + "," + to_string(p5);
}
 
int solve(string board, int p1, int p2, int p3, int p4, int p5) { 
    int pos = board.find('1');
    if (pos == string::npos) return 0; 
    string key = encode(board, p1, p2, p3, p4, p5);
    if (memo.find(key) != memo.end()) return memo[key];
    
    int ans = INF;
    int row = pos / N;
    int col = pos % N; 
    for (int s = 5; s >= 1; s--) { 
        int used;
        if (s == 1) used = p1;
        else if (s == 2) used = p2;
        else if (s == 3) used = p3;
        else if (s == 4) used = p4;
        else used = p5;
        if (used >= 5) continue;     
        if (row + s > N || col + s > N) continue;
        bool canPlace = true;
        for (int i = 0; i < s && canPlace; i++) {
            for (int j = 0; j < s; j++) {
                int idx = (row + i) * N + (col + j);
                if (board[idx] != '1') {
                    canPlace = false;
                    break;
                }
            }
        }
        if (!canPlace) continue; 
        string newBoard = board;
        for (int i = 0; i < s; i++) {
            for (int j = 0; j < s; j++) {
                int idx = (row + i) * N + (col + j);
                newBoard[idx] = '0';
            }
        } 
        int res;
        if (s == 1)
            res = solve(newBoard, p1 + 1, p2, p3, p4, p5);
        else if (s == 2)
            res = solve(newBoard, p1, p2 + 1, p3, p4, p5);
        else if (s == 3)
            res = solve(newBoard, p1, p2, p3 + 1, p4, p5);
        else if (s == 4)
            res = solve(newBoard, p1, p2, p3, p4 + 1, p5);
        else // s == 5
            res = solve(newBoard, p1, p2, p3, p4, p5 + 1);
        
        if (res != INF) {
            ans = min(ans, res + 1);
        }
    }
    
    memo[key] = ans;
    return ans;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(nullptr); 
    // 보드 상태를 DP에 넣기 위해 string으로 쉽게
    string board;
    for (int i = 0; i < N; i++){
        for (int j = 0; j < N; j++){
            int cell;
            cin >> cell;
            board.push_back(cell ? '1' : '0');
        }
    } 
    int res = solve(board, 0, 0, 0, 0, 0);
    cout << (res == INF ? -1 : res);
    return 0;
}

해당코드 참고부탁드립니다.

 

 


 

또 질문 있으시면 언제든지 질문 부탁드립니다.

좋은 수강평과 별점 5점은 제게 큰 힘이 됩니다. :)

감사합니다.

강사 큰돌 올림.

 

tangbole6065님의 프로필 이미지
tangbole6065
Questioner

와 보드 상태를 string 으로 저장해서 쓰는 방법이 있군요..! dp도 unordered_map으로 하면되는군요..!! 근데 여기서 그냥 map이 아니고 unordered_map을 사용하신건 최대 모든 경우의 수를 저장한다고 했을때 저장할 데이터가 많기때문에 저장하는 값이 많이지면 unordered_map이 훨씬 빨라져서 그렇게 하신건가요?? 이렇게 친절한 코드까지 ㅜㅜ 감사합니다!! 앗 그리고 dp를 안쓰고 백트래킹만으로 구현한다고 했을때에 네방향 탐색이 문제가 되진 않나요?? 완전탐색 자체는 가능하지만 "탐색 방향이 여러곳으로 나뉘면 완탐을 해도 매개변수가 연결되지 않고 각자 다른 경우의 수로서 존재하면서 (ex. 1이 5개 남았을 때 왼쪽으로 탐색하여 3개 완료, 오른쪽으로 이동하여 2개 완료하는 완전탐색이 가능하나 실제로 매개변수가 0이 되는 경우가 없음)" 라는 문제가 있을 것 같습니다..

kundol님의 프로필 이미지
kundol
Instructor

unordered_map이 훨씬 빨라져서 그렇게 하신건가요??

-> 빨라지는 것을 담보할 수는 없습니다. 최소O(1)이지만 최대 O(N)입니다. 일반적으로 O(logN)인 map 써도 됩니다 ㅎㅎ 한번 그냥 해봤어요

 

dp를 안쓰고 백트래킹만으로 구현한다고 했을때에 네방향 탐색이 문제가 되진 않나요?? 완전탐색 자체는 가능하지만 "탐색 방향이 여러곳으로 나뉘면 완탐을 해도 매개변수가 연결되지 않고 각자 다른 경우의 수로서 존재하면서 (ex. 1이 5개 남았을 때 왼쪽으로 탐색하여 3개 완료, 오른쪽으로 이동하여 2개 완료하는 완전탐색이 가능하나 실제로 매개변수가 0이 되는 경우가 없음)" 라는 문제가 있을 것 같습니다..

-> 4방향은 문제가 되지 않습니다 다만 로직이 좀 추가가 될 것 같습니다. y, x뿐만이 아니라 지금까지 내가 커버한 정점들(보드의 변화)들을 담는 식의 코드가 필요할 것 같습니다.

tangbole6065님의 프로필 이미지
tangbole6065
Questioner

지금까지 내가 커버한 정점들(보드의 변화)을 담는다는 것은 위에서 보여주신 코드에서 board라는 string 값을 말씀하시는 거죠..??

 

사실 저는 네 방향으로 움직일 때 dp를 쓰기 위해서 이값을 추가해야한다고 생각했는데, dp를 안써도 이러한 값이 필요한이유가 무엇일까요?? 제가 처음 질문할때에 쓴 "현재 탐색 방향 외의 방향에 존재할 수 있는 1이 있는 칸을 탐색하는 별도의 함수(a)가 필요하다. 이 함수를 현재 재귀함수의 네 방향 반복문이 종료된 후 호출되어야 완전한 탐색이 가능해진다." 이 부분과 맥락이 같은 것인가요??

그리고 보통 dp가 가능한 문제들은 "상태값이 4개 이상"이어서 struct를 만들고 그걸 자료형으로 쓰는 경우까지는 잘 안나오나요..?

 

추가로.. 이 문제랑 비슷한데 간단한 버전이 2-A 라고 생각해서 그 문제도 dfs , dp로 풀어보았는데요.. 실패했습니다

처음 실패한건 dp를 쓰지 않고 dfs만 써서 시간초과가 났기 때문이라고 생각을 합니다.(시간제한이 없다면 맞는 로직이라고 생각합니다)

코드 : https://www.acmicpc.net/source/89702691

 

그래서 dp를 써봐야겠다 했더니 틀렸더라구요

코드 : https://www.acmicpc.net/source/89702818

그 이유도 "지금까지 내가 커버한 정점들(보드의 변화)을 담지 않아서" 인 것 같아요..

 

그래서 큰돌님 코드를 참고해서 baord를 string 으로 받고, 방문하면 0으로 바꾸는 식으로 처리하고 다시 원복하고,

dp 는 map 으로 처리하고 key 값은 현재 board 상태 + r+c로 해줬습니다

코드 : https://www.acmicpc.net/source/89702884

이 코드는 메모리 초과가 나더라구요. 계산해보니 2^10000×10000 바이트 가 필요해서 당연히 메모리초과가 나는 것 같지만 이 코드도 로직 자체는 맞았다고 이해했습니다.

 

제 이해가 맞을까요..?

 

kundol님의 프로필 이미지
kundol
Instructor

안녕하세요 지민님 ㅎㅎ

 

사실 저는 네 방향으로 움직일 때 dp를 쓰기 위해서 이값을 추가해야한다고 생각했는데, dp를 안써도 이러한 값이 필요한이유가 무엇일까요??

-> 원래 y, x를 기반으로 순차적으로 보드의 왼쪽에서부터 오른쪽까지 정해서 탐색한다고 하면 y, x만 필요한데 그게 아니라 4방향으로 한다면 y, x로는 불가, 상태값을 저장할 보드에 대한 정보가 필요하게 됩니다.

 

그리고 보통 dp가 가능한 문제들은 "상태값이 4개 이상"이어서 struct를 만들고 그걸 자료형으로 쓰는 경우까지는 잘 안나오나요..?

-> 음.. 3 ~ 4개까지 하는 경우가 간혹 나옵니다.

 

 이러한 방식으로 문제를 해결하려면 현재 탐색 방향 외의 방향에 존재할 수 있는 1이 있는 칸을 탐색하는 별도의 함수(a)가 필요하다. 이 함수를 현재 재귀함수의 네 방향 반복문이 종료된 후 호출되어야 완전한 탐색이 가능해진다.

-> 4방향외로 호출할 필요는 없습니다. 예를 들어 한 정점에서 각각 4방향씩 탐색하면 모든 정점을 탐색할 수 있습니다. 이 때문에 해당 방향외를 탐색하는 함수는 필요하지 않습니다.

 

이 코드는 메모리 초과가 나더라구요. 계산해보니 2^10000×10000 바이트 가 필요해서 당연히 메모리초과가 나는 것 같지만 이 코드도 로직 자체는 맞았다고 이해했습니다.

-> 네 맞습니다. 저렇게 모든 경우의 수를 생각해서 짤 수 있습니다. 그러나 이거는 예시로 든 문제가 잘못된 것 같습니다. 이 문제는 BFS로 풀어야 하는 문제입니다.

 

감사합니다.

 

tangbole6065님의 프로필 이미지
tangbole6065
Questioner

넵 감사합니다 !! 하나만 더 여쭤보고자합니다ㅎㅎ.... '-> 원래 y, x를 기반으로 순차적으로 보드의 왼쪽에서부터 오른쪽까지 정해서 탐색한다고 하면 y, x만 필요한데 그게 아니라 4방향으로 한다면 y, x로는 불가, 상태값을 저장할 보드에 대한 정보가 필요하게 됩니다.' 이 부분은 마치 tsp 에서 앞으로의 경로탐색을 위해 지금까지 지나온 노드를 비트마스킹으로 저장해야하는 이유와 유사한것인가요?? 물론 거기서는 dp를 사용하지만 tsp에서 dp를 사용하지 않아도 완탐을 위해서는 지나온 노드 상태를 저장해야한다고 이해하고있습니다..!

kundol님의 프로필 이미지
kundol
Instructor

안녕하세요 지민님 ㅎㅎ

왼쪽에서 오른쪽으로 방향을 산정한다면 그 이전의 정점들은 탐색하지 않아도 됩니다. 방향이 일정하기 때문에 지금 2, 2를 탐색한다면 그 이전 2, 1 / 2, 0 정점들은 탐색이 완료가 되었다가 자명하니까요.

그러나 4방향이면 해당 부분이 사라지게 되므로 보드전체를 기억할 필요가 있게 됩니다.

 

마치 tsp 에서 앞으로의 경로탐색을 위해 지금까지 지나온 노드를 비트마스킹으로 저장해야하는 이유와 유사한것인가요??

-> 네 유사합니다.

 

감사합니다.

 

tangbole6065's profile image
tangbole6065

asked

Ask a question