ICPC WF Moscow Invitational Contest B J L 題解


link

我切的三個題,感覺都挺好的,來補個題解。

B - Building Forest Trails

如果不規定交叉算連通的話,那直接加邊!加邊!加邊!然后,並查集查詢。

不難發現兩條邊 \((a,b),(c,d)\) 交叉,當且僅當:設 \(a<b,c<d\),則 \(a<c<b<d\)\(c<a<d<b\)

我們要做的是,連接 \((x,y)\)​ 后,不光合並 \(x,y\)​,還要將所有滿足內部有邊與 \((x,y)\)​ 交叉的連通塊與 \(x,y\)​ 所在連通塊合並。不難發現,我們根本不需要關心連通塊內到底有哪些邊,因為若 \(z,w\)​ 都在該連通塊,且 \((z,w)\)​ 與 \((x,y)\)​ 交叉,則不論 \((z,w)\)​​ 這條邊是否真的存在,該連通塊內都存在與 \((x,y)\)​ 相交的邊。證明的話,在 \(z\to w\)​ 上應用一下介值定理即可。反之,不難發現上面說的顯然是必要條件,而我們用介值定理證明了充分性,自然就是充要條件。

那么設連通塊內元素排序之后為 \(p_{1\sim s}\),該連通塊應與 \(x,y\) 所在連通塊合並當且僅當存在 \(p_{i-1}<x<p_i<y\)\(x<p_i<y<p_{i+1}\)。合並次數顯然是線性的,我只需要命中率盡量高,讓失敗次數控制在線性以內即可。不難想到這樣一個方案:每次找區間 \((x,y)\) 內后繼最大的,直到后繼落在該區間內為止;對前驅類似。不難想到對每個連通塊用 set 維護前驅后繼,合並的時候可以啟發式合並,將一個點加入 set 時改變的前驅后繼數量顯然是常數,於是可以線段樹維護區間前驅 / 后繼最值。

總復雜度大常數 2log(啟發式 + set),不過 TL = 3s 加上 cf 神機(再神也沒有 20034 神,然而它掛了()),還是讓我過了。

code
#include<bits/stdc++.h>
using namespace std;
#define mp make_pair
#define X first
#define Y second
const int inf=0x3f3f3f3f;
const int N=200010;
int n,qu;
set<int> st[N];
struct ufset{
	int fa[N];
	ufset(){memset(fa,0,sizeof(fa));}
	int root(int x){return fa[x]?fa[x]=root(fa[x]):x;}
	void mrg(int x,int y){
		x=root(x),y=root(y);
		fa[x]=y;
	}
}ufs;
struct segtree{
	struct node{pair<int,int> mn,mx;}nd[N<<2];
	#define mn(p) nd[p].mn
	#define mx(p) nd[p].mx
	void init(){
		for(int i=0;i<N<<2;i++)nd[i]=node({mp(inf,inf),mp(-inf,-inf)});
	}
	void sprup(int p){mn(p)=min(mn(p<<1),mn(p<<1|1)),mx(p)=max(mx(p<<1),mx(p<<1|1));}
	void chgmn(int x,int v,int p=1,int tl=1,int tr=n){
		if(tl==tr)return mn(p)=mp(v,tl),void();
		int mid=tl+tr>>1;
		if(x<=mid)chgmn(x,v,p<<1,tl,mid);
		else chgmn(x,v,p<<1|1,mid+1,tr);
		sprup(p);
	}
	void chgmx(int x,int v,int p=1,int tl=1,int tr=n){
		if(tl==tr)return mx(p)=mp(v,tl),void();
		int mid=tl+tr>>1;
		if(x<=mid)chgmx(x,v,p<<1,tl,mid);
		else chgmx(x,v,p<<1|1,mid+1,tr);
		sprup(p);
	}
	pair<int,int> _mn(int l,int r,int p=1,int tl=1,int tr=n){
		if(l>r)return mp(inf,inf);
		if(l<=tl&&r>=tr)return mn(p);
		int mid=tl+tr>>1;pair<int,int> res(inf,inf);
		if(l<=mid)res=min(res,_mn(l,r,p<<1,tl,mid));
		if(r>mid)res=min(res,_mn(l,r,p<<1|1,mid+1,tr));
		return res;
	}
	pair<int,int> _mx(int l,int r,int p=1,int tl=1,int tr=n){
		if(l>r)return mp(-inf,-inf);
		if(l<=tl&&r>=tr)return mx(p);
		int mid=tl+tr>>1;pair<int,int> res(-inf,-inf);
		if(l<=mid)res=max(res,_mx(l,r,p<<1,tl,mid));
		if(r>mid)res=max(res,_mx(l,r,p<<1|1,mid+1,tr));
		return res;
	}
}segt;
void insert(int x,int to){
	st[to].insert(x);
	set<int>::iterator fd=st[to].find(x);
	if(fd!=st[to].begin()){
		set<int>::iterator prv=fd--;swap(fd,prv);
		segt.chgmn(*fd,*prv),segt.chgmx(*prv,*fd);
	}else segt.chgmn(*fd,*fd);
	if(fd!=--st[to].end()){
		set<int>::iterator nxt=fd++;swap(fd,nxt);
		segt.chgmx(*fd,*nxt),segt.chgmn(*nxt,*fd);
	}else segt.chgmx(*fd,*fd);
}
void mrg(int x,int y){
	x=ufs.root(x),y=ufs.root(y);
	if(st[x].size()>st[y].size())swap(x,y);
	ufs.mrg(x,y);
	while(st[x].size())insert(*st[x].begin(),y),st[x].erase(st[x].begin());
}
int main(){
	cin>>n>>qu;
	segt.init();
	for(int i=1;i<=n;i++)st[i].insert(i);
	string out;
	while(qu--){
		int tp,x,y;
		scanf("%d%d%d",&tp,&x,&y);
		x=ufs.root(x),y=ufs.root(y);
		if(tp==2){out+=(x==y)^48;continue;}
		if(x==y)continue;
		if(x>y)swap(x,y);
		mrg(x,y);
		while(true){
			pair<int,int> mn=segt._mn(x+1,y-1);
			if(mn.X>=x)break;
			mrg(mn.Y,x);
		}
		while(true){
			pair<int,int> mx=segt._mx(x+1,y-1);
//			cout<<mx.X<<"!\n";
			if(mx.X<=y)break;
			mrg(mx.Y,x);
		}
	}
	puts(out.c_str());
	return 0;
}

