首页 > 其他分享 >Data Structure 3

Data Structure 3

时间:2024-12-22 22:31:16浏览次数:7  
标签:rt le return int void mid Data Structure

时间非常紧迫,应该会动态更新。

主要是一些分治 + 树的杂题,其它的可以看 Data Structure 1(杂题) + Data Structure 2(扫描线与根号数据结构)


Part 1 简单题 / 神秘题


CF696E ...Wait for it...

给定一个 \(n\) 个节点的树,有 \(m\) 个物品,第 \(i\) 个在节点 \(c_i\),初始权值为 \(i\)。要求支持两种操作:

对于两个物品,如果它们的权值不同,那么权值更小的物品更好;如果它们的权值相同,那么所在节点编号 \(c_i\) 更小的物品更好。可以发现在这种定义方式下,任意两个物品都能比较出哪个更好。

  • 1 u v k:对于树上的一条简单路径 \((u,v)\),删除路径上所有物品中最好的 \(k\) 个物品,并将这些物品的编号从最好到最不好的顺序输出;
  • 2 v x:对于一棵子树,将该子树内所有物品权值加 \(x\)。

\(n,m,q \le 10^5\)。

先来一道煎蛋题热一下身。


用线段树维护区间最好的物品,那么每一次查询其实是树剖之后在 \(\mathcal O(\log n)\) 个区间中查询最小值。

那么我们每次取出最小值,删掉它就可以了。

每次删除是单点修改,具体实现可以每个位置用一个队列维护。

修改操作直接区间加就可以了。

时间复杂度 \(\mathcal O(n \log^2 n)\)。代码(没有细节,就放在这里了)


CF1767F Two Subtrees

给你一棵有根树,每次给出两个子树(可能有交),求这两个子 树中所有的点权的最小众数。如果一个点被两个子树覆盖算两次。

\(n \le 2 \times 10^5\)。

四维莫队题。


直接四维莫队即可做到 \(\mathcal O(n^{\frac 74})\),卡卡常好像可以过,但是我不会四维莫队。

由于是子树的询问所以我们考虑 dsu on tree 的顺序。

于是我们把它写下来,可以得到一个长度为 \(\mathcal O(n \log n)\) 的序列,每次子树的询问相当于操作了一个前缀。

那么这个东西就变成二维莫队了,于是只要存在一个 \(\mathcal O(1)-\mathcal O(\sqrt V)\) 的解决全局最小众数的东西,这个问题就可以做到 \(\mathcal O(n\log \sqrt n+q\sqrt V)\)。


显然是存在的,也就是我们考虑值域分块,每个块内用一个桶维护。

那么块内的众数出现次数是容易维护的,因为每次加入和删除的改变都是 \(\mathcal O(1)\) 的。

查询的时候直接找到众数的出现次数,然后从小往大扫去找到第一个出现这么多次的数就可以了。


这样就做完了,疑似跑得比较快。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back

const int N=2e5+5,D=450,M=450;
int n,val[N],st[N],ed[N],idx=0,sz[N],son[N],rev[N],pos[N],ans[N],a[N],b[N],len=0,B,m;
vector<int> G[N];

struct Qry{
  int x,y,id;
  bool operator <(const Qry &a) const{return (val[x]-1)/B==(val[a.x]-1)/B?y<a.y:(val[x]-1)/B<(val[a.x]-1)/B;}
}q[N];

struct Block{
  int c[M][N],res[M],cnt[N];
  
  void ins(int x){
    int p=(x-1)/D+1;
    c[p][cnt[x]]--,++cnt[x],c[p][cnt[x]]++;
    res[p]=max(res[p],cnt[x]);
  }
  
  void del(int x){
    int p=(x-1)/D+1;
    c[p][cnt[x]]--,--cnt[x],c[p][cnt[x]]++;
    if(!c[p][res[p]]) --res[p];
  }
  
  int ask(){
    int mx=*max_element(res+1,res+M);
    for(int i=1;i<M;i++) if(res[i]==mx)
      for(int j=(i-1)*D+1;j<=i*D;j++) if(cnt[j]==mx) return j;
    return -1;
  }
}T;

void trans(int x,int y){
  x=rev[x],y=rev[y];
  if(ed[x]<st[y]||ed[y]<st[x]){
    for(int i=st[y];i<=ed[y];i++) T.ins(b[i]);
    for(int i=st[x];i<=ed[x];i++) T.del(b[i]);
    return;
  }
  
  for(int i=ed[x]+1;i<=ed[y];i++) T.ins(b[i]);
  for(int i=st[x]-1;i>=st[y];i--) T.ins(b[i]);
  for(int i=ed[x];i>ed[y];i--) T.del(b[i]);
  for(int i=st[x];i<st[y];i++) T.del(b[i]);
}

void dfs1(int u,int fa){
  sz[u]=1,son[u]=-1;
  for(auto v:G[u]) if(v!=fa){
    dfs1(v,u),sz[u]+=sz[v];
    if(!~son[u]||sz[v]>sz[son[u]]) son[u]=v;
  }
}

void dfs2(int u,int fa){
  st[u]=++idx;
  for(auto v:G[u]) if(v!=fa&&v!=son[u]) dfs2(v,u);
  if(~son[u]) dfs2(son[u],u);
  ed[u]=idx,rev[++len]=u,pos[u]=len;
}

int calc(int x,int y){return min(abs(st[y]-st[x])+abs(ed[y]-ed[x]),ed[x]-st[x]+1+ed[y]-st[y]+1);}

