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

28-9kyo-hwang #111

Merged
merged 1 commit into from
Feb 22, 2024
Merged

28-9kyo-hwang #111

merged 1 commit into from
Feb 22, 2024

Conversation

9kyo-hwang
Copy link
Collaborator

🔗 문제 링크

SWEA 5644 무선 충전(회원가입 필요)

✔️ 소요된 시간

총 2시간...?

✨ 수도 코드

1. 문제 이해

image

위와 같은 (10 x 10) 크기의 맵에 유저 A는 (1, 1) 위치에서 출발하고, 유저 B는 (10, 10) 위치에서 출발해 매 초마다 북, 동, 남, 서 중 한 방향으로 각각 이동한다. 맵의 곳곳에 배터리 차저(BC)가 존재하는데, 해당 차저의 커버리지(색칠된 영역)에 들어서면 해당 차저의 성능만큼 유저의 스마트폰이 충전된다.

  • 이 때 유저 A, B가 같은 배터리 차저 커버리지에 존재할 수 있는데, 이 경우에는 해당 차저 충전량이 A, B에게 균등하게 반반씩 분배되어 들어간다.
  • 위 그림에서 T=11의 경우 유저 A는 BC 1과 BC 3 커버리지에 존재하고, 유저 B는 BC 1 커버리지에 존재한다. 이 때 A와 B 모두 BC 1을 이용하면 A 차저의 충전량(100)을 반반씩(50씩) 나눠갖지만, A는 BC 3(충전량 70)을 이용하고 B는 BC 1을 이용하면 A는 70, B는 100만큼 충전해 이 방법이 더 효율적이다.

BC 정보와 유저 A, B의 이동 정보가 주어졌을 때 모든 사용자가 충전한 양의 최대값을 구하는 프로그램을 작성해야 한다.
입력은 다음과 같이 주어진다.

5                                        // 테스트 케이스 T
20 3                                     // 첫 번째 테스트 케이스 M: 20, A: 30
2 2 3 2 2 2 2 3 3 4 4 3 2 2 3 3 3 2 2 3  // 사용자 A의 이동 정보
4 4 1 4 4 1 4 4 1 1 1 4 1 4 3 3 3 3 3 3  // 사용자 B의 이동 정보
4 4 1 100                                // BC 1의 정보 (4, 4), C1 = 1, P1 = 100 
7 10 3 40                                // BC 2의 정보 (7, 10), C2 = 3, P2 = 40
6 3 2 70                                 // BC 3의 정보 (6, 3), C3 = 2, P3 = 70
…

2. 문제 풀이

친구가 추천해준 시뮬레이션(구현) 문제이다. 문제에서 주어진 정보를 갖고, 차근히 밀어붙여보자.

  int T;
  cin >> T;
  for (int tc = 1; tc <= T; ++tc) {
    Input();
    cout << "#" << tc << " " << Move() << "\n";
  }

테스트케이스의 개수를 입력받고, 해당 횟수만큼 Input을 수행한 뒤 Move를 수행해 최대 충전량을 출력한다.

  const vector<Loc> offset = {{}, {-1, 0}, {0, 1}, {1, 0}, {0, -1}};

  int M, A;
  vector<int> aMvTrajectories, bMvTrajectories;
  vector<BC> BCs;
  Loc aLoc, bLoc;

  auto Input = [&]() {
    cin >> M >> A;
    aMvTrajectories.assign(M + 1, 0);
    bMvTrajectories.assign(M + 1, 0);
    BCs.assign(A, {});

    for (int T = 1; T <= M; ++T) {
      cin >> aMvTrajectories[T];
    }
    for (int T = 1; T <= M; ++T) {
      cin >> bMvTrajectories[T];
    }

    int bcCnt = 0;
    for (auto &[Index, X, Y, C, P] : BCs) {
      cin >> Y >> X >> C >> P;
      Index = bcCnt++;
    }

    aLoc = {1, 1};
    bLoc = {10, 10};
  };

