본문 바로가기

알고리즘/백준 문제 풀이

[C++] INU 코드페스티벌 2025 업솔빙

반응형

 

 

https://www.acmicpc.net/category/detail/4580


 

25/10/16

 

 

백준 34552번~ 34562번까지 총 11문제로 이루어져 있다.


 

A번 - 디딤돌 장학금(34552번)

문제에서 주어진 그대로 구현하면 된다.

// 34552번 디딤돌 장학금
// 구현
#include <iostream>
#include <vector>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

int main(void){
    fastio

    vector<int> M(11);
    for (int i = 0; i < 11; ++i) cin >> M[i];
    int N; cin >> N;
    int ans = 0;
    int B; float L; int S;
    while (N--){
        cin >> B >> L >> S;
        if (L >= 2.0 && S >= 17){
            ans += M[B];
        }
    }
    cout << ans;
    return 0;
}

 

 

B번 - 알파벳 점수 계산기(34553번)

문제에서 주어진 그대로 구현하면 된다.

// 34553번 알파벳 점수 계산기
// 구현, 문자열
#include <iostream>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

int main(void){
    fastio

    string S; cin >> S;
    int ans = 1;
    int add = 1;
    for (int i = 1; i < S.size(); ++i){
        if (S[i] > S[i-1]){
            add++;
        } else {
            add = 1;
        }
        ans += add;
    }
    cout << ans;
    return 0;
}

 

 

C번 - 특수한 정수쌍(34554번)

약수의 개수가 정확히 $2$개가 되는 수는 소수이다.

 

두 자연수의 곱이 소수라면, 두 자연수는 각각 $1$과 자기 자신이 된다.

 

따라서, $B$는 소수, $A$는 $1$이 되어야 하며, $B-A = N \rightarrow B = N+1$.

 

즉, $N+1$이 소수라면 $N+1$과 $1$을 출력하고 그게 아니라면 $0$을 출력하면 된다.

// 34554번 특수한 정수 쌍
// 소수 판정
#include <iostream>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

bool is_prime(int N){
    if (N == 1) return false;
    for (int i = 2; i*i <= N; ++i){
        if (N % i == 0){
            return false;
        }
    }
    return true;
}

void solve(void){
    int N; cin >> N;
    if (is_prime(N+1)){
        cout << 1 << endl << 1 << ' ' << N+1 << endl;
    } else {
        cout << 0 << endl;
    }
    return;
}

int main(void){
    fastio

    int T; cin >> T;
    while (T--){
        solve();
    }
    return 0;
}

 

 

D번 - INU 순열(34555번)

인접한 두 수의 차가 오름차순이 되도록 나열하면 된다.

 

배열해야하는 수가 $N$개라면, $1, \ 2, \ 3, \cdots, N-1$이 인접한 수의 차이가 되도록 나열해야 한다.

 

따라서, 수의 중간부터 시작하여 $1$을 빼고, $2$를 더하고, $3$을 빼고, ... 그런 식으로 수열을 구성하면 된다. 

// 34555번 INU 순열
// 애드혹, 해 구성하기
/*
접근 방법:
숫자의 절반 오름차순
숫자의 절반 내림차순
교대로 배열
3 2 4 1
3 2 4 1 5
4 3 5 2 6 1
...
*/
#include <iostream>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

int main(void){
    fastio

    int N; cin >> N;
    int odd = N/2 + 1, even = N/2;
    for (int i = 0; i < N; ++i){
        if (i % 2 == 0){
            cout << odd << ' ';
            odd++;
        } else {
            cout << even << ' ';
            even--;
        }
    }
    return 0;
}

 

 

E번 - MBTI 소개팅(34556번)

한쪽 성별은 고정하고, 다른 성별 사람들의 순서를 섞은 다음에 매칭하면 된다.

 

즉, 순열을 구하는 문제로 바뀌게 되는데, 이는 백트래킹으로 쉽게 구할 수 있다.

 

가지치기를 할 수 있는데, 남은 사람들이 모두 4점을 얻는다고 해도 최대 점수를 갱신할 수 없다면 리턴하는 식으로 잘라낼 수 있다.

