kmjp's blog

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

Codeforces #268 Div1 D. Tree

面白いパートよりつまらないパートの方が実装がめんどい。
http://codeforces.com/contest/468/problem/D

問題

N頂点からなる木が与えられる。
各辺は距離付の無向辺である。

頂点x,y間の最短距離をd(x,y)と表記する。
木の頂点番号を1~Nとするとき、 \displaystyle \sum_{i=1}^N d(i,P[i])が最大となる、1~Nのpermutation P[i]を答えよ。
最大値となるP[i]の候補が複数あるなら、辞書順最小のものを答えよ。

解法

根をuとすると、d(i,P[i])=d(u,i)+d(u,P[i])-2*d(u,LCA(i,P[i]) )である。
これを各iに対して和を取ると、permutation分がうまく消えて \displaystyle \sum_{i=1}^N d(i,P[i]) = 2 \times \sum_i d(u,i) - 2 \times \sum_i d(u,LCA(i,P[i]) )となる。
この値を最大化するには、後者のLCA(i,P[i])==uとなるようにすれば最後の項が0になり、それにはuとしてグラフの重心を選べばよい。
重心であればP[i]を考慮しなくてよくなり、回答が容易になる。

よって、まず2回DFSを行い、重心となりうる頂点uのうち、 \sum_i d(u,i)が最大となるものを求めればよい。

ここまで前半パートはPermutationと木の特性を元にした割と面白いパート。
面倒なのは辞書順最小のP[i]を求める部分。

iに対応するP[i]として、uにおける異なるsubtreeの頂点を選択していかなければならない。
基本的には二部マッチングを取るだけだが、辞書順最小を満たす取り方をする必要がある。
EditorialはSegTreeを使っているが、自分は別解法で解いた。

各subtreeに含まれる集合を準備する。
さらに各subtreeのうち最小の番号からなる集合(indord)と、各subtree内の残り要素数からなる集合(numord)の2つを取っておく。

辞書順最小の定番として、iを小さい順にP[i]を定めていく。

  • 残り要素数が過半数となる集合ができてしまうと、二部マッチングが成立しなくなるので、そのような可能性がある場合、過半数となる集合のうち最小の頂点番号をP[i]にする。
  • そうでない場合、残された最小の頂点番号をP[i]にする。ただしP[i]とiが同じsubtreeに含まれそうなら、他の集合の最小頂点番号を持ってくる。
int N,cent;
vector<pair<int,ll> > E[100001];
ll num[100001];
ll sc1[100001],sc2[100001],msc;

int M[100001];
int snum[100001];
vector<vector<int> > SS;
set<int> idord;
map<int,set<int> > numord;


void dfs1(int cur,int pre) {
	int i;
	num[cur]=1;
	FOR(i,E[cur].size()) {
		int tar=E[cur][i].first;
		if(tar!=pre) {
			dfs1(tar,cur);
			sc1[cur]+=sc1[tar]+num[tar]*E[cur][i].second;
			num[cur]+=num[tar];
		}
	}
}
void dfs2(int cur,int pre,ll csc) {
	int i;
	FOR(i,E[cur].size()) {
		int tar=E[cur][i].first;
		if(tar!=pre) dfs2(tar,cur,csc+sc1[cur]+(N-2*num[tar])*E[cur][i].second-sc1[tar]);
	}
	
	ll mav=N-num[cur];
	FOR(i,E[cur].size()) if(E[cur][i].first!=pre) mav=max(mav,num[E[cur][i].first]);
	if(mav<=N/2 && csc+sc1[cur]>msc) msc=csc+sc1[cur], cent=cur;
}
void dfs3(int cur,int pre,int id) {
	int i;
	M[cur]=id;
	FOR(i,E[cur].size()) if(E[cur][i].first != pre) dfs3(E[cur][i].first,cur,id);
}


void solve() {
	int i,j,k,l,r,x,y; string s;
	
	cin>>N;
	FOR(i,N-1) {
		cin>>x>>y>>j;
		E[x-1].push_back(make_pair(y-1,j));
		E[y-1].push_back(make_pair(x-1,j));
	}
	dfs1(0,-1);
	dfs2(0,-1,0);
	
	_P("%lld\n",2*msc);
	
	M[cent]=0;
	FOR(i,E[cent].size()) dfs3(E[cent][i].first,cent,i+1);
	SS.resize(E[cent].size()+1);
	
	for(i=N-1;i>=0;i--) SS[M[i]].push_back(i);
	FOR(j,SS.size()) snum[j]=SS[j].size()*2, idord.insert(SS[j].back()), numord[-snum[j]].insert(j);
	FOR(i,N) {
		//del old
		j=M[i];
		numord[-snum[j]].erase(j);
		if(numord[-snum[j]].empty()) numord.erase(-snum[j]);
		snum[j]--;
		if(SS[j].size()) numord[-snum[j]].insert(j);
		
		if(-numord.begin()->first>=(N-i) && *numord.begin()->second.begin()!=0) j=*numord.begin()->second.begin(); //largest
		else { //lexico lowest
			ITR(it,idord) if(M[*it]!=j || j==0) {
				j=M[*it];
				break;
			}
		}
		_P("%d ",SS[j].back()+1);
		//del
		numord[-snum[j]].erase(j);
		if(numord[-snum[j]].empty()) numord.erase(-snum[j]);
		idord.erase(SS[j].back());
		SS[j].pop_back();
		snum[j]--;
		if(SS[j].size()) idord.insert(SS[j].back()), numord[-snum[j]].insert(j);
	}
	_P("\n");
}

まとめ

重心求めるところまでは面白いんだけど…ってのはCodeforcesのコメントでも書かれているな。