題解

houbur發表於2024-11-08

最近模擬賽抽象題太多了,讓他們聚一聚

T1 多校A層衝刺NOIP2024模擬賽18 T3 DBA

暴力比較顯然,直接數位dp就好,記搜的複雜度是\(O(n^4)\)的,用遞推加字首和最佳化可以最佳化為\(O(n^3)\)但我還是不理解\(O(n^3)\)憑啥能\(拿97pts\),雖然和正解沒啥關係,但還是放一下記搜的程式碼:

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2000+5,p=1e9+7;
int n,m,a[N];
int x,y,z,l,r,k,t;
int dp[N][15000],ans,tot;
//dp[i][j]表示截止到第i位,和為j的方案數
int dfs(int pos,int sum,bool limit){
	if(pos>n){
		if(sum==tot)return 1;
		else return 0; 
	}
	if(sum>tot)return 0;
	if(dp[pos][sum]!=-1&&(!limit))return dp[pos][sum];
	int lim=limit?a[pos]:m-1,res=0;
	for(int i=0;i<=lim;i++){
		res+=dfs(pos+1,sum+i,limit&&i==lim);
	}
	dp[pos][sum]=res%p;
	return res%p;
}
signed main(){
	freopen("dba.in","r",stdin);
	freopen("dba.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>m>>n;
	memset(dp,-1,sizeof dp);
	for(int i=1;i<=n;i++)cin>>a[i],tot+=a[i];
	cout<<dfs(1,0,1)-1<<endl;
	return 0;
}
然後考慮正解,我去,竟然是一道純種的推式子題,首先題目要求的顯然是: $$\sum_{i=1}^{n}x_i=sum(x_1≤p,0≤ \forall x_i < m)$$ 先考慮沒有$x_1$的限制,直接欽定有k個$x_i≥m$,直接寫出容斥式子即為: $$ans=\sum_{k=0}^{n}(-1)^k {a\choose k} { sum-km+a-1\choose a-1}$$ 再考慮加上$x_1$的限制,發現並沒有什麼好辦法,只能列舉,式子即為: $$ans=\sum_{i=0}^{p-1}\sum_{k=0}^{n}(-1)^k {a\choose k} { sum-km-i+a-1\choose a-1}$$ 最後挨位統計答案貢獻即可,但是發現如果挨位統計答案的話複雜度是$O(n^3)$的,所以嘗試最佳化式子: $$ans=\sum_{i=0}^{p-1}\sum_{k=0}^{n}(-1)^k {a\choose k} { sum-km-i+a-1\choose a-1}$$ $$=\sum_{k=0}^{n}(-1)^k{a\choose k} \sum_{i=0}^{p-1}{ sum-km-i+a-1\choose a-1}$$ $$=\sum_{k=0}^{n}(-1)^k{a\choose k} ({ sum-km+a\choose a}-{ sum-km-p+a\choose a})$$ 因為第二行式子的後面那一坨是楊輝三角上的一列,是連續的,直接就能化成一個$O(1)$的 然後,額,嗯,就完了,最後貼下程式碼:
點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=2000+5,p=1e9+7;
int n,m,fac[N*N],inv[N*N];
int x,y,z,l,r,k,t,sum;
int ans,tot,a[N];
inline int quickpow(int a,int b){
	int res=1;
	while(b){
		if(b&1)res=res*a%p;
		b>>=1;
		a=a*a%p;
	}return res;
}
void iint(){
	fac[0]=1;
	for(int i=1;i<=n*m;i++)fac[i]=fac[i-1]*i%p;
	inv[n*m]=quickpow(fac[n*m],p-2);
	for(int i=n*m-1;i>=0;i--)inv[i]=inv[i+1]*(i+1)%p;
}
inline int C(int n,int m){
	if(n<m)return 0;
	return fac[n]*inv[n-m]%p*inv[m]%p;
}
inline int slove(int a,int w){
	int res=0;
	for(int i=0,f=1;i<=a&&i*m<=sum;i++,f=-f){
		res+=f*(C(sum-i*m+a,a)-C(sum-w-i*m+a,a)+p)%p*C(a,i)%p+p;
	}return res%p;
}
signed main(){
	freopen("dba.in","r",stdin);
	freopen("dba.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	cin>>m>>n;
	iint();	
	for(int i=1;i<=n;i++)cin>>a[i],sum+=a[i];
	for(int i=1;i<n;i++)ans+=slove(n-i,a[i]),sum-=a[i];
	cout<<ans%p<<'\n';
	return 0;
}

T2 多校A層衝刺NOIP2024模擬賽18 T4 銀行的源起

首先講一下\(O(n^2)\)的做法。
先考慮如果只有一個銀行,有一個比較一眼但又不太好想到的性質即為:對於每條邊將樹分為兩部分1,2,如果要想讓其最優,要麼1中的點全都透過這條邊跑到2,要麼2中的點全都透過這條邊跑到1,即對於每條邊的貢獻為\(e[i].w*min(size[x],totsize-size[x])\),這樣可以保證讓每條邊的貢獻是最優的,推廣到全域性即能保證答案最優,這樣就可以\(O(n)\)求解最小代價。
考慮兩個銀行怎麼做,如果有兩個銀行,那麼有一條邊肯定是不會被任何點經過的,那麼原圖就被分成了兩個部分,這兩個部分都只有一個銀行,可以直接\(O(n)\)求解,具體如圖:

直接列舉哪條邊未被經過即可,複雜度\(O(n^2)\)
既然我講了\(O(n^2)\)做法,那正解肯定和它有點關係,考慮如果要想保證答案正確性,列舉那條邊未被經過這個過程肯定不能少,那隻能考慮去最佳化求解貢獻的過程,這個時候看這個式子是帶著\(min\)的,考慮拆開分類討論一下:
\(size[x]>totsize/2\)時貢獻為\(e[i].w*size[x]\),反之\(size[x]<=totsize/2\)貢獻為\(totsize*e[i].w-size[x]*e[i].w\),觀察到貢獻是和\(size[x]\)緊密相關的,可以直接開一個以\(size\)為下標的權值線段樹,查詢貢獻時直接找到\(size=totsize/2\)的位置,左邊,右邊分別算貢獻即可。
考慮線段樹要維護什麼資訊,看上面的式子,分別用到了\(e[i].w,e[i].w*size[x]\),直接分別維護即可,至於線段樹資訊怎麼更新,額....,好問題,你發現如果只考慮一棵子樹內的貢獻的話,你直接線段樹合併即可,但是另一部分的答案就會變得很難統計,這個時候就不太能用這個線段樹上的資訊來計算了,所以這個時候我們就要考慮怎麼再去維護那一部分的資訊。
首先先上一個圖,便於一會描述:

現在我們以箭頭指向的那條邊為界將樹分為了兩部分:綠色子樹和(紅+黑)的那棵樹
首先綠色子樹的貢獻直接就可以線段樹合併的時候計算,沒啥要說的
主要還是紅黑部分的貢獻的計算:
首先這紅黑兩部分的\(size_{紅黑}=totsize-size_{綠色子樹}\)
先考慮紅色這條鏈的貢獻計算,可以發現紅色這條鏈完全可以用一個權值樹狀陣列,當你進入一個節點時加上它的貢獻,離開時再減去就好,貢獻的計算和上面一樣,只不過它這條鏈上的每一個節點的\(size\)都減去了一個\(size_{綠色子樹}\),查詢時分界點稍微左偏一下即可
再考慮黑色部分的貢獻,雖說它的\(size\)並沒有發生改變,但你發現它根本沒法用任何東西維護,這個時候就可以直接統計全域性對於\(size_{紅黑}\)的貢獻,然後直接減去綠色子樹和紅色鏈上對於\(size_{紅黑}\)的貢獻就好了,這兩個東西都是可以\(O(log)\)做的,至於全域性貢獻直接開個字首和陣列記一下就好了,直接\(O(1)\)解決了
然後就沒啥了,哦,記得離散化一下\(size\)再拿來做下標,要不然可能有點卡常,最後貼個程式碼:

點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define ls lc[rt]
#define rs rc[rt]
#define pii pair<int,int>
#define fi first
#define se second
#define met(x) memset(x,0,sizeof x)
const int N=1e6+5;
int n,m,a[N],ans[N],sum1[N],sum2[N],w[N];
int x,y,z,l,r,k,t,sum,res,len,root[N];
int h[N],tot,b[N],size[N],wp[N],h1,h2;
struct sb{
	int nxt,to,w;
}e[N];
inline void add(int x,int y,int z){
	e[++tot]={h[x],y,z};
	h[x]=tot;
}
inline int read(){
	int s=0;char c=getchar_unlocked();
	while(c<'0'&&c>'9')c=getchar_unlocked();
	while(c>='0'&&c<='9'){s=(s<<1)+(s<<3)+(c^48);c=getchar_unlocked();}
	return s;
}
inline void dfs(int x,int fa){
	size[x]=a[x];
	for(int i=h[x];i;i=e[i].nxt){
		int y=e[i].to;
		if(y==fa)continue;
		dfs(y,x);
		w[y]=e[i].w;
		size[x]+=size[y];
	}
	b[x]=size[x];
	// cout<<x<<" "<<b[x]<<endl;
}
struct tree{
	int tot,w1[N<<2],w2[N<<2],lc[N<<2],rc[N<<2];
	inline void clear(){tot=0;met(w1);met(w2);met(lc);met(rc);}
	inline void tag(int rt,int v1,int v2){w1[rt]+=v1,w2[rt]+=v2;}
	inline void pushup(int rt){w1[rt]=w1[ls]+w1[rs];w2[rt]=w2[ls]+w2[rs];}
	inline void add(pii &a,pii b){a.fi+=b.fi;a.se+=b.se;}
	inline void add(int &rt,int l,int r,int pos,int v1,int v2){
		if(!rt)rt=++tot;
		if(l==r)return tag(rt,v1,v2);
		int mid=(l+r)>>1;
		pos<=mid?add(ls,l,mid,pos,v1,v2):add(rs,mid+1,r,pos,v1,v2);
		pushup(rt);
	}
	inline pii query(int rt,int l,int r,int L,int R){
		if(!rt)return {0,0};
		if(L<=l&&r<=R)return {w1[rt],w2[rt]};
		int mid=(l+r)>>1;pii res={0,0};
		if(L<=mid)add(res,query(ls,l,mid,L,R));
		if(R>mid)add(res,query(rs,mid+1,r,L,R));
		return res;
	}
	inline int merge(int x,int y){
		if(!x||!y)return x^y;
		w1[x]+=w1[y];w2[x]+=w2[y];
		lc[x]=merge(lc[x],lc[y]);
		rc[x]=merge(rc[x],rc[y]);
		return x;
	}
}T1;
struct shu{
	//支援單點修改,字首查詢
	int c1[N],c2[N];
	inline int lb(int x){return x&-x;}
	inline void clear(){for(int i=1;i<=n;i++)c1[i]=c2[i]=0;}
	inline void add(int x,int w1,int w2){while(x<=len)c1[x]+=w1,c2[x]+=w2,x+=lb(x);}
	inline pii ask(int x){pii res={0,0};while(x){res.fi+=c1[x],res.se+=c2[x],x-=lb(x);}return res;}
}T2;
//ans=size[x]*e[i].w(size[x]<=totsize/2)+(totsize-size[x])*e[i].w(size[x]>totsize/2)
//ans=size[x]*e[i].w(<=)  -size[x]*e[i].w(>)  +totsize*((size[x]>totsize/2)*e[i].w)
//開一個下標為size的值域線段樹儲存兩個資訊 e[i].w e[i].w*size[x]
inline void get_ans(int x,int fa,int w){
	T1.add(root[x],1,len,wp[x],w,size[x]*w);
	T2.add(wp[x],w,size[x]*w);ans[x]=0;
	h1+=w;h2+=size[x]*w;
	for(int i=h[x];i;i=e[i].nxt){
		int y=e[i].to;
		if(y==fa)continue;
		get_ans(y,x,e[i].w);
		//子樹內
		pii res=T1.query(root[y],1,len,upper_bound(b+1,b+1+len,size[y]/2)-b,len);
		ans[y]+=res.fi*size[y]-res.se//size>size[y]/2的貢獻
		+T1.w2[root[y]]-res.se;//size<size[y]/2的貢獻

		//鏈上
		int sz=size[1]-size[y];
		int pos=upper_bound(b+1,b+1+len,sz/2+size[y])-b;
		res=T2.ask(pos-1);
		ans[y]+=res.se-res.fi*size[y]+(h1-res.fi)*size[y]+(h1-res.fi)*sz-(h2-res.se);

		//全域性貢獻(對於size==size[1]-size[y])(一會再減去算重的貢獻)
		pos=upper_bound(b+1,b+1+len,sz/2)-b;
		ans[y]+=sum2[pos-1]//size<sz/2的貢獻
		+sz*(sum1[len]-sum1[pos-1])-(sum2[len]-sum2[pos-1]);//size>sz/2的貢獻


		//減去子樹裡重複的貢獻
		res=T1.query(root[y],1,len,pos,len);
		ans[y]-=res.fi*sz-res.se//size>size[y]/2的貢獻
		+T1.w2[root[y]]-res.se;//size<size[y]/2的貢獻

		//減去鏈上重複的貢獻
		res=T2.ask(pos-1);
		ans[y]-=res.se+(h1-res.fi)*sz-(h2-res.se);

		T1.merge(root[x],root[y]);
	}
	T2.add(wp[x],-w,-size[x]*w);
	h1-=w;h2-=size[x]*w;
}
signed main(){
	freopen("banking.in","r",stdin);
	freopen("banking.out","w",stdout);
	ios::sync_with_stdio(0);
	cin.tie(0);cout.tie(0);
	t=read();
	while(t--){
		n=read();tot=0;
		T1.clear();T2.clear();
		for(int i=1;i<=n;i++)root[i]=h[i]=0;
		for(int i=1;i<=n;i++)a[i]=read();
		for(int i=1;i<n;i++){
			x=read(),y=read(),z=read();
			add(x,y,z);add(y,x,z);
		}
		dfs(1,0);
		sort(b+1,b+1+n);
		len=unique(b+1,b+1+n)-b-1;
		for(int i=1;i<=n;i++)sum1[i]=sum2[i]=0;
		for(int i=1;i<=n;i++){
			wp[i]=lower_bound(b+1,b+1+len,size[i])-b;
			sum1[wp[i]]+=w[i];sum2[wp[i]]+=w[i]*size[i];
		}
		for(int i=1;i<=len;i++)sum1[i]+=sum1[i-1],sum2[i]+=sum2[i-1];
		get_ans(1,0,0);
		int ANS=1e18;
		for(int i=2;i<=n;i++)ANS=min(ANS,ans[i]);
		cout<<ANS<<'\n';
	}
	return 0;
}

to be continue