// 34556번 MBTI 소개팅
// 백트래킹, 완전 탐색
#include <iostream>
#include <vector>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

int MAX = 0;
int visited[11] = {0,};
int N;
vector<string> man_MBTI(10);
vector<string> woman_MBTI(10);
int score = 0;

int compare(string& a, string& b){
    int ret = 0;
    for (int i = 0; i < 4; ++i){
        if (a[i] != b[i]){
            ret++;
        }
    }
    return ret;
}

void dfs(int depth){
    if (score + 4*(N-depth) < MAX){
        return;
    }
    if (depth == N){
        MAX = max(score, MAX);
        return;
    }
    for (int i = 0; i < N; ++i){
        if (!visited[i]){
            visited[i] = 1;
            score += compare(woman_MBTI[depth], man_MBTI[i]);
            dfs(depth+1);
            score -= compare(woman_MBTI[depth], man_MBTI[i]);
            visited[i] = 0;
        }
    }
    return;
}

int main(void){
    fastio

    cin >> N;
    for (int i = 0; i < N; ++i){
        cin >> man_MBTI[i];
    }
    for (int i = 0; i < N; ++i){
        cin >> woman_MBTI[i];
    }
    dfs(0);
    cout << MAX << endl;
    return 0;
}

 

 

F번 - 횃불이의 모험(34557번)

사용자가 누른 키에 대해서 각 이벤트가 부여되는데, 'W', 'A', 'S', 'D' 순으로 우선적으로 일치하는 이벤트를 처리해야 한다.

 

이를 구현하기 위해 현재 입력된 키와 이전에 입력된 키의 우선 순위를 비교했다. 두 키 중 우선순위가 높은 쪽 먼저 이벤트를 처리하도록 구현했다. 

// 34557번 횃불이의 모험
// 구현
/*
접근 방법:

*/
#include <iostream>
#include <vector>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

int dr[4] = {-1, 0, 1, 0}, dc[4] = {0, -1, 0, 1};

int find_idx(char ch){
    string wasd = "WASD";
    for (int i = 0; i < 4; ++i){
        if (wasd[i] == ch){
            return i;
        }
    }
    return -1;
}

int main(void){
    fastio

    int N, M; cin >> N >> M;
    vector<vector<int>> MAP(N, vector<int>(N, 0));
    int r, c;
    for (int i = 0; i < N; ++i){
        for (int j = 0; j < N; ++j){
            cin >> MAP[i][j];
            if (MAP[i][j] == 2){
                r = i; c = j;
                MAP[i][j] = 0;
            }
        }
    }

    vector<string> keyevent(4);
    for (int i = 0; i < 4; ++i){
        cin >> keyevent[i];
    }
    string order; cin >> order;
    char before_key = ' ';
    for (int i = 0; i < M; ++i){
        char now_key = order[i];
        string now_event = "", before_event = "";
        if (before_key == now_key){
            now_event = "Stay";
        } else {
            now_event = "Down";
            before_event = "Up";
        }
        int j = find_idx(now_key);
        int k = find_idx(before_key);
        if (j < k){
            if (j != -1 && keyevent[j] == now_event){
                int nr = r+dr[j], nc = c+dc[j];
                if (0 <= nr && nr < N && 0 <= nc && nc < N && MAP[nr][nc] == 0){
                    r = nr, c = nc;
                }
            }
            if (k != -1 && keyevent[k] == before_event){
                int nr = r+dr[k], nc = c+dc[k];
                if (0 <= nr && nr < N && 0 <= nc && nc < N && MAP[nr][nc] == 0){
                    r = nr, c = nc;
                }
            }
        } else {
            if (k != -1 && keyevent[k] == before_event){
                int nr = r+dr[k], nc = c+dc[k];
                if (0 <= nr && nr < N && 0 <= nc && nc < N && MAP[nr][nc] == 0){
                    r = nr, c = nc;
                }
            }
            if (j != -1 && keyevent[j] == now_event){
                int nr = r+dr[j], nc = c+dc[j];
                if (0 <= nr && nr < N && 0 <= nc && nc < N && MAP[nr][nc] == 0){
                    r = nr, c = nc;
                }
            }
        }
        before_key = now_key;
    }
    cout << r+1 << ' ' << c+1;
    return 0;
}

 

 