int main(){
  /*2024.12.18 H_W_Y CF1767F Two Subtrees dsu on tree*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1,u,v;i<n;i++) cin>>u>>v,G[u].pb(v),G[v].pb(u);
  dfs1(1,0),dfs2(1,0),st[0]=0,ed[0]=-1;
  for(int i=1;i<=n;i++) b[st[i]]=a[i],val[i]=val[i-1]+calc(rev[i-1],rev[i]);
  B=sqrt(val[n]);
  
  cin>>m;
  for(int i=1,x,y;i<=m;i++){
    cin>>x>>y,x=pos[x],y=pos[y];
    if(x>y) swap(x,y);
    q[i]={x,y,i};
  }
  sort(q+1,q+m+1);
  for(int i=1;i<=m;i++) trans(q[i-1].x,q[i].x),trans(q[i-1].y,q[i].y),ans[q[i].id]=T.ask();
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

这个东西是还可以优化的,根据根号分治可以把 \(log\) 放在根号里面。

但是我不会了,而且常数看起来非常大,也不好写,大概率是没有这个跑得快了。


CF1976F Remove Bridges

你有一棵 \(n\) 个点的树,根是 \(1\) 号点,且根只与 \(1\) 个点相邻。

你可以在图上任意两点间连一条无向边,一共连 \(k\) 条,允许重边与和原来的树上相同的边。且新图要求:

  • 对每条割边,它的在树上深度较大的节点的子树内所有树边也都是割边。
  • 割边数最少。

对 \(k\) 为 \(1\) 到 \(n - 1\) 之间所有整数求解割边数的最小值。一个测试点 \(t\) 组数据。

\(2 \le n \le 3 \times 10^5\)。

真简单题。


首先考虑每次你可以连成一个环,然后对答案的贡献就是环上点的数量。

所以我们考虑第一条边,一定是一条从根开始的最长链。

然后考虑每一次的拓展,一定是从已经开发的链上面找两条最长链衍生出去。

于是直接用优先队列维护即可,时间复杂度 \(\mathcal O(n \log n)\)。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define pii pair<int,int>
#define fi first
#define se second

const int N=5e5+5;
int n,len[N],to[N],ans,cnt,fa[N];
vector<int> G[N];
priority_queue<pii> Q;

void dfs(int u,int pre){
  len[u]=to[u]=0,fa[u]=pre;
  for(auto v:G[u]) if(v!=fa[u]){
    dfs(v,u);
	if(len[v]+1>len[u]) len[u]=len[v]+1,to[u]=v;
  }
}

void wrk(int u){
  if(!u) return;
  for(auto v:G[u]) if(v!=fa[u]&&v!=to[u]) Q.push({len[v],v});
  wrk(to[u]);
}

void SOLVE(){
  cin>>n;
  for(int i=1;i<=n;i++) G[i].clear();
  for(int i=1,u,v;i<n;i++) cin>>u>>v,G[u].pb(v),G[v].pb(u);
  while(!Q.empty()) Q.pop();
  dfs(1,0),ans=cnt=0;

  Q.push({len[G[1][0]],G[1][0]});
  while(!Q.empty()){
	++cnt;
	int u=Q.top().se;Q.pop();
	ans+=len[u]+1,wrk(u);
	if(cnt>1&&!Q.empty()){
	  u=Q.top().se;Q.pop();
	  ans+=len[u]+1,wrk(u);
	}
	cout<<n-1-ans<<' ';
  }
  for(int i=cnt+1;i<n;i++) cout<<0<<' ';
  cout<<'\n';
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  int _;cin>>_;
  while(_--) SOLVE();
  return 0;
}

P6780 [Ynoi2009] pmrllcsrms

给你一个长度为 \(n\) 的整数序列 \(a\),和一个常数 \(c\) 。

有 \(m\) 次操作:

1 x y:将位置 \(x\) 的值修改为 \(y\)。

2 l r:表示询问区间 \([l,r]\) 中 \(\max\left(\max_{l \leq l' \leq r' \leq r\atop r'-l'+1\leq c} ~~ \left(\sum_{i=l'} ^{r'} a_i\right), 0\right)\)。

\(1 \le n\le 10^6, 1\le m\le 2\times10^6, 1\le x,c\le n, 1\le l\le r\le n, -10^9 \le a_i,y \le 10^9\) 。

P9877 [EC Final 2021] Vacation


考虑按 \(c\) 分块,那么答案可以被分成每个块内的答案和相邻两块间的答案。

块内的答案十分好维护,直接维护最大子段和——前缀 / 后缀最大值 + 最大子段和。

而对于相邻块间,我需要保证区间长度 \(\le c\),于是考虑把后面的一段移动到前面一段的下面。

发现设 \(f_i\) 表示前一块 \(i\) 往后的后缀和,\(g_j\) 表示后一块前 \(j\) 个位置的前缀和,那么我们相当于求

\[\max _{j \lt i} g_j + f_i \]

于是可以类似于区间最大子段和维护了。


然而这里的块数有可能非常多,所以我们还需要用一棵线段树维护一个区间块内的答案 / 相邻块间的答案。

于是修改的时候先在所在块上面修改,再在全局的线段树上面修改。

查询的时候,先特判掉 \(r-l+1 \le c\) 的情况,然后在全局线段树上面查询块内 / 相邻块间的答案。

那么唯一需要处理的就是左右散块和相邻块间的答案。

这个东西可以在相邻块的线段树上面区间查询得到答案,画一下图发现是简单的。


于是就做完了,具体实现细节是用 vector 存每个块的线段树比较好写?

时间复杂度 \(\mathcal O(n \log n)\),没有细节,比较好写。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define il inline
#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

const int N=2e6+5;
const ll inf=1e18;
int n,m,C,bl[N],q;
ll a[N];

il int Lp(int x){return (x-1)*C+1;}
il int Rp(int x){return x*C;}

struct SGT1{
  int L,R;
  struct nod{
    ll pmx,smx,s,ans;
  };
  vector<nod> tr;
  
  friend nod operator +(nod a,nod b){
    nod c={0ll,0ll,0ll,0ll};
    c.s=a.s+b.s;
    c.pmx=max(a.pmx,b.pmx+a.s);
    c.smx=max(b.smx,a.smx+b.s);
    c.ans=max({a.ans,b.ans,a.smx+b.pmx});
    return c;
  }
  il void pu(int p){tr[p]=tr[lc]+tr[rc];}
  il void val(nod &a,ll v){
  	a.s=v;
  	a.pmx=a.smx=a.ans=max(v,0ll);
  }
  il void build(int l=1,int r=C,int p=1){
    if(l==r) return val(tr[p],a[L+l-1]);
	build(lson),build(rson),pu(p);
  }
  il void upd(int x,int l=1,int r=C,int p=1){
  	if(l==r) return val(tr[p],a[L+l-1]);
  	x<=mid?upd(x,lson):upd(x,rson);pu(p);
  }
  il nod qry(int x,int y,int l=1,int r=C,int p=1){
  	if(x<=l&&y>=r) return tr[p];
  	if(y<=mid) return qry(x,y,lson);
  	if(x>mid) return qry(x,y,rson);
  	return qry(x,y,lson)+qry(x,y,rson);
  }
  il void Upd(int x){upd(x-L+1);}
  il nod Qry(int x,int y){return qry(x-L+1,y-L+1);}
  il void init(int _l,int _r){L=_l,R=_r,tr.resize(((R-L+1)<<2)+1),build();}
}T1[N>>1];

struct SGT2{
  int L,R;
  struct nod{
    ll p,s,pmx,smx,ans;
  };
  vector<nod> tr;
  
  friend nod operator +(nod a,nod b){
  	nod c={0ll,0ll,0ll,0ll,0ll};
  	c.p=a.p+b.p;
  	c.s=a.s+b.s;
  	c.pmx=max(a.pmx,a.p+b.pmx);
  	c.smx=max(b.smx,b.s+a.smx);
  	c.ans=max({a.ans+b.s,b.ans+a.p,a.pmx+b.smx});
  	return c;
  }
  il void val(nod &a,ll v1,ll v2){
    a.s=v1,a.p=v2;
    a.smx=max(v1,0ll),a.pmx=max(v2,0ll);
    a.ans=0;
  }
  il void pu(int p){tr[p]=tr[lc]+tr[rc];}
  il void build(int l=1,int r=C,int p=1){
  	if(l==r) return val(tr[p],a[L+l-1],a[L+l-1+C]);
  	build(lson),build(rson),pu(p);
  }
  il void upd(int x,int l=1,int r=C,int p=1){
  	if(l==r) return val(tr[p],a[L+l-1],a[L+l-1+C]);
  	x<=mid?upd(x,lson):upd(x,rson);pu(p);
  }
  il nod qry(int x,int y,int l=1,int r=C,int p=1){
  	if(x>y) return {0,0,0,0,0};
  	if(x<=l&&y>=r) return tr[p];
  	if(y<=mid) return qry(x,y,lson);
  	if(x>mid) return qry(x,y,rson);
  	return qry(x,y,lson)+qry(x,y,rson);
  }
  il void Upd(int x){upd(x-L+1);}
  il nod Qry(int x,int y){return qry(x-L+1,y-L+1);}
  il void init(int _l,int _r){L=_l,R=_r,tr.resize(((R-L+1)<<2)+1),build();}
}T2[N>>1];

struct SGT3{
  int op;
  vector<ll> tr;
  
  il void val(int p,int l){
  	if(!op) tr[p]=T1[l].tr[1].ans;
  	else tr[p]=l<m?T2[l].tr[1].ans:0;  	
  }
  il void pu(int p){tr[p]=max(tr[lc],tr[rc]);}
  il void build(int l=1,int r=m,int p=1){
  	if(l==r) return val(p,l);
	build(lson),build(rson),pu(p);
  }
  il void upd(int x,int l=1,int r=m,int p=1){
  	if(l==r) return val(p,l);
  	x<=mid?upd(x,lson):upd(x,rson),pu(p);
  }
  il ll qry(int x,int y,int l=1,int r=m,int p=1){
  	if(x>y) return 0;
  	if(x<=l&&y>=r) return tr[p];
  	if(y<=mid) return qry(x,y,lson);
  	if(x>mid) return qry(x,y,rson);
  	return max(qry(x,y,lson),qry(x,y,rson));
  }
  il void init(int _op){op=_op,tr.resize((m<<2)+1),build();}
}T[2];

il void init(){
  T1[0].tr.resize((n<<2)+1),T1[0].L=1,T1[0].build(1,n,1);
  for(int i=1;i<=m;i++) T1[i].init(Lp(i),Rp(i));
  for(int i=1;i<m;i++) T2[i].init(Lp(i),Rp(i));
  T[0].init(0),T[1].init(1);
  for(int i=1;i<=m;i++) for(int j=Lp(i);j<=Rp(i);j++) bl[j]=i;
}

il void Op1(int x){
  T1[0].upd(x,1,n,1),T1[bl[x]].Upd(x),T[0].upd(bl[x]);
  if(bl[x]<m) T2[bl[x]].Upd(x),T[1].upd(bl[x]);
  if(bl[x]>1) T2[bl[x]-1].Upd(x-C),T[1].upd(bl[x]-1);
}

il ll qry(int id,int s,int p){
  auto p1=T2[id].Qry(Lp(id),s-1),p2=T2[id].Qry(s,p-C),p3=T2[id].Qry(p-C+1,Rp(id));
  return max({p2.ans+p3.s+p1.p,p1.pmx+p2.smx+p3.s,p1.pmx+p3.smx,p2.pmx+p1.p+p3.smx});
}

il ll Op2(int l,int r){
  if(r-l+1<=C) return T1[0].qry(l,r,1,n,1).ans;
  ll res=0,bll=bl[l],blr=bl[r];
  res=max({0ll,T[0].qry(bll+1,blr-1),T[1].qry(bll+1,blr-2),T1[bll].Qry(l,Rp(bll)).ans,T1[blr].Qry(Lp(blr),r).ans});
  if(bll+1==blr) res=max({res,qry(bll,l,r)});
  else res=max({res,qry(bll,l,Rp(bll+1)),qry(blr-1,Lp(blr-1),r)});
  return res;
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>q>>C;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=n+1;i<=((n-1)/C+1)*C;i++) a[i]=-1e9;
  n=((n-1)/C+1)*C,m=n/C,init();
  
  while(q--){
  	int op,x,y;cin>>op>>x>>y;
  	if(op==1) a[x]=y,Op1(x);
  	else cout<<Op2(x,y)<<'\n';
  }
  return 0;
}

我要拷打牢麦又长又慢!


Part 2 Tree


CF1740H:结论 \(\to\) ddp。

Qoj9914:重心 + 构造。

Qoj9911:dfs + 构造。


CF1740H MEX Tree Manipulation

定义一棵树上每个节点的值为其所有儿子的值的 MEX,叶子节点的值为 \(0\)。

现在有一个初始只有节点 \(1\) 的树,每次输入一个 \(x_i\) 代表加入一个点 \(i+1\),它的父亲为 \(x_i\),求加入这个点之后树上所有点的权值和。

\(1\leq q\le 3\times 10 ^ 5\),\(1\le x_i\le i\)。


感觉这里每个权值不是很大,如何证明?

设 \(f_i\) 表示权值为 \(i\) 的最小子树大小,那么容易发现

\[f_i = \sum_{j=0}^{i-1} f_j+1 \]

解得 \(f_i = 2^i\),所以权值是 \(\mathcal O(\log n)\) 级别的。


接着考虑如何处理,由于需要动态改变树的形态,所以我们考虑 ddp。

具体来说,对于每个节点,树链剖分之后考虑轻儿子的贡献,可以用一个数组记录哪些元素出现了多少次,找到只考虑轻儿子的 MEX 为 \(val\),那么如果重儿子权值 \(x \neq val\),那么答案就是 \(val\);否则我们找到下一个没有出现的数 \(val'\),于是当 \(x=val\) 时,答案就是 \(val'\)。

这是一个非常简单的映射形式,我们同时再记录一下区间的 \(\sum val\) 就可以了,合并是简单的。

这样每次修改相当于是把当前节点到根的链都修改,于是正常 ddp 维护就可以做到 \(\mathcal O(n \log^2n)\)。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back

const int N=3e5+5;
int n,son[N],sz[N],top[N],ans=0,dfn[N],rev[N],idx=0,fa[N],c[N][20],ed[N];
vector<int> G[N];
struct Nod{
  int val,x,y,sx,sy;
};

Nod operator + (Nod a,Nod b){
  Nod c=a;
  if(a.x==b.val) c.x=b.x,c.sx+=b.sx;
  else c.x=b.y,c.sx+=b.sy;
  if(a.y==b.val) c.y=b.x,c.sy+=b.sx;
  else c.y=b.y,c.sy+=b.sy;
  return c;
}

void dfs1(int u,int pre){
  fa[u]=pre,sz[u]=1,son[u]=-1;
  for(auto v:G[u]) if(v!=pre){
    dfs1(v,u),sz[u]+=sz[v];
    if(!~son[u]||sz[v]>sz[son[u]]) son[u]=v;
  }
}

void dfs2(int u,int pre){
  top[u]=pre,rev[dfn[u]=++idx]=u,ed[pre]=idx;
  if(~son[u]) dfs2(son[u],pre);
  for(auto v:G[u]) if(v!=fa[u]&&v!=son[u]) dfs2(v,v);
}

namespace SGT{
  #define mid ((l+r)>>1)
  #define lc p<<1
  #define rc p<<1|1
  #define lson l,mid,lc
  #define rson mid+1,r,rc
  
  Nod tr[N<<2];
  
  void pu(int p){tr[p]=tr[rc]+tr[lc];}
  
  void build(int l=1,int r=n,int p=1){
    if(l==r){
      if(rev[l]==1) tr[p]={0,1,0,1,0};
      else tr[p]={-1,-1,-1,0,0};
      return;
    }
    build(lson),build(rson),pu(p);
  }
  
  void upd(int x,Nod v,int l=1,int r=n,int p=1){
    if(l==r) return tr[p]=v,void();
    x<=mid?upd(x,v,lson):upd(x,v,rson);pu(p);
  }
  
  Nod qry(int x,int y,int l=1,int r=n,int p=1){
    if(x<=l&&y>=r) return tr[p];
    if(y<=mid) return qry(x,y,lson);
    if(x>mid) return qry(x,y,rson);
    return qry(x,y,rson)+qry(x,y,lson);
  }
}

Nod calc(int x){
  Nod res={-1,-1,-1,0,0};
  int i=0;
  while(c[x][i]) ++i;
  res.val=i,res.y=res.sy=i;
  ++i;
  while(c[x][i]) ++i;
  res.x=res.sx=i;
  return res;
}

void upd(int x){
  int ret=x;
  while(x){
    Nod lst=SGT::qry(dfn[top[x]],ed[top[x]]);
    ans-=lst.sy;
    
    if(x==ret) SGT::upd(dfn[x],(Nod){0,1,0,1,0});
    else SGT::upd(dfn[x],calc(x));
    
    Nod nw=SGT::qry(dfn[top[x]],ed[top[x]]);
    ans+=nw.sy;
    
    int y=fa[top[x]];
    if(x==ret&&top[x]==x) c[y][nw.y]++;
    else c[y][lst.y]--,c[y][nw.y]++;
    x=y;
  }
}

int main(){
  /*2024.12.19 H_W_Y CF1740H MEX Tree Manipulation ddp*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n,++n;
  for(int i=2,x;i<=n;i++) cin>>x,G[x].pb(i);
  dfs1(1,0),dfs2(1,1),SGT::build();
  for(int i=2;i<=n;i++) upd(i),cout<<ans<<'\n';
  return 0;
}

可以用 全局平衡二叉树 做到 \(\mathcal O(n \log n)\)。


Qoj9914 前往何方

交互题。猜一棵 \(n\) 个点的树,你有一个点集 \(S\) 支持加点、删点,每次操作后交互库返回 \(S\) 导出子图最大连通块大小,最多可以进行 \(Q\) 次操作。

\(n \le 10000,Q \le 600000\)。

CTT 2025 D3T3。


首先,一个比较直接的思路就是我们在全局里面删点,可以得到什么?

发现可以得到删去每个点的最大连通块大小——重心!!!

进而对于不是重心的点,我们发现它的子树大小(以重心为根)就是 \(sz_i = n-mx_i\),其中 \(mx_i\) 是删去这个点返回的值。


于是我们考虑类似于 点分树 的操作,找到每个点属于哪些子树。

首先,根的最大儿子是可以找出来的(也就是 \(sz_i\) 最大的),然后我们删去它,再依次删除其它的节点,如果返回值不变,则说明这个点在最大的子树中。

而对于哪些非最大的儿子,我们可以通过一次遍历,按照 \(sz_i\) 从大到小的顺序,每次删去一个节点。

如果返回值变小,则说明这个点是根的儿子。

接着就是去确定每个点属于那个子树中,这个可以直接对那些不是最大子树的儿子 二进制分组

每次删掉当前第 \(i\) 位为 \(0\) 的儿子,那么对于一个点,如果删去它返回值不变,则它在第 \(i\) 位为 \(0\) 的儿子的子树中,反之亦然。

那么这样每次就可以用 \(\mathcal O(n \log n)\) 的时间复杂度求出根下的那些子树,调用次数是 \(\mathcal O(n \log^2n)\) 的。

不精细实现可以获得 \(50pts\)。代码


接着,你发现其实并不用把每个子树都求出来,我们只需要类似 整体二分 去做。

即把整棵树划分成两个部分,使得都包含根,并且大小平均。

而上面的做法中,我们判断一个点是否是根的儿子依赖于我们没有删除根最大的儿子,这样才能保证每次的返回值是根所在的连通块的大小。

但实际上我们并不需要如此。

还是按照子树大小从大往小枚举,删除一个点,如果返回值 \(\le \frac n 2\),我们就停止。

这样就把树分成了两块,第一块是前面删除的那些儿子,第二块是后面删除的那些儿子。

然后对于后面的那些点,如果删除对返回值没有影响,说明这个点属于第一块,反之属于第二块。


这样做的次数上限是什么呢?

首先我们设当前重心下儿子的大小是 \(s_1\ge s_2 \ge s_3 \ge \cdots \ge s_k\),那么我们一定能找到一个 \(t\) 满足

\[|s_1+s_2+\cdots + s_t -\frac {n-1} 2 | \le \frac {n-1} 6 \]

证明直接讨论 \(s_1\) 和 \(\frac {n-1}3\) 的大小关系,是容易的。


而这个 \(t\) 就是我们停止的位置,那么设 \(T(n)\) 表示初始询问的集合是全集,返回出来的询问集合是空集的最小操作次数。

那么我们有

\[T(n) = \begin{cases} 1 & n=1 \\ 2 & n=2 \\ \min_m \left( T(m+1)+T(n-m)+4n-1 \right ) & n \gt 2 \end{cases} \]

其中 \(m\) 就是一个合法的 \(\sum_{i=1}^t s_i\)。

这个东西计算出来是 \(\le 60000\) 的。

于是我们就做完了。


最后,这里有一些卡常的小技巧:

我们需要每次 dfs 时记录当前的集合是否被加进了询问集合。

于是如果 \(sz \le 2\),并且本身没有加入,我们没有必要再加入+删除。


总操作次数是 \(\mathcal O(n \log n)\) 级别的。代码

Code
#include "wheretoreach.h"
#include <bits/stdc++.h>
using namespace std;
#define pb push_back

const int N=1e4+5;
int sz[N],fa[N];
struct Nod{
  int v,id;
  bool operator <(const Nod &a) const{return v>a.v;}
};

void sol(vector<int> V,bool vs=0){
  int m=(int)V.size(),rt=0;
  if(m<=2){
    if(m==2) report(V[0],V[1]);
    if(vs) for(auto i:V) remove(i);
    return;
  }
  
  if(!vs) for(auto i:V) add(i);
  
  for(auto i:V){
    sz[i]=remove(i),add(i);
    if(!rt||sz[i]<sz[rt]) rt=i;
  }
  vector<Nod> cur;
  vector<int> v[2];
  
  for(auto i:V) if(i!=rt) sz[i]=m-sz[i],cur.pb({sz[i],i});
  sort(cur.begin(),cur.end());
  
  int mx=m,fl=0,val=3*m;
  for(auto i:cur){
    if(fl){
      if(remove(i.id)<mx) add(i.id),v[0].pb(i.id);
      else v[1].pb(i.id);
    }else{
      int tmp=remove(i.id),del=max(m-tmp+1,tmp);
      if(del<=val) v[1].pb(i.id),mx=tmp,val=del;
      else v[0].pb(i.id),fl=1,add(i.id);
    }
  }
  v[0].pb(rt),v[1].pb(rt);
  
  sol(v[0],1),sol(v[1],0);
}

void solve(int n){
  vector<int> V;
  for(int i=1;i<=n;i++) V.pb(i);
  sol(V);
}

Qoj9911 路南柯

称一个 \(1 \sim n\) 的排列 \(\{p\}=\{p_1,p_2,\cdots, p_n\}\) 是一棵 \(n\) 个点、点编号为 \(1\) 至 \(n\) 的树 \(T\) 的拓扑序列,当且仅当对于任意 $1 \le i \lt n $,恰好存在唯一的 \(j \gt i\) 满足\(p_i\) 与 \(p_j\) 之间有连边。

给定树 \(T\),你需要给出尽可能少的该树的拓扑序列 \(\{p_1\},\{p_2\},\cdots ,\{p_k\}\),使得有且仅有树 \(T\) 满足 \(\{p_1\},\{p_2\},\cdots ,\{p_k\}\) 均为该树的合法拓扑序列。

\(1 \le n \le 100\)。

CTT 2025 D2T3。


考虑一个树形拓扑序能证明什么?

它能说明一个点的父亲在一个集合中,而这个集合中有它的祖先或者和它没关系的点。

首先是容易确定哪些点是 \(u\) 的祖先的,即以任意点为根,我们用两次 dfs 实现。

第一次每个点以任意顺序 dfs 儿子,得到一个拓扑序;第二次,我们将每个点的儿子顺序反转,再次 dfs 得到拓扑序。

那么两次拓扑序对于每个 \(u\) 的合法父亲节点的交集,就是 \(u\) 的祖先。


现在问题就变成如何区分 \(u\) 的父亲和祖先。

我们发现需要再从 \(u\) 的父亲子树中找一个点为根开始一个拓扑序,而这个拓扑序每次就先删原来的父亲就可以了。

那么稍微画一下图就可以知道,操作次数是把整棵树删去叶子节点后的新树的叶子节点数量,以它们为根一定是更优的。


而对于最开始我们说的两次操作,其实也是不必要的,可以把它们融到后面的遍历中。

即我们在第一次求完拓扑序之后,将所有点的儿子节点都 reverse 一次就可以了。

对于那些原树的叶子节点,我们需要在它父亲节点最后再访问,这样才能确保叶子一定是叶子。


上述答案为原树删去叶子节点后得到的新树的叶子数量的构造方法,对大部分树都满足条件。

而唯一的特例就是菊花,对于菊花,我们直接做两次,按照两种顺序遍历儿子就可以了。

这样就做完了,时间复杂度 \(\mathcal O(n^2)\)。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back

const int N=105;
int n,deg[N];
vector<int> G[N];
vector<vector<int> > ans;

void dfs(int u,int fa){
  if(fa) G[u].erase(find(G[u].begin(),G[u].end(),fa)),G[u].insert(G[u].begin(),fa);
  for(auto v:G[u]) if(v!=fa) dfs(v,u);
  ans.back().pb(u);
}

void SOLVE(){
  cin>>n,ans.clear();
  for(int i=1;i<=n;i++) G[i].clear(),deg[i]=0;

  for(int i=1,u,v;i<n;i++) cin>>u>>v,G[u].pb(v),G[v].pb(u);
  if(n<3) return cout<<"0\n",void(); 
  
  for(int i=1;i<=n;i++){
    if((int)G[i].size()==n-1){
      cout<<"2\n";
      for(int j:G[i]) cout<<j<<' ';
      cout<<i<<'\n',reverse(G[i].begin(),G[i].end());
      for(int j:G[i]) cout<<j<<' ';
      cout<<i<<'\n';
      return;
    }
    deg[i]+=(int)G[i].size();
    if((int)G[i].size()==1) --deg[G[i][0]],deg[i]=0;
    sort(G[i].begin(),G[i].end(),[&](int x,int y){return G[x].size()>G[y].size();});
  }
  for(int i=1;i<=n;i++) if(deg[i]==1){
    ans.pb({}),dfs(i,0);
    if((int)ans.size()==1) for(int j=1;j<=n;j++) reverse(G[j].begin()+1,G[j].end());
  }
  
  cout<<(int)ans.size()<<'\n';
  for(auto i:ans){
    for(auto j:i) cout<<j<<' ';
    cout<<'\n';
  }
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  int _;cin>>_;
  while(_--) SOLVE();
  return 0;
}

Part 3 分治


感觉序列分治题目的难点其实是在于想到序列分治/qd

有关 \(\min/\max\) 的题目 + 区间子区间 + 全局查询,都可以往这方面思考。

这一部分主要是整理性的作用,许多题都是近期的联考中的。


Qoj9245:序列分治 + dp 解决 区间子区间

9.30 BSZX 联考 T3 宝藏(sagiri,CF1787I):序列分治 + 区间子区间

11.28 BSZX 联考 T4 最小值(min):序列分治 + 绝对值。

ABC248Ex 加强版:序列分治 + 二维数点,钦定两侧的大小关系。

ABC282Ex:\(\min/\max\) 练手题。

11.21 BDF 联考 T4 图(graph,CF603E):线段树分治。

P4566:cdq 分治实现自卷型 FFT。


Qoj9245 Bracket Sequence

A sequence is good when it can be represented by several () connected together.
Formally, a sequence \(s\) of length \(2 n\) is good when \(s_1=s_3=\cdots=s_{2 n-1}=\left(\right.\) and \(\left.s_2=s_4=\cdots=s_{2 n}=\right)\). In that case, we call \(n\) its depth.
Given a sequence \(s\) of length \(n\) which consists of (and ). Let \(f(l, r, k)\) be the number of good subsequences with depth \(k\) in the sequence \(t\) formed by \(s_l, s_{l+1}, \ldots, s_r\).
You are given \(q\) queries, each query contains four numbers \(o p, l, r, k\).

  • If \(o p=1\), you need to calculate \(f(l, r, k)\).
  • If \(o p=2\), you need to calculate \(\sum_{l \leq l^{\prime} \leq r^{\prime} \leq r} f\left(l^{\prime}, r^{\prime}, k\right)\).

Since the answer could be huge, you need to output the answer modulo 998244353.

\(1 \le n \le 10^5,1 \le q \le 10^6,1 \le k \le 20\)。

注意是 子序列


不一定所有的 区间子区间 都要往 扫描线和莫队 上面想,这道题中我们可以用更简单的分治来完成。

当给定一个区间 \([l,r]\) 时,我们容易得到一个 dp:

设 \(f_{i,j,0/1}\) 表示前 \(i\) 个数,子序列长度为 \(j\),最后一个括号为 左括号/右括号 的方案数。容易发现转移是简单的

\[\begin{aligned} f_{i,j+1,a_i} & \leftarrow f_{i-1,j,a_i \oplus1} \\ f_{i,j,0} & \leftarrow f_{i-1,j,0} \\ f_{i,j,1} & \leftarrow f_{i-1,j,1} \end{aligned} \]

其中 \(a_i\) 表示这一位是左括号还是右括号。


于是考虑对询问 分治,每次只处理跨过当前 \(mid\) 和 \(mid+1\) 的询问,设当前分治区间为 \([l,r]\)。

那么对于 \(op=1\),我们直接 \(\mathcal O((r-l+1)K)\) 处理出当前 \(mid\) 两边的 dp 数组,一次询问可以在 \(\mathcal O(K)\) 的时间复杂度之内解决。

而对于 \(op=2\),我们用上面的数组求一个前缀/后缀和,就可以处理出跨过 \(mid\) 的子区间的答案,用同样的方法可以在 \(\mathcal O(K)\) 的时间复杂度之内处理出贡献。

反之,对于不跨过当前分治中点 \(mid\) 的子区间,我们同样可以用一次 dp 处理出来。

这样每次统计答案是 \(\mathcal O(1)\) 的,具体的 dp 就是在初始值部分进行一些修改。


那么这样我们就做完了,时间复杂度 \(\mathcal O(nK \log n+qK)\),实现没有细节,相当简单。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back
#define mid ((l+r)>>1)

const int N=1e5+5,H=998244353,K=40;
int n,m,ans[N*10],f[N][K+5][2],g[N][K+5][2],a[N];
struct Nod{int op,l,r,k,id;};
vector<Nod> Q;
char ch;

int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}

void clean(int l,int r){
  for(int i=l;i<=r;i++)
    for(int j=0;j<=K;j++)
	  for(int k=0;k<2;k++)
	    f[i][j][k]=g[i][j][k]=0;
}

void solve(int l,int r,vector<Nod> &q){
  if(l==r) return;

  vector<Nod> ql,qr,qm;
  for(auto i:q){
	if(i.r<=mid) ql.pb(i);
	else if(i.l>mid) qr.pb(i);
    else qm.pb(i);
  }
  q.clear();

  solve(l,mid,ql),solve(mid+1,r,qr);

  for(int i=0;i<2;i++) f[mid+1][0][i]=g[mid][0][i]=1;

  for(int i=mid;i>=l;i--) for(int j=0;j<=K;j++){
	add(f[i][j+1][a[i]],f[i+1][j][a[i]^1]);
	add(f[i][j][0],f[i+1][j][0]),add(f[i][j][1],f[i+1][j][1]);
  }
  for(int i=mid+1;i<=r;i++) for(int j=0;j<=K;j++){
	add(g[i][j+1][a[i]],g[i-1][j][a[i]^1]);
	add(g[i][j][0],g[i-1][j][0]),add(g[i][j][1],g[i-1][j][1]);
  }

  for(auto i:qm) if(i.op==1){
	int nw=i.k<<1;
	for(int j=0;j<=nw;j++)
	  add(ans[i.id],mul(f[i.l][j][0],g[i.r][nw-j][1]));
  }

  for(int i=mid;i>=l;i--) for(int j=0;j<=K;j++) for(int k=0;k<2;k++) add(f[i][j][k],f[i+1][j][k]);
  for(int i=mid+1;i<=r;i++) for(int j=0;j<=K;j++) for(int k=0;k<2;k++) add(g[i][j][k],g[i-1][j][k]);

  for(auto i:qm) if(i.op==2){
	int nw=i.k<<1;
	for(int j=0;j<=nw;j++)
	  add(ans[i.id],mul(f[i.l][j][0],g[i.r][nw-j][1]));
  }

  clean(l,r);
  for(int i=mid;i>=l;i--) for(int k=0;k<2;k++) f[i][0][k]=1;
  for(int i=mid+1;i<=r;i++) for(int k=0;k<2;k++) g[i][0][k]=1;

  for(int i=mid;i>=l;i--) for(int j=0;j<=K;j++){
    add(f[i][j+1][a[i]],f[i+1][j][a[i]^1]);
	add(f[i][j][0],f[i+1][j][0]),add(f[i][j][1],f[i+1][j][1]);
  }
  for(int i=mid+1;i<=r;i++) for(int j=0;j<=K;j++){
	add(g[i][j+1][a[i]],g[i-1][j][a[i]^1]);
	add(g[i][j][0],g[i-1][j][0]),add(g[i][j][1],g[i-1][j][1]);
  }

  for(int i=mid;i>=l;i--) for(int j=0;j<=K;j++) for(int k=0;k<2;k++) add(f[i][j][k],f[i+1][j][k]);
  for(int i=mid+1;i<=r;i++) for(int j=0;j<=K;j++) for(int k=0;k<2;k++) add(g[i][j][k],g[i-1][j][k]);

  for(auto i:qm) if(i.op==2){
	int nw=i.k<<1;
	add(ans[i.id],adc(f[i.l][nw][0],g[i.r][nw][1]));
  }
  
  clean(l,r);
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;Q.resize(m);
  for(int i=1;i<=n;i++) cin>>ch,a[i]=(ch==')');
  for(int i=0;i<m;i++) cin>>Q[i].op>>Q[i].l>>Q[i].r>>Q[i].k,Q[i].id=i;
  solve(1,n,Q);
  for(int i=0;i<m;i++) cout<<ans[i]<<'\n';
  return 0;
}

9.30 BSZX 联考 T3 宝藏(sagiri)

对于一个长度为 \(k\) 的的序列 \(p _ {1 \sim k}\),令 \(p _ 0 = 0\),定义其权值 \(V (p) = \max \left( \sum _ {i = 0} ^ q {p _ i} + \sum _ {i = s} ^ t {p _ i} \right)\), 其中 \(s, t, q\) 满足 \(s, t, q \in [0, k]\), \(s ≤ t, t ≤ q ∨ s > q\)。

给定一个长度为 \(n\) 的序列 \(a\),求 \(\sum _ {l = 1} ^ n \sum _ {r = l} ^ n V (a [l : r])\)。

其中 \(a[l : r]\) 表示由 \(a\) 的第 \(l\) 个元素到 \(a\) 的第 \(r\) 个元素 依次 构成的序列。

由于答案可能很大,你只需要输出答案对 \(998244353\) 取模后的结果即可。

\(1 \le T \le 10,1 \le \sum n \le 10^6,-10^6 \le a_i \le 10^6\)。

CF1787I Treasure Hunt


首先,容易发现那个条件是没有用的。

于是问题就转化成了求每个区间的前缀 \(\max\),和最大子段和。

考虑分两个部分来解决,也就是上面提到的两个部分:

  • 前缀 \(\max\),考虑差分之后变成前缀和之差,而之差的 \(s_{l-1}\) 是一定的,所以我们只需要维护 \(s_r\) 的 \(\max\) 就可以了。

    这个东西容易用单调栈做到 \(\mathcal O(n)\)。

      for(int i=1,res=0;i<=n;i++){
    	del(res,mod(s[i-1]));ll nw=max(s[i-1],s[i]);
        while(tp&&nw>=val[tp]) del(res,mul(st[tp]-st[tp-1],mod(val[tp]))),--tp;
        st[++tp]=i,val[tp]=nw,add(res,mul(st[tp]-st[tp-1],mod(val[tp])));
    	add(ans,res);
      }
    
  • 最大子段和,同样单调栈之后直接用吉司机线段树维护似乎就是 \(\mathcal O(n \log n)\),但是比较复杂。

    处理这种问题的另外一个方向就是 分治,我们希望处理跨过当前分治中心的答案。

    首先维护一下 \(f_i\) 表示 \([i,mid]/[mid+1,i]\) 区间内的最大子段和,\(g_i\) 表示 \([i,mid]/[mid+1,i]\) 区间内的最大前缀/后缀。

    这两个东西都是容易直接线性得到了。

    那么现在考虑统计答案,手玩一下不难发现其实有一些单调性,也就是我们的左端点 \(mid \to l\) 扫过去的过程中,右端点一定是分了三段:

    • \([mid+1,p)\) 中 \(f_i\) 是最优的(当前左端点在 \(i\))。
    • \([p,q)\) 中 \(g_i + g_j\) 是最优的(左端点为 \(i\),右端点为 \(j\))。
    • \([q,r]\) 中 \(f_j\) 是最优的(右端点为 \(j\))。

    容易感性理解到他们的单调性,于是我们就维护一下 \(f,g\) 的前缀/后缀和就可以完成了。

    那么每次的时间复杂度是线性的,分治下来的时间复杂度就是 \(\mathcal O(n \log n)\)。

    void solve(int l,int r){
      if(l==r) return add(ans,max(a[l],0ll));
    
      int mid=((l+r)>>1),p=mid+1,q=mid+1,sumf=0,sumg=0;
      for(ll i=mid,sum=0;i>=l;i--) sum=max(sum+a[i],0ll),f[i]=max(i<mid?f[i+1]:0,sum);
      for(ll i=mid+1,sum=0;i<=r;i++) sum=max(sum+a[i],0ll),f[i]=max(i>mid+1?f[i-1]:0,sum),add(sumf,mod(f[i]));
      for(int i=mid;i>=l;i--) g[i]=max(i<mid?g[i+1]:0,s[mid]-s[i-1]);
      for(int i=mid+1;i<=r;i++) g[i]=max(i>mid+1?g[i-1]:0,s[i]-s[mid]);
    
      for(int i=mid;i>=l;i--){
    	while(p<=r&&f[i]>=max(f[p],g[i]+g[p])) del(sumg,mod(g[p++]));
        while(q<=r&&(q<p||g[i]+g[q]>=f[q])) add(sumg,mod(g[q])),del(sumf,mod(f[q++]));
    
    	add(ans,adc(adc(mul(p-mid-1,mod(f[i])),mul(q-p,mod(g[i]))),adc(sumg,sumf)));
      }
      solve(l,mid),solve(mid+1,r);
    }
    

分治在很多 ds 题目中都是一个比较好的思路,不会的时候可以往这方面考虑。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define int long long

const int N=1e6+5,H=998244353;
const ll inf=1e18;
int n,ans=0,tp=0,st[N];
ll a[N],s[N],val[N],f[N],g[N];

int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}

int mod(ll x){return (x%H+H)%H;}

void solve(int l,int r){
  if(l==r) return add(ans,max(a[l],0ll));

  int mid=((l+r)>>1),p=mid+1,q=mid+1,sumf=0,sumg=0;
  for(ll i=mid,sum=0;i>=l;i--) sum=max(sum+a[i],0ll),f[i]=max(i<mid?f[i+1]:0,sum);
  for(ll i=mid+1,sum=0;i<=r;i++) sum=max(sum+a[i],0ll),f[i]=max(i>mid+1?f[i-1]:0,sum),add(sumf,mod(f[i]));
  for(int i=mid;i>=l;i--) g[i]=max(i<mid?g[i+1]:0,s[mid]-s[i-1]);
  for(int i=mid+1;i<=r;i++) g[i]=max(i>mid+1?g[i-1]:0,s[i]-s[mid]);

  for(int i=mid;i>=l;i--){
	while(p<=r&&f[i]>=max(f[p],g[i]+g[p])) del(sumg,mod(g[p++]));
    while(q<=r&&(q<p||g[i]+g[q]>=f[q])) add(sumg,mod(g[q])),del(sumf,mod(f[q++]));

	add(ans,adc(adc(mul(p-mid-1,mod(f[i])),mul(q-p,mod(g[i]))),adc(sumg,sumf)));
  }
  solve(l,mid),solve(mid+1,r);
}

void SOLVE(){
  cin>>n;tp=ans=0;
  for(int i=1;i<=n;i++) cin>>a[i],s[i]=s[i-1]+a[i];
  
  for(int i=1,res=0;i<=n;i++){
	del(res,mod(s[i-1]));ll nw=max(s[i-1],s[i]);
    while(tp&&nw>=val[tp]) del(res,mul(st[tp]-st[tp-1],mod(val[tp]))),--tp;
    st[++tp]=i,val[tp]=nw,add(res,mul(st[tp]-st[tp-1],mod(val[tp])));
	add(ans,res);
  }

  solve(1,n);
  cout<<ans<<'\n';
}

signed main(){
//   freopen("sagiri.in","r",stdin);
//   freopen("sagiri.out","w",stdout);
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  int T,_;cin>>T>>_;
  while(_--) SOLVE();
  return 0;
}

11.28 BSZX 联考 T4 最小值(min)

小 X 有两个长度为 \(N\) 的整数序列 \(A_1, A_2, \cdots , A_N\) 和 \(B_1, B_2, \cdots , B_N\)。

小 X 现在越来越喜欢对称的东西了,当他看到两个序列 \(C, D\) 时,他会找到序列 \(C\) 的最小值 \(C_{\min}\) 和 \(D\) 的最小值 \(D_{\min}\),他认为这两个最小值相差越小越好,所以他定义 \(f(C, D) = |C_{\min} − D_{\min}|\)。

更进一步的,他定义 \(w(l, r)\) 表示当 \(C = \{A_l , A_{l+1}, \cdots , A_r\}, D = \{B_l , B_{l+1}, \cdots , B_r\}\) 时 \(f(C, D)\) 的值,即 \(\{A_l , A_{l+1}, \cdots , A_r\}\) 的最小值和 \(\{B_l , B_{l+1}, \cdots, B_r\}\) 的最小值的差的绝对值。

对于每个 \(k = 1, 2, \cdots , N\),小 X 想求出所有长度为 \(k\) 的区间 \([l, r]\) 中,\(w(l, r)\) 的最小值,即求 \(\min_{r−l+1=k}w(l, r)\)。

\(1 \le N \le 2 \times 10^5,1 \le A_i,B_i \le 10^9\)。

L - Min Diff Min


考虑序列分治然后结束了。

设当前的分治中心是 \([l,r]\),那么我们去统计跨过 \(mid\) 的区间的答案。

设左区间 \(A,B\) 的后缀 \(\min\) 是 \(C,D\),右区间的前缀 \(\min\) 是 \(E,F\)。

于是实际上答案分成了四种情况:

  • \(|C_l-D_l |\)。
  • \(|E_r-F_r|\)。
  • \(|C_l-F_r|\)。
  • \(|D_l-E_r|\)。

然后用上 \(C,D\) 递增,\(E,F\) 递减的性质。

我们发现对于一个固定区间长度 \(len\) 的若干个区间,前面是第一种情况,中间部分是后两种情况之一,然后最后是第二种情况。

于是分界点我们可以双指针求出,而前两种情况的答案可以直接区间 \(\min\) 求出。

反之,中间部分有正有负,但是单调。

所以我们可以通过 二分 得到最接近 \(0\) 的位置即可。

这样的时间复杂度是 \(\mathcal O(n \log^2n)\) 的。代码

Code
#include <bits/stdc++.h>
using namespace std;

const int N=2e5+5,inf=2e9;
int n,a[2][N],ans[N],d[N],p[2][N],it[2];

void ckmn(int &a,int b){a=min(a,b);}

namespace ST{
  int st[20][N];
  void build(int l,int r){
  	for(int i=l;i<=r;i++) st[0][i]=d[i];
  	for(int i=1;i<=__lg(r-l+1);i++)
  	  for(int j=l;j+(1<<i)-1<=r;j++)
  	    st[i][j]=min(st[i-1][j],st[i-1][j+(1<<(i-1))]);
  }
  int qry(int l,int r){
  	if(l>r) return inf;
	int s=__lg(r-l+1);
	return min(st[s][l],st[s][r-(1<<s)+1]);
  }
}

void wrk(int i,int j,int len){
  int l=it[j]+1,r=it[i],res=r+1;
  while(l<=r){
  int mi=((l+r)>>1);
  	if(p[i][mi]-p[j][mi+len-1]>=0) res=mi,r=mi-1;
  	else l=mi+1;
  }
  if(res>it[j]&&res<=it[i]) ckmn(ans[len],abs(p[i][res]-p[j][res+len-1]));
  --res;
  if(res>it[j]&&res<=it[i]) ckmn(ans[len],abs(p[i][res]-p[j][res+len-1]));
}

void sol(int L,int R){
  if(L>R) return;
  if(L==R) return ans[1]=min(ans[1],abs(a[0][L]-a[1][L])),void();
  int mid=((L+R)>>1);
  
  for(int i:{0,1}){
  	p[i][mid]=a[i][mid],p[i][mid+1]=a[i][mid+1];
  	for(int j=mid-1;j>=L;j--) p[i][j]=min(p[i][j+1],a[i][j]);
  	for(int j=mid+2;j<=R;j++) p[i][j]=min(p[i][j-1],a[i][j]);
  }
  for(int i=L;i<=R;i++) d[i]=abs(p[0][i]-p[1][i]);
  ST::build(L,R);
  
  it[0]=it[1]=mid;
  for(int len=2;len<=R-L+1;len++){
  	int liml=max(L,mid-len+2),limr=min(mid,R-len+1);
  	if(liml>limr) continue;
  	
    for(auto i:{0,1}) while(it[i]>limr||(it[i]>=liml&&p[i][it[i]]>p[i][it[i]+len-1])) --it[i];
    ckmn(ans[len],min(ST::qry(liml,min(it[0],it[1])),ST::qry(max(it[0],it[1])+len,limr+len-1)));
  	
  	if(it[1]<it[0]) wrk(0,1,len);
	else if(it[1]>it[0]) wrk(1,0,len);
  }
  
  sol(L,mid),sol(mid+1,R);
}

int main(){
//  freopen("min.in","r",stdin);
//  freopen("min_correct.out","w",stdout);
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;
  for(int i:{0,1}) for(int j=1;j<=n;j++) cin>>a[i][j];
  for(int i=1;i<=n;i++) ans[i]=inf;
  sol(1,n);
  for(int i=1;i<=n;i++) cout<<ans[i]<<'\n';
  return 0;
}

实际上我们是容易做到单 \(\log\) 的。

也就是你钦定答案一定是 \(A_{min} -B_{min}\),然后算两次,如果是负数就不管他。

那么分治的时候你分别去枚举区间的左端点和右端点(做两次),然后每次处理掉你钦定的满足条件的那些东西就可以了。

注意到我们需要一个反向 ST 表去实现区间 \(\min\) 的操作。Hanghang 的代码


ABC248Ex Beautiful Subsequences - 加强版

给定排列 $ P_n $ 和整数 $ k $,求满足如下条件的点对 $ (l, r) $ 数量。

  • $ 1 \le l \le r \le n $。
  • $ \max_{i = l}^rP_i - \min_{i = l}^rP_i \le r - l + k $。

\(n \le 1.4 \times 10^5\)。

温馨提示:样例非常水。


首先,原题有条件 \(0\le k \le 3\),于是可以直接用 pudding monster 的套路,扫描线维护前四个最小值就可以了。

这样可以做到较大常数的 \(\mathcal O(n \log n)\),感兴趣的读者可以自行实现。


现在我们抛开 \(k\) 的限制不谈,考虑 分治

设当前分治区间是 \([l,r]\),那么此时我们只处理跨过 \(mid\) 的区间,其它的递归下去处理。

那么我们维护四个值 \(a_i,b_i,c_i,d_i\) 表示左边 \(i \to mid\) 的最大/最小值和右边 \(mid+1 \to i\) 的最大/最小值。

于是只需要统计满足

\[\max(a_i,c_j) - \min(b_i,d_j) \le j-i+k \]

的 \((i,j)\) 数量。


直接对大小关系进行讨论:

  • \(a_i \ge c_j\),满足条件的 \(j\) 是一段 \(mid+1\) 开始的前缀。
    • \(b_i \le d_j\),原式形如 \(a_i-b_i+i-k \le j\),合法的 \(j\) 是一段区间。
    • \(b_i \ge d_j\),原式形如 \(a_i+i-k \le j+d_j\)。
  • \(a_i \le c_j\) 同理。

所以后面的问题就是一个二维数点,用 BIT 维护即可做到 \(\mathcal O(n \log^2n)\)。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

const int N=2e5+5;
int n,a[N],m,mn[N],mx[N];
ll ans=0;
struct Nod{int v,op;};
vector<Nod> G[N];

namespace BIT{
  int tr[N*2];
  int lowbit(int i){return i&(-i);}
  void upd(int x,int v){for(int i=x;i>0;i-=lowbit(i)) tr[i]+=v;}
  int qry(int x){
    int res=0;
    for(int i=max(x,1);i<=n*2;i+=lowbit(i)) res+=tr[i];
    return res;
  }
}

void add(int l,int r,int v){
  if(l>r) return;
  G[l-1].pb({v,-1}),G[r].pb({v,1});
}

void solve(int l,int r){
  if(l==r) return;
  
  int mid=((l+r)>>1);
  for(int i=mid;i>=l;i--){
    mn[i]=min(i==mid?n:mn[i+1],a[i]);
    mx[i]=max(i==mid?0:mx[i+1],a[i]);
  }
  for(int i=mid+1;i<=r;i++){
    mn[i]=min(i==mid+1?n:mn[i-1],a[i]);
    mx[i]=max(i==mid+1?0:mx[i-1],a[i]);
  }
  
  for(int i=mid+1;i<=r;i++) G[i].clear();
  for(int i=mid,j=mid+1,k=mid+1;i>=l;i--){
    while(j<=r&&mx[i]>=mx[j]) ++j;
    while(k<=r&&mn[i]<=mn[k]) ++k; 
    
    ans+=max(0,min(j,k)-max(mid+1,mx[i]-mn[i]+i-m));
    add(k,j-1,mx[i]+i-m);
  }
  for(int i=mid+1;i<=r;i++){
    BIT::upd(mn[i]+i,1);
    for(auto j:G[i]) ans+=j.op*BIT::qry(j.v);
  }
  for(int i=mid+1;i<=r;i++) BIT::upd(mn[i]+i,-1);

  for(int i=l;i<=mid;i++) G[i].clear();
  for(int i=mid+1,j=mid,k=mid;i<=r;i++){
    while(j>=l&&mx[i]>mx[j]) --j;
    while(k>=l&&mn[i]<=mn[k]) --k;
    
    ans+=max(0,min(mid,-mx[i]+mn[i]+i+m)-max(j,k));
    add(j+1,k,mx[i]-i-m+n);
  }
  for(int i=l;i<=mid;i++){
    BIT::upd(mn[i]-i+n,1);
    for(auto j:G[i]) ans+=j.op*BIT::qry(j.v);
  }
  for(int i=l;i<=mid;i++) BIT::upd(mn[i]-i+n,-1);
  
  solve(l,mid),solve(mid+1,r);
}

int main(){
  /*2024.12.19 H_W_Y [ABC248Ex] Beautiful Subsequences 分治*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m,ans=n;
  for(int i=1;i<=n;i++) cin>>a[i];
  solve(1,n);
  cout<<ans<<'\n';
  return 0;
}

ABC282Ex Min + Sum

给定序列 \(A,B\),求有多少对 \((l,r)\) 满足 \(1\le l\le r\le n\),且

\[\sum_{i=l}^rB_i+\min_{i=l}^rA_i\le S \]

\(1 \le N \le 2 \times 10^5,0 \le A_i \le 10^{14},0 \le B_i \le 10^9,0 \le S \le 3 \times 10^{14}\)。


还是考虑去枚举 \(\min A_i\),分治之后是一个二维数点问题。

由于值域较大,我们可以交换值域和下标进行二维数点,也可以直接用 pbds。

于是时间复杂度 \(\mathcal O(n \log ^2 n)\)。代码


11.21 BDF 联考 T4 图(graph,CF603E)

给定一张 \(n\) 个点的无向带权图,初始没有边。

定义合法边集为每个点度数均为奇数的边集。

依次加入 \(m\) 条边,每次加入后询问合法边集中最大边权最小值,若不存在合法边集输出 -1

\(1 \le n \le 10^5,1 \le m\le 3 \times10^5,1 \le u,v \le n,1 \le w \le 10^9\)。

CF603E Pastoral Oddities


首先,对于 每个点度数均为奇数 这个条件,我们大胆猜测就是 每个连通块大小均为偶数

证明稍微构造一下就可以得到一个充分必要性。


然后对于 最大边权最小值,我们考虑 Kruskal 重构树。

于是对于静态的问题,我们按照边权从小到大排序之后依次加入直到满足条件。


但是动态起来你就有一堆单调性,似乎就可以整体二分做。

我们换一种做法,发现对于一条边,一旦它被淘汰了,那就永远被淘汰了。

于是一条边贡献的一定是一个 区间,那么我们考虑 线段树分治

从后往前扫,每次到一个节点时,我们处理在这个位置结束的线段,并且把它加到线段树里面既可以了。

如何处理其实就是按照值域扫过来,直到满足条件。

于是这样就做完了,时间复杂度 \(\mathcal O(n \log n)\)。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back

const int N=5e5+5;
int n,m,cnt,fa[N],sz[N],tp=0,pos=1,ans[N];
struct Edge{
  int u,v,w,id;
  bool operator <(const Edge &a) const{return w<a.w;}
}e[N];
struct Nod{
  int u,v,dlt;
}st[N];
vector<int> G[N<<2];

int find(int x){return x==fa[x]?x:find(fa[x]);}

void merge(int u,int v){
  u=find(u),v=find(v);
  if(u==v) return;
  if(sz[u]<sz[v]) swap(u,v);
  int dlt=-(sz[u]&1)-(sz[v]&1)+((sz[u]+sz[v])&1);
  st[++tp]={u,v,dlt},cnt+=dlt;
  fa[v]=u,sz[u]+=sz[v];
}

void undo(){
  int u=st[tp].u,v=st[tp].v,dlt=st[tp].dlt;
  fa[v]=v,sz[u]-=sz[v],cnt-=dlt,--tp;
}

#define mid ((l+r)>>1)
#define lc p<<1
#define rc p<<1|1
#define lson l,mid,lc
#define rson mid+1,r,rc

void upd(int l,int r,int p,int x,int y,int id){
  if(x>y||y<l||x>r) return;
  if(x<=l&&y>=r) return G[p].pb(id),void();
  if(x<=mid) upd(lson,x,y,id);
  if(y>mid) upd(rson,x,y,id);
}

void solve(int l,int r,int p){
  int pre=tp;
  for(auto i:G[p]) merge(e[i].u,e[i].v);
  if(l==r){
    while(cnt&&pos<=m){
      if(e[pos].id<=l){
        merge(e[pos].u,e[pos].v);
        upd(1,m,1,e[pos].id,l-1,pos);
      }
      ++pos;
    }
    if(!cnt) ans[l]=e[pos-1].w;
    else ans[l]=-1;
  }else solve(rson),solve(lson);
  while(tp>pre) undo();
}

int main(){
  freopen("5.in","r",stdin);
  freopen("graph.out","w",stdout);
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m,cnt=n;
  for(int i=1;i<=n;i++) fa[i]=i,sz[i]=1;
  for(int i=1;i<=m;i++) cin>>e[i].u>>e[i].v>>e[i].w,e[i].id=i;
  sort(e+1,e+m+1),solve(1,m,1);
  for(int i=1;i<=m;i++) cout<<ans[i]<<'\n';
  return 0;
}

*自卷型分治 FFT

让我们来魔怔一下

在普通的分治 FFT 中,我们都解决的是

\[f_n = \sum_{i=1}^n f_i g_{n-i} \]

自己卷别人 是相对容易的。


而现在我们来探究另一种分治 FFT:自己卷自己,也就是说

\[f_n = \sum_{i=1}^n f_{i} f_{n-i} \]

有什么区别呢?

我们发现原先的分治 FFT 中,每一次我们是把 \(f_{l \sim mid}\) 和 \(g_{1 \sim r-l+1}\) 卷起来,而这回,有可能 \(f_{1 \sim r-l+1}\) 没有准备好!

那怎么办呢?


考虑这种情况的出现 当且仅当 \(l=1\),因为由于我们是分治,并且左区间一定 大于等于 右区间的长度,

所以如果 \(l \neq 1\),那么 \(1 \sim r-l+1\) 一定是准备好了的。

那么唯一的问题就是 \(l=1\) 时——有些部分需要推迟。

推迟 如何处理,我们考虑每一次 \(l\neq 1\) 的左区间对右区间的贡献,这些贡献刚好是当时在某个 \(l=1\) 的区间中没有得到贡献的,

也就是那些推迟的贡献,但由于对称性,我们只需要把这部分贡献 \(\times 2\) 即可。


所以我在说什么?

其实自卷型分治 FFT 也就是两个部分:

  • \(l=1\) 时,计算 \(f_{l \sim mid}\) 卷上 \(f_{l \sim mid}\),贡献到 \(f_{mid+1,r}\)。
  • \(l \neq 1\) 时,计算 \(f_{l \sim mid}\) 卷上 \(f_{1 \sim r-l+1}\),将其 两倍 贡献到 \(f_{mid+1,r}\)。

于是就做完了,时间复杂度不变。\(\mathcal O(n \log^2n)\)。


*P4566 [CTSC2018] 青蕈领主

小绿同学因为微积分这门课,对“连续”这一概念产生了浓厚的兴趣。小绿打算把连续的概念放到由整数构成的序列上,他定义一个长度为 \(m\) 的整数序列是连续的,当且仅当这个序列中的最大值与最小值的差,不超过\(m-1\)。例如 \(\{1,3,2\}\) 是连续的,而 \(\{1,3\}\) 不是连续的。

某天,小绿的顶头上司板老大,给了小绿 \(T\) 个长度为 \(n\) 的排列。小绿拿到之后十分欢喜,他求出了每个排列的每个区间是否是他所定义的“连续”的。然而,小绿觉得被别的“连续”区间包含住的“连续”区间不够优秀,于是对于每个排列的所有右端点相同的“连续”区间,他只记录下了长度最长的那个“连续”区间的长度。也就是说,对于板老大给他的每一个排列,他都只记录下了在这个排列中,对于每一个 \(1 \le i \le n\),右端点为 \(i\) 的最长“连续”区间的长度 \(L_i\)。显然这个长度最少为 \(1\),因为所有长度为 \(1\) 的整数序列都是连续的。

做完这一切后,小绿爬上绿色床,美美地做了一个绿色的梦。

可是第二天醒来之后,小绿惊讶的发现板老大给他的所有排列都不见了,只剩下他记录下来的 \(T\) 组信息。小绿知道自己在劫难逃,但是作为一个好奇的青年,他还是想知道:对于每一组信息,有多少个和信息符合的长度为 \(n\) 的排列。

由于小绿已经放弃治疗了,你只需要告诉他每一个答案对 \(998244353\) 取模的结果。

我们并不保证一定存在至少一个符合信息的排列,因为小绿也是人,他也有可能犯错。

\(1 \le T \le 100,1 \le N \le 50000,1 \le L_{i,j} \le j\)。

好题啊。


先来考虑如何判无解。

容易发现区间一定是 相离或包含,而最后一个数一定是 \(n\)。

因为只要区间相交,那么后面的区间一定就不优,和定义违背。


如果让每个区间向第一个 包含 它的区间连边,这样就会形成一棵树。

我们发现每个节点的贡献是一定的,也就是我们可以把它的每一个儿子看成一个点,那么这个长度为 \(cnt+1\) 的序列需要满足 任意一个连续的区间都包含最后一个节点

我们不妨把这个记作 \(f_{cnt}\)(前面 \(+1\) 是因为还有它自己),那么其实答案就是

\[\prod_{i=1}^n f_{cnt_i} \]

现在问题就变成了求 \(f_i\)。


考虑 dp,\(f_i\) 的定义是有多少个长度为 \(i+1\) 的序列满足 任意连续的区间都包含最后一个元素,也就是 \(L\) 的序列形如 \(\left<1,1,1, \cdots,1,n \right>\)。

由于每一次最后一个 元素 要变,所以我们考虑一种 映射,也就是将 位置数值 翻转,这样对连续区间定义不变。

那么问题就变成了 任意一个连续的区间都包含最大值


可是每一次 \(i+1\) 时又会改变最大值,这时不好维护的。

所以我们考虑在上一个 \(i\) 的基础上面插入 \(1\),也就是让 \(i-1\) 的那些数从 \(2\) 开始,这样就非常好计数了。


我们分两种情况:

  • 之前的序列 合法

    则当前插入的 \(1\) 只要不在 \(2\) 的两边就可以了,那么可以插的空隙数量是 \(i-1\),贡献为 \((i-1)f_{i-1}\)。

  • 之前的序列 不合法

    那么我们利用当前的这个 \(1\) 让序列合法,容易发现只会存在一个区间(可以不断包含)不合法。

    因为如果存在两个 相离 的区间不合法,而只能插入一个 \(1\),所以必然不能变为合法。

    考虑枚举这个区间长度为 \(l\),由于它不合法,所以一定不包含最大值;又因为插入 \(1\) 之后就合法了,所以一定不包含 \(2\)。

    我们设这个区间的最小值是 \(x\),那么区间值域是 \([x,x+l-1]\),需要满足 \(x \gt 2,x+l-1 \le i,l \ge 2\)。

    也就是说 \(l \in [2,i-2]\),对于每一个 \(l\),值域范围 \(x\) 有 \(i-l-1\) 中取值。


    对于当前长度为 \(l\) 区间内的点,\(1\) 插入的合法位置满足 任意一个连续区间 都需要经过 \(1\),我们发现这个和之前的 dp 定义非常像,也就是我们把 \(1\) 看成 \(l+1\),那么其实合法的序列数量是 \(f_l\)。

    进而,对于整体的贡献,我们就把这个长度为 \(l+1\) 的区间看成一个数,那么外面的答案就是 \(f_{i-l}\)。

    所以这种情况的贡献就是 \(\sum_{l=2}^{i-2} (i-l-1) f_l f_{i-l}\)。

以上两种情况外面就得到了整个 dp 转移式

\[f_n = (n-1) f_{n-1} + \sum_{i=2}^{n-2} (n-i-1) f_i f_{n-i},n \ge 2,f_1=2,f_0=1 \]

这很明显是一个 标准卷积形式,可以用 自卷型分治 FFT 解决。时间复杂度 \(\mathcal O(n \log^2n)\)。


注意到卷积中有 \((n-i-1)\) 的系数,于是每次对于 \(l \neq 1\) 的部分贡献时,我们的系数其实是 \((n-i-1)+(i-1)=n-2\)(之前推迟的贡献是 \(i-1\)),

于是可以直接卷积之后统一乘了。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned long long

const int N=1e6+5,H=998244353;
int n,m,U[N],p[N],tp=0,ans;
ull w[N],F[N],a[N],b[N];

struct Nod{
  int l,r;
}st[N];

int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(ull &a,int b){a=adc(a,b);}

int qpow(int a,int b=H-2){
  int res=1;
  while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
  return res;
}

const int g=3,ig=qpow(g);

void Init(int n){
  for(int i=0;i<n;i++) U[i]=(U[i>>1]>>1)|((i&1)?n>>1:0),a[i]=b[i]=0;
}

void NTT(ull *p,int len,int op){}

#define mid ((l+r)>>1)

void solve(int l,int r){
  if(l==r) return add(F[l],mul(F[l-1],l-1));
  
  solve(l,mid);
  if(l==1){
	int P;for(P=1;P<mid+1+mid+1;P<<=1);Init(P);

	for(int i=2;i<=mid;i++) a[i]=F[i];NTT(a,P,1);
	for(int i=2;i<=mid;i++) b[i]=mul(F[i],i-1);NTT(b,P,1);

	for(int i=0;i<P;i++) a[i]=a[i]*b[i]%H;
	NTT(a,P,-1);

	for(int i=mid+1;i<=r;i++) add(F[i],a[i]);
  }else{
	int P;for(P=1;P<mid-l+1+r-l+2;P<<=1);Init(P);

	for(int i=l;i<=mid;i++) a[i-l]=F[i];NTT(a,P,1);
	for(int i=2;i<=r-l+1;i++) b[i]=F[i];NTT(b,P,1);

	for(int i=0;i<P;i++) a[i]=a[i]*b[i]%H;
	NTT(a,P,-1);

	for(int i=mid+1;i<=r;i++) add(F[i],mul(i-2,a[i-l]));
  }
  solve(mid+1,r);
}

void SOLVE(){
  ans=1,tp=0;
  bool fl=true;

  for(int i=1,ct,x;i<=n;i++){
	ct=0;cin>>x;
	int l=i-x+1,r=i;
	while(tp&&st[tp].r>=l){
	  if(st[tp].l<l) fl=false;
	  ++ct,--tp;
	}
	st[++tp]={l,r},ans=mul(ans,F[ct]);
  }
  if(!fl||tp!=1) return cout<<0<<'\n',void();

  cout<<ans<<'\n';
}

int main(){
  /*2024.7.22 H_W_Y P4566 [CTSC2018] 青蕈领主 分治 FFT*/
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  int _;cin>>_>>n;
  F[1]=2,F[0]=1,solve(1,n);
  while(_--) SOLVE();
  return 0;
}

