Data Structure 3

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

主要是一些分治 + 树的杂题,其它的可以看 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 n \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;
}

CF960H Santa's Gift

  • 给出一棵 \(n\) 个节点的有根树,根为 \(1\)。点 \(2\sim n\) 的父亲为 \(p_2\sim p_n\)。有 \(m\) 种颜色。点 \(i\) 的颜色为 \(f_i\),颜色 \(i\) 的权值为 \(c_i\)。有两种操作共 \(q\) 次:

    • \(\texttt{1 }x\text{ }y\),将 \(f_x\leftarrow y\)
  • \(\texttt{2 }x\),从树中等概率选取一个点 \(i\),得到 \((S_i\times b_x-C)^2\) 的价值。求期望价值。其中 \(S_i\) 表示以 \(i\) 为根的子树中颜色为 \(x\) 的节点数。\(C\) 是给定的常数。

\(1 \le n,m,q \le 5 \times 10^4\)


直接根据期望的线性型拆开,然后去维护每个颜色的 \(S_i\) 之和和 \(S_i^2\) 的和即可。

时间复杂度 \(\mathcal O(n \log^2 n)\)?动态开点即可。代码


CF217E Alien DNA

给出一串字符串,有 \(n\) 个操作,将区间 \([l,r]\) 的字符中编号为偶数的写下,再把奇数的写在后面,然后把它们插入 \(r\)\(r+1\) 之间,问最后字符串的前 \(k\) 位。

\(1 \le k \le 3 \times 10^6,0 \le n \le 5000\)

做法比较多,这里提供一个 \(\mathcal O((n+k)\log k)\) 的做法。


考虑从后往前扫时间,用并查集维护应该是什么。

那么设当前存在的位置为 \(1\),那么我们需要找到第 \(l\)\(1\) 的位置和第 \(r\)\(1\) 的位置,然后把后面的 \(r-l+1\)\(1\) 都标上一个 \(fa\) 之后删除。

这个操作可以用并查集 + 树状数组 / 线段树上二分得到。

所以时间复杂度是 \(\mathcal O((n+k)\log k)\),有点卡常。代码

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

const int N=3e6+5,M=5e3+5;
int n,m,fa[N],nxt[N],num;
string S;
char ch[N];
struct Nod{int l,r;}a[M];

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

namespace BIT{
  int tr[N];
  il int lowbit(int i){return i&(-i);}
  il void upd(int x,int v){for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=v;}
  il int get(int x){
    int res=0,cur=0;
    for(int i=21;~i;i--) if(cur+(1<<i)<=n&&res+tr[cur+(1<<i)]<x) cur+=(1<<i),res+=tr[cur];
    return cur+1; 
  }
}