Input 함수를 정의해 데이터들을 입력받는다.

  1. 사용자들의 이동 정보가 (1, 2, 3, 4) 중 하나의 숫자로 M개가 주어지는데, 각각 북/동/남/서 방향을 의미한다. 이를 위해 offset을 정의하고 이용한다.
  • 각 사용자의 초기 위치((1, 1), (10, 10))에서도 충전을 할 수 있기 때문에 M + 1 크기의 리스트로 만든 뒤, 0번째 칸에는 0 값을 저장한다.
  • 0값은 offset에서 아무것도 없음을 의미하며, 다시 말해 '제자리'를 의미한다.
  1. 배터리 차저의 정보를 저장하기 위해 구조체를 하나 선언했다.
  • 인덱스, 위치(X, Y), 커버 영역(C), 충전량(P)를 기록한다.
  • 또한 사용자 위치와 차저 간의 거리를 계산하기 위한 D 함수도 정의해뒀다.
  1. 마지막으로 aLoc과 bLoc 정보를 초기화해주면 준비는 완료된다.
  auto Move = [&]() {
    int totalCharge = 0;
    for (int time = 0; time <= M; ++time) {
      aLoc = aLoc + offset[aMvTrajectories[time]];
      bLoc = bLoc + offset[bMvTrajectories[time]];

      totalCharge += Charge();
    }
    return totalCharge;
  };

Move에서는 매 시간마다 사용자들의 위치를 옮겨주고, Charge() 함수를 호출한다.

  • Input에서 M초 간의 이동 정보를 MvTrajectories에 저장했으니, 반복문을 통해 0초부터 M초까지 각 사용자의 위치를 변경시킨다.
  • 참고로 pair<int, int> 끼리는 기본적으로 + 연산자를 지원하지 않아 위에서 operator overloading을 구현해놓은 상황이다.
  auto Charge = [&]() {
    int maxCharge = 0;
    for (const auto &aBC : BCs) {
      const int aDist = aBC.D(aLoc);
      const int aAmt = aDist <= aBC.C ? aBC.P : 0;

      for (const auto &bBC : BCs) {
        const int bDist = bBC.D(bLoc);
        const int bAmt = bDist <= bBC.C ? bBC.P : 0;

        const int curCharge =
            (aBC.Index != bBC.Index ? aAmt + bAmt : max(aAmt, bAmt));

        maxCharge = max(maxCharge, curCharge);
      }
    }
    return maxCharge;
  };

사용자 A, B의 스마트폰을 충전시키는 역할을 하는 Charge 함수이다. Move에서 a의 위치와 b의 위치를 옮겨줬으므로, 해당 위치를 기반으로 충전 정보를 찾아본다.

  • A, B가 각각 어느 충전기를 이용할 지 모르기 때문에 모든 경우의 수를 확인해본다.({BC1, BC1}, {BC1, BC2}, ..., {BC2, BC1}, {BC2, BC2}, ...)
  • 각각의 충전기에 대해 얼마나 떨어져있는 지를 구할 수 있고, 이를 기반으로 얼마나 충전할 수 있는지 또한 알 수 있다.

이 때 얼마나 충전할 수 있는지는 크게 3가지 경우로 볼 수 있다.

  1. A, B 둘 다 충전기 커버리지에 없는 경우
  • 이 경우는 그냥 둘 다 (0, 0)이니 최대 충전량 또한 0이다.
  1. A 또는 B 한 명만 충전기 커버리지에 있는 경우
  • 이 경우는 커버리지에 들어온 유저가 충전한 충전량이 곧 최대 충전량이다.
  1. A, B 둘 다 충전기 커버리지에 있는 경우
  • 이 경우에는 서로 다른 충전기 영역에 존재하면 각각의 충전기를 이용하는 게 최선이고, 같은 충전기 영역이면 절반씩 가져가는 수 밖에 없다.