G번 - Prime Median(34558번)

쿼리의 개수가 $100 \ 000$개이고, 구간의 범위가 최대 $10^6$이기 때문에, 나이브하게 모든 범위의 수에 대해서 소수 판정을 진행하는 풀이는 $\mathcal{O}(100 \ 000 \cdot 10^6 \cdot \sqrt{n})$의 시간복잡도가 소요된다.

 

따라서, 일단 에라토스테네스의 체를 사용하여 $10^6$까지의 모든 소수를 구한 후, 해당 소수들만 저장한다.

 

이후 각 쿼리 $[a_i, b_i]$마다 $b_i$가 소수 리스트에 어디에 위치할 지, $a_i$가 소수 리스트에 어디에 위치할 지를 각각 lower_bound와 upper_bound를 통해 $\mathcal{O}(\log N)$으로 구한다. ($N$은 $10^6$까지의 소수의 개수)

// 34558번 Prime Median
// 에라토스테네스의 체, 이분 탐색
/*
접근 방법:
에테체로 구하고 쿼리마다 이분 탐색 갈기기
*/
#include <iostream>
#include <vector>
#include <algorithm>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

int sieve[1'000'001] = {0,};

int main(void){
    fastio

    sieve[0] = 1; sieve[1] = 1;
    for (int i = 2; i*i < 1'000'001; ++i){
        if (sieve[i] == 0){
            for (int j = i*i; j < 1'000'001; j += i){
                sieve[j] = 1;
            }
        }
    }
    vector<int> primes;
    for (int i = 0; i < 1'000'001; ++i){
        if (sieve[i] == 0){
            primes.push_back(i);
        }
    }

    int N; cin >> N;
    int a, b;
    while (N--){
        cin >> a >> b;
        int p = distance(primes.begin(), lower_bound(primes.begin(), primes.end(), a));
        int q = distance(primes.begin(), upper_bound(primes.begin(), primes.end(), b));
        int cnt = q-p;
        if (cnt % 2 == 0 || cnt < 0){
            cout << -1 << endl;
        } else {
            cout << primes[(p+q)/2] << endl;
        }
    }
    return 0;
}

 

 

H번 - 건물 측량(34559번)

치즈 문제의 아이디어를 사용할 수 있다.

 

0에서부터 시작하여 지도 테두리로 이동한다고 생각하지 않고, 지도 테두리에서 역으로 출발하여 도달할 수 없다면 건물 내부라고 생각하면 된다.

 

그래서 먼저 BFS를 진행하여 건물을 모두 1로 표현한다.

 

고객의 수 $Q = 10^6$이기 때문에, 각 쿼리마다 나이브하게 주어진 범위에서 $1$의 개수를 세는 건 시간초과를 받는다.

 

따라서, $2$차원 누적 합을 이용하여, 각 쿼리를 $\mathcal{O}(1)$로 처리하면 된다. 

// 34559번 건물 측량
// BFS, 누적 합
/*
접근 방법:
테두리에서 BFS 돌려서 건물을 모두 1로 표시
이후 2차원 누적합 때린다.
*/
#include <iostream>
#include <vector>
#include <queue>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

int dr[4] = {-1, 1, 0, 0}, dc[4] = {0, 0, -1, 1};

