SRM 478 500pt: KiwiJuice

問題概要

N(<15)本のビンがあり、キャパシティはC(<49)その中にはA[i]だけのジュースが最初入っている。任意回数だけ、ジュースを他のビンに移し替えることができる。その際、片方が満杯か空になるまで移し替える。各ビンはキャパシティに応じてP[i]円で売れる。利益を最大化する問題。

考えたこと

  • (当時は手も足もでなかった。何か当時O(3^N)とか言ってたような記憶がおぼろげにある)
  • N=15は確かに3^Nのような気もするけど、よく分からんのであまり気にしないようにしよう。
  • しばらく考えて、一度空や満杯になったビンは以降触る必要が無いことに気づいた。
  • これで探索したら案外間に合うんではないだろうか。嘘っぽいな。嘘か。
  • 考えてみたけどまったく案が出なかったので、諦めて他の人の解答見た。
  • ビットDPしてるように見える。あと部分集合全列挙。でも今一よく分からない。
  • Member SRMでEditorialが無かったのでForumの読んでみたらwriter(りんごさん)のコード読めと言われた。
  • 短くて驚く。でも要するに、どの部分集合をまだ売り払って無いか残しておいて、部分集合のうちいくらかを使うことにしたら1本を残して全部空か満杯になる、ということか。確かに…。
  • でも計算量が読めない。各ビットに対して部分集合は2^popcount(bit)だけあるから、自分の直感では計算量2^(2*N)位になってそうな気がする。これで間に合うのか?
  • なるほど、Σ(n,i)*2^i = (1 + 2)^n = 3^nか。二項定理すごい。
  • 計算量の面さえはっきりすれば後は書くだけ。無事通った。

practiceで通ったコード

計算量O(3^N)。

#include <vector>
#include <algorithm>
using namespace std;

const int MAX_N = 15;

bool visited[1<<MAX_N];
int memo[1<<MAX_N];
int score[1<<MAX_N];

struct KiwiJuice {

	int theProfit(int C, vector <int> bottles, vector <int> prices) {
		const int N = bottles.size();

		//前処理
		for(int bit=0; bit<(1<<N); bit++){
			int sum = 0, n = __builtin_popcount(bit);
			for(int i=0; i<N; i++)if( (bit>>i)&1 ){
				sum += bottles[i];
			}

			score[bit] += (sum / C) * prices[C]; //満杯のボトル
			score[bit] += (n - (sum / C) - (sum % C == 0 ? 0 : 1)) * prices[0]; //空のボトル
			if(sum % C != 0){
				score[bit] += prices[sum%C]; //余ったもののボトル
			}
		}
		return rec((1<<N)-1);
	}

	//ビットが0ならもう固定されている。計上済み
	int rec(int bit){
		//メモ化処理
		if(visited[bit]){
			return memo[bit];
		}
		visited[bit] = true;

		int& ret = memo[bit];
		ret = 0;

		//基底
		if(bit == 0){
			return ret = 0;
		}

		for(int subset = bit; subset > 0; subset = ((subset - 1) & bit)){
			ret = max(ret, rec(bit & (~subset)) + score[subset]);
		}

		return ret;
	}

};