int main(){
  /*2024.12.24 H_W_Y CF217E Alien DNA 并查集*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>S>>n>>m;
  for(int i=1;i<=m;i++) cin>>a[i].l>>a[i].r;
  num=n;
  for(int i=1;i<=n;i++) nxt[i]=i+1,fa[i]=i,BIT::upd(i,1);
  nxt[n+1]=n+1;
  
  for(int i=m,l,r,x,y,t,lst;i>=1;i--){
    l=a[i].l,r=a[i].r;
    if(r>=num) continue;
    
    x=BIT::get(l),lst=BIT::get(r),t=nxt[lst],y=nxt[x];
    
    for(int c=1;t<n+1&&c<=r-l+1;c++,t=nxt[t]){
      if(c==(r-l+1)/2+1) y=x;
      fa[t]=y,y=nxt[nxt[y]];
      BIT::upd(t,-1),--num;
    }
    nxt[lst]=t;
  }
 
  for(int i=1,j=0;i<=n;i++){
    if(!ch[find(i)]) ch[find(i)]=S[j++];
    cout<<ch[find(i)];
  }
  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 + 构造。

CF1824E:枚举 \(\min\) + dsu on tree。


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)\)。(这里面对于找 mex 可以用 bitset 中的一个操作做掉)

实际上如果你只需要做两个 log 根本不需要用到 mex 是 \(\mathcal O(\log)\) 级别的结论,因为你可以对于每一个节点都用数据结构维护一下轻儿子的值,这样直接在数据结构上面找 mex 就可以了。


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,P11425 [清华集训 2024] 路南柯


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

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

首先是容易确定哪些点是 \(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;
}

CF226E Noble Knight's Path

给定一棵树,每个节点有两种颜色黑和白,初始颜色为全白,有 \(m\) 个时间节点,同时有 \(m\) 次操作,第 \(i\) 次在第 \(i\) 个时间节点,有两种操作,分别为将树上某个节点染成黑色,和查询在第 \(y+1\) 个时间节点到当前时间节点中树上路径 \(a\)\(b\) 中第 \(k\) 个没有被染成黑色的节点。

\(1 \le n,m \le 10^5\)


由于涉及到时间轴,所以考虑用主席树维护那些点改变过。

那么每次查询就是树剖之后不断在主席树上面二分,时间复杂度 \(\mathcal O(n \log^2 n)\)代码


CF1824E LuoTianyi and Cartridge

给定一棵 \(n\) 个结点的树 \(T\),点有点权 \(a_i, b_i\),边有边权 \(c_i, d_i\)

按如下方式构建另一棵树 \(T'\)

  • 选择 \(T\)\(p\) 个结点作为 \(T'\) 的点集 \(V'\)\(p\) 可以是 \(1\sim n\) 的任意正整数。
  • 接下来 依次 选择 \(T\)\(p - 1\)互不相同 的边。
  • 对于第 \(i\) 次选边,假设选择第 \(j\) 条边 \((x_j, y_j)\),那么你可以选择 \(V'\)不连通 的两个点 \(u, v\),要求 \((x_j, y_j)\) 包含于 \(u, v\)\(T\) 上的简单路径,在 \(u, v\) 之间以 \(c_j, d_j\) 为权值连边。

\(A, C\) 分别为 \(T'\)\(a_i, c_i\) 的最小值,\(B, D\) 分别为 \(T'\)\(b_i, d_i\) 之和,则 \(T'\) 的权值为 \(\min(A, C)\cdot (B + D)\)

\(T'\) 的最大权值。

\(3\leq n\leq 2\times 10 ^ 5\)\(1\leq a_i, b_i, c_i, d_i\leq 2\times 10 ^ 5\),保证 \((x_i, y_i)\) 形成一棵树。


注意到这里的 \(\min(A,C)\) 比较难以处理,所以我们考虑枚举。

设当前枚举到 \(x\),于是当前只需要保留 \(A_i \ge x\) 的点,和 \(C_i \ge x\) 的边。

我们把一条边称之为有用当且仅当它两侧都有合法的点,不然这条边一定不会被选到集合中。

于是我们用这些有用的边和有用的点建图,具体来说,对于一条无用的边,我们可以把两端合并起来。

那么图上的每一个点其实代表若干个点,有可能是 \(0\)

为了让所有边都能选,我们需要让每个叶子节点都有至少一个点被选了。

贡献就是:

  • 边的贡献:前 \(\min(|V|-1,|E|)\) 大。
  • 点的贡献:每个叶子节点的最大值之和 \(sum\),和其它的 \(\min (|E|+1,|V|)-leaf\) 大,其中 \(leaf\) 是叶子的数量。

这样我们就完成了固定 \(x\) 的计数。


考虑 \(x\) 从小往大扫的过程中,我们相当于是删去一些边和一些点。

同样分两种情况讨论

  • 删去边:相当于是合并两个节点,于是直接启发式合并就可以了。

  • 删去点:直接删除即可。

    注意如果当前点是叶子,并且空了,我们需要不断往上跳,把那些不合法的边都删掉。

于是具体实现时用两棵权值线段树分别维护边的前 \(k\) 大和点的除去叶子最大值的前 \(k\) 大就可以了。

同时每一个节点需要维护上面点的 \(B_i\) 与连出去的边?

时间复杂度 \(\mathcal O(n \log^2n)\) 或者 \(\mathcal O(n \log n)\)?这里的实现是 \(\mathcal O(n \log^2n)\) 的。代码

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

const int N=2e5+5,V=2e5;
int n,a[N],b[N],rt[N],num,leaf,nume;
ll sum=0,ans=0;
struct Edge{int u,v,c,d;}e[N];
multiset<int> S[N];
set<pii> E[N];
vector<int> del[N];
bool vis[N];

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

struct 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
  
  ll tr[N<<2],ct[N<<2];
  
  void pu(int p){tr[p]=tr[lc]+tr[rc],ct[p]=ct[lc]+ct[rc];}
  
  void upd(int x,int v,int l=1,int r=V,int p=1){
    if(l==r){
      assert(ct[p]+v>=0);
      return ct[p]+=v,tr[p]=1ll*ct[p]*l,void();
    }
    x<=mid?upd(x,v,lson):upd(x,v,rson);pu(p);
  }
  
  ll qry(ll k,int l=1,int r=V,int p=1){
    if(!k) return 0;
    if(l==r) return 1ll*min(k,ct[p])*l;
    if(ct[rc]>=k) return qry(k,rson);
    return tr[rc]+qry(k-ct[rc],lson);
  }
}T[2];

void init(){
  num=n,nume=n-1;
  for(int i=1;i<=n;i++) rt[i]=i,S[i].insert(b[i]);
  for(int i=1;i<n;i++) T[0].upd(e[i].d,1);
  for(int i=1;i<n;i++) E[e[i].u].insert({e[i].v,i}),E[e[i].v].insert({e[i].u,i});  
  for(int i=1;i<=n;i++){
    if((int)E[i].size()<=1) sum+=b[i],++leaf;
    else T[1].upd(b[i],1);
  }
}

int to(int id,int v){return e[id].u==v?e[id].v:e[id].u;}

void deli(int x){
  --num,rt[x]=find(x);
  if(b[x]==*S[rt[x]].rbegin()&&E[rt[x]].size()<=1){
    sum-=*S[rt[x]].rbegin(); 
    S[rt[x]].erase(S[rt[x]].find(b[x]));
    
    if(S[rt[x]].size()){
      sum+=*S[rt[x]].rbegin();
      T[1].upd(*S[rt[x]].rbegin(),-1);
    }else{
      while(!S[find(x)].size()&&E[find(x)].size()==1){
        auto v=*E[rt[x]].begin();
        T[0].upd(e[v.se].d,-1),vis[v.se]=1;
        E[find(v.fi)].erase({to(v.se,v.fi),v.se});
        x=v.fi,--nume;
      }
      if(E[find(x)].size()!=1) --leaf;
      else if(E[rt[x]].size()==1){
        sum+=*S[rt[x]].rbegin();
        T[1].upd(*S[rt[x]].rbegin(),-1);
      }
    }
  }else T[1].upd(b[x],-1),S[rt[x]].erase(S[rt[x]].find(b[x]));
}

void dele(int x){
  if(vis[x]) return;
  int u=find(e[x].u),v=find(e[x].v);
  if(E[u].size()==1) T[1].upd(*S[u].rbegin(),1),--leaf,sum-=*S[u].rbegin();
  if(E[v].size()==1) T[1].upd(*S[v].rbegin(),1),--leaf,sum-=*S[v].rbegin();
  E[u].erase({e[x].v,x}),E[v].erase({e[x].u,x});
  
  if(S[u].size()<S[v].size()) swap(S[u],S[v]);
  if(E[u].size()<E[v].size()) swap(E[u],E[v]);
  for(auto i:S[v]) S[u].insert(i);
  for(auto i:E[v]) E[u].insert(i);
  
  if(E[u].size()<=1) T[1].upd(*S[u].rbegin(),-1),++leaf,sum+=*S[u].rbegin();
  rt[v]=u;
  T[0].upd(e[x].d,-1),--nume;
}

ll calc(){
  if(!nume) return sum;
  return sum+T[0].qry(min(num-1,nume))+T[1].qry(min(num,nume+1)-leaf);
}

int main(){
  /*2024.12.23 H_W_Y CF1842E LuoTianyi and Cartridge 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],del[a[i]].pb(i);
  for(int i=1;i<=n;i++) cin>>b[i];
  
  for(int i=1;i<n;i++){
    cin>>e[i].u>>e[i].v>>e[i].c>>e[i].d;
    del[e[i].c].pb(i+n);
  }
  init();
  
  for(int i=1;i<=V;i++) if(del[i].size()){
    ans=max(ans,1ll*i*calc());
    for(auto j:del[i]) j>n?dele(j-n):deli(j);
  }
  cout<<ans<<'\n';
  return 0;
}

P5439 【XR-2】永恒

有一棵 \(n\) 个点的永恒的树,树中每个点 \(x(1 \le x \le n)\) 上都有一个永恒的字符串 \(S(x)\)

但这世界没有永恒,所有的一切都会随着时光消失。我们只能给每个所谓永恒的东西定义一个永恒值 \(f\)。这个值本身没有意义,只是一个象征罢了。

  • 一个字符串 \(S\) 的永恒值 \(f(S)\) 定义为它的长度 \(\mathrm{Len}(S)\),即:

\[f(S) = \mathrm{Len}(S) \]

  • 树上的一条无向路径 \(K = [u, v](u < v)\) 指的是 \(u,v\) 之间的简单路径(包括 \(u, v\)),其永恒值 \(f(K)\) 定义为路径上所有不同的无序点对 \((x, y)(x \in K, y \in K, x < y)\) 上的字符串 \(S(x), S(y)\) 的最长公共前缀 \(\mathrm{LCP}(S(x), S(y))\) 的永恒值 \(f(\mathrm{LCP}(S(x), S(y)))\) 之和,即:

\[f(K) = \sum_{x \in K, y \in K, x < y} f(\mathrm{LCP}(S(x), S(y))) \]

  • 一棵树 \(T\) 的永恒值 \(f(T)\) 定义为树上所有的无向路径 \([u, v](u \in T, v \in T, u < v)\) 的永恒值之和,即:

\[f(T) = \sum_{u \in T, v \in T, u < v} f([u,v]) \]

特别的是,树中每个点上的字符串都来自一棵永恒的以点 \(1\) 为根的 Trie 树,即每个树中的点都对应着一个 Trie 树中的点,点上的字符串就是 Trie 树中从根节点到其对应的点形成的字符串。

你需要求出这棵树的永恒值,答案对 \(998244353\) 取模。

\(2 \le n,m \le 3\times 10^5\)

经典套路题,没有什么思维难度。


考虑交换求和顺序,那么如果 \((x,y)\) 在第一棵树上面不是祖孙关系,对答案的贡献是

\[dep[LCA(d_x,d_y)] \times sz_x \times sz_y \]

其中前面的 \(dep\)\(LCA\) 都是第二棵树上面的,而后面的是第一棵树上的。

这个东西可以在第一棵树的一次 dfs 中,在第二棵树的树剖后的线段树上面做区间加。

而对于祖孙关系,我们可以同样完成,只是对于祖先不是乘的 \(sz_x\),而是 \(n-sz_y\) 其中 \(y\)\(x\) 下面的点。

同样在第二棵树上树剖后用线段树维护即可。

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


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):线段树分治。

CF1458F:树上圆理论 + 分治 + 点分树 + 区间子区间?。

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^2 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;
}

树上圆理论

我们考虑把树上的一些点看成坐标系上面的点,那么直径就是覆盖它们最小圆的直径。

于是圆心就是树上所有直径的终点,半径就是直径的一半,显然圆心是唯一的。

我们用 \((v,r)\) 表示一个圆,其中 \(v\) 表示圆心,可以是一条边的中点,\(r\) 表示半径。

那么两个圆 \((v_1,r_1)\)\((v_2,r_2)\) 合并的时候,就有如下三种情况:

  • \(dis(v_1,v_2) + r_1 \le r_2 \to (v_2,r_2)\)\(C_2\) 包含 \(C_1\)
  • \(dis(v_1,v_2)+r_2 \le r_1 \to (v_1,r_1)\)\(C_1\) 包含 \(C_2\)
  • \(\to \left (go \left(v_1,v_2,\frac 1 2 \left(dis(v_1,v_2)-r_1+r_2 \right) \right),\frac 1 2 \left( dis(v_1,v_2) + r_1 + r_2 \right) \right)\)

画图后容易理解。


CF1458F Range Diameter Sum

给定一棵包含 \(n\) 个节点(编号 \(1\)\(n\)), \(n-1\) 条长度为 \(1\) 的无向边的树.

\(d(u,v)\) 为编号 \(u\) 到编号 \(v\) 两点之间唯一路径的长度.

\(f(l,r)\)\(\max\{d(u,v)\}.(l\leq u,v\leq r)\)

求:

\[\sum_{l=1}^{n}\sum_{r=l}^{n}f(l,r) \]

\(1 \le n \le 10^5\)


用普通的思路,也就是对于每一个右端点,我们尝试记录答案,发现并不是很好维护。

所以我们考虑分治,对于每个分治区间计算 \([l,r]\) 中跨过 \(mid\) 的答案。

发现其实比较好维护,在当前分治区间中,我们可以预处理出,左边的点到 \(mid\) 的圆,右边的点到 \(mid+1\) 的圆。

合并两个圆就像上面树上圆理论中直接合并即可。


现在考虑对于每一个 \(L_i\),我们要计算另外一个圆 \(R_j\) 和它的贡献。

注意到右边的 \(L_i,R_j\) 的关系一定是,先是第二种情况,再是第三种情况,最后是第一种情况。

因为顺序一定是先包含,再相交,最后被包含。

而这两个分界点显然随着 \(i\) 减小而递增,所以可以直接双指针维护。

对于第一种和第二种,我们是容易用 \(sum\) 直接维护的,而对于第三种,每次需要求一个 \(u\)\(\sum_{v \in S}dis(u,v)\)

这是 点分树 经典问题,直接用点分树维护就可以了,总时间复杂度 \(\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;
vector<int> G[N],E[N];
ll ans=0;

void add_edge(int u,int v){G[u].pb(v),G[v].pb(u);}

namespace ST{
  int dfn[N],st[20][N],idx=0,dep[N],f[N][20];
  
  void dfs(int u,int pre){
    f[u][0]=pre;
    for(int i=1;f[f[u][i-1]][i-1];i++) f[u][i]=f[f[u][i-1]][i-1];
    dfn[u]=++idx,st[0][idx]=pre,dep[u]=dep[pre]+1;
    for(auto v:G[u]) if(v!=pre) dfs(v,u);
  }
  
  int Min(int u,int v){return dfn[u]<dfn[v]?u:v;}
  
  int LCA(int u,int v){
    if(u==v) return u;
    u=dfn[u],v=dfn[v];
    if(u>v) swap(u,v);
    int s=__lg(v-u);
    return Min(st[s][u+1],st[s][v-(1<<s)+1]);
  }
  
  void init(){
    dfs(1,0);
    for(int i=1;i<=__lg(n)+1;i++)
      for(int j=1;j+(1<<i)-1<=n;j++)
        st[i][j]=Min(st[i-1][j],st[i-1][j+(1<<(i-1))]);
  }
  
  int dis(int u,int v){return dep[u]+dep[v]-2*dep[LCA(u,v)];}

  int climb(int x,int k){
    for(int i=17;~i;i--) if(k>>i&1) x=f[x][i];
    return x;
  }

  int jump(int u,int v,int k){
    int w=LCA(u,v),d=dep[u]+dep[v]-2*dep[w];
    if(dep[u]-dep[w]>=k) return climb(u,k);
    return climb(v,d-k);
  }
}

namespace DFS{
  int sz[N],fa[N],tot=0,rt=0;
  bool vis[N];
  ll s[N],s_fa[N],c[N];
  
  void findrt(int u,int pre){
    sz[u]=1;int mx=0;
    for(auto v:G[u]) if(v!=pre&&!vis[v]) findrt(v,u),sz[u]+=sz[v],mx=max(mx,sz[v]);
    if(max(mx,tot-sz[u])<=tot/2) rt=u;
  }
  
  void getsz(int u,int pre){
    sz[u]=1;
    for(auto v:G[u]) if(v!=pre&&!vis[v]) getsz(v,u),sz[u]+=sz[v];
  }
  
  void init(int u){
    getsz(u,0),vis[u]=1;
    for(auto v:G[u]) if(!vis[v]){
      tot=sz[v],rt=0,findrt(v,u);
      E[u].pb(rt),fa[rt]=u,init(rt);
    }
  }
  
  void upd(int x,int op){
    int u=x;
    for(;x;x=fa[x]){
      s[x]+=ST::dis(x,u)*op,c[x]+=op;
      if(fa[x]) s_fa[x]+=ST::dis(fa[x],u)*op;
    }
  }
  
  ll qry(int x){
    int u=x;ll res=0;
    for(;x;x=fa[x]){
      res+=s[x]+c[x]*ST::dis(u,x);
      if(fa[x]) res-=s_fa[x]+c[x]*ST::dis(u,fa[x]);
    }
    return res;
  }
}

struct Nod{
  int x,r;
  friend Nod operator + (Nod a,Nod b){
    int d=ST::dis(a.x,b.x);
    if(a.r+d<=b.r) return b;
    if(b.r+d<=a.r) return a;
    return (Nod){ST::jump(a.x,b.x,(d-a.r+b.r)/2),(d+a.r+b.r)/2};
  }
}a[N];

void init(){
  ST::init();
  DFS::tot=n,DFS::findrt(1,0);
  DFS::init(DFS::rt);
}

void solve(int l,int r){
  if(l==r) return;
  int mid=((l+r)>>1);
  ll sum=0,opl=mid,opr=mid+1;
  
  for(int i=mid;i>=l;i--){
    a[i]={i,0};
    if(i<mid) a[i]=a[i]+a[i+1];
  }
  for(int i=mid+1;i<=r;i++){
    a[i]={i,0};
    if(i>mid+1) a[i]=a[i]+a[i-1];
    sum+=2ll*a[i].r;
  }
  
  for(int i=mid;i>=l;i--){
    while(opr<=r&&ST::dis(a[opr].x,a[i].x)+a[i].r>a[opr].r) DFS::upd(a[opr].x,1),sum-=a[opr].r,++opr;
    while(opl<opr-1&&ST::dis(a[opl+1].x,a[i].x)+a[opl+1].r<=a[i].r) DFS::upd(a[++opl].x,-1),sum-=a[opl].r;
    
    ans+=2ll*(opl-mid)*a[i].r+DFS::qry(a[i].x)+1ll*(opr-opl-1)*a[i].r+sum;
  }
  
  for(int i=opl+1;i<opr;i++) DFS::upd(a[i].x,-1);
  
  solve(l,mid),solve(mid+1,r);
}

int main(){
  /*2024.12.25 H_W_Y CF1458F Range Diameter Sum 树上圆理论 + 点分树 + 分治*/ 
  ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
  cin>>n;
  for(int i=1,u,v;i<n;i++) cin>>u>>v,add_edge(u,n+i),add_edge(v,n+i);
  n+=n-1,init();
  
  solve(1,(n+1)/2);
  cout<<ans/2<<'\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)\) 的,而且对边的顺序要求比较严格,我们最后做线段树合并的时候需要按照点分树深度的从大到小的那些边去合并

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

upd:复杂度是假的,这是一个假做法。

具体可以看代码。代码

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)\)

  • \(01\) 序列,\(\mathcal O(1)\) 修改 \(0 \to 1\)\(\mathcal O(\sqrt n)\) 查询区间的最长连续段相关信息:P5386 [Cnoi2019] 数字游戏

    考虑对于每一个 \(1\) 的连续段,我们类似链表去维护它,其 \(l\) 指向 \(r\)\(r\) 指向 \(l\)

    那么从 \(0 \to 1\) 的修改就是合并两个连续段,信息是容易维护的。

    但由于我们需要区间查询,所以需要对序列分块,然后每个块内维护链表状的连续段,查询时直接扫面整块,中间相邻的合并。

    对于散块直接暴力枚举里面连续段找到一个分界点就可以了。

    注意到这种做法不能 \(1 \to 0\),也就是不能支持删除,所以经常和 回滚莫队 一起出现。


