CF1987E 題解

Rnfmabj發表於2024-07-01

CF1987E 題解

題意

給定一棵大小為 \(n\) 的有根樹,各點各有一點權 \(a_i\)。每次操作可以選定一節點使其點權加一,求最小的運算元,使得任一節點滿足其點權不大於其所有兒子的點權之和。

\(n \le 5000 ,0 \le a_i \le 10^9\)


題解

麻了,賽後十五分鐘調出來,可惜為時已晚。

讀懂題之後容易發現,題目最核心的地方在於當我們需要一個節點 \(+1\) 時,如果這個節點剛好等於兒子的點權和,那麼就意味著需要有一個兒子節點 \(+1\) ;而這個兒子如果剛好等於其兒子的點權和,那就意味著它的兒子也有一個需要 \(+1\),一直遞迴下去......直到某一節點點權嚴格小於其兒子的點權和,或者這個節點是葉子。

也就是說,如果一個節點需要 \(+1\) ,那麼需要操作的節點就是包含其自身往下一直延伸的一條鏈,到葉子或者點權嚴格小於兒子點權和的節點為止,代價即為這條鏈的長度。

那麼我們就可以令 \(b_v=(\sum_{u ∈ L}a_u)-a_v\) ,其中 \(L\) 為點 \(v\) 的子節點構成的集合。特別的,葉子節點的 \(b\) 為無限。

對於新定義的權值 \(b\) ,原題意就轉化為:對任意節點 \(v\),單次操作可以選定 \(v\) 子樹中除自己外任一節點 \(u\) ,使\(b_v=b_v+1,b_u=b_u-1\);操作的代價為 \(u\)\(v\) 的距離。最小化使得所有節點 \(v\) 滿足 \(0 \le b_v\) 的代價。

這是怎麼來的呢?當我們按原題意操作了一條鏈時,注意到對鏈上除最深的節點也就是鏈尾之外,其他的節點權值與其兒子節點權值和之差保持不變;而鏈尾的這個差(也就是 \(b\) )縮小了 \(1\),鏈首父親的 \(b\) 增加了 \(1\)。根據原題意,我們要的就是所有的節點的 \(b\) 值不小於 \(0\)

所以對於每個 \(b < 0\) 的節點,我們只要貪心地先選深度小的且其 \(b\) 值仍為正數的後代,扣它的 \(b\) 值來填自己的,如果它的 \(b\) 值不夠填那就把它的 \(b\) 值耗完再找下一個深度小的,不夠再往下,反正葉子節點的 \(b\) 是正無窮,只要有後代就一定能填上,只是代價大小的問題。容易證明這樣貪心能使總代價最小——把深度淺的後代自己不拿去填,留給自己的父親填不會更優。

具體實現就是在遍歷過程中對每個節點維護其後代 \(b\) 值尚可被壓榨利用的點集,如果自身 \(b>0\) 則向上合併點集時將自身納入點集(容易證明自身一定是自己父親利用的第一選擇);反之,則優先選擇自身後代貢獻的點集中深度較小的來填自身的 \(b\) 值直到 \(b=0\) 。容易發現我們需要在維護過程中保持這個點集是單調的,用歸併即可。推薦用 std::vector 維護點集並按降序維護,因為將自身納入點集時我們沒有 push_front() 這個函式 。

歸併幫我們在合併時壓掉了一個 \(\log n\) 的複雜度,但是由於樹本身形狀的不確定,失去了點分治中樹重心那樣優秀的性質,最終的時間複雜度仍然是 \(O(n^2)\)


const ll maxn=5e3+5;
ll a[maxn],siz[maxn],dep[maxn];
ll b[maxn],ans;
ll n;
vector<ll>e[maxn];
void dfs(ll x){//預處理出 b 值
	siz[x]=1;
	b[x]=-a[x];
	for(auto v:e[x]){
		dep[v]=dep[x]+1;//標 dep 便於計算代價
		b[x]+=a[v];
		dfs(v);
		siz[x]+=siz[v];
	}
	if(siz[x]==1)b[x]=1ll<<31;//葉子的 b 值為正無窮
}
vector<ll>work(ll x){//返回的是點集
	vector<ll>lst;//自己的點集
	for(auto v:e[x]){
		vector<ll>res=work(v);
		vector<ll>tmp;
		ll l=0,r=0,len1=lst.size(),len2=res.size();
		while(l<len1||r<len2){//歸併
			if(l==len1)tmp.push_back(res[r++]);
			else if(r==len2)tmp.push_back(lst[l++]);
			else if(dep[lst[l]]>dep[res[r]])tmp.push_back(lst[l++]);
			else tmp.push_back(res[r++]);
		}
		lst.swap(tmp);
	}
	if(b[x]>0)lst.push_back(x);//b>0 則放入自身
	else if(b[x]<0){
		ll p=lst.size()-1;//不用驗空,b<0 其必有兒子,有兒子必有後代為葉子
		while(b[x]<0){
			ll it=lst[p];
			ll sum=min(-b[x],b[it]);//要麼直接填滿,要麼把這個用完
			b[it]-=sum,b[x]+=sum;
			ans+=sum*(dep[it]-dep[x]);//代價
			if(b[it]==0)p--,lst.pop_back();//用完扔掉不然單調性沒法保證
		}
	}
	return lst;//自身點集向上合併
}
void solve(){
	n=R;
	ans=0;//清多測
	for(ll i=1;i<=n;i++){
		a[i]=R;
		e[i].clear();//這裡好一點的寫法是用 tmp.swap(e[i]) 徹底釋放空間
        //但由於題目保證了資料總和,搶時間就不寫太麻煩了
	}
	for(ll i=2;i<=n;i++)e[R].push_back(i);
	dfs(1);
	work(1);
	we(ans);
	return ;
}

賽時還是不要放棄啊,我要是中間沒有開擺這題就有時間切掉了。