코딩/백준 문제 풀이

IOI 2020 day1 - Carnival Tickets

stonejjun 2020. 10. 11. 14:25

문제 링크 : www.acmicpc.net/problem/19935

문제 소개

k 판의 게임을 진행한다. 각각의 게임은 m개의 숫자카드중 일부가 있는 n개의 카드 세트에서 1개씩을 뽑는다. 주최측이 원하는대로 수를 선정하고, 그 수와 뽑은 n개의 수의 차이의 절댓값 만큼 점수를 얻는다. 이때 주최측은 얻는 점수가 최소화 되도록 수를 선정한다. 이때 k 판을 진행하면서 얻는 점수가 최대가 되도록 각 판에서 뽑을 숫자카드를 설정하고, 이때 얻을 점수를 구하여라. 

문제 풀이(+의식의 흐름)

part 1. 문제를 읽으면서 생각한 것들 - 1차 생각, 해야할 일들 정리

이 문제를 시작한 이유. 1분 보고 나서 DP 느낌이 확 왔다. 뭔가 난이도의 대부분은 관찰이고, 구현은 그렇게 어렵지 않을 것 같다는 생각이 크게 들었다.

일단 n개의 숫자가 있으면 주최측이 고를 수는 크기 순서대로 n/2번째에 해당하는 수를 고를 것이다. 이는 조금만 생각해도 그 값이 최솟값이라는 것을 알 수 있다. 여러가지 문제에서 사용하기도 해봤고, 수직선 위에서 생각하면 굉장히 직관적인 사실이다.
이를 통해서 한 게임에서 선택된 카드들이 있을 때 그 중 중간값을 구하고, 그 수로부터 차이를 모두 구해서 더하는 것으로 그 게임에서 얻을 점수를 구할 수 있게 되었다. 

part 2. 점수를 구하는 법

점수는 이 문제의 핵심이자 가장 중요한 부분이다. 하지만, 위의 형식은 너무나도 당연하고 단순하고 나에게 정보를 주지 않았다. 나는 이 문제가 애초에 DP라고 생각을 해봤기 때문에 위의 점수체계를 식으로 정리해보았다.
n개의 숫자중 가장 작은 숫자가 num[1], 가장 큰 숫자가 num[n]이라고 하자. 식은 아래와 같이 나온다.
(num[n/2]-num[1])+(num[n/2]-num[2])+(num[n/2]-num[2])....+(num[n-1]-num[n/2])+(num[n]-num[n/2])

오... 식을 써보니 몇가지 사실이 확실히 관찰되었다. 위의 값은 num[n/2]와는 큰 관련이 없었다. 절반의 경우에는 빼지고, 절반의 경우에는 더해지니까. 얻는 점수는 가장 큰 n/2개의 수의 합에서 가장 작은 n/2개의 수의 합을 뺀 것이다!

part 3. 2의 확장. (감과 증명)

근데 이것은 내가 어떤 숫자를 선택해야하는지에 큰 도움을 주지 않는다. 한 번의 게임에만 도움을 준다. 그래서 나는 여기서 한 가지 생각을 했다.
k판에 사용할 수들 nk개가 정해져 있으면, 가장 큰 nk/2 개를 항상 더하는 쪽에 사용할 수 있고, 가장 작은 nk/2 개를 항상 빼는 쪽에 사용할 것이라는 추측이다. 사실 몇 번의 경험과 진전이 없는 문제에 거의 확신을 가지고 있었다.

어쩌피 최종적으로 몇 라운드에 어떤 수 카드를 선정해야할지 모두 정해야 하기 때문에 nk개를 어떻게 나눠야 할지 생각을 해보았다.

n개의 세트에는 각각 ai개의 큰 그룹에 속하는 수와 bi개의 작은 그룹에 속해야 하는 수가 있을 것이다. 이때 n개의 세트를 ai가 큰 순서대로 정렬한다. 이 후 앞의 n/2개에서는 큰 그룹에서 선정하고, 뒤의 n/2개에서는 작은 그룹에서 선정한다. 그 숫자들을 모으면 앞에서 선정한 숫자들이 뒤에서 선정한 숫자들보다 크기 때문에, 앞에 선정한 숫자들이 더해지고, 뒤에 선정한 숫자들이 빼진다. 위와 같은 방식으로 k번을 반복하면 $O(NKlgN)$에 전체에서 큰 그룹의 nk/2개는 어떤 그룹의 큰 n/2개쪽에 속하게 할 수 있다.

part 4. k개를 선택하는 법

이제 우리가 해결해야할 문제는 n개의 그룹에서 각각 k개의 수들을 선택해서 전체 nk개 의 수 들 중에서 절반이 크게 절반이 작게 만들어야 한다.

위에서 설정했듯이 한 세트에서 ai개는 큰 그룹으로 선택될 것이고, bi개는 작은 그룹으로 선택될 것이다. 당연히 한 세트 내에서 가장 큰 ai개와 가장 작은 bi개를 선택할 것이고, 한 세트당 선택하는 수는 총 O(k)가지 경우가 나오게 된다.