*P6578 [Ynoi2019] 魔法少女网站

雨谷小雨给你一个长为 \(n\) 的序列,定义区间 \([l,r]\) 的子区间为所有形如 \([l',r']\) 的区间,满足 \(l',r'\) 为整数,且 \(l \le l' \le r' \le r\)

\(m\) 次操作:

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

2 l r x:查询区间 \([l,r]\) 中有多少子区间的最大值小于或等于 \(x\)

\(n,m \le 3 \times 10^5\)\(1 \le l \le r \le n\)\(1 \le x, y \le n\),序列中每个元素都在 \([1,n]\) 中,所有数均为整数。

应 mea 建议,于是放在了这里。


这是上面第二个 trick 的一个拓展。

如果不带修,我们直接按照值域从小往大扫,每次维护 \(01\) 序列就可以了。

但是带修之后,我们考虑 操作分块,我们将每 \(\mathcal O(\sqrt n)\) 个操作拉出来,对于没有出现在修改中的位置,我们直接在扫的过程中维护。

而每次询问时,加入那些修改点的值,然后询问完撤销即可。

时间复杂度 \(\mathcal O(n \sqrt n)\),非常卡常。代码

Code
#include <bits/stdc++.h>
#define il inline
#define ll long long
#define pb push_back
#define S(l,r) pw[r-l+1]