void BFS(vector<vector<int>>& MAP){
    int N = MAP.size(), M = MAP[0].size();
    vector<vector<int>> visited(N, vector<int>(M, 0));

    // 테두리 담기
    queue<pair<int, int>> Q;
    for (int i = 0; i < N; ++i){
        if (i == 0 || i == N-1){
            for (int j = 0; j < M; ++j){
                if (MAP[i][j] == 0){
                    Q.push({i, j});
                    visited[i][j] = 1;
                }
            }
        } else {
            if (M == 1){
                if (MAP[i][0] == 0){
                    Q.push({i, 0});
                    visited[i][0] = 1;
                }
            } else {
                if (MAP[i][0] == 0){
                    Q.push({i, 0});
                    visited[i][0] = 1;
                }
                if (MAP[i][M-1] == 0){
                    Q.push({i, M-1});
                    visited[i][M-1] = 1;
                }
            }
        }
    }

    // BFS 수행
    while (!Q.empty()){
        int r = Q.front().first, c = Q.front().second;
        Q.pop();

        for (int i = 0; i < 4; ++i){
            int nr = r+dr[i], nc = c+dc[i];
            if (0 <= nr && nr < N && 0 <= nc && nc < M){
                if (MAP[nr][nc] == 0 && visited[nr][nc] == 0){
                    visited[nr][nc] = 1;
                    Q.push({nr, nc});
                }
            }
        }
    }

    // visited되지 않은 모든 MAP의 원소들 바꾸기
    for (int i = 0; i < N; ++i){
        for (int j = 0; j < M; ++j){
            if (visited[i][j] == 0){
                MAP[i][j] = 1;
            }
        }
    }
    return;
}

vector<vector<int>> accumsum(vector<vector<int>>& MAP){
    int N = MAP.size(), M = MAP[0].size();
    vector<vector<int>> acc(N+1, vector<int>(M+1, 0));
    for (int i = 0; i < N; ++i){
        for (int j = 0; j < M; ++j){
            acc[i+1][j+1] = acc[i][j+1] + acc[i+1][j] - acc[i][j] + MAP[i][j];
        }
    }
    return acc;
}

int main(void){
    fastio;

    int N, M; cin >> N >> M;
    vector<vector<int>> MAP(N, vector<int>(M));
    char ch;
    for (int i = 0; i < N; ++i){
        for (int j = 0; j < M; ++j){
            cin >> ch;
            MAP[i][j] = ch-'0';
        }
    }

    BFS(MAP);

    vector<vector<int>> A = accumsum(MAP);

    int Q; cin >> Q;
    int r1, c1, r2, c2;
    while (Q--){
        cin >> r1 >> c1 >> r2 >> c2;
        int P = A[r2][c2] - A[r1-1][c2] - A[r2][c1-1] + A[r1-1][c1-1];
        if (P){
            cout << "No " << P << endl;
        } else {
            cout << "Yes" << endl;
        }
    }
    return 0;
}

 

 

I번 - 브레인롯 챔피언십(34560번)

전형적인 위상 정렬 문제다.

 

하나의 캐릭터를 하나의 정점으로 생각하고, 모든 정점 쌍에 대해서 승/패 여부를 따져주어 그래프의 간선을 만들어준다.

 

이때 간선을 만들어줌과 동시에 indegree값 또한 구해준다.

 

이후 위상 정렬을 실시한다.

 

indegree가 0인 정점을 계속해서 뜯어내는데, 뜯어내는게 불가능하다면 Paradoxe Absurdo출력하면 된다.

 

시간 복잡도는 $\mathcal{O}(N^2)$가 된다.

// 34560번 브레인롯 챔피언십
// 위상 정렬
/*
접근 방법:
모든 정점들에 대해서 승패 여부를 따진 뒤 간선 만들기
이후 위상정렬
위상 정렬 불가능 하면
Paradoxe Absurdo 출력
궁극의 승리자가 여러 개라면 이름순으로 출력합니다.
*/
#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

