我切的三个题,感觉都挺好的,来补个题解。
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;
}