namespace Fastio {}

const int N=3e5+2,C=2200;
int n,m,L[N],R[N],bl[N],a[N],tp=0,to[N],mx=0,B;
struct Nod{
  int op,x,y,l,r,tim;
  bool operator <(const Nod &a) const{return x<a.x;}
}q[N];
ll sum[N],ans[N],pw[N];
struct Undo{int x;ll v;}st[N];
std::vector<int> G[C+2];
bool vis[N];

struct Edge{int nxt,v;}e[N];
int head[N],tot=0;

il int min(int a,int b){return a<b?a:b;}
il int max(int a,int b){return a>b?a:b;}

il void add_edge(int u,int v){e[++tot]=(Edge){head[u],v},head[u]=tot;}

il void init(){
  B=sqrt(n);
  for(int i=1;;i++){
    L[i]=R[i-1]+1,R[i]=min(R[i-1]+B,n);
    for(int j=L[i];j<=R[i];j++) bl[j]=i;
    if(R[i]==n) break;
  }
  for(int i=1;i<=n;i++) pw[i]=1ll*i*(i+1)/2;
}

il void wrt(int x){if(st[tp].x!=x) st[++tp]=(Undo){x,to[x]};}

il void undo(){
  if(st[tp].x>n) sum[st[tp].x-n]=st[tp].v;
  else to[st[tp].x]=st[tp].v;
  --tp;
}