int main(void){
    fastio

    int N; cin >> N; // 캐릭터의 수
    vector<int> P(N), A(N), S(N);
    vector<string> name(N);
    for (int i = 0; i < N; ++i){
        cin >> name[i] >> P[i] >> A[i] >> S[i];
    }
    
    // 그래프 구성, 진입차수 설정
    vector<vector<int>> G(N);
    vector<int> indegree(N, 0);
    for (int i = 0; i < N; ++i){
        for (int j = i+1; j < N; ++j){
            int win_i = 0, win_j = 0;
            if (P[i] >= P[j]) ++win_i;
            if (P[j] >= P[i]) ++win_j;
            if (A[i] >= A[j]) ++win_i;
            if (A[j] >= A[i]) ++win_j;
            if (S[i] >= S[j]) ++win_i;
            if (S[j] >= S[i]) ++win_j;
            if (win_i > win_j){
                G[i].push_back(j);
                indegree[j]++;
            }
            if (win_j > win_i){
                G[j].push_back(i);
                indegree[i]++;
            }
        }
    }

    // 진입 차수 0인 노드들 찾기
    vector<string> ans;
    queue<int> Q;
    for (int i = 0; i < N; ++i){
        if (indegree[i] == 0){
            Q.push(i);
            ans.push_back(name[i]);
        }
    }

    // 위상 정렬 실시
    vector<int> order;
    while (!Q.empty()){
        int now = Q.front();
        order.push_back(now);
        Q.pop();
        for (int near : G[now]){
            indegree[near]--;
            if (indegree[near] == 0){
                Q.push(near);
            }
        }
    }
    if (order.size() != N){
        cout << "Paradoxe Absurdo" << endl;
    } else {
        sort(ans.begin(), ans.end());
        for (string S : ans){
            cout << S << endl;
        }
    }
    return 0;
}

 

 

J번 - 수열과 쿼리 2025(34561번)

$N$의 제한이 $300 \ 000$이고 쿼리의 개수가 최대 $500 \ 000$이기 때문에, 1번 쿼리(업데이트 쿼리)를 $\mathcal{O}(N)$에 처리하면 시간초과를 받는다.

 

업데이트 쿼리를 최대한 빠르게 하는 것이 관건이고, 다음과 같은 아이디어로부터 빠르게 수행할 수 있다.

 

어떤 하나의 값을 "모두" 선택하여 "모두" 바꾸면, 수열에서 점점 같은 값을 가지는 원소가 많아진다.

 

즉, 만약 $y$값이 이미 존재하고 $x$값에 해당하는 원소들을 모두 $y$값으로 변경한다면, $y$값을 가지는 원소들의 개수가 늘어난다는 뜻이다.

 

따라서, 어떤 특정한 값을 가지고 있는 원소들을 서로 "묶어"준다는 점에서 "분리 집합"을 생각할 수 있다.

 

이때 우리는 조회 쿼리를 위해 $i \rightarrow A_i$매핑(mapping1) 하나와 업데이트 쿼리를 위해 $A_i \rightarrow i$매핑(mapping2) 하나를 만든다.

 

당연히 이때의 인덱스 매핑은 해당 집합의 대표 노드의 인덱스이다.

 

$1 \ x \ y$가 들어올 때, 만약 $x = y$이거나 $x$가 mapping2에 존재하지 않는다면 무시한다.

 

그 외의 경우, 2개의 케이스로 나눌 수 있다.

 

1. $y$가 mapping2에 존재하는 경우.

이때, $\text{mapping2}[x] = i, \text{mapping2}[y] = j$ 두 노드를 merge한다.

이후 대표노드를 $k$라고 하면 $\text{mapping2}[y] = k, \text{mapping1}[k] = y$를 넣어주고, 기존의 $x, y$에 매핑된 것들은 제거해준다.

 

2. $y$가 mapping2에 존재하지 않은 경우.

이때는 merge할 필요가 없으므로, 그냥 mapping1의 값만 변경해주면 된다.

 

조회 쿼리를 수행할 때는 mapping1을 이용하면 된다.

 

따라서, 업데이트를 거의 $\mathcal{O}(1)$에 가깝게 수행할 수 있다.

