CodeChef July Challenge 2019: LOVEMUFFIN
CodeChef July Challenge 2019 の問題: LOVEMUFFIN (Code: LVMFFN)
問題ページ: Contest Page | CodeChef
問題概要
1からまで番号が付いた人のメンバーがいる。
このメンバーでドルを分配する。
この分配の方法の決め方は から以下のように行う。
- 番号の人が現在いる各メンバーに何ドル分配するか提案する
- この提案に(自分含めて) 人が賛成すればその分配方法に決まり、終了する
- は現在いるメンバーの数()とする
- 賛成数が足りず、分配方法が決まらなかった場合、番号iの人は屋上から放り出され、その場からいなくなる
- つまり、メンバーの数が1人減る
- 番号の人が提案するところから(1)に戻る
各提案について各メンバーは、その提案に決まった場合に自身が得られる分配金に比べ、この案に決まらずその後の案で自分が確実に得られる分配金が同じもしくはそれ以上の場合、その提案に反対する。また、メンバー全員は自分が最大限の分配金を得られるように最適に動くとする。
また、提案に失敗し、屋上から放り出された人は を得るのと同じと考える。
今から番号1の人がメンバーに対し分配方法を提案しようとしている。
同じについて、が異なりうる個のシナリオについて、自分が最大いくら得られるか、もしくは確実に屋上から放り出されるかを求めよ。
制約
解法
各人がどのように最適に提案していくのかが分かりづらくて苦労した。
逆にそれが分かればなんとかなる問題だった。
まず、ある人を案に賛成させる場合、この案に決まらなかった場合の後の案で確実に得られる分配金より大きくなければならず、そのような人が(自分含めて) 人必要となる。(提案する人は自分の提案に決まらなければ投げ出されるので必ず賛成する)
解としては、
- 人に、この案を決まらなかった場合に確実に得られる分配金 + 1 を配る
- 配る分配金が少ない人から配る
- この後確実に投げ出されてしまう人は0でも賛成するので配らなくてよい
- 残った分が得られる分配金の最大値、逆に足りなかった場合は投げ出される
となる。
各人が確実に得られる分配金は の時から順番にシミュレーションしていけば分かる。
この問題で気をつけるべき点は、分配方法の仕方によって分配金が得られたり得られなかったりする時、それは確実に得られる分配金でないという点である。例えば、分配方法によって3もしくは0が得られる場合は、確実に得られる分配金は0であると考える。
以下は の時の分配方法である。(同じ列は同じ人を示している)
0以上は得られる分配金、-1は投げ出される状態、X?は分配方法によってはXもしくは0の分配金が得られることを示している。
N | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
1 | 5 | |||||||||||
2 | 5 | -1 | ||||||||||
3 | 0 | 0 | 5 | |||||||||
4 | 1 | 1 | 0 | 3 | ||||||||
5 | 2? | 2? | 1 | 0 | 2 | |||||||
6 | 1 | 1 | 0 | 1 | 0 | 2 | ||||||
7 | 2? | 2? | 1 | 2? | 1 | 0 | 1 | |||||
8 | 1 | 1 | 0 | 1 | 0 | 1 | 0 | 1 | ||||
9 | 2? | 2? | 1 | 2? | 1 | 2? | 1 | 2? | 0 | |||
10 | 1? | 1? | 0 | 1? | 0 | 1? | 0 | 1? | 1? | 0 | ||
11 | 1? | 1? | 1? | 1? | 1? | 1? | 1? | 1? | 1? | 1? | 0 | |
12 | 1? | 1? | 1? | 1? | 1? | 1? | 1? | 1? | 1? | 1? | 1? | -1 |
部分点は、この分配方法をの時からシミュレーションしていけば取れる。(計算量: )
ここまで通すのに結構時間かかった。
ここからは、得られる解をから並べた数列を眺めて考えていく。
例えば、 の場合の の数列を以下に示す。
5,-1,5,3,2,2,1,1,0,0,0,-1,0,-1,-1,-1,0,-1,-1,-1,-1,-1,-1,-1,0,-1,-1,-1,-1,-1
別の例として、 の場合の の数列を以下に示す。
5,5,4,4,4,3,3,3,2,2,2,1,1,1,0,0,0,-1,0,-1,-1,0,-1,-1,-1,0,-1,-1,-1,-1
数列としては、はじめを除き個ごとに1減少し、その後はいくつかの-1ごとに0が出現する並びになっている。
具体的に個ごとに1減少していくのは の場合である。 と の場合で初めの並びが異なることに注意する。
次に0か-1が並ぶ の場合について考えていく。
に出現した各0について直前に0以上の値が何個前に出現したかで数列をつくってみる。
の時は以下のようになることが分かる。 の形になる。
1, 1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, ...
の時は以下のような感じ。パッと見の規則性が分かりづらい。
1, 1, 1, 2, 3, 4, 6, 9, 14, 21, 31, 47, 70, 105, 158, ...
この数列をOEISで調べると以下の数列が出てくる。
K = 3 の時に出てくる数列: A073941 - OEIS
K = 4 の時に出てくる数列: A072493 - OEIS
で一般化すると以下の数列になることが分かる。
この の和 を計算することで、0がいつ出現するかを計算することができる。
おまけにの総和は以下の数列で計算できる。(今回は という制約から使わなかった)
K = 3 の時に出てくる数列: A061419 - OEIS
K = 4 の時に出てくる数列: A087192 - OEIS
0になるか-1になるか判定するのには となる全ての が必要になるが、これらの値は制約的に計算することができる。
最も多くなるのが の時で、この場合でも の個数は約 個 になる。
まとめると、以下が解となる。
- の場合
- の時
- の時 → 解は
- の時 → 投げ出される
- の時 → 解は
- の時 → 解は
- の時
- の場合
- 上記の数列の各総和を計算し、 となる が存在するかを判定
- 存在する時 → 解は 0
- 存在しない時 → 投げ出される
- 上記の数列の各総和を計算し、 となる が存在するかを判定
の場合については、調べる必要のある値 を列挙しておき、を全列挙しながら判定すればよい。
実装
この問題の制限時間が0.5秒だから、変な実装して無限にTLEしてめっちゃ思考錯誤してた。
(単純に計算方針が悪かっただけだった...)
制約がlong long intギリギリなので、オーバーフローに気をつけながら計算してる。
提出コード(C++14): Solution: 25200754 | CodeChef
// IO高速化 class FastIO { static const int rdata_sz = (1 << 25), wdata_sz = (1 << 25); char rdata[rdata_sz], wdata[wdata_sz], *rb, *wb; char tmp_s[20]; public: FastIO() { rdata[fread(rdata, 1, rdata_sz, stdin)] = 0; rb = rdata; wb = wdata; } ~FastIO() { fwrite(wdata, 1, wb - wdata, stdout); } template<typename T> inline void read_i(T &x) { bool neg = false; x = 0; while((*rb < '0' || *rb > '9') && *rb != '-') ++rb; if(*rb == '-') { neg = true; ++rb; } while('0' <= *rb && *rb <= '9') { x = 10 * x + (*rb - '0'); ++rb; } if(neg) x = -x; } #define pc(x) *(wb++) = x template<typename T> inline void writeln_i(T x) { if (x == 0) { pc('0'); pc('\n'); return; } if(x < 0) { pc('-'); x = -x; } char *t = tmp_s; while(x) { T y = x / 10; *(t++) = (x - y*10) + '0'; x = y; } while(t != tmp_s) pc(*(--t)); pc('\n'); } inline void writeln_s(string s) { for(int i=0; i<s.size(); ++i) pc(s[i]); pc('\n'); } #undef pc }; FastIO io; #define Q 500001 int k, q; ll p0[Q], q0[Q]; unordered_map<ll, bool> nmap; const string cant = "Thrown off the roof."; int main() { cin.tie(0); ios_base::sync_with_stdio(false); io.read_i(k); io.read_i(q); const ll inf = 1e18; nmap.reserve(8000000); if(k == 2) { // K = 2 の場合の前処理 for(int i=0; i<q; ++i) { ll n, m; io.read_i(n); io.read_i(m); p0[i] = n; q0[i] = m; if((n+1)/2 > m) { nmap[n+1 - 2*m] = 0; } } } else { // K > 2 の場合の前処理 ll limit = (inf+k-1)/k; for(int i=0; i<q; ++i) { ll n, m; io.read_i(n); io.read_i(m); p0[i] = n; q0[i] = m; if(m < limit && m*k < n) { nmap[n+1 - k*m] = 0; } } } // 数列とその総和の計算 ll sum = 0, a = 1; while(sum + a >= 0 && sum + a <= inf) { sum += a; if(nmap.count(sum)) { nmap[sum] = 1; } a = (sum + k-2) / (k-1); } if(k == 2) { // K = 2 の場合: 解を計算 for(int i=0; i<q; ++i) { ll n = p0[i], m = q0[i]; if((n+1)/2 <= m) { if(n == 2) io.writeln_s(cant); else if(n <= 3) io.writeln_i(m); else { io.writeln_i(m-1 - ((n-1)/2)); } } else { if(nmap[n+1-2*m]) { io.writeln_s("0"); } else { io.writeln_s(cant); } } } } else { // K > 2 の場合: 解の計算 ll limit = (inf+k-1)/k; for(int i=0; i<q; ++i) { ll n = p0[i], m = q0[i]; if(limit <= m || n <= m*k) { io.writeln_i(m - n/k); } else { if(nmap[n+1-k*m]) { io.writeln_s("0"); } else { io.writeln_s(cant); } } } } return 0; }