il void ins(int x,int op=0){
  int id=bl[x],l=x,r=x;
  ll &res=sum[id];
  if(op) st[++tp]=(Undo){id+n,res};
  
  if(x!=R[id]&&~to[x+1]) r=to[x+1],res-=pw[r-x];
  if(x!=L[id]&&~to[x-1]) l=to[x-1],res-=pw[x-l]; // Swapped
  res+=pw[r-l+1];
  
  if(op) wrt(l),wrt(r),wrt(x);
  to[x]=x,to[l]=r,to[r]=l;
}

il ll ask(int l,int r){
  ll res=0;
  if(bl[l]==bl[r]){
    int lst=0,fl=0;
    for(int i=l;i<=r;i++){
      if(~to[i]){
      	if(!fl) ++lst;
        else res+=pw[min(to[i],r)-i+1],i=to[i];
      }else res+=pw[lst],lst=0,fl=1;
    }
    res+=pw[lst];
    return res;
  }
  
  int lst=0,p=R[bl[l]],tmp=0;
  if(~to[p]) lst=p-max(l,to[p])+1;
  
  for(int i=p-lst;i>=l;i--)
    if(~to[i]) res+=pw[i-max(l,to[i])+1],i=to[i];

  for(int i=bl[l]+1;i<bl[r];i++){
    if(~to[L[i]]&&to[L[i]]==R[i]){
      lst+=R[i]-L[i]+1;
      continue;
    }
    
    res+=sum[i];
    if(~to[L[i]]){
      res-=S(L[i],to[L[i]]);
      lst+=to[L[i]]-L[i]+1;
    }
    res+=pw[lst],lst=0;
    if(~to[R[i]]) res-=S(to[R[i]],R[i]),lst+=R[i]-to[R[i]]+1;
  }
  
  p=L[bl[r]];
  if(~to[p]) tmp=min(r,to[p])-p+1;
  res+=pw[lst+tmp];
  
  for(int i=p+tmp;i<=r;i++)
  	if(~to[i]) res+=pw[min(r,to[i])-i+1],i=to[i];
  return res;
}