// 34561번 수열과 쿼리 2025
// 분리 집합, 집합과 맵
/*
문제 : 
길이가 $N$이고 정수로 구성된 수열 $A_1$, $A_2$, $\dots$, $A_N$이 주어집니다.
이때, 다음 쿼리를 수행하는 프로그램을 작성해 주세요.
 
$1$ $x$ $y$: 수열에서 $x$인 값을 모두 선택하여, $y$로 바꿉니다.
$2$ $z$: $A_z$를 출력합니다.

접근 방법:
기본적으로 분리 집합 초기화 해주고, 같은 수가 주어질 때 merge하는 식으로 ㄱㄱ
그리고
mapping1 = 대표 인덱스 i -> A_i
mapping2 = A_i -> 대표 인덱스 i
로 매핑 2개를 만들어준다...

1 x y
1-1. 만약 y가 mapping2에 있다면
mapping2[x] = idx1와 mapping2[y] = idx2를 merge
-> idx1과 idx2 중 대표가 되는 노드를 idx라고 하자.

mapping2[x] 삭제, mapping2[y] = idx로 변경
mapping1[idx1] 삭제, mapping1[idx2] 삭제, mapping1[idx] = y 추가

1-2. 만약 y가 mapping2에 없다면
mapping2[x] = idx를 찾고,
mapping1[idx] = y로 변경 -> mapping2[x]를 삭제 후 mapping2[y] = idx로 재할당

2 z
find(z) = idx
mapping1[idx] 출력
*/
#include <iostream>
#include <vector>
#include <unordered_map>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'

int _find(vector<int>& parent, int node){
    if (parent[node] != node){
        parent[node] = _find(parent, parent[node]);
    }
    return parent[node];
}

void _merge(vector<int>& parent, vector<int>& size, int node1, int node2){
    int p1 = _find(parent, node1);
    int p2 = _find(parent, node2);
    if (p1 == p2) return;
    if (size[p1] > size[p2]){
        size[p1] += size[p2];
        parent[p2] = p1;
    } else {
        size[p2] += size[p1];
        parent[p1] = p2;
    }
    return;
}

int main(void){
    fastio
    

    int N; cin >> N; // 수열의 크기

    // init
    vector<int> parent(N);
    vector<int> size(N, 1);
    for (int i = 0; i < N; ++i){
        parent[i] = i;
    }
    unordered_map<int, int> mapping1, mapping2;

    vector<int> A(N);
    for (int i = 0; i < N; ++i){
        cin >> A[i];
        if (mapping2.find(A[i]) != mapping2.end()){
            int j = mapping2[A[i]];
            mapping1.erase(j);
            _merge(parent, size, i, j);
            int rep = _find(parent, i);
            mapping2[A[i]] = rep;
            mapping1[rep] = A[i];
        } else {
            mapping2[A[i]] = i;
            mapping1[i] = A[i];
        }
    }

    int M; cin >> M; // 쿼리의 개수
    int query, x, y, z;
    while (M--){
        cin >> query;
        if (query == 1){
            cin >> x >> y;
            if (x == y) continue;
            if (mapping2.find(x) == mapping2.end()) continue;
            if (mapping2.find(y) != mapping2.end()){
                int node1 = mapping2[x], node2 = mapping2[y];
                _merge(parent, size, node1, node2);
                int rep = _find(parent, node1);
                mapping2.erase(x);
                mapping2[y] = rep;
                mapping1.erase(node1); mapping1.erase(node2);
                mapping1[rep] = y;
            } else {
                int node1 = mapping2[x];
                mapping1[node1] = y;
                mapping2.erase(x); mapping2[y] = node1;
            }
        } else {
            cin >> z;
            z--;
            int rep = _find(parent, z);
            cout << mapping1[rep] << endl;
        }
    }
    return 0;
}

 

 

K번 - 터치 앤 리턴(34562번)

1번에서 시작하여 1번으로 "돌아온다"는 점에서 TSP를 생각할 수 있다.

 

다만 기존 TSP와 다른 점은 중복 정점의 방문을 허용한다는 것인데, 이것을 처리하는 아이디어가 까다롭다.

 

플로이드-워셜을 통해 직접적으로 이어지지 않은 두 정점 사이에 "간접 간선"을 만들어준다.

 

이러면 기존 TSP를 활용할 수 있다. 다만, DP 정의식이 약간 변경된다.

 

실제로 없는 간선을 만들어준 것이기 때문에 만약 $3$개의 정점을 지난 상태에서 "간접 간선"을 통해 다른 정점으로 이동했다면, 아직 방문하지 않은 정점 하나로 움직이는 것이 아니라, 내가 방문하지 않았던 다른 정점을 거쳐서 이동할 수도 있기 때문이다.

 