Part 4 点分治与点分树


把分治放在树上,就变成了点分治/边分治,在近期的联考中常常涉及。


11.1 GDF 省选联考 T1 联邦(federation):点分治 + 直径。

12.11 GDF 联考 T3 情报大师(zdd):点分治 + 二维数点。

10.4 BDF NOI 联考 T3 黄金矿工(mine,P9260 加强版):点分治维护 dp / 线段树合并。


11.1 GDF 省选联考 T1 联邦(federation)

小 N 希望世界和平。

这个世界上有 \(n\) 个城市,城市之间有 \(n − 1\) 条直接往来的商路,通过这些商路,任何两个城市都能直接或间接地进行商贸往来(即可以通过若干条商路从任意一个城市到其他所有城市)。

作为传播爱与和平的使者,小 N 想建立一个包含若干城市的联邦,设包含城市集合为 \(S\)。由于过远的距离会阻碍交流,所以设 \(d(x, y)\) 表示从城市 \(x\) 到城市 \(y\) 最少需要经过的商路个数,对于任意 \(u, v \in S\),应满足 \(d(u, v) \le m\),其中 \(m\) 是一个常数。

同时,联邦包含的城市个数 \(|S|\) 也很重要,经过考察,小 N 认为 \(|S|\) 有 \(q\) 种合理的取值 \(c_1, c_2, \cdots, c_q\),他想请你帮忙求出对于每一个合理的取值,有多少种建立联邦的方案。两个方案不同当且仅当两个联邦的 \(S\) 不同。由于答案可能很大,你只需要求出其对 \(998244353\) 取模的结果。