il void solve(int l,int r){
  memset(to,-1,(n+1) << 2);
  memset(sum,0,(bl[n]+1) << 3);
  memset(head,0,(n+1) << 2),tot=0;
  
  std::basic_string<int> V;
  for(int i=1;i<=n;i++){
    if(!vis[i]) add_edge(a[i],i);
    else V.pb(i),vis[i]=0;
  }
  
  for(int i=l;i<=r;i++){
  	if(q[i].op==2){
  	  for(int j:V) if(a[j]<=q[i].x) G[i-l].pb(j);
  	}else a[q[i].x]=q[i].y;
  }
  std::stable_sort (q+l,q+r+1);
  
  for(int i=1,j=l;i<=mx;i++){
    for(int j=head[i];j;j=e[j].nxt) ins(e[j].v);
    for(;j<=r&&q[j].x==i;j++)
      if(q[j].op==2){
	    for(int k:G[q[j].tim-l]) ins(k,1);
	    G[q[j].tim-l].clear(); // Swapped
	    ans[q[j].tim]=ask(q[j].l,q[j].r);
	    while(tp) undo();
	  }
  }
  mx=0;
}

int main(){
  cin>>n>>m,init();
  for(int i=1;i<=n;i++) cin>>a[i];
  
  for(int i=1,op,x,y,l,r,lst=0;i<=m;i++){
    cin>>op,ans[i]=-1;
    if(op==1) cin>>x>>y,vis[x]=1,q[i]=(Nod){1,x,y,0,0,i};
    else cin>>l>>r>>x,mx=max(mx,x),q[i]=(Nod){2,x,0,l,r,i};
    if(i==m||i%C==0) solve(lst+1,i),lst=i;
  }
  for(int i=1;i<=m;i++) if(~ans[i]) cout<<ans[i]<<'\n';
  return 0;
}

posted @ 2024-12-22 22:25  H_W_Y  阅读(78)  评论(10编辑  收藏  举报