3번 케이스를 커버하기 위해 아래와 같은 코드가 존재하는데

const int curCharge = (aBC.Index != bBC.Index ? aAmt + bAmt : max(aAmt, bAmt));
  • 이를 더 자세히 뜯어보면 아래와 같다.
const int curCharge = 0;
if(aDist == 0 && bDist == 0) {
  continue;
} else if(aDist == 0) {
  curCharge = bAmt;
} else if(bDist == 0) {
  curCharge = aAmt;
} else if(aBC.Index != bBC.Index) {
  curCharge = aAmt + bAmt;
} else {
  curCharge = aAmt / 2 + bAmt / 2;
}
maxCharge = max(maxCharge, curCharge);

맨 위의 예시 그림에서 T=11일 때의 상황을 다시 확인해보자.

  • 사용자 A는 BC 1과 BC 3의 커버리지에 존재하고, 사용자 B는 BC 1의 커버리지에 존재한다. 그러므로 A, B가 이용할 수 있는 충전기의 경우는 {BC1, BC1}, {BC3, BC1} 둘 중 하나가 된다.
  • 하지만 코드 상으로는 {BC1, BC1}, {BC1, BC2}, ..., {BC2, BC1}, ..., {BC3, BC3}와 같이 모든 경우의 수를 검사한다. 그렇기 때문에 충전량이 0인 경우도 존재한다.
  • �a, b가 같은 충전기를 사용하는 경우라도 max를 걸은 이유가 이 때문이다.

최종적으로 가장 많이 충전할 수 있는 충전기 조합을 찾아 해당 충전량 maxCharge를 반환하면, Move 함수에서 반환받아 이를 누적하고, 모든 움직임에 대해 해당 작업을 완료하면 총 최대 충전량을 알 수 있게 되어 main에서 출력할 수 있게 되는 것이다.

3. 전체 코드

#include <iostream>
#include <vector>

using namespace std;
using Loc = pair<int, int>;

#define x first
#define y second

Loc operator+(const Loc &a, const Loc &b) { return {a.x + b.x, a.y + b.y}; }

struct BC {
  int Index, X, Y, C, P;

  BC() : Index(-1), X(-1), Y(-1), C(-1), P(-1) {}
  BC(int inIndex, int inX, int inY, int inC, int inP)
      : Index(inIndex), X(inX), Y(inY), C(inC), P(inP) {}

  [[nodiscard]] inline int D(const Loc &loc) const {
    return abs(X - loc.x) + abs(Y - loc.y);
  }
};

int main() {
  cin.tie(nullptr)->sync_with_stdio(false);
  const vector<Loc> offset = {{}, {-1, 0}, {0, 1}, {1, 0}, {0, -1}};

  int M, A;
  vector<int> aMvTrajectories, bMvTrajectories;
  vector<BC> BCs;
  Loc aLoc, bLoc;

  auto Input = [&]() {
    cin >> M >> A;
    aMvTrajectories.assign(M + 1, 0);
    bMvTrajectories.assign(M + 1, 0);
    BCs.assign(A, {});

    for (int T = 1; T <= M; ++T) {
      cin >> aMvTrajectories[T];
    }
    for (int T = 1; T <= M; ++T) {
      cin >> bMvTrajectories[T];
    }

    int bcCnt = 0;
    for (auto &[Index, X, Y, C, P] : BCs) {
      cin >> Y >> X >> C >> P;
      Index = bcCnt++;
    }

    aLoc = {1, 1};
    bLoc = {10, 10};
  };

  auto Charge = [&]() {
    int maxCharge = 0;
    for (const auto &aBC : BCs) {
      const int aDist = aBC.D(aLoc);
      const int aAmt = aDist <= aBC.C ? aBC.P : 0;

      for (const auto &bBC : BCs) {
        const int bDist = bBC.D(bLoc);
        const int bAmt = bDist <= bBC.C ? bBC.P : 0;

        const int curCharge =
            (aBC.Index != bBC.Index ? aAmt + bAmt : max(aAmt, bAmt));

        maxCharge = max(maxCharge, curCharge);
      }
    }
    return maxCharge;
  };

  auto Move = [&]() {
    int totalCharge = 0;
    for (int time = 0; time <= M; ++time) {
      aLoc = aLoc + offset[aMvTrajectories[time]];
      bLoc = bLoc + offset[bMvTrajectories[time]];

      totalCharge += Charge();
    }
    return totalCharge;
  };

  int T;
  cin >> T;
  for (int tc = 1; tc <= T; ++tc) {
    Input();
    cout << "#" << tc << " " << Move() << "\n";
  }

  return 0;
}

