Data Structure 3
时间非常紧迫,应该会动态更新。
主要是一些分治 + 树的杂题,其它的可以看 Data Structure 1(杂题) + Data Structure 2(扫描线与根号数据结构)。
Part 1 简单题 / 神秘题
CF696E ...Wait for it...
给定一个
个节点的树,有 个物品,第 个在节点 ,初始权值为 。要求支持两种操作: 对于两个物品,如果它们的权值不同,那么权值更小的物品更好;如果它们的权值相同,那么所在节点编号
更小的物品更好。可以发现在这种定义方式下,任意两个物品都能比较出哪个更好。
1 u v k
:对于树上的一条简单路径,删除路径上所有物品中最好的 个物品,并将这些物品的编号从最好到最不好的顺序输出; 2 v x
:对于一棵子树,将该子树内所有物品权值加。
。
先来一道煎蛋题热一下身。
用线段树维护区间最好的物品,那么每一次查询其实是树剖之后在
那么我们每次取出最小值,删掉它就可以了。
每次删除是单点修改,具体实现可以每个位置用一个队列维护。
修改操作直接区间加就可以了。
时间复杂度
CF1767F Two Subtrees
给你一棵有根树,每次给出两个子树(可能有交),求这两个子 树中所有的点权的最小众数。如果一个点被两个子树覆盖算两次。
。
四维莫队题。
直接四维莫队即可做到
由于是子树的询问所以我们考虑 dsu on tree 的顺序。
于是我们把它写下来,可以得到一个长度为
那么这个东西就变成二维莫队了,于是只要存在一个
显然是存在的,也就是我们考虑值域分块,每个块内用一个桶维护。
那么块内的众数出现次数是容易维护的,因为每次加入和删除的改变都是
查询的时候直接找到众数的出现次数,然后从小往大扫去找到第一个出现这么多次的数就可以了。
这样就做完了,疑似跑得比较快。代码。
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;
}
这个东西是还可以优化的,根据根号分治可以把
但是我不会了,而且常数看起来非常大,也不好写,大概率是没有这个跑得快了。
CF1976F Remove Bridges
你有一棵
个点的树,根是 号点,且根只与 个点相邻。 你可以在图上任意两点间连一条无向边,一共连
条,允许重边与和原来的树上相同的边。且新图要求:
- 对每条割边,它的在树上深度较大的节点的子树内所有树边也都是割边。
- 割边数最少。
对
为 到 之间所有整数求解割边数的最小值。一个测试点 组数据。
。
真简单题。
首先考虑每次你可以连成一个环,然后对答案的贡献就是环上点的数量。
所以我们考虑第一条边,一定是一条从根开始的最长链。
然后考虑每一次的拓展,一定是从已经开发的链上面找两条最长链衍生出去。
于是直接用优先队列维护即可,时间复杂度
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
给出一棵
个节点的有根树,根为 。点 的父亲为 。有 种颜色。点 的颜色为 ,颜色 的权值为 。有两种操作共 次:
,将 。
,从树中等概率选取一个点 ,得到 的价值。求期望价值。其中 表示以 为根的子树中颜色为 的节点数。 是给定的常数。
。
直接根据期望的线性型拆开,然后去维护每个颜色的
时间复杂度
CF217E Alien DNA
给出一串字符串,有
个操作,将区间 的字符中编号为偶数的写下,再把奇数的写在后面,然后把它们插入 和 之间,问最后字符串的前 位。
。
做法比较多,这里提供一个
考虑从后往前扫时间,用并查集维护应该是什么。
那么设当前存在的位置为
这个操作可以用并查集 + 树状数组 / 线段树上二分得到。
所以时间复杂度是
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
给你一个长度为
的整数序列 ,和一个常数 。 有
次操作:
1 x y
:将位置的值修改为 。
2 l r
:表示询问区间中 。
。
P9877 [EC Final 2021] Vacation。
考虑按
块内的答案十分好维护,直接维护最大子段和——前缀 / 后缀最大值 + 最大子段和。
而对于相邻块间,我需要保证区间长度
发现设
于是可以类似于区间最大子段和维护了。
然而这里的块数有可能非常多,所以我们还需要用一棵线段树维护一个区间块内的答案 / 相邻块间的答案。
于是修改的时候先在所在块上面修改,再在全局的线段树上面修改。
查询的时候,先特判掉
那么唯一需要处理的就是左右散块和相邻块间的答案。
这个东西可以在相邻块的线段树上面区间查询得到答案,画一下图发现是简单的。
于是就做完了,具体实现细节是用 vector 存每个块的线段树比较好写?
时间复杂度
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:结论
Qoj9914:重心 + 构造。
Qoj9911:dfs + 构造。
CF1824E:枚举
CF1740H MEX Tree Manipulation
定义一棵树上每个节点的值为其所有儿子的值的 MEX,叶子节点的值为
。 现在有一个初始只有节点
的树,每次输入一个 代表加入一个点 ,它的父亲为 ,求加入这个点之后树上所有点的权值和。
, 。
感觉这里每个权值不是很大,如何证明?
设
解得
接着考虑如何处理,由于需要动态改变树的形态,所以我们考虑 ddp。
具体来说,对于每个节点,树链剖分之后考虑轻儿子的贡献,可以用一个数组记录哪些元素出现了多少次,找到只考虑轻儿子的 MEX 为
这是一个非常简单的映射形式,我们同时再记录一下区间的
这样每次修改相当于是把当前节点到根的链都修改,于是正常 ddp 维护就可以做到
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;
}
可以用 全局平衡二叉树 做到
实际上如果你只需要做两个 log 根本不需要用到 mex 是
Qoj9914 前往何方
交互题。猜一棵
个点的树,你有一个点集 支持加点、删点,每次操作后交互库返回 导出子图最大连通块大小,最多可以进行 次操作。
。
CTT 2025 D3T3。
首先,一个比较直接的思路就是我们在全局里面删点,可以得到什么?
发现可以得到删去每个点的最大连通块大小——重心!!!
进而对于不是重心的点,我们发现它的子树大小(以重心为根)就是
于是我们考虑类似于 点分树 的操作,找到每个点属于哪些子树。
首先,根的最大儿子是可以找出来的(也就是
而对于哪些非最大的儿子,我们可以通过一次遍历,按照
如果返回值变小,则说明这个点是根的儿子。
接着就是去确定每个点属于那个子树中,这个可以直接对那些不是最大子树的儿子 二进制分组。
每次删掉当前第
那么这样每次就可以用
不精细实现可以获得
接着,你发现其实并不用把每个子树都求出来,我们只需要类似 整体二分 去做。
即把整棵树划分成两个部分,使得都包含根,并且大小平均。
而上面的做法中,我们判断一个点是否是根的儿子依赖于我们没有删除根最大的儿子,这样才能保证每次的返回值是根所在的连通块的大小。
但实际上我们并不需要如此。
还是按照子树大小从大往小枚举,删除一个点,如果返回值
这样就把树分成了两块,第一块是前面删除的那些儿子,第二块是后面删除的那些儿子。
然后对于后面的那些点,如果删除对返回值没有影响,说明这个点属于第一块,反之属于第二块。
这样做的次数上限是什么呢?
首先我们设当前重心下儿子的大小是
证明直接讨论
而这个
那么我们有
其中
这个东西计算出来是
于是我们就做完了。
最后,这里有一些卡常的小技巧:
我们需要每次 dfs 时记录当前的集合是否被加进了询问集合。
于是如果
总操作次数是
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 路南柯
称一个
的排列 是一棵 个点、点编号为 至 的树 的拓扑序列,当且仅当对于任意 ,恰好存在唯一的 满足 与 之间有连边。 给定树
,你需要给出尽可能少的该树的拓扑序列 ,使得有且仅有树 满足 均为该树的合法拓扑序列。
。
CTT 2025 D2T3,P11425 [清华集训 2024] 路南柯。
考虑一个树形拓扑序能证明什么?
它能说明一个点的父亲在一个集合中,而这个集合中有它的祖先或者和它没关系的点。
首先是容易确定哪些点是
第一次每个点以任意顺序 dfs 儿子,得到一个拓扑序;第二次,我们将每个点的儿子顺序反转,再次 dfs 得到拓扑序。
那么两次拓扑序对于每个
现在问题就变成如何区分
我们发现需要再从
那么稍微画一下图就可以知道,操作次数是把整棵树删去叶子节点后的新树的叶子节点数量,以它们为根一定是更优的。
而对于最开始我们说的两次操作,其实也是不必要的,可以把它们融到后面的遍历中。
即我们在第一次求完拓扑序之后,将所有点的儿子节点都 reverse 一次就可以了。
对于那些原树的叶子节点,我们需要在它父亲节点最后再访问,这样才能确保叶子一定是叶子。
上述答案为原树删去叶子节点后得到的新树的叶子数量的构造方法,对大部分树都满足条件。
而唯一的特例就是菊花,对于菊花,我们直接做两次,按照两种顺序遍历儿子就可以了。
这样就做完了,时间复杂度
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
给定一棵树,每个节点有两种颜色黑和白,初始颜色为全白,有
个时间节点,同时有 次操作,第 次在第 个时间节点,有两种操作,分别为将树上某个节点染成黑色,和查询在第 个时间节点到当前时间节点中树上路径 到 中第 个没有被染成黑色的节点。
。
由于涉及到时间轴,所以考虑用主席树维护那些点改变过。
那么每次查询就是树剖之后不断在主席树上面二分,时间复杂度
CF1824E LuoTianyi and Cartridge
给定一棵
个结点的树 ,点有点权 ,边有边权 。 按如下方式构建另一棵树
:
- 选择
的 个结点作为 的点集 。 可以是 的任意正整数。 - 接下来 依次 选择
的 条 互不相同 的边。 - 对于第
次选边,假设选择第 条边 ,那么你可以选择 的 不连通 的两个点 ,要求 包含于 在 上的简单路径,在 之间以 为权值连边。 设
分别为 的 的最小值, 分别为 的 之和,则 的权值为 。 求
的最大权值。
, ,保证 形成一棵树。
注意到这里的
设当前枚举到
我们把一条边称之为有用当且仅当它两侧都有合法的点,不然这条边一定不会被选到集合中。
于是我们用这些有用的边和有用的点建图,具体来说,对于一条无用的边,我们可以把两端合并起来。
那么图上的每一个点其实代表若干个点,有可能是
为了让所有边都能选,我们需要让每个叶子节点都有至少一个点被选了。
贡献就是:
- 边的贡献:前
大。 - 点的贡献:每个叶子节点的最大值之和
,和其它的 大,其中 是叶子的数量。
这样我们就完成了固定
考虑
同样分两种情况讨论
-
删去边:相当于是合并两个节点,于是直接启发式合并就可以了。
-
删去点:直接删除即可。
注意如果当前点是叶子,并且空了,我们需要不断往上跳,把那些不合法的边都删掉。
于是具体实现时用两棵权值线段树分别维护边的前
同时每一个节点需要维护上面点的
时间复杂度
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】永恒
有一棵
个点的永恒的树,树中每个点 上都有一个永恒的字符串 。 但这世界没有永恒,所有的一切都会随着时光消失。我们只能给每个所谓永恒的东西定义一个永恒值
。这个值本身没有意义,只是一个象征罢了。
- 一个字符串
的永恒值 定义为它的长度 ,即:
- 树上的一条无向路径
指的是 之间的简单路径(包括 ),其永恒值 定义为路径上所有不同的无序点对 上的字符串 的最长公共前缀 的永恒值 之和,即:
- 一棵树
的永恒值 定义为树上所有的无向路径 的永恒值之和,即: 特别的是,树中每个点上的字符串都来自一棵永恒的以点
为根的 Trie 树,即每个树中的点都对应着一个 Trie 树中的点,点上的字符串就是 Trie 树中从根节点到其对应的点形成的字符串。 你需要求出这棵树的永恒值,答案对
取模。
。
经典套路题,没有什么思维难度。
考虑交换求和顺序,那么如果
其中前面的
这个东西可以在第一棵树的一次 dfs 中,在第二棵树的树剖后的线段树上面做区间加。
而对于祖孙关系,我们可以同样完成,只是对于祖先不是乘的
同样在第二棵树上树剖后用线段树维护即可。
于是时间复杂度
Part 3 分治
感觉序列分治题目的难点其实是在于想到序列分治/qd
有关
这一部分主要是整理性的作用,许多题都是近期的联考中的。
Qoj9245:序列分治 + dp 解决 区间子区间。
9.30 BSZX 联考 T3 宝藏(sagiri,CF1787I):序列分治 + 区间子区间。
11.28 BSZX 联考 T4 最小值(min):序列分治 + 绝对值。
ABC248Ex 加强版:序列分治 + 二维数点,钦定两侧的大小关系。
ABC282Ex:
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 sequenceof length is good when and . In that case, we call its depth.
Given a sequenceof length which consists of (and ). Let be the number of good subsequences with depth in the sequence formed by .
You are givenqueries, each query contains four numbers .
- If
, you need to calculate . - If
, you need to calculate . Since the answer could be huge, you need to output the answer modulo 998244353.
。
注意是 子序列。
不一定所有的 区间子区间 都要往 扫描线和莫队 上面想,这道题中我们可以用更简单的分治来完成。
当给定一个区间
设
其中
于是考虑对询问 分治,每次只处理跨过当前
那么对于
而对于
反之,对于不跨过当前分治中点
这样每次统计答案是
那么这样我们就做完了,时间复杂度
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)
对于一个长度为
的的序列 ,令 ,定义其权值 , 其中 满足 , 。 给定一个长度为
的序列 ,求 。 其中
表示由 的第 个元素到 的第 个元素 依次 构成的序列。 由于答案可能很大,你只需要输出答案对
取模后的结果即可。
。
首先,容易发现那个条件是没有用的。
于是问题就转化成了求每个区间的前缀
考虑分两个部分来解决,也就是上面提到的两个部分:
-
前缀
,考虑差分之后变成前缀和之差,而之差的 是一定的,所以我们只需要维护 的 就可以了。这个东西容易用单调栈做到
。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); }
-
最大子段和,同样单调栈之后直接用吉司机线段树维护似乎就是
,但是比较复杂。处理这种问题的另外一个方向就是 分治,我们希望处理跨过当前分治中心的答案。
首先维护一下
表示 区间内的最大子段和, 表示 区间内的最大前缀/后缀。这两个东西都是容易直接线性得到了。
那么现在考虑统计答案,手玩一下不难发现其实有一些单调性,也就是我们的左端点
扫过去的过程中,右端点一定是分了三段: 中 是最优的(当前左端点在 )。 中 是最优的(左端点为 ,右端点为 )。 中 是最优的(右端点为 )。
容易感性理解到他们的单调性,于是我们就维护一下
的前缀/后缀和就可以完成了。那么每次的时间复杂度是线性的,分治下来的时间复杂度就是
。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 有两个长度为
的整数序列 和 。 小 X 现在越来越喜欢对称的东西了,当他看到两个序列
时,他会找到序列 的最小值 和 的最小值 ,他认为这两个最小值相差越小越好,所以他定义 。 更进一步的,他定义
表示当 时 的值,即 的最小值和 的最小值的差的绝对值。 对于每个
,小 X 想求出所有长度为 的区间 中, 的最小值,即求 。
。
考虑序列分治然后结束了。
设当前的分治中心是
设左区间
于是实际上答案分成了四种情况:
。 。 。 。
然后用上
我们发现对于一个固定区间长度
于是分界点我们可以双指针求出,而前两种情况的答案可以直接区间
反之,中间部分有正有负,但是单调。
所以我们可以通过 二分 得到最接近
这样的时间复杂度是
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;
}
实际上我们是容易做到单
也就是你钦定答案一定是
那么分治的时候你分别去枚举区间的左端点和右端点(做两次),然后每次处理掉你钦定的满足条件的那些东西就可以了。
注意到我们需要一个反向 ST 表去实现区间
ABC248Ex Beautiful Subsequences - 加强版
给定排列
和整数 ,求满足如下条件的点对 数量。
。 。
。
温馨提示:样例非常水。
首先,原题有条件
这样可以做到较大常数的
现在我们抛开
设当前分治区间是
那么我们维护四个值
于是只需要统计满足
的
直接对大小关系进行讨论:
,满足条件的 是一段 开始的前缀。 ,原式形如 ,合法的 是一段区间。 ,原式形如 。
同理。
所以后面的问题就是一个二维数点,用 BIT 维护即可做到
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
给定序列
,求有多少对 满足 ,且
。
还是考虑去枚举
由于值域较大,我们可以交换值域和下标进行二维数点,也可以直接用 pbds。
于是时间复杂度
11.21 BDF 联考 T4 图(graph,CF603E)
给定一张
个点的无向带权图,初始没有边。 定义合法边集为每个点度数均为奇数的边集。
依次加入
条边,每次加入后询问合法边集中最大边权最小值,若不存在合法边集输出 -1
。
。
首先,对于 每个点度数均为奇数 这个条件,我们大胆猜测就是 每个连通块大小均为偶数。
证明稍微构造一下就可以得到一个充分必要性。
然后对于 最大边权最小值,我们考虑 Kruskal 重构树。
于是对于静态的问题,我们按照边权从小到大排序之后依次加入直到满足条件。
但是动态起来你就有一堆单调性,似乎就可以整体二分做。
我们换一种做法,发现对于一条边,一旦它被淘汰了,那就永远被淘汰了。
于是一条边贡献的一定是一个 区间,那么我们考虑 线段树分治。
从后往前扫,每次到一个节点时,我们处理在这个位置结束的线段,并且把它加到线段树里面既可以了。
如何处理其实就是按照值域扫过来,直到满足条件。
于是这样就做完了,时间复杂度
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;
}
树上圆理论
我们考虑把树上的一些点看成坐标系上面的点,那么直径就是覆盖它们最小圆的直径。
于是圆心就是树上所有直径的终点,半径就是直径的一半,显然圆心是唯一的。
我们用
那么两个圆
, 包含 。 , 包含 。 。
画图后容易理解。
CF1458F Range Diameter Sum
给定一棵包含
个节点(编号 到 ), 条长度为 的无向边的树. 设
为编号 到编号 两点之间唯一路径的长度. 设
为 求:
。
用普通的思路,也就是对于每一个右端点,我们尝试记录答案,发现并不是很好维护。
所以我们考虑分治,对于每个分治区间计算
发现其实比较好维护,在当前分治区间中,我们可以预处理出,左边的点到
合并两个圆就像上面树上圆理论中直接合并即可。
现在考虑对于每一个
注意到右边的
因为顺序一定是先包含,再相交,最后被包含。
而这两个分界点显然随着
对于第一种和第二种,我们是容易用
这是 点分树 经典问题,直接用点分树维护就可以了,总时间复杂度
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 中,我们都解决的是
自己卷别人 是相对容易的。
而现在我们来探究另一种分治 FFT:自己卷自己,也就是说
有什么区别呢?
我们发现原先的分治 FFT 中,每一次我们是把
那怎么办呢?
考虑这种情况的出现 当且仅当
所以如果
那么唯一的问题就是
推迟 如何处理,我们考虑每一次
也就是那些推迟的贡献,但由于对称性,我们只需要把这部分贡献
所以我在说什么?
其实自卷型分治 FFT 也就是两个部分:
时,计算 卷上 ,贡献到 。 时,计算 卷上 ,将其 两倍 贡献到 。
于是就做完了,时间复杂度不变。
*P4566 [CTSC2018] 青蕈领主
小绿同学因为微积分这门课,对“连续”这一概念产生了浓厚的兴趣。小绿打算把连续的概念放到由整数构成的序列上,他定义一个长度为
的整数序列是连续的,当且仅当这个序列中的最大值与最小值的差,不超过 。例如 是连续的,而 不是连续的。 某天,小绿的顶头上司板老大,给了小绿
个长度为 的排列。小绿拿到之后十分欢喜,他求出了每个排列的每个区间是否是他所定义的“连续”的。然而,小绿觉得被别的“连续”区间包含住的“连续”区间不够优秀,于是对于每个排列的所有右端点相同的“连续”区间,他只记录下了长度最长的那个“连续”区间的长度。也就是说,对于板老大给他的每一个排列,他都只记录下了在这个排列中,对于每一个 ,右端点为 的最长“连续”区间的长度 。显然这个长度最少为 ,因为所有长度为 的整数序列都是连续的。 做完这一切后,小绿爬上绿色床,美美地做了一个绿色的梦。
可是第二天醒来之后,小绿惊讶的发现板老大给他的所有排列都不见了,只剩下他记录下来的
组信息。小绿知道自己在劫难逃,但是作为一个好奇的青年,他还是想知道:对于每一组信息,有多少个和信息符合的长度为 的排列。 由于小绿已经放弃治疗了,你只需要告诉他每一个答案对
取模的结果。 我们并不保证一定存在至少一个符合信息的排列,因为小绿也是人,他也有可能犯错。
。
好题啊。
先来考虑如何判无解。
容易发现区间一定是 相离或包含,而最后一个数一定是
因为只要区间相交,那么后面的区间一定就不优,和定义违背。
如果让每个区间向第一个 包含 它的区间连边,这样就会形成一棵树。
我们发现每个节点的贡献是一定的,也就是我们可以把它的每一个儿子看成一个点,那么这个长度为
我们不妨把这个记作
现在问题就变成了求
考虑 dp,
由于每一次最后一个 元素 要变,所以我们考虑一种 映射,也就是将 位置 和 数值 翻转,这样对连续区间定义不变。
那么问题就变成了 任意一个连续的区间都包含最大值。
可是每一次
所以我们考虑在上一个
我们分两种情况:
-
之前的序列 合法:
则当前插入的
只要不在 的两边就可以了,那么可以插的空隙数量是 ,贡献为 。 -
之前的序列 不合法:
那么我们利用当前的这个
让序列合法,容易发现只会存在一个区间(可以不断包含)不合法。因为如果存在两个 相离 的区间不合法,而只能插入一个
,所以必然不能变为合法。考虑枚举这个区间长度为
,由于它不合法,所以一定不包含最大值;又因为插入 之后就合法了,所以一定不包含 。我们设这个区间的最小值是
,那么区间值域是 ,需要满足 。也就是说
,对于每一个 ,值域范围 有 中取值。
对于当前长度为
区间内的点, 插入的合法位置满足 任意一个连续区间 都需要经过 ,我们发现这个和之前的 dp 定义非常像,也就是我们把 看成 ,那么其实合法的序列数量是 。进而,对于整体的贡献,我们就把这个长度为
的区间看成一个数,那么外面的答案就是 。所以这种情况的贡献就是
。
以上两种情况外面就得到了整个 dp 转移式
这很明显是一个 标准卷积形式,可以用 自卷型分治 FFT 解决。时间复杂度
注意到卷积中有
于是可以直接卷积之后统一乘了。代码。
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 认为 有 种合理的取值 ,他想请你帮忙求出对于每一个合理的取值,有多少种建立联邦的方案。两个方案不同当且仅当两个联邦的 不同。由于答案可能很大,你只需要求出其对 取模的结果。
。
很好的一道题。
场上把一个连通块定义到这个连通块的直径中点上面去了,于是没有什么前途。
但是稍微换一个思路,我们是否把它定义到一个直径的端点上面去更简单。
发现事实是这样的,具体来说,我们发现:深度最深的点一定是一个直径的端点。
于是我们把整棵树按照深度排序,对于一个点可以选的点数
然后我们的答案可以每次
于是问题就变得非常经典了。
这个东西明显直接用 点分治 做,随便维护一下可以做到
实际上题解中给出了一个
这样就做完了,时间复杂度
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 先生掌握着一张巨大的情报网。这个情报网共有
个人,编号为 。如果 和 常常亦或是曾经互通有无(即交互情报),我们称 有情报链接。恰巧的是,整个情报网正好有 个情报链接且每个人的情报都能经过情报员传递到任何人。第 个情报链接连接 。 现在,共有
条情报,第 个人掌握第 条情报。接下来的 个时刻,每个时刻有两种操作可能存在。
zdd 先生会指定一个
,情报员 和 会交换情报。(记 掌握的情报编号的集合为 ,那么一次交换情报会将 都变为 ) zdd 先生会询问第
条情报被几个人知道。(即有多少 使得 ) 而你情报大师的头号程序员,需要对每个 zdd 先生的询问做出回答。
。
这种树上的问题,很容易想到 点分治。
然后每次处理跨过分治中心的点的相互的贡献。
于是考虑当前分治中心在
那么分成两部分:
,我们希望 到 的时间最早。 ,我们希望从 出发的时间最晚,并且能在 时刻之前到达。
发现这两个东西都是可以 dp 的,记
这个东西就是一个 二维数点 问题。
于是直接用树状数组维护就可以做到
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)
小
是蛤蟆国最大的金矿老板,最近它在西部开辟了一片新矿场,由于开采难度较大,所以小 决定用爆破的方式开矿。 矿道形成一棵
个结点的树,结点 之间以长度为 的矿道连接。 现在每个结点
上已经安放了一枚爆炸半径为 ,爆炸威力为 的 ,一枚 引爆后,所有与其距离不超过 的 也会被一起引爆。 为了评估开矿的危险程度,小
定义一枚 的震惊度为初始引爆这枚 , 最终所有被引爆的 的爆炸威力之和。你作为工程师,需要帮小 求出所有 的震惊度。 注意每枚
是独立的。
。
P9260 [PA 2022] Miny 加强版,Pjudge #21728. 【CTS Round #1 Day 1】地雷。很好的一道题。
首先,一种比较直接的思路就是建图之后用 bitset 做,时间复杂度
考虑优化,我们希望让边数少一点,于是考虑 点分树,维护当前子树中的点到根的距离,那么
于是前缀和优化建图,边数就是
然后你考虑对上面那个东西把 bitset 换成 线段树合并,然后就可以冲过去了。
不会证明复杂度,似乎是 。
感觉这个东西可以感性理解一下,似乎可以节约比较多的东西,而且可以通过哈希值判断子树同构就不线段树合并了。
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 直观的思路就是去对于每一个重心维护
表示从 开始炸,可以炸到任意点,最终能给 的半径。 表示至少最初需要多少半径才能从 炸到 ,过程中同样可以炸到任意点。
那么我们的转移是从上往下的,枚举到一个节点时,它的答案已经被上面的父亲节点表示出来了。
现在假设你已经知道全局的根节点的
对于一个点分树中心
- 对于
, 能炸到 当且仅当 ,有贡献 。 - 对于
, 能炸到 当且仅当 ,有贡献 。
具体实现的时候,我们对于每一个分治中心
将
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);
}
有些东西看不懂先别急。
那么问题就变成了如何求全局的
发现这个东西我们就可以换一下
表示在 的子树中炸,从 开始,第一次 炸到 给的半径。 表示在 子树中炸,初始需要多少半径才能炸到 。
这个 dp 的转移时从下到上的,转移同样也是类似的。
对于一个分治中心
, 能炸到 当且仅当 ,有贡献 。 , 能炸到 当且仅当 ,有贡献 。
然后在每个分治重心统计答案的时候再更新一下
同样这些东西都可以用双指针排序之后完成,时间复杂度同样
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]);
}
}
这样就做完了,时间复杂度
统计答案是容易的。代码。
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。
-
修改, 查询区间最小众数:在 Two Subtrees 这道题中,我们用到的分块技巧。考虑 值域分块,每个数维护其出现次数,并且块内维护每种出现次数的出现个数,于是我们可以得到每个块的众数出现次数。
于是修改显然是
的,而查询的时候我们可以首先遍历所有块,得到众数出现次数。接着从左往右找到第一个存在众数的块,直接遍历其中的元素,找到第一个众数即可,时间复杂度
。 -
序列, 修改 , 查询区间的最长连续段相关信息:P5386 [Cnoi2019] 数字游戏。考虑对于每一个
的连续段,我们类似链表去维护它,其 指向 , 指向 。那么从
的修改就是合并两个连续段,信息是容易维护的。但由于我们需要区间查询,所以需要对序列分块,然后每个块内维护链表状的连续段,查询时直接扫面整块,中间相邻的合并。
对于散块直接暴力枚举里面连续段找到一个分界点就可以了。
注意到这种做法不能
,也就是不能支持删除,所以经常和 回滚莫队 一起出现。
*P6578 [Ynoi2019] 魔法少女网站
雨谷小雨给你一个长为
的序列,定义区间 的子区间为所有形如 的区间,满足 为整数,且 。 有
次操作:
1 x y
:将位置的值修改为 。
2 l r x
:查询区间中有多少子区间的最大值小于或等于 。
, , ,序列中每个元素都在 中,所有数均为整数。
应 mea 建议,于是放在了这里。
这是上面第二个 trick 的一个拓展。
如果不带修,我们直接按照值域从小往大扫,每次维护
但是带修之后,我们考虑 操作分块,我们将每
而每次询问时,加入那些修改点的值,然后询问完撤销即可。
时间复杂度
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;
}
本文作者:H_W_Y
本文链接:https://www.cnblogs.com/H-W-Y/p/18622700/ds3
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步