AGC033: C - Removing Coins

問題概要

 N 頂点からなる木が与えられる。 i (1 \leq i \leq N-1) 番目の辺は  a_i b_i を結んでいる。

最初それぞれの頂点にコインが 1枚おいてある。高橋くんと青木くんはそれぞれコインが置いてある頂点  v 1つ選びその頂点に置いてあるコインをすべて取り,そのあとすべてのコインを  v に距離  1 だけ近づけるという操作を繰り返す。

操作ができなくなった人を負けとすると,お互いに最善を尽くしたときどちらが勝つかを求めよ。

(問題文は https://atcoder.jp/contests/agc033/tasks/agc033_c に記載されている。)

制約

  •  1 \leq N \leq 2\times 10^5
  •  1 \leq a_i \leq N (1 \leq i \leq N-1)
  •  1 \leq b_i \leq N (1 \leq i \leq N-1)
  •  a_i \neq b_i
  • 与えられるグラフは木

アイディア

 1回の操作でコインは選んだ頂点 vに少しずつ引き寄せられるので,葉となる頂点からはコインは出ていくが入ってこない。つまり, 1回の操作はグラフから葉となる頂点をすべて消す(ただし選んだ頂点 vについては葉であっても当然消されない)のと同値である。

ここで,木上の最長パスのうちの 1つ(このパスを Pとする)についてのみ考えることとする。この最長パスの端点を S, Tとすると S Tを選ぶとこのパスの長さは 1減り, S, T以外の頂点を選んだ場合についてはこのパスの長さは 2減ることになる。

次に, P上にない頂点について考える。葉ではない頂点を選ぶとき,すべての葉が消されるので, Pよりも長いパスができることはなく, Pのパスを 2減らすこととなる。また, S, Tのどちらかを選ぶときも同様に S, Tのうち選ばれなかった頂点とすべての葉が消されるので, Pよりも長いパスができることはなく, Pのパスを 1減らすこととなる。

さらに, P上にない葉を消すことを考えると,もしもその葉が Pとは別の最長パスの端点であったとしたらそのパスを新たに Pとして考えることができ(つまり Pの長さが 1だけ減る),それ以外のときは Pより長いパスができることはないので結局 Pのパスを 2減らすこととなる。

以上のことから,1回の操作は Pの長さを 1または 2減らすことと同値であることが分かる。

実装

木の最長パスを一般に直径という。直径の長さ(及びその長さを持つパス)はどの頂点でもよいので頂点を1つ選び Vとし, Vから最も遠い頂点の 1つを Sとして, Sからもっとも遠い頂点の 1つを Tとすると, S Tを端点とするパスの長さと一致する。 S Tは簡単なBFSで求められるので,直径の長さは O(N)で求まる。

そして,直径の長さをもとに青木くんが勝つか高橋くんが勝つかは単純なメモ化再帰*1で求めることができる。この計算量は O(N)である。

よって全体的な計算量は O(N)である。

コード

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

const int INF = (1<<30);

vector<int> g[225816],q[2];
int dist[225816];
bool done[225816],memo[225816];

bool solve(int r){
    if(done[r]){
        return memo[r];
    }
    done[r] = true;
    if(r==0){
        memo[r] = true;
    }else if(r==1){
        memo[r] = false;
    }else{
        memo[r] = (!(solve(r-1)&&solve(r-2)));
    }
    return memo[r];
}

int main(void){
    int N,a,b,i,j,k;
    cin >> N;
    for(i=1; i<N; ++i){
        cin >> a >> b;
        --a; --b;
        g[a].push_back(b);
        g[b].push_back(a);
    }
    a = 0;
    for(k=0; k<2; ++k){
        fill(dist,dist+N,INF);
        q[k].push_back(a);
        dist[a] = 0;
        for(i=0; i<q[k].size(); ++i){
            a = q[k][i];
            for(j=0; j<g[a].size(); ++j){
                b = g[a][j];
                if(dist[b] > dist[a]+1){
                    dist[b] = dist[a]+1;
                    q[k].push_back(b);
                }
            }
        }
    }
    cout << ((solve(dist[a]))?"First":"Second") << endl;
    return 0;
}

(提出したものはこちら)

感想

コンテスト中に 800を通したので喜んでいたら任意人類が通していて「うーん」となってしまった。問題自体はとても綺麗で好き。

*1:なおメモ化再帰も実は必要ない。直径の長さを 3で割った余りが 1のときは後手必勝であり,それ以外の場合は先手必勝であることを示すことができる。