kmjp's blog

競技プログラミング参加記です

dwangoプログラミングコンテスト本選 : C - ドライブ

わかってしまうとなかなか面白い問題。
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パートがうまく書けなさそう。