조금 더 생각을 해보자. 모든 세트에서 가장 큰 k개를 선택해 놓는다. 당연히 우리는 이 k개 중 가장 작은 수 대신 세트의 가장 작은 수로 바꿔서 빼는 그룹에 넣을 것이다. 이 과정을 nk/2 번 반복하면 된다. 그리고 그 선택은 n개의 세트 중 바꿨을 때 가장 이득이 되는 세트에서 진행하는 것이 좋다. 같은 그룹에서는 맨 처음 선택하는 것이 가장 이득이기 때문에 이 문제를 위와 같이 greedy 적으로 해결할 수 있다.

part 5. 정리

1. n개의 각각의 세트에서 가장 큰 k개를 선택해 모두 더하는 그룹에 넣는다. 
2. 더하는 그룹에서 수 하나를 빼고, 빼는 그룹에 수 하나 넣는 것을 했을 때, n개의 세트에 대해서 각각 어느정도의 이득을 보는지 구한다.
3. 2번의 과정을 통해서 가장 큰 이득을 보는 세트에서 더하는 숫자를 하나 줄이고 빼는 숫자 하나를 늘리는 과정을 한다.
4. 위의 과정을 nk/2번 반복한다.
5. 4번 과정을 진행하면, 어떤 수 카드들을 선택해야 할지가 모두 정해지며, 이 과정에서 얻는 점수도 구할 수 있다.
6. 더할 숫자가 가장 많이 남은 n/2개의 그룹에서 더할 숫자를 뽑고, 나머지 n/2개의 그룹에서 뺄 숫자를 뽑는 과정으로 한 라운드에서 선택할 n개의 수를 정한다.
7. 6번의 과정을 k번 반복하여 nk의 수 카드에 대해서 어느 라운드에 선택할지를 모두 결정한다.

코드

더보기
#include "tickets.h"
#include<bits/stdc++.h>
using namespace std;
typedef int ll;
typedef pair<ll,ll> pii;

#define ff first
#define ss second
#define pb push_back

struct poi{
	ll a,b,idx;
};

ll fl[1010101];
ll fl2[1010101];
ll yak[1010101];


bool sf2(ll a,ll b){
    return fl[a]+fl2[a]<fl[b]+fl2[b];
}

std::vector<std::vector<ll>> arr;

struct sf{
	bool operator()(poi &a,poi &b){
		return arr[a.idx][a.a]+arr[a.idx][a.b]<arr[b.idx][b.a]+arr[b.idx][b.b];
	}
};

priority_queue<poi,vector<poi>,sf> pq;

long long find_maximum(ll k, std::vector<std::vector<ll>> x) {
	arr=x;
	ll n = x.size();
	ll m = x[0].size();
	long long ans=0;
	ll i,j,kk=k;
	std::vector<std::vector<int>> answer;
	for (int i = 0; i < n; i++) {
		std::vector<int> row(m);
		for (int j = 0; j < m; j++) {
			if (j < k) {
				ans-=arr[i][j];
				row[j] = j;
			} else {
				row[j] = -1;
			}
		}
		answer.push_back(row);
	}

	for(i=0;i<n;i++){
		fl[i]=k-1;
		poi x;
		x.a=k-1;
		x.b=x.a+m-k;
		x.idx=i;
		pq.push(x);
	}
	for(ll t=1;t<=n*k/2;t++){
		auto k=pq.top();
		pq.pop();
		ans+=arr[k.idx][k.a]+arr[k.idx][k.b];
		swap(answer[k.idx][k.a],answer[k.idx][k.b]);
        fl[k.idx]--;
		if(fl[k.idx]>=0){
			poi x;
			x.a=fl[k.idx];
			x.b=x.a+m-kk;
			x.idx=k.idx;
			pq.push(x);
		}

	}

    for(i=0;i<n;i++){
        yak[i]=i;
        fl2[i]=fl[i]+m-kk+1;
    }

    for(i=0;i<k;i++){
        sort(yak,yak+n,sf2);
        for(j=0;j<n/2;j++){
            answer[yak[j]][fl2[yak[j]]]=i;
            fl2[yak[j]]++;
        }
        for(j=n/2;j<n;j++){
            answer[yak[j]][fl[yak[j]]]=i;
            fl[yak[j]]--;
        }
    }


	allocate_tickets(answer);
	return ans;
}

'코딩 > 백준 문제 풀이' 카테고리의 다른 글

BOJ 8217 - 유성  (0) 2020.11.01
IOI 2020 day2 - stations  (0) 2020.10.17
IOI 2020 day1 - Connecting Supertrees  (0) 2020.10.07
BOJ 1167 - 트리의 지름  (1) 2020.10.04
BOJ 5916 - 농장 관리  (0) 2020.09.15