J - Just Kingdom

這題我拿了「二血」(當然是場外的),感覺很妙的一題啊。又一次現場切 3100

不難發現,子樹 \(x\)​​ 的需求量為 \(sum_x=\sum\limits_{y\in\mathrm{subt}(x)}a_y\)​​。而某個節點將一些錢分發給兒子們的過程,就像往一個柱狀水槽里注水一樣,每個兒子對應一個柱子,高度為其 \(sum\)​​ 值。那么 \(x\)​​ 想要得到 \(sum_x\)​​,可以轉化為父親要得到多少。是多少呢?將父親的兒子序列的 \(sum\)​​ 值(除去自己!)排序得到 \(p\)​​,則父親要得到 \(\sum\limits_{p_i\leq sum_x}p_i+\left(1+\sum\limits_{p_i>sum_x}1\right)p_i\)​​。父親要得到這么多,還可以往父親的父親轉化,這樣一直轉化上去,直到 root。至此我們可以得到一個暴力做法:對每個點暴力往上迭代,復雜度是 \(\sum dep_i\) 的 polylog​​ ,亦即 \(\sum sz_i\) 的 polylog​。

這個 \(\sum sz_i\) 就很有趣啊,啟示我們用 dsu on tree。先考慮怎么由 \(\sum dep_i\) 轉化到 \(\sum sz_i\)​ 的這樣一個過程,其實就是每個點處有一個 todo-list,都是后代們迭代上來的,它的任務是把這些 todo-list 里的值進行加工並且上傳給父親。這不就是一個子樹合並問題嗎?

考慮 dsu on tree,最難的問題是如何直接繼承重兒子,復雜度不能跟重兒子的 size 相關。但是跟該節點的度數相關是沒問題的呀!因為 \(\sum\deg_i=\mathrm O(n)\)​。我們設兒子序列 \(sum\)​ 值排序得到 \(p\)​,那么對同一個 \(i\)​,\(p_i\leq x< p_{i+1}\)​ 的待加工值 \(x\)​ 的加工方式都是一樣的,都是作用上同一個一次函數 \(ax+b\)​。也就是我們要對若干個區間 \([l,r]\)​,將 todo-list 中處於這個區間中的值都作用上一個一次函數,這顯然可以平衡樹 + 加乘懶標記維護,因為單調性不變。這樣就實現了直接繼承。