\(1 \le m \le n \le 10^5,1 \le q \le 100,1 \le c_i \le n\)。

很好的一道题。


场上把一个连通块定义到这个连通块的直径中点上面去了,于是没有什么前途。

但是稍微换一个思路,我们是否把它定义到一个直径的端点上面去更简单。

发现事实是这样的,具体来说,我们发现:深度最深的点一定是一个直径的端点

于是我们把整棵树按照深度排序,对于一个点可以选的点数 \(c_u\) 其实就是序列中在 \(u\) 前面且距离 \(dis(u,v) \le m\) 的那些 \(v\)。

然后我们的答案可以每次 \(\mathcal O(n)\) 用 \(\binom {c_u} k\) 计算完成。


于是问题就变得非常经典了。

这个东西明显直接用 点分治 做,随便维护一下可以做到 \(\mathcal O(n\log^2n)\)。

实际上题解中给出了一个 \(\mathcal O(n \log n)\) 的做法,但是有些难写且没啥前途。


这样就做完了,时间复杂度 \(\mathcal O(n \log ^2 n)\)。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back

const int N=1e5+5,H=998244353;
int n,m,q,fac[N],ifac[N],c[N],rt,tot,dep[N],sz[N],mx[N],id[N],pos[N];
vector<int> G[N],V;
bool vis[N];

