GCJ2008 WF E: The Year of Code Jam
蟻本の一番最後に載っている問題。
最終的には蟻本と同じになるのだけど、前回の記事で書いた方法でより機械的に解いてみる。
まず前段階として、問題のサイズはそんな大きくないし、基本的には?の部分を開催するかどうかという2択から選ぶのだから最小カットで行けそうだと信じることが前提。部分問題に落とすのも辛そうなのでDPも上手く行かなそうだし…。
方針
x[i] = {i番目の日にコンテストを開く?1:0}
としてみる(とても自然)。
今回は最大化の問題なので、例によって損害を最小化する問題だと考える。基本的には毎日コンテストを開いて幸福度4を得られたものだと考えてみる。
このとき、最小化すべき損害の式(目的関数)は次のようになる。長くなるので4つに分ける。
- sum f(x[i])*0 + f(1-x[i])*4
コンテストを開かなかったら損害4が発生する。
- sum_{i,jは隣接している} f(x[i]+x[j]-1)*2
x[i]=x[j]=1のときは2の損害を受ける。
- sum_{i日目はコンテストが無いと確定している} f(x[i])*inf
x[i]=1のときinfのペナルティを受けると思えば良い。
- sum_{i日目はコンテストが有ると確定している} f(1-x[i])*inf
x[i]=0のときinfのペナルティを受けると思えば良い。
ここまではx[i]を上の様に定義したら勝手に出てくる。
さて、2つ目の式ではfの中に差分でないf(x[i]+x[j]-1)が出ている。このままでは最小カットにならないので、この形の常套手段としてx[j]=1-x[j]と置き換えてみる。つまり、二部グラフの片側に属する頂点に対しては
x[i] = {i番目の日にコンテストを開く?0:1}
と入れ替えることにする。f(x[i]+x[j]-1)と書いている部分の、iとjは二部グラフのことなる成分に属しているので今回はこれでfの中身を全部差分にできる。後は1,3,4つ目の目的関数についても二部グラフの片方についてx[i]と1-x[i]を入れ替えてやれば良い。
ということで、この問題で二部グラフの片側について0,1を入れ替えるのは極めて自然で、特別な発想を要するものではないことであったことがわかった(もちろん実際にコンテスト中に気づけるかは別問題だけど、少なくとも冷静な状態だとそんな突飛ではない)。
結局、x[i](=x[i]-0)か1-x[i]かx[i]-x[j]という差分の形を作ることを目標にすればよいだけだった。
acceptされたコード
int toIndex(int y, int x) { return y * W + x; } int solve() { Graph G; G.init(H*W + 2); const int s = H*W, t = s + 1; for (int y = 0; y < H; ++y) { for (int x = 0; x < W; ++x) { if ((y+x)%2 == 0) { G.addDirectedEdge(s, toIndex(y, x), 4); for (int k = 0; k < 4; ++k) { const int ny = y + dy[k], nx = x + dx[k]; if ( 0 <= ny && ny < H && 0 <= nx && nx < W ) { G.addDirectedEdge(toIndex(y,x), toIndex(ny,nx), 2); } } if (board[y][x] == '.') { G.addDirectedEdge(toIndex(y, x), t, INF); } else if (board[y][x] == '#') { G.addDirectedEdge(s, toIndex(y, x), INF); } } else { G.addDirectedEdge(toIndex(y, x), t, 4); if (board[y][x] == '.') { G.addDirectedEdge(s, toIndex(y, x), INF); } else if (board[y][x] == '#') { G.addDirectedEdge(toIndex(y, x), t, INF); } } } } return 4 * H * W - calcMaxFlow(G, s, t); }