然后輕兒子們暴力一個一個值插入就很 trivial 了,直接在兒子序列里二分得到處理之后的值,然后 insert 進平衡樹。總復雜度大常數 2log(dsu on tree + fhq-treap),而且 \(n\leq 3\mathrm e5\),不過 TL = 5s,直接沖就是了。

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define mp make_pair
#define X first
#define Y second
#define pb push_back
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=600010;
int n;
int a[N];
int fa[N];
vector<int> nei[N];
int sz[N],wson[N],sum[N];
void dfs(int x=n+1){
	sz[x]=1;sum[x]=a[x];
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];
		dfs(y);
		sz[x]+=sz[y],sum[x]+=sum[y];
		if(sz[y]>sz[wson[x]])wson[x]=y;
	}
}
mt19937 rng(20060729);
struct fhq_treap{
	int sz,root;
	struct node{unsigned key;int lson,rson,sz,v,u,ad,mu;}nd[N+1];
	#define key(p) nd[p].key
	#define lson(p) nd[p].lson
	#define rson(p) nd[p].rson
	#define sz(p) nd[p].sz
	#define v(p) nd[p].v
	#define u(p) nd[p].u
	#define ad(p) nd[p].ad
	#define mu(p) nd[p].mu
	void init(){nd[sz=root=0]=node({0,0,0,0,0,0,0,0});}
	void sprup(int p){sz(p)=sz(lson(p))+1+sz(rson(p));}
	void tag(int p,int A,int M){
		v(p)*=M,v(p)+=A;
//		if(M==1&&A==6)cout<<v(p)<<"?\n";
		mu(p)*=M,ad(p)*=M,ad(p)+=A;
	}
	void sprdwn(int p){
		tag(lson(p),ad(p),mu(p)),tag(rson(p),ad(p),mu(p));
		ad(p)=0,mu(p)=1;
	}
	pair<int,int> split(int x,int p=-1){~p||(p=root);
		if(!x)return mp(0,p);
		sprdwn(p);
		pair<int,int> sp;
		if(x<=sz(lson(p)))return sp=split(x,lson(p)),lson(p)=sp.Y,sprup(p),mp(sp.X,p);
		return sp=split(x-sz(lson(p))-1,rson(p)),rson(p)=sp.X,sprup(p),mp(p,sp.Y);
	}
	int mrg(int p,int q){
		if(!p||!q)return p|q;
		sprdwn(p),sprdwn(q);
		if(key(p)<key(q))return rson(p)=mrg(rson(p),q),sprup(p),p;
		return lson(q)=mrg(p,lson(q)),sprup(q),q;
	}
	int lss(int v,int p=-1){~p||(p=root);
		if(!p)return 0;
		sprdwn(p);
		if(v(p)<v)return sz(lson(p))+1+lss(v,rson(p));
		return lss(v,lson(p));
	}
	int leq(int v,int p=-1){~p||(p=root);
		if(!p)return 0;
		sprdwn(p);
		if(v(p)<=v)return sz(lson(p))+1+leq(v,rson(p));
		return leq(v,lson(p));
	}
	int nwnd(int v,int u){return nd[++sz]=node({rng(),0,0,1,v,u,0,1}),sz;}
	void insert(int v,int u){
		pair<int,int> sp=split(lss(v));
		root=mrg(mrg(sp.X,nwnd(v,u)),sp.Y);
	}
	void dfs(vector<pair<int,int> > &vec,int p=-1){~p||(p=root);
		if(!p)return;
		sprdwn(p);
		dfs(vec,lson(p));
		vec.pb(mp(v(p),u(p)));
		dfs(vec,rson(p));
	}
	void prt(int p=-1){~p||(p=root);
		if(!p)return;
		sprdwn(p);
		prt(lson(p));
		cout<<v(p)<<","<<u(p)<<" ";
		prt(rson(p));
	}
	void am(int l,int r,int ad,int mu){
//		cout<<l<<" to "<<r<<":";
		l=lss(l),r=leq(r);
//		cout<<l<<" "<<r<<"!\n";
		pair<int,int> sp=split(l),sp0=split(r-l,sp.Y);
		tag(sp0.X,ad,mu);
		root=mrg(sp.X,mrg(sp0.X,sp0.Y));
	}
}trp;
vector<pair<int,int> > v[N];
void dfs0(int x=n+1){
	if(nei[x].empty())return trp.insert(a[x],x);
	vector<int> u,w;
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];
		w.pb(sum[y]);
		if(y==wson[x])continue;
		u.pb(sum[y]);
		dfs0(y);
		trp.dfs(v[y]),trp.init();
	}
	u.pb(-inf),u.pb(inf),sort(u.begin(),u.end());
	if(wson[x]){
		dfs0(wson[x]);
		vector<int> U(u.size(),0);
		for(int i=1;i<u.size();i++)U[i]=U[i-1]+u[i];
		for(int i=u.size()-2;~i;i--){
//			if(x==6)cout<<u.size()-i-1<<" "<<U[i]<<"!!!\n";
			trp.am(u[i],u[i+1]-1,U[i],u.size()-i-1);
//			trp.prt(),puts("!!!!!!!!!!");
		}
	}
	if(x<=n)trp.insert(sum[x],x);
	sort(w.begin(),w.end());
	vector<int> Sum(w.size()+1,0);
	for(int i=1;i<Sum.size();i++)Sum[i]=Sum[i-1]+w[i-1];
	for(int i=0;i<nei[x].size();i++){
		int y=nei[x][i];
		u.pb(sum[y]);
		if(y==wson[x])continue;
		vector<pair<int,int> > &vec=v[y];
		for(int j=0;j<vec.size();j++){
			int x=vec[j].X,id=vec[j].Y;
			vector<int>::iterator fd=upper_bound(w.begin(),w.end(),x);
			int A=Sum[fd-w.begin()],M=w.end()-fd+1;
			if(fd==w.end()||sum[y]<*fd)A-=sum[y];
			else M--;
			trp.insert(M*x+A,id);
		}
	}