int adc(int a,int b){return a+b>=H?a+b-H:a+b;}
int dec(int a,int b){return a<b?a-b+H:a-b;}
int mul(int a,int b){return 1ll*a*b%H;}
void add(int &a,int b){a=adc(a,b);}
void del(int &a,int b){a=dec(a,b);}

int qpow(int a,int b=H-2){
  int res=1;
  while(b){if(b&1) res=mul(res,a);a=mul(a,a),b>>=1;}
  return res;
}

void init(){
  fac[0]=1;
  for(int i=1;i<N;i++) fac[i]=mul(fac[i-1],i);
  ifac[N-1]=qpow(fac[N-1]);
  for(int i=N-1;i>=1;i--) ifac[i-1]=mul(ifac[i],i);
}

int binom(int n,int m){
  if(m<0||n<m) return 0;
  return mul(fac[n],mul(ifac[m],ifac[n-m]));
}

namespace BIT{
  int tr[N];
  int lowbit(int i){return i&(-i);}
  void upd(int x,int v){for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=v;}
  int qry(int x){
    int res=0;
    for(int i=x;i>=1;i-=lowbit(i)) res+=tr[i];
    return res;
  }
}

void dfs(int u,int fa){
  dep[u]=dep[fa]+1,V.pb(u),sz[u]=1;
  for(auto v:G[u]) if(v!=fa&&!vis[v]) dfs(v,u),sz[u]+=sz[v];
}

