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;
}
posted @ 2021-10-02 23:08  ycx060617  阅读(373)  评论(1编辑  收藏  举报