Post

백준 8980 택배 문제 풀이

백준 8980 택배 문제 풀이

백준 온라인 저지, 8980번: 택배

도입

주어진 구간별로 배송이 필요한 택배 상자 양이 평이한 경향을 보인다면, 다소 많은 택배 상자의 배달을 요구하는 긴 구간보다 양이 적은 짧은 구간을 여러 개 선택하는 것이 더 낫다.


1번 마을에서 2번 마을 구간 $A$에서 10개의 택배 상자 배송을, 2번 마을에서 3번 마을 구간 $B$에서 5개의 택배 상자 배송을, 그리고 1번 마을에서 3번 마을 구간 $C$에서 $N$개의 택배 상자를 배송해야 한다고 가정한다.

이 상황에서 구간 $A$와 구간 $B$ 두 개를 선택하는 것과 구간 $C$ 한 개를 선택하는 것 차이에 총 배송량에는 차이가 없다. 하지만 대부분의 상황에서 두 구간을 선택하는 것이 잠재적으로 더 이롭다.

  1. 트럭의 용량이 10보다 작다면, 구간 $A$와 구간 $C$에서 요구하는 배송량을 모두 처리할 수는 없다. 하지만 구간 $C$의 배송을 처리하는 기회비용으로 구간 $A$의 배송량과 구간 $B$의 배송량 모두를 처리할 수 있다.
  2. 트럭의 용량이 10 이상 15 미만이라면, 구간 $A$와 구간 $B$가 요구하는 모든 배송량을 처리할 수 있다. 동시에 남는 용량을 구간 $C$, 혹은 다른 구간의 배송에 대응할 수 있다.
  3. 트럭의 용량이 15 이상이어도, 구간 $C$를 우선 선택하는 것보다 위의 2번과 같이 $A$와 $B$를 먼저 선택한 후 남는 용량을 구간 $C$의 대응에 사용하는 것이 더 낫다.


사실 위 상황은 “구간별로 배송량이 평이한” 것과 관계가 없다. 문제 상황에서는 트럭의 한계 용량이 결정되어있고 배송 구간마다의 택배를 모두 보내지 않고 일부만 보내는 것이 가능하므로, 항상 *더 짧은 구간을 우선 선택*하는 것이 좋다.

문제 풀이

예제 1의 택배 배송 상황을 (1) 구간의 길이 (2) 끝점 기준 (3) 박스 개수 순으로 오름차순 정렬하면 다음과 같다.1

1 사실 도입 문단에서 파악했듯, (1) 조건 외에는 큰 의미가 없다.

  1. 1 → 2 (길이 1, 끝점 2, 박스 10)
  2. 2 → 3 (길이 1, 끝점 3, 박스 10)
  3. 3 → 4 (길이 1, 끝점 4, 박스 20)
  4. 1 → 3 (길이 2, 끝점 3, 박스 20)
  5. 2 → 4 (길이 2, 끝점 4, 박스 20)
  6. 1 → 4 (길이 3, 끝점 4, 박스 30)

우선 1. ~ 3. 구간을 모두 선택하더라도 트럭의 용량 40을 초과하지 않으므로, 구간을 더 선택할 수 있다.

1 → 22 → 33 → 4합계
10/40 110/40 220/40 340

1, 2, 3번 구간의 배송량을 처리하기로 했음에도, 4번 구간의 배송량을 온전히 처리할 수 있다.

1 → 22 → 33 → 4합계
30/40 1, 430/40 2, 420/40 360

5번 구간의 배송량은 박스 20개이지만, 2 → 3 구간에서 트럭의 여유 수용 능력은 박스 10개뿐이다. 5번 구간에서는 박스 10개를 배송한다.

1 → 22 → 33 → 4합계
30/40 1, 440/40 2, 4, 530/40 3, 570

6번 구간의 배송 수요에는 대응할 수 없다.

구현

1
2
3
4
5
6
7
using delivery = tuple<int, int, int>;

bool compare(delivery a, delivery b) {
  if (get<1>(a) != get<1>(b)) return get<1>(a) < get<1>(b);
  if (get<0>(a) != get<0>(b)) return get<0>(a) > get<0>(b);
  return get<2>(a) > get<2>(b);
}

실제 문제 풀이에서는 위와 같은 비교 함수를 구현하였으나, 단순히 구간 길이를 뺄셈 연산으로 계산하여 비교하는 것이 더 직관적이다. 그렇게 구현하더라도 치명적인 결과 차이는 발생하지 않는다.

1
int *loaded_at = (int *)calloc(sizeof(int) * (N + 1));

트럭의 배송 한계를 고려하기 위해, 개별 구간마다 배송량을 추적할 필요가 있다. 위의 loaded_at[i]는 $i - 1$번 마을에서 $i$번 마을까지의 구간에서 적재된 택배 상자의 양을 기록하는데 사용한다.

각 구간에서 배송량을 선택하기 위해서는, 구간의 길이만큼 loaded_at 배열을 순회해야 한다. 하지만 시간이 지나치게 오래 걸리지는 않는다. 정렬에 의해서 구간이 길어질수록 반복 과정 상에서 더 늦게 처리되기 때문에, 앞서 선택된 구간들의 배송량에 의해 반복 과정이 조기에 종료되는 경우가 더 많아진다.

마무리

8980번 C++ 답안 - 생각을 완전히 정리하고 구현한 것이 아니어서 본문보다는 다소 복잡하게 구현되어있다.