void findrt(int u,int fa){
  sz[u]=1,mx[u]=0,V.pb(u);
  for(auto v:G[u]) if(v!=fa&&!vis[v]) findrt(v,u),sz[u]+=sz[v],mx[u]=max(mx[u],sz[v]);
  mx[u]=max(mx[u],tot-sz[u]);
  if(!rt||mx[u]<mx[rt]) rt=u;    
}

void calc(int op){
  sort(V.begin(),V.end(),[&](int x,int y){return dep[x]==dep[y]?pos[x]<pos[y]:dep[x]<dep[y];});

  int sz=(int)V.size()-1;
  for(int i=0,j=sz;i<=sz;i++){
    BIT::upd(pos[V[i]],1);
    if(i!=sz&&dep[V[i+1]]==dep[V[i]]) continue;
    while(j>=0&&dep[V[j]]+dep[V[i]]>m) --j;
    while(j>=0&&(i==sz||dep[V[j]]+dep[V[i+1]]>m)) c[V[j]]+=op*BIT::qry(pos[V[j]]-1),--j;
  }
  for(int i=0;i<=sz;i++) BIT::upd(pos[V[i]],-1);
}

void solve(int u){
  vis[u]=1,V.clear(),dfs(u,0),calc(1);
  
  for(auto v:G[u]) if(!vis[v]){
    V.clear(),rt=0,tot=sz[v],findrt(v,u);
    calc(-1);
    solve(rt);
  }
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m>>q;init(),m+=2;
  for(int i=1,u,v;i<n;i++) cin>>u>>v,G[u].pb(v),G[v].pb(u);
  dfs(1,0);
  
  iota(id+1,id+n+1,1);
  sort(id+1,id+n+1,[&](int x,int y){return dep[x]==dep[y]?x<y:dep[x]<dep[y];});
  for(int i=1;i<=n;i++) pos[id[i]]=i;
    
  rt=0,tot=n,findrt(1,0),solve(rt);
  
  while(q--){
    int x,res=0;cin>>x;
    for(int i=1;i<=n;i++) add(res,binom(c[i],x-1));
    cout<<res<<'\n';
  }
  
  return 0;
}

12.11 GDF 联考 T3 情报大师(zdd)

zdd 先生是闻名遐迩的情报大师。

zdd 先生掌握着一张巨大的情报网。这个情报网共有 \(n\) 个人,编号为 \(1 ... n\)。如果 \(u\) 和 \(v\) 常常亦或是曾经互通有无(即交互情报),我们称 \(u, v\) 有情报链接。恰巧的是,整个情报网正好有 $ n − 1$ 个情报链接且每个人的情报都能经过情报员传递到任何人。第 \(i\) 个情报链接连接 \(u _ i, v _ i\) 。

现在,共有 \(n\) 条情报,第 \(i\) 个人掌握第 \(i\) 条情报。接下来的 \(m\) 个时刻,每个时刻有两种操作可能存在。

  1. zdd 先生会指定一个 \(k\),情报员 \(u _ k\) 和 \(v _ k\) 会交换情报。(记 \(x\) 掌握的情报编号的集合为 \(S _ x\),那么一次交换情报会将 \(S _ {u _ k}, S _ {v _ k}\) 都变为 \(S _ {u _ k} \cap S _ {v _ k}\))

  2. zdd 先生会询问第 \(k\) 条情报被几个人知道。(即有多少 \(x\) 使得 \(k \in S _ x\))

    而你情报大师的头号程序员,需要对每个 zdd 先生的询问做出回答。

\(1 \le n \le 2 \times 10^5,1 \le m \le 6 \times 10^5\)。


这种树上的问题,很容易想到 点分治

然后每次处理跨过分治中心的点的相互的贡献。

于是考虑当前分治中心在 \(o\),我们像要去判断 \(x\) 能不能染到 \(y\),以及需要在 \(T\) 时刻之前。

那么分成两部分:

  • \(x \to o\),我们希望 \(x\) 到 \(o\) 的时间最早。
  • \(o \to y\),我们希望从 \(o\) 出发的时间最晚,并且能在 \(T\) 时刻之前到达。

发现这两个东西都是可以 dp 的,记 \(f_x\) 表示 \(x\) 到 \(o\) 的最早时间,\(g_{x,t_i}\) 表示 \(o\) 到 \(x\) 的最晚出发时间,并且最后一条边用的 \(t_i\),那么 \(y\) 能到达相当于 \(f_x \le g_{y,t_i} , t_i \le T\)。

这个东西就是一个 二维数点 问题。

于是直接用树状数组维护就可以做到 \(\mathcal O((n+q) \log^2n)\)。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define pb push_back

const int N=2e5+5;
int n,m,pos[N],cnt=0,sz[N],rt=0,lst[N],ans[N*3];
bool vis[N];
struct Nod{
  int st,ed;
  bool operator <(const Nod &a) const{return ed==a.ed?st<a.st:ed<a.ed;}
};
struct nod{
  int l,r,x;
  bool operator <(const nod &a) const{return r<a.r;}
};
struct Edge{int v,id;};

vector<nod> Q1,Q2,mdf1,mdf2;
vector<Edge> G[N];
vector<int> E[N],Q[N];

namespace BIT{
  int tr[N*3];
  int lowbit(int i){return i&(-i);}
  void upd(int x,int v){for(int i=x;i;i-=lowbit(i)) tr[i]+=v;}
  int qry(int x){
  	int res=0;
  	for(int i=x;i<=m;i+=lowbit(i)) res+=tr[i];
  	return res;
  }
}

void dfs(int u,int fa){
  sz[u]=1;
  for(auto i:G[u]) if(i.v!=fa&&!vis[i.v]) dfs(i.v,u),sz[u]+=sz[i.v];
}

void findrt(int u,int fa,int tot){
  int mx=0;sz[u]=1;
  for(auto i:G[u]) if(i.v!=fa&&!vis[i.v]) findrt(i.v,u,tot),sz[u]+=sz[i.v],mx=max(mx,sz[i.v]);
  if(max(tot-sz[u],mx)<=tot/2) rt=u;
}

void dfs1(int u,int fa,vector<Nod> a){
  if(!a.size()) return ;
  for(auto t:Q[u]) Q1.pb({a[0].st,t,u});
  for(auto i:G[u]) if(i.v!=fa&&!vis[i.v]){
  	vector<Nod> b;
  	for(auto p:E[i.id]){
  	  auto it=upper_bound(a.begin(),a.end(),(Nod){0,p});
	  if(it!=a.end()) b.pb({(*it).st,p});	
	}
	dfs1(i.v,u,b);
  }
}

void dfs2(int u,int fa,vector<Nod> a){
  if(!a.size()) return;
  for(auto v:a) mdf1.pb({v.st,v.ed,u});
  for(auto i:G[u]) if(i.v!=fa&&!vis[i.v]){
  	vector<Nod> b;
    for(auto p:E[i.id]){
      auto it=upper_bound(a.begin(),a.end(),(Nod){0,p});
      if(it!=a.begin()) --it,b.pb({(*it).st,p});
	}
	dfs2(i.v,u,b);
  }
}

void calc(vector<nod> &Q,vector<nod> &mdf,int op){
  cnt=0;
  sort(Q.begin(),Q.end()),sort(mdf.begin(),mdf.end());

  for(int i=0,j=0;i<(int)Q.size();i++){
  	while(j<(int)mdf.size()&&mdf[j].r<=Q[i].r){
  	  int x=mdf[j].x;
  	  if(!lst[x]) pos[++cnt]=x;
  	  else BIT::upd(lst[x],-1);
  	  BIT::upd(lst[x]=mdf[j].l,1);
	  ++j;	
	}
	if(Q[i].l<=Q[i].r) ans[Q[i].r]+=op*BIT::qry(Q[i].l)+(op==1);
  }
  for(int i=1;i<=cnt;i++) BIT::upd(lst[pos[i]],-1),lst[pos[i]]=0;
}

void solve(int u){
  dfs(u,0),vis[u]=1;
  Q2.clear(),mdf2.clear();
  
  for(auto i:G[u]) if(!vis[i.v]){
	vector<Nod> a;
    for(auto p:E[i.id]) a.pb({p,p});
    Q1.clear(),mdf1.clear();
	dfs1(i.v,0,a),dfs2(i.v,0,a),calc(Q1,mdf1,-1);
    for(auto p:Q1) Q2.pb(p);
    for(auto p:mdf1) mdf2.pb(p);
  }
  for(auto p:Q[u]) Q2.pb({1,p,u});
  calc(Q2,mdf2,1);
  
  for(auto i:G[u]) if(!vis[i.v])
  	findrt(i.v,u,sz[i.v]),solve(rt);
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n>>m;
  for(int i=1,u,v;i<n;i++) cin>>u>>v,G[u].pb({v,i}),G[v].pb({u,i});
  for(int i=1,op,x;i<=m;i++) cin>>op>>x,(op==1)?E[x].pb(i):Q[x].pb(i);
  findrt(1,0,n),solve(rt);
  for(int i=1;i<=m;i++) if(ans[i]) cout<<ans[i]<<'\n';
  return 0;
}

10.4 BDF NOI 联考 T3 黄金矿工(mine)

小 \(\operatorname {pia}\) 是蛤蟆国最大的金矿老板,最近它在西部开辟了一片新矿场,由于开采难度较大,所以小 \(\operatorname {pia}\) 决定用爆破的方式开矿。

矿道形成一棵 \(n\) 个结点的树,结点 \(u _ i, v _ i\) 之间以长度为 \(w _ i\) 的矿道连接。

现在每个结点 \(i\) 上已经安放了一枚爆炸半径为 \(R _ i\),爆炸威力为 \(H _ i\) 的 \(\operatorname {TNT}\),一枚 \(\operatorname {TNT}\) 引爆后,所有与其距离不超过 \(R _ i\) 的 \(\operatorname {TNT}\) 也会被一起引爆。

为了评估开矿的危险程度,小 \(\operatorname {pia}\) 定义一枚 \(\operatorname {TNT}\) 的震惊度为初始引爆这枚 \(\operatorname {TNT}\), 最终所有被引爆的 \(\operatorname {TNT}\) 的爆炸威力之和。你作为工程师,需要帮小 \(\operatorname {pia}\) 求出所有 \(\operatorname {TNT}\) 的震惊度。

注意每枚 \(\operatorname {TNT}\) 是独立的。

\(1 \le n \le 10^5,1 \le H_i \le 10^9,0 \le R_i \le 10^{18},1 \le u_i,v_i \le n ,1 \le w_i \le 10^{12}\)。

P9260 [PA 2022] Miny 加强版,Pjudge #21728. 【CTS Round #1 Day 1】地雷。很好的一道题。


首先,一种比较直接的思路就是建图之后用 bitset 做,时间复杂度 \(\mathcal O(\frac {n^3} w)\),因为图中存在 \(\mathcal O(n^2)\) 条边。

考虑优化,我们希望让边数少一点,于是考虑 点分树,维护当前子树中的点到根的距离,那么 \(u\) 可以炸到的点 \(v\) 一定是一个前缀。

于是前缀和优化建图,边数就是 \(\mathcal O(n \log n)\) 的,同样 tarjan 之后缩点优化可以做到 \(\mathcal O(\frac {n^2 \log n} w + n\log n)\)。


然后你考虑对上面那个东西把 bitset 换成 线段树合并,然后就可以冲过去了。

不会证明复杂度,似乎是 \(\mathcal O(n \log^2 n)\) 的,而且对边的顺序要求比较严格,我们最后做线段树合并的时候需要按照点分树深度的从大到小的那些边去合并。

感觉这个东西可以感性理解一下,似乎可以节约比较多的东西,而且可以通过哈希值判断子树同构就不线段树合并了。

具体可以看代码。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back
#define ui unsigned int

mt19937 rd(chrono::steady_clock::now().time_since_epoch().count());

const int N=2e5+5,M=N*20;
int n,mx[N],sz[N],RT=0,lim,ct=0,tot=0,pre[N],pos[M],m=0,used[M],rt[M];
ll a[M],R[N];
bool vis[M];

struct vec{
  ll dis;
  int id;
  bool operator <(const vec &a) const{return dis==a.dis?id<a.id:dis<a.dis;}
}c[N];

struct Nod{int v;ll w;};
vector<Nod> G[N];
vector<int> E[M],V[M];

void dfs1(int u,int fa){
  sz[u]=1,mx[u]=0;
  for(auto i:G[u]) if(i.v!=fa&&!vis[i.v]){
	dfs1(i.v,u);
	sz[u]+=sz[i.v],mx[u]=max(mx[u],sz[i.v]);
  }
  mx[u]=max(mx[u],lim-sz[u]);
  if(mx[u]<mx[RT]) RT=u;
}

void dfs2(int u,int fa,ll dis){
  c[++ct]={dis,u},sz[u]=1;
  for(auto i:G[u]) if(i.v!=fa&&!vis[i.v]) dfs2(i.v,u,dis+i.w),sz[u]+=sz[i.v];
}

void sol(int u){
  ct=0,dfs2(u,0,0),sort(c+1,c+ct+1),pre[1]=c[1].id;

  ll mx=0;
  for(int i=1;i<=ct;i++) mx=max(mx,R[c[i].id]-c[i].dis);
  for(int i=2;i<=ct&&c[i].dis<=mx;i++) pre[i]=++tot,E[tot].pb(pre[i-1]),E[tot].pb(c[i].id);

  for(int i=1;i<=ct;i++) if(R[c[i].id]>=c[i].dis)
    E[c[i].id].pb(pre[lower_bound(c+1,c+ct+1,(vec){R[c[i].id]-c[i].dis+1,0})-c-1]);

  vis[u]=1;
  for(auto i:G[u]) if(!vis[i.v]) lim=sz[i.v],RT=0,dfs1(i.v,u),sol(RT);
}

int dfn[M],low[M],st[M],tp=0,idx=0,bl[M],scc=0;
ll s[M];

void tarjan(int u){
  dfn[u]=low[u]=++idx,st[++tp]=u;
  for(auto v:E[u]){
	if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
	else if(!bl[v]) low[u]=min(low[u],dfn[v]);
  }
  if(low[u]==dfn[u]){
	++scc;int x;
	while((x=st[tp--])){
	  bl[x]=scc,s[scc]+=a[x],V[scc].pb(x);
	  if(x==u) break;
	}
	if(s[scc]) pos[scc]=++m;
  }
}

namespace SGT{
  #define mid ((l+r)>>1)
  #define lc(p) tr[p].s[0]
  #define rc(p) tr[p].s[1]

  struct sgt{
	int s[2];
	ll v;
	ui key;
  }tr[M*7];
  int cnt=0,lstcnt=0;

  void pu(int p){tr[p].v=tr[lc(p)].v+tr[rc(p)].v,tr[p].key=tr[lc(p)].key^tr[rc(p)].key;}

  void upd(int &p,int l,int r,int x,ll v,ui key){
	p=++cnt,tr[p]={0,0,v,key};
	if(l==r) return;
	x<=mid?upd(lc(p),l,mid,x,v,key):upd(rc(p),mid+1,r,x,v,key);
  }

  int merge(int p1,int p2){
	if(!p1||!p2||p1==p2) return p1|p2;
	if(tr[p1].key==tr[p2].key) return p1;
    int p=p1<=lstcnt?++cnt:p1;
	lc(p)=merge(lc(p1),lc(p2)),rc(p)=merge(rc(p1),rc(p2));
	return pu(p),p;
  }
}
using namespace SGT;

int main(){
  freopen("mine14.in","r",stdin);
  freopen("mine.out","w",stdout);

  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1;i<=n;i++) cin>>R[i];
  for(int i=1;i<n;i++){
	int u,v;ll w;cin>>u>>v>>w;
	G[u].pb({v,w}),G[v].pb({u,w});
  }
//   for(int i=1;i<=n;i++) reverse(G[i].begin(),G[i].end());

  mx[0]=1e9,lim=tot=n,dfs1(1,0),sol(RT);
  for(int i=1;i<=tot;i++) reverse(E[i].begin(),E[i].end());//没有这个会 MLE
  for(int i=1;i<=tot;i++) if(!dfn[i]) tarjan(i);

  for(int i=1;i<=scc;i++){
	used[i]=i,lstcnt=cnt;
	if(pos[i]) upd(rt[i],1,m,pos[i],s[i],rd());

	for(auto u:V[i]) for(auto v:E[u])
      if(used[bl[v]]!=i) rt[i]=merge(rt[i],rt[bl[v]]),used[bl[v]]=i;
  }
    
  for(int i=1;i<=n;i++) cout<<tr[rt[bl[i]]].v<<' ';
  return 0;
}

这个东西啥都能过,但是过不了出题人自己造的数据,因为空间 512MB。


所以让我们来考虑一些正经做法。


同样是在点分树考虑,一种除了 bitset 直观的思路就是去对于每一个重心维护

  • \(f(rt,x)\) 表示从 \(x\) 开始炸,可以炸到任意点,最终能给 \(rt\) 的半径。
  • \(g(rt,x)\) 表示至少最初需要多少半径才能从 \(rt\) 炸到 \(x\),过程中同样可以炸到任意点。

那么我们的转移是从上往下的,枚举到一个节点时,它的答案已经被上面的父亲节点表示出来了。

现在假设你已经知道全局的根节点的 \(f(rt,x),g(rt,x)\) 的值了,考虑如何往下转移。


对于一个点分树中心 \(rt\),我们考虑枚举它是从那个父亲节点炸出去的,假设为 \(rt'\)。(下面 \(h_x\) 是 \(x\) 点的半径)

  • 对于 \(f(rt,x)\),\(x\) 能炸到 \(y\) 当且仅当 \(f(rt',x) \ge g(rt',y)\),有贡献 \(f(rt,x) \leftarrow h_y-dis(rt,y)\)。
  • 对于 \(g(rt,x)\),\(y\) 能炸到 \(x\) 当且仅当 \(g(rt',x) \le f(rt',y)\),有贡献 \(g(rt,x) \leftarrow dis(rt,y)\)。

具体实现的时候,我们对于每一个分治中心 \(rt'\),处理他对所有子节点的贡献。

将 \(f(rt',x),g(rt',x)\) 都排序,那么上述过程可以直接 双指针 完成,时间复杂度 \(\mathcal O(n \log^2 n)\)。

void sol1(int u,int dep){
  calc(u,dep,1);
  for(auto v:dE[u]) calc(v,dep,-1);

  sort(F[u].begin(),F[u].end()),sort(G[u].begin(),G[u].end());
  int ct=(int)F[u].size();

  for(int i=0;i<ct;i++) val[F[u][i].id]=-inf;
  for(int i=0,j=0;i<ct;i++){
	while(j<ct&&G[u][j].v<=F[u][i].v){
	  int id=G[u][j].id;
	  for(int k=dep;k<(int)ds[id].size();k++)
	    ckmx(val[ds[id][k].u],h[id]-ds[id][k].d);
	  ++j;
	}
	int id=F[u][i].id;
	for(int k=dep+1;k<(int)ds[id].size();k++) ckmx(F[ds[id][k].u][ds[id][k].id].v,max(val[ds[id][k].u],val[u]-ds[ds[id][k].u][dep].d));
  }

  for(int i=0;i<ct;i++) val[G[u][i].id]=inf;
  for(int i=ct-1,j=ct-1;i>=0;i--){
	while(j>=0&&F[u][j].v>=G[u][i].v){
	  int id=F[u][j].id;
	  for(int k=dep;k<(int)ds[id].size();k++)
	    ckmn(val[ds[id][k].u],ds[id][k].d);
	  --j;
	}
	int id=G[u][i].id;
    for(int k=dep+1;k<(int)ds[id].size();k++) ckmn(G[ds[id][k].u][ds[id][k].id].v,min(val[ds[id][k].u],val[u]+ds[ds[id][k].u][dep].d));
  }

  for(auto v:dE[u]) sol1(v,dep+1);
}

有些东西看不懂先别急


那么问题就变成了如何求全局的 \(f(rt,x),g(rt,x)\) 了。

发现这个东西我们就可以换一下 \(f,g\) 的定义来完成,变成

  • \(f(rt,x)\) 表示在 \(rt\) 的子树中炸,从 \(x\) 开始,第一次 炸到 \(rt\) 给的半径。
  • \(g(rt,x)\) 表示在 \(rt\) 子树中炸,初始需要多少半径才能炸到 \(x\)。

这个 dp 的转移时从下到上的,转移同样也是类似的。


对于一个分治中心 \(rt\),我们考虑它所有子树中的分治中心 \(rt'\)。

  • \(f(rt,x)\),\(x\) 能炸到 \(y\) 当且仅当 \(f(rt',x) \ge g(rt',y)\),有贡献 \(f(rt,x) \leftarrow h_y - dis(rt,y)\)。
  • \(g(rt,x)\),\(y\) 能炸到 \(x\) 当且仅当 \(g(rt',x)\le f(rt',y)\),有贡献 \(g(rt,x) \leftarrow dis(rt,y)\)。

然后在每个分治重心统计答案的时候再更新一下 \(g\)。

同样这些东西都可以用双指针排序之后完成,时间复杂度同样 \(\mathcal O(n \log^2 n)\)。

void dfs(int u,int fa,int rt,int &ct,ll d){
  sz[u]=1,f[rt].pb({u,-inf}),g[rt].pb({u,inf}),ds[u].pb({rt,ct++,d});
  for(auto i:E[u]) if(!vis[i.v]&&i.v!=fa) dfs(i.v,u,rt,ct,d+i.w),sz[u]+=sz[i.v];
}

struct nod{
  ll f,g;
  int id;
  bool operator <(const nod &a) const{return g<a.g;}
}p[N];

void sol0(int u,int dep){
  vis[u]=1;

  int ct=0;
  dfs(u,0,u,ct,0);
  
  for(auto i:E[u]) if(!vis[i.v]){
    RT=0,tot=sz[i.v],findrt(i.v,0);
	dE[u].pb(RT);
	sol0(RT,dep+1);
  }
  f[u][0].v=h[u],g[u][0].v=0;

  for(int i=0;i<ct;i++) p[i]={f[u][i].v,g[u][i].v,g[u][i].id};
  sort(p,p+ct);

  for(int i=0,j=0;i<ct;i=j){
	ll cur=p[i].g;
	for(j=i;j<ct&&p[j].g<=cur;j++) ckmx(cur,p[j].f),g[u][ds[p[j].id][dep].id].v=p[i].g;
  }
  F[u]=f[u],G[u]=g[u];
  sort(f[u].begin(),f[u].end()),sort(g[u].begin(),g[u].end());

  for(int i=0;i<dep;i++) val[i]=-inf;
  for(int i=0,j=0;i<ct;i++){
	while(j<ct&&g[u][j].v<=f[u][i].v){
	  int id=g[u][j].id;
	  for(int k=0;k<dep;k++) ckmx(val[k],h[id]-ds[id][k].d);
	  ++j;
	}
	int id=f[u][i].id;
	for(int k=0;k<dep;k++) ckmx(f[ds[id][k].u][ds[id][k].id].v,val[k]);
  }

  for(int i=0;i<dep;i++) val[i]=inf;
  for(int i=ct-1,j=ct-1;i>=0;i--){
	while(j>=0&&f[u][j].v>=g[u][i].v){
	  int id=f[u][j].id;
	  for(int k=0;k<dep;k++) ckmn(val[k],ds[id][k].d);
	  --j;
	}
	int id=g[u][i].id;
	for(int k=0;k<dep;k++) ckmn(g[ds[id][k].u][ds[id][k].id].v,val[k]);
  }
}

这样就做完了,时间复杂度 \(\mathcal O(n \log^2 n)\),比较牛,具体实现可以看代码,不要把 \(\max,\min\) 写错了。

统计答案是容易的。代码

Code
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back

void ckmn(ll &a,ll b){a=min(a,b);}
void ckmx(ll &a,ll b){a=max(a,b);}

const int N=2e5+5;
const ll inf=1e18;
int n,RT,tot,sz[N],mn[N],Rt;
ll a[N],h[N],val[N],ans[N];
bool vis[N];

struct Edge{int v;ll w;};
vector<Edge> E[N];
vector<int> dE[N]; 

struct Nod{
  int id;ll v;
  bool operator <(const Nod &a) const{return v<a.v;}
};
vector<Nod> f[N],g[N],F[N],G[N],ff,gg;

struct Nod2{int u,id;ll d;};
vector<Nod2> ds[N];

void findrt(int u,int fa){
  sz[u]=1,mn[u]=0;
  for(auto i:E[u]) if(!vis[i.v]&&i.v!=fa) findrt(i.v,u),sz[u]+=sz[i.v],mn[u]=max(mn[u],sz[i.v]);
  mn[u]=max(mn[u],tot-sz[u]);
  if(mn[u]<=mn[RT]) RT=u;
}

void dfs(int u,int fa,int rt,int &ct,ll d){
  sz[u]=1,f[rt].pb({u,-inf}),g[rt].pb({u,inf}),ds[u].pb({rt,ct++,d});
  for(auto i:E[u]) if(!vis[i.v]&&i.v!=fa) dfs(i.v,u,rt,ct,d+i.w),sz[u]+=sz[i.v];
}

struct nod{
  ll f,g;
  int id;
  bool operator <(const nod &a) const{return g<a.g;}
}p[N];

void sol0(int u,int dep){
  vis[u]=1;

  int ct=0;
  dfs(u,0,u,ct,0);
  
  for(auto i:E[u]) if(!vis[i.v]){
    RT=0,tot=sz[i.v],findrt(i.v,0);
	dE[u].pb(RT);
	sol0(RT,dep+1);
  }
  f[u][0].v=h[u],g[u][0].v=0;

  for(int i=0;i<ct;i++) p[i]={f[u][i].v,g[u][i].v,g[u][i].id};
  sort(p,p+ct);

  for(int i=0,j=0;i<ct;i=j){
	ll cur=p[i].g;
	for(j=i;j<ct&&p[j].g<=cur;j++) ckmx(cur,p[j].f),g[u][ds[p[j].id][dep].id].v=p[i].g;
  }
  F[u]=f[u],G[u]=g[u];
  sort(f[u].begin(),f[u].end()),sort(g[u].begin(),g[u].end());

  for(int i=0;i<dep;i++) val[i]=-inf;
  for(int i=0,j=0;i<ct;i++){
	while(j<ct&&g[u][j].v<=f[u][i].v){
	  int id=g[u][j].id;
	  for(int k=0;k<dep;k++) ckmx(val[k],h[id]-ds[id][k].d);
	  ++j;
	}
	int id=f[u][i].id;
	for(int k=0;k<dep;k++) ckmx(f[ds[id][k].u][ds[id][k].id].v,val[k]);
  }

  for(int i=0;i<dep;i++) val[i]=inf;
  for(int i=ct-1,j=ct-1;i>=0;i--){
	while(j>=0&&f[u][j].v>=g[u][i].v){
	  int id=f[u][j].id;
	  for(int k=0;k<dep;k++) ckmn(val[k],ds[id][k].d);
	  --j;
	}
	int id=g[u][i].id;
	for(int k=0;k<dep;k++) ckmn(g[ds[id][k].u][ds[id][k].id].v,val[k]);
  }
}

void dfs2(int u,int d){
  ff.pb(F[ds[u][d].u][ds[u][d].id]),gg.pb(G[ds[u][d].u][ds[u][d].id]);
  for(auto v:dE[u]) dfs2(v,d);
}

void calc(int u,int d,int op){
  ff.clear(),gg.clear(),dfs2(u,d);
  sort(ff.begin(),ff.end()),sort(gg.begin(),gg.end());
  int len=(int)ff.size();ll res=0;
  for(int i=0,j=0;i<len;i++){
	while(j<len&&gg[j].v<=ff[i].v) res+=a[gg[j].id],++j;
    ans[ff[i].id]+=res*op;
  }
}

void sol1(int u,int dep){
  calc(u,dep,1);
  for(auto v:dE[u]) calc(v,dep,-1);

  sort(F[u].begin(),F[u].end()),sort(G[u].begin(),G[u].end());
  int ct=(int)F[u].size();

  for(int i=0;i<ct;i++) val[F[u][i].id]=-inf;
  for(int i=0,j=0;i<ct;i++){
	while(j<ct&&G[u][j].v<=F[u][i].v){
	  int id=G[u][j].id;
	  for(int k=dep;k<(int)ds[id].size();k++)
	    ckmx(val[ds[id][k].u],h[id]-ds[id][k].d);
	  ++j;
	}
	int id=F[u][i].id;
	for(int k=dep+1;k<(int)ds[id].size();k++) ckmx(F[ds[id][k].u][ds[id][k].id].v,max(val[ds[id][k].u],val[u]-ds[ds[id][k].u][dep].d));
  }

  for(int i=0;i<ct;i++) val[G[u][i].id]=inf;
  for(int i=ct-1,j=ct-1;i>=0;i--){
	while(j>=0&&F[u][j].v>=G[u][i].v){
	  int id=F[u][j].id;
	  for(int k=dep;k<(int)ds[id].size();k++)
	    ckmn(val[ds[id][k].u],ds[id][k].d);
	  --j;
	}
	int id=G[u][i].id;
    for(int k=dep+1;k<(int)ds[id].size();k++) ckmn(G[ds[id][k].u][ds[id][k].id].v,min(val[ds[id][k].u],val[u]+ds[ds[id][k].u][dep].d));
  }

  for(auto v:dE[u]) sol1(v,dep+1);
}

int main(){
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;mn[0]=1e9;
  for(int i=1;i<=n;i++) cin>>a[i];
  for(int i=1;i<=n;i++) cin>>h[i];
  for(int i=1;i<n;i++){
	int u,v;ll w;cin>>u>>v>>w;
	E[u].pb({v,w}),E[v].pb({u,w});
  }

  tot=n,RT=0,findrt(1,0),Rt=RT;
  sol0(Rt,0),sol1(Rt,0);

  for(int i=1;i<=n;i++) cout<<ans[i]<<(i==n?'\n':' ');
  return 0;
}

Part 5 分块整理

应 gyy 建议,我们计划在这里放一些分块题目。

并且整理一些分块的常有小技巧,有些东西可能比较常见,但有些并不是这样。

欢迎读者向我提出建议,共同来丰富这里的 tips。


  • \(\mathcal O(1)\) 修改,\(\mathcal O(\sqrt V)\) 查询区间最小众数:在 Two Subtrees 这道题中,我们用到的分块技巧。

    考虑 值域分块,每个数维护其出现次数,并且块内维护每种出现次数的出现个数,于是我们可以得到每个块的众数出现次数。

    于是修改显然是 \(\mathcal O(1)\) 的,而查询的时候我们可以首先遍历所有块,得到众数出现次数。

    接着从左往右找到第一个存在众数的块,直接遍历其中的元素,找到第一个众数即可,时间复杂度 \(\mathcal O(\sqrt V)\)。


标签:rt,le,return,int,void,mid,Data,Structure
From: https://www.cnblogs.com/H-W-Y/p/18622700/ds3

相关文章

  • 深入解析RuoYi框架中的DataScopeAspect:不同权限类型的SQL语句生成与作用
    目录AOP简介面向切面编程(AOP)的概念AOP在RuoYi框架中的应用DataScopeAspect类的作用类的功能切点选择权限检查​编辑在前端如何给不同的用户设置不同的权限实际代码示例控制层服务层Mapper层DataScopeAspect类1.全部权限2.自定义权限3. 本部门及以下权限4.仅......
  • 专业数据恢复软件 iFind Data Recovery v9.2.3 绿色便携版
    睿共享*关注我前言iFinDDataRecovery一款特别实用的数据找回工具,它很厉害,能帮你在SSD硬盘和Windows10系统上找回丢失的数据。而且,它还能深度扫描并恢复各种主流数码相机里的RAW格式照片,速度超快,用起来也很稳定顺畅,就算是新手也能轻松上手使用。安装环境[名称]:iFinDDataRec......
  • Data Wrangling
    DataWrangling以整理系统日志为例,journalctl获取系统中的所有日志获取ssh中试图登录服务器用户过滤出ssh的信息journalctl|grepsshd其中的内容,除了登录用户还有其他内容,所以需要进一步过滤journalctl|grepsshd|grep"Disconnectedfrom"查找到许多试图登录服......
  • CSharp: Connecting to Oracle 11g Database in C#
     usingSystem;usingSystem.Collections.Generic;usingSystem.ComponentModel;usingSystem.Data;usingSystem.Drawing;usingSystem.Linq;usingSystem.Text;usingSystem.Threading.Tasks;usingSystem.Windows.Forms;//usingOracle.DataAccess.Client;usin......
  • python: Connecting to Oracle 11g Database in Python
     #encoding:utf-8#版权所有2024涂聚文有限公司#许可信息查看:言語成了邀功盡責的功臣,還需要行爲每日來值班嗎#描述:python-mpipinstalloracledb#python-mpipinstallcx_Oracle--upgrade#pipinstallcx_Oracle#Author:geovindu,GeovinDu涂聚文.#......
  • DTS207TC Database Development and Design
    Modulecodeand TitleDatabase Developmentand Design ( DTS207TC)SchoolTitleSchoolofAIandAdvanced ComputingAssignmentTitle002:AssessmentTask 2 (CW)Submission Deadline23:59, 24th Dec (Friday)Database Deve......
  • 数据集-目标检测系列- 篮球 检测数据集 basketball >> DataBall
    数据集-目标检测系列-篮球检测数据集basketball>>DataBallDataBall助力快速掌握数据集的信息和使用方式,会员享有百种数据集,持续增加中。 需要更多数据资源和技术解决方案,知识星球:“DataBall-X数据球(free)”贵在坚持!数据样例项目地址:*相关项目1)数据集......
  • CSCI-GA.2662 Data Communications & Networks
    ObjectivesSoftware-definednetworking(SDN)isarecentparadigmforrunningnetworks.Asperthenetworkinglayertopicscoveredinthecourse,thenetworkisdividedintohecontrolanddataplanes.Thecontrolplaneprovidesasetofprotocolsandconfi......
  • 用pandas读取MRPC数据库时报错:pandas.errors.ParserError: Error tokenizing data. C
    读取的代码很简单,如下:data_path='MRPC/msr_paraphrase_test.txt'df=pd.read_csv(data_path,sep='\t',encoding='utf-8')困扰了一下午,最后本来不打算解决了。想着直接跳过错误,即:df=pd.read_csv(data_path,sep='\t',encoding='utf-8',on_......
  • PbootCMS的config、data和runtime目录分别有什么作用
    PbootCMS的config、data和runtime目录各自有不同的作用:config目录:这个目录主要用于存放授权码和数据库配置文件。PbootCMS在启动时会读取这些配置文件,以连接数据库和其他系统资源。确保这个目录具有适当的写入权限,以便系统可以在需要时更新配置文件。data目录:这个目录主要用于......