📚 새롭게 알게된 내용

최대한 자세히 설명해본다고 설명했는데, 뭔가 말로만 하려니 쉽지 않다. 그림을 첨부하기에도 좀 애매한 내용이라...
저 최대 충전량을 찾는 데 조금 시간을 날렸다. 완전탐색의 경우를 최후로 미루고 다른 방법 찾겠다고 난리를 쳤더니...
게다가 그림 똑바로 안보고 내가 코드 짜는 행(X)-열(Y) 방식이 아닌 행(Y)-열(X) 방식인 걸 뒤늦게 눈치챘다... 이상한 값이 계속 나와서 이거 찾는다고 시간 또 많이 날렸다.

Copy link
Member

@xxubin04 xxubin04 left a comment

Choose a reason for hiding this comment

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

항상 저의 부족한 지식으로 제대로 리뷰해드리지 못해 죄송할 따름입니다..😭
이런 복잡한 문제는 정말 어떻게 푸시는 건지... 진짜 대단하시다고 생각합니다!👍👍
최근에 cpp을 공부하면서 조금씩 코드가 눈에 들어오기 시작하는데 조금 뿌듯하네요.😊
[[nodiscard]]가 궁금해서 찾아봤는데 이러한 깊은 지식까지 어떻게 익히신 건지 궁금합니다.
제가 파이썬의 combinations() 함수를 오늘 안 것처럼 공부하시는 중간에 하나둘씩 익히신 것인가요?

@9kyo-hwang
Copy link
Collaborator Author

항상 저의 부족한 지식으로 제대로 리뷰해드리지 못해 죄송할 따름입니다..😭

이런 복잡한 문제는 정말 어떻게 푸시는 건지... 진짜 대단하시다고 생각합니다!👍👍

최근에 cpp을 공부하면서 조금씩 코드가 눈에 들어오기 시작하는데 조금 뿌듯하네요.😊

[[nodiscard]]가 궁금해서 찾아봤는데 이러한 깊은 지식까지 어떻게 익히신 건지 궁금합니다.

제가 파이썬의 combinations() 함수를 오늘 안 것처럼 공부하시는 중간에 하나둘씩 익히신 것인가요?

저런 특수한 기능은 높은 확률로 IDE의 인텔리센스가 추천해주면서 알게된 것들입니다... ㅋㅋ
이 사이트를 처음 이용해봐서 IDE로 코드를 작성했는데, nodiscard를 넣어라고 계속 경고를 띄우더라구요 ㅋㅋㅋ

Copy link
Collaborator

@Dolchae Dolchae left a comment

Choose a reason for hiding this comment

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

저는 제 문제를 풀 때에도 변수는 몇 개가 필요한지, 함수는 어떻게 구현해야 할지, 경우는 어떻게 따져봐야할지 전부 다 어려운데 이렇게 복잡한 문제들을 작게작게 나눠서 구현을 완성시킨다는 게 정말 너무 신기하고 멋져요.. 이번 PR도 수고하셨습니다😊

@9kyo-hwang 9kyo-hwang merged commit 46d6c0d into main Feb 22, 2024
2 checks passed
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.

4 participants