//	cout<<x<<":";trp.prt();puts("");
}
int ans[N];
signed main(){
//	freopen("j.in","r",stdin);freopen("j.out","w",stdout);
	cin>>n;
	if(!n)return 0;
	for(int i=1;i<=n;i++){
		scanf("%lld%lld",fa+i,a+i);
		if(fa[i]==0)fa[i]=n+1;
		nei[fa[i]].pb(i);
	}
	dfs();
	trp.init();
	dfs0();
	vector<pair<int,int> > vec;
	trp.dfs(vec);
	assert((int)vec.size()==n);
	for(int i=0;i<n;i++)ans[vec[i].Y]=vec[i].X;
	for(int i=1;i<=n;i++)printf("%lld\n",ans[i]);
	return 0;
}

L - Labyrinth

這個 2400 的題就略顯遜色了。

這樣考慮:當吃胖到一定程度時,某些邊就會消失。注意到:對於圖中邊權最小的邊 \((x,y)\),其它邊消失都不早於它,於是可以先讓它割開,如果不分裂那就沒事了,否則分裂成 \(x,y\) 分別所在的兩個連通塊 \(p,q\)。我們要在 \((x,y)\) 消失前將 \(p\) 或者 \(q\) 吃完,然后沿着這條邊走到 \(q,p\)​,將其吃完。

問題就轉化為兩個連通塊(子圖)的子問題,考慮求出其答案(最大初始寬度)\(dp_{p/q}\)。那么考慮原圖答案 \(dp_G\),應該是很容易通過 \(dp_{p/q}\) 求出的。假設先吃完 \(p\),再到 \(q\),那么我們有如下限制:

  • \(dp_G\) 作為 \(p\) 的初始值要可行,即 \(dp_G\leq dp_p\)
  • 吃完 \(p\) 后要能通過 \((x,y)\),即 \(dp_G+sum_p\leq w(x,y)\)
  • \(dp_G+sum_p\)​ 作為 \(q\)​ 的初始值要可行,即 \(dp_G+sum_p\leq dp_q\)​。

取個 min 即可。對先 \(q\) 類似,兩者取個 max。

分裂不太好處理,考慮時光倒流轉化為合並,用並查集輔助維護這個「無向圖上 DP」,跟最大生成樹 Kruskal 過程差不多。

code
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define mp make_pair
#define X first
#define Y second
const int inf=0x3f3f3f3f3f3f3f3f;
const int N=100010;
int n,m;
int a[N];
pair<int,pair<int,int> > eg[N];
struct ufset{
	int fa[N];
	ufset(){memset(fa,0,sizeof(fa));}
	int root(int x){return fa[x]?fa[x]=root(fa[x]):x;}
}ufs;
int dp[N],sum[N];
signed main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)scanf("%lld",a+i);
	for(int i=1;i<=m;i++)scanf("%lld%lld%lld",&eg[i].Y.X,&eg[i].Y.Y,&eg[i].X);
	sort(eg+1,eg+m+1);
	for(int i=1;i<=n;i++)dp[i]=inf,sum[i]=a[i];
	for(int i=m;i;i--){
		int x=eg[i].Y.X,y=eg[i].Y.Y,w=eg[i].X;
		x=ufs.root(x),y=ufs.root(y);
		if(x==y)continue;
		int dx=dp[x],dy=dp[y];
		dp[x]=max(min(dx,min(w,dy)-sum[x]),min(dy,min(w,dx)-sum[y]));
		ufs.fa[y]=x,sum[x]+=sum[y];
	}
	for(int i=1;i<=n;i++)if(ufs.fa[i]==0)return cout<<(dp[i]<=0?-1:dp[i])<<"\n",0;
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM