わかってしまうとなかなか面白い問題。
http://dwango2015-honsen.contest.atcoder.jp/tasks/dwango2015_finals_3
問題
N頂点M無向辺のグラフが与えられる。
i番目の辺の移動コストは2^iである。
ある点から始め、全辺を1回以上通って始点に戻る閉路のうち、最小コストのものを求めよ。
解法
オイラー閉路を構築するためには各頂点の次数が偶数であればよく、逆に奇数の点があるようなら同じ辺を2本に複製して条件を満たすようにしなければならない。
また、i番目の辺を複製するより、(i-1)番目以下の辺を全部複製する方がコストが低いため、(i-1)番目以下の複製で条件を満たせるなら、i番目を複製する必要はない。
つまり、複製する可能性があるのは最小全域木上の辺のみである。
まず辺をコストの小さい順に処理し、Union-Findで点を連結して最小最小木を作る。
- i番目の辺を処理する際、両端2頂点がすでに(i-1)番以下の辺で連結されていない場合:
- この辺を最小全域木の構築辺として加える。同時に両端の頂点を連結する。
- i番目の辺を処理する際、両端2頂点がすでに(i-1)番以下の辺で連結されている場合:
- この辺は最小全域木には入らない。
- 両端2頂点の偶奇を反転させて終了。
以後、この最小全域木について適当な点からDFSしていく。
この際、子のサブツリーの次数の和が偶数なら、親と子の間の辺を複製させる、という処理を繰り返していけば良い。
(最小全域木を作る段階では、まだ1回目の辺をカウントしてないので、この時点で次数が偶数である=1回目の辺を足すことで奇数になってしまう、という考え方)
template<int um> class UF { public: vector<int> par,rank,cnt; UF() {rank=vector<int>(um,0); cnt=vector<int>(um,1); for(int i=0;i<um;i++) par.push_back(i);} int operator[](int x) {return (par[x]==x)?(x):(par[x] = operator[](par[x]));} int count(int x) { return cnt[operator[](x)];} int operator()(int x,int y) { if((x=operator[](x))==(y=operator[](y))) return x; cnt[y]=cnt[x]=cnt[x]+cnt[y]; if(rank[x]>rank[y]) return par[x]=y; rank[x]+=rank[x]==rank[y]; return par[y]=x; } }; int N,M; ll p2[505050]; int odd[505050]; vector<pair<int,int> > E[505000]; ll mo=1000000007; ll ret; ll dfs(int cur,int pre) { ll ret=0; FORR(r,E[cur]) if(r.first!=pre) { ret += dfs(r.first,cur); if(odd[r.first]==0) ret += r.second; odd[cur]^=odd[r.first]; } return ret%mo; } void solve() { int i,j,k,l,r,x,y; string s; UF<500000> uf; cin>>N>>M; p2[0]=1; FOR(i,M) { p2[i+1]=p2[i]*2%mo; cin>>x>>y; x--,y--; ret = (ret+p2[i+1])%mo; if(uf[x]==uf[y]) { odd[x]^=1; odd[y]^=1; } else { uf(x,y); E[x].emplace_back(y,p2[i+1]); E[y].emplace_back(x,p2[i+1]); } } cout<<(ret+dfs(0,-1))%mo<<endl; }
まとめ
本番はBで時間切れしてたので見てないけど、Cは本番でも解けなさそうだな…。
最小全域木までは思いつけても、その後のDFSパートがうまく書けなさそう。