따라서 TSP[visited][u] = 적어도 visited의 정점을 방문했고(더 많이 방문했을 수도 있음), 정점 u에 있을 때의 최소 값이라고 정의가 된다.

 

이후 $i^2$점을 얻기 위한 최소 비용은 $\min(TSP[visited][u] + W[u][0])$로 정의가 되며 $visited$의 비트 개수 $-1$이 $i$로 정의된다.

 

$K$의 체력이 있을 경우 여러 번 턴을 진행하여 "최대 점수"를 얻고자 하는 것이므로, 냅색 DP를 이용하여 값을 구하면 된다.

// 34562번 터치 앤 리턴
// 최단 경로, 플로이드 워셜, TSP, Bitfield DP, 배낭 문제
/*
접근 방법:
두 지점을 잇는 통로가 여러개 일 경우 -> 가장 작은거 하나만 남긴다
그렇게 만들어진 인접행렬을 토대로 TSP를 돌린다.
+) 인접행렬을 그대로 쓰는게 아니라 플로이드 워셜 돌리자...
DP[visited][i]인데,
i에서 1번으로 돌아오는 최단 경로를 더해주면 visited된 노드들을 방문한 최소 순환 사이클이 나온다.
(visited.bit_count()-1)**2 = value
DP[visited][i] + min_dist(i, 1) = cost

해당 value와 cost쌍을 토대로 냅색을 돌린다.
*/
#include <iostream>
#include <vector>
#include <queue>

using namespace std;
#define fastio ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
#define endl '\n'
#define INF 1'000'000'000

int main(void){
    fastio

    // 지점의 수, 통로의 수, 용준이의 체력
    int N, M, K; cin >> N >> M >> K;
    
    // 인접 행렬
    vector<vector<int>> G(N, vector<int>(N, INF));
    
    // 간선 추가
    int a, b, c;
    for (int i = 0; i < M; ++i){
        cin >> a >> b >> c;
        a--; b--;
        if (G[a][b] > c) G[a][b] = c;
        if (G[b][a] > c) G[b][a] = c;
    }

    // 플로이드 워셜 돌리기
    for (int m = 1; m < N; ++m){
        for (int s = 0; s < N; ++s){
            for (int e = 0; e < N; ++e){
                G[s][e] = min(G[s][e], G[s][m] + G[m][e]);
            }
        }
    }

    // TSP돌리기
    vector<vector<int>> tsp(1 << N, vector<int>(N, INF));
    tsp[1][0] = 0;
    for (int visited = 1; visited < (1 << N); ++visited){
        if ((visited & 1) == 0) continue;
        for (int u = 0; u < N; ++u){
            if ((visited & (1 << u)) == 0) continue; // 정점 u는 방문한 정점.
            for (int v = 0; v < N; ++v){
                if (visited & (1 << v)) continue; // 정점 v는 방문하지 않은 정점.
                tsp[visited | (1 << v)][v] = min(tsp[visited | (1 << v)][v], tsp[visited][u] + G[u][v]);
            }
        }
    }

    // TSP 결과와 floyd_warshall결과를 합쳐서 순환 결과 찾기
    vector<int> cost_arr(N, INF); // cost_arr[i] = i*i점을 얻기 위한 최소 cost
    for (int visited = 1; visited < (1 << N); ++visited){
        if ((visited & 1) == 0) continue;
        int i = __builtin_popcount(visited);
        i--;
        for (int u = 0; u < N; ++u){
            if (visited & (1 << u)){
                int min_cost = tsp[visited][u] + G[u][0];
                if (cost_arr[i] > min_cost){
                    cost_arr[i] = min_cost;
                }
            }
        }
    }

    // 냅색 돌리기.
    int ans = 0;
    vector<int> knapsack(K+1, 0);
    for (int i = 0; i <= K; ++i){
        for (int j = 0; j < N; ++j){
            if (i >= cost_arr[j]){
                knapsack[i] = max(knapsack[i], knapsack[i-cost_arr[j]] + j*j);
            }
        }
        ans = max(ans, knapsack[i]);
    }
    cout << ans << endl;
    return 0;
}

 

반응형