[題解]P9755 [CSP-S 2023] 種樹

Sinktank發表於2024-07-24

P9755 [CSP-S 2023] 種樹

遲來的補題


本題是讓最小化所有樹長到指定高度日期的最大值,於是想到二分答案。

那麼,對於一個給定的期限\(x\),如何判斷是否能在這個日期內完成任務呢?

首先我們發現前\(n\)天每天都要種樹,那麼假設我們已經知道了每個地塊最晚哪個日期種樹,能保證在期限\(x\)之內長到指定高度,我們就可以貪心地選擇:把每個地塊的最晚日期陣列\(late\)從小到大排序,依次選擇每一個元素:

  • 如果該元素已經被選擇了,就什麼操作也不做。
  • 如果該元素還沒被選擇,就先把祖先節點中沒有被選擇過的選擇上(因為要在一個地塊種樹,必須先在它的祖先種樹),每種一棵樹\(tim+1\)

每個元素\(i\)選完後,判斷\(tim\)\(late_i\)的大小關係,如果\(tim>late_i\),說明當前這棵樹已經種晚了,返回false

為什麼這樣貪心地選日期要求最早(種樹需求最緊迫)的,正確性就能得到保證呢?

我們考慮\(2\)個地塊,第\(1\)個地塊最晚種樹日期\(<\)\(2\)個地塊,這說明第\(1\)個地塊種樹的需求更緊迫。按照貪心的思想,我們先選第\(1\)個,那麼必須把它的祖先地塊全都種上樹,再考慮第\(2\)個。如果我們先選第\(2\)個地塊種上樹,那麼選第\(1\)個地塊時,仍然避免不了要先在它的祖先地塊上種樹。因此後者不一定能得到最優答案。

現在還有\(1\)個問題:如何求\(late\)陣列?

這就跟輸入的\(b,c\)有關係了。我們知道第\(j\)天第\(i\)棵樹會生長\(\max\{b_i+c_i\times j,1\}\)米,因此我們要找的\(late_i\),就是(\(x\)表示日期限制):

\[\large{\max\limits_{\sum\limits_{j=k}^x \max\{b_i+c_i\times j,1\}\ge a_i}k} \]

我們可以\(O(1)\)求出\(\sum\limits_{j=k}^x b_i+c_i\times j\),因此可以二分求出\(k\)作為\(late_i\)

具體怎麼求呢?我們設\(\sum\limits_{j=l}^r b_x+c_x\times j\)\(f(l,r,x)\)

計算\(f(l,r,x)\),可以對\(c_x\)的值進行分類討論。

如果\(c_x\ge 0\),那麼我們完全不需要考慮和\(1\)\(\max\),直接套用等差數列求和公式即可:

\[(r-l+1)\times b_x+(l+r)\times \frac{r-l+1}{2}\times c_x \]

如果\(c_x<0\),我們解一個不等式,得到第幾項開始值\(<1\),再分成兩部分分別計算,再求和。

\[b_x+c_x\times k<1\\ k>\frac{1-b_x}{c_x}\]

故兩部分分別是:

  • \(j\in [l,k]:\quad b_x+c_x\times j\ge 1\)
    求和:\((k-l+1)\times b+\frac{(l+k)\times(k-l+1)}{2}\times c\)
  • \(j\in [k+1,r]:\quad b_x+c_x\times j<1\)
    求和:\(r-k\)
    注意這裡只給出了\(k\in[l,r)\)的情況,程式碼實現需要注意特判只有一個部分的情況,可以結合程式碼理解。

時間複雜度:(二分)套(二分+排序),外層二分\(O(\log V)\)\(V\)是值域\(10^9\)),內層\(O(n\log n)\),總共\(O(n\log V\log n)\)

注意:

  • \(f()\)計算出的值可能超出long long,需要開__int128

點選檢視程式碼
#include<bits/stdc++.h>
#define int long long
#define N 100010
using namespace std;
int n,a[N],b[N],c[N],k[N],late[N],fa[N],pos[N];
bool vis[N];
vector<int> G[N];
__int128 calc(int l,int r,int x){//第x棵樹在[l,r]天能長到多少
	if(c[x]>=0) return (__int128)(r-l+1)*b[x]+(__int128)(l+r)*(r-l+1)/2*c[x];
	if(k[x]<l) return r-l+1;//只有右邊
	if(k[x]>=r) return (__int128)(r-l+1)*b[x]+(__int128)(l+r)*(r-l+1)/2*c[x];//只有左邊
	return (__int128)(r-k[x])+(__int128)(k[x]-l+1)*b[x]+(__int128)(l+k[x])*(k[x]-l+1)/2*c[x];
}
void dfs(int u,int f){
	fa[u]=f;
	for(int i:G[u])
		if(i!=f) dfs(i,u);
}
bool cmp(int a,int b){return late[a]<late[b];}
int cnt;
bool check(int x){//x天能否完成
	for(int i=1;i<=n;i++){//二分找每個位置最晚種樹時間
		int l=1,r=n;
		while(l<r){
			int mid=(l+r+1)>>1;
			if(calc(mid,x,i)>=a[i]) l=mid;
			else r=mid-1;
		}
		if(l>x) return 0;
		pos[i]=i,late[i]=l;
	}
	sort(pos+1,pos+1+n,cmp);
	int tim=0;
	memset(vis,0,sizeof vis);
	vis[0]=1;//不加下面會死迴圈
	for(int i=1;i<=n;i++){
		for(int j=pos[i];!vis[j];j=fa[j])
			vis[j]=1,tim++;
		if(tim>late[pos[i]]) return 0;
	}
	return 1;
}
signed main(){
	ios::sync_with_stdio(false);
	cin.tie(nullptr);
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i]>>b[i]>>c[i];
		if(c[i]<0) k[i]=(1-b[i])/c[i];
	}
	for(int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		G[u].emplace_back(v);
		G[v].emplace_back(u);
	}
	dfs(1,0);
	int l=n,r=1e9;
	while(l<r){
		int mid=(l+r)>>1;
		if(check(mid)) r=mid;
		else l=mid+1;
	}
	cout<<l<<"\n";
	return 0;
}

相關文章