这题虽然水但是有好多方法,是个不可多得的好题(
洛谷题目页面传送门 & CF 题目页面传送门
题意见洛谷里的翻译。
首先,容易想到的是,对于每个节点维护与它有交集的节点集合。显然,设 (subtree(x)) 表示子树 (x) 内的节点集合,那么第 (i) 次操作就是将 (subtree(a_i)cup subtree(b_i)) 内所有节点的维护的集合并上 (subtree(a_i)cup subtree(b_i))。注意到,看 DFS 序的话,是两个区间,由于是静态的,可以差分,对于每个节点维护一个要并上的区间序列和一个要消除影响的序列。实际上这一步操作用树上差分更好理解?就是子树修改的话在根打个标记,然后每个节点要算上祖先贡献和,一路 DFS 下来该回溯回溯。
接下来的难点在于:如何维护一个数据结构,支持:
- 给当前集合并上一个区间;
- 消除以前某次操作 (1) 的影响;
- 查询当前不在集合内的数的数量(实际上应该是当前集合的大小,为了方便就维护这个,减一下就可以了)。
方法一
大家好我是 yxh,我用我最喜欢的暴力数据结构——分块 A 了这道题(其实并没有,她写挂了)
很简单,每个块内维护一个基于被插入次数的桶和整体偏移量即可。块长取 (sqrt n) 的时候时空复杂度皆为 (mathrm O(nsqrt n))。注意,空间卡得很紧,桶要用 short
开。
代码(现场,下面 (3) 种代码都是这个版本魔改的):
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=100000,M=100000,DB_SZ=333;
int n,m;
vector<int> nei[N+1];
int dfn[N+1],mxdfn[N+1],mng[N+1],nowdfn;
void dfs(int x=1,int fa=0){
mng[dfn[x]=mxdfn[x]=++nowdfn]=x;
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i];
if(y==fa)continue;
dfs(y,x);
mxdfn[x]=mxdfn[y];
}
}
int d[N+2];
struct dvdblk{
int sz,sz1;
int a[N+1];
struct block{int l,r,added;short cnt[2*M+1];}blk[DB_SZ];
#define l(p) blk[p].l
#define r(p) blk[p].r
#define cnt(p) blk[p].cnt
#define added(p) blk[p].added
void bld(int p,int l,int r){
l(p)=l;r(p)=r;
memset(cnt(p),0,sizeof(cnt(p)));
cnt(p)[m]=r-l+1;
added(p)=0;
}
void init(){
sz1=sqrt(n);
sz=(n+sz1-1)/sz1;
for(int i=1;i<=sz;i++)bld(i,(i-1)*sz1+1,min(i*sz1,n));
}
void add(int l,int r,int v){
int pl=(l+sz1-1)/sz1,pr=(r+sz1-1)/sz1;
if(pl==pr){
for(int i=l;i<=r;i++)cnt(pl)[a[i]+m]--,cnt(pl)[(a[i]+=v)+m]++;
return;
}
add(l,r(pl),v),add(l(pr),r,v);
for(int i=pl+1;i<pr;i++)added(i)+=v;
}
int zero(){
int res=0;
for(int i=1;i<=sz;i++)res+=cnt(i)[m-added(i)];
return res;
}
}db;
vector<pair<pair<int,int>,int> > add[N+2];
int ans[N+1];
int main(){
freopen("b.in","r",stdin);freopen("b.out","w",stdout);
cin>>n>>m;
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
nei[x].pb(y);nei[y].pb(x);
}
dfs();
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
d[dfn[x]]++,d[mxdfn[x]+1]--,d[dfn[y]]++,d[mxdfn[y]+1]--;
if(dfn[x]<=dfn[y]&&mxdfn[y]<=mxdfn[x]){
add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
}
else if(dfn[y]<=dfn[x]&&mxdfn[x]<=mxdfn[y]){
add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
}
else{
add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
add[dfn[y]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
add[dfn[x]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
}
}
for(int i=1;i<=n;i++)d[i]+=d[i-1];
db.init();
for(int i=1;i<=n;i++){
for(int j=0;j<add[i].size();j++){
int l=add[i][j].X.X,r=add[i][j].X.Y,v=add[i][j].Y;
db.add(l,r,v);
}
// cout<<db.zero()<<"!
";
ans[mng[i]]=n-db.zero()-!!d[i];
}
for(int i=1;i<=n;i++)printf("%d%c",ans[i],"
"[i==n]);
return 0;
}
方法二
不难发现刚刚那个分块算法其实是远强于这题的需要的。追求更低的复杂度,尝试用线段树解决。一样维护每个数的插入次数,然后每个节点内维护当前区间内 (0) 的数量。
注意到这题相比于分块这个强算法的特殊性质:永远查询 (0) 的数量,而 (0) 是值域里的最小值。
注意到有区间修改,尝试懒标记。然后发现,要加的话,显然根据性质,当前区间就没有 (0) 了;但如果要减的话,哦吼,你猜不到答案了。
于是尝试标记永久化。哦吼,可以了!修改时在树中经过的节点,用上传更新它们。考虑如何作用当前节点的标记的贡献:若为 (0),相当于没有贡献,直接上传两个儿子;若 (>0),则 (0) 的数量为 (0);由于每次减法是对之前加法的消除,所以标记恒非负。
时间 (mathrm O(mlog n)),空间 (mathrm O(n))。
代码:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=100000,M=100000,DB_SZ=333;
int n,m;
vector<int> nei[N+1];
int dfn[N+1],mxdfn[N+1],mng[N+1],nowdfn;
void dfs(int x=1,int fa=0){
mng[dfn[x]=mxdfn[x]=++nowdfn]=x;
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i];
if(y==fa)continue;
dfs(y,x);
mxdfn[x]=mxdfn[y];
}
}
int d[N+2];
struct segtree{
struct node{int l,r,cnt,lz;}nd[N<<2];
#define l(p) nd[p].l
#define r(p) nd[p].r
#define cnt(p) nd[p].cnt
#define lz(p) nd[p].lz
void bld(int l=1,int r=n,int p=1){
l(p)=l;r(p)=r;lz(p)=0;cnt(p)=r-l+1;
if(l==r)return;
int mid=l+r>>1;
bld(l,mid,p<<1);bld(mid+1,r,p<<1|1);
}
void init(){bld();}
void sprup(int p){
if(l(p)==r(p))cnt(p)=!lz(p);
else cnt(p)=lz(p)?0:cnt(p<<1)+cnt(p<<1|1);
}
void add(int l,int r,int v,int p=1){
if(l<=l(p)&&r>=r(p))return lz(p)+=v,sprup(p);
int mid=l(p)+r(p)>>1;
if(l<=mid)add(l,r,v,p<<1);
if(r>mid)add(l,r,v,p<<1|1);
sprup(p);
}
int zero(){return cnt(1);}
}segt;
vector<pair<pair<int,int>,int> > add[N+2];
int ans[N+1];
int main(){
freopen("b.in","r",stdin);freopen("b.out","w",stdout);
cin>>n>>m;
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
nei[x].pb(y);nei[y].pb(x);
}
dfs();
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
d[dfn[x]]++,d[mxdfn[x]+1]--,d[dfn[y]]++,d[mxdfn[y]+1]--;
if(dfn[x]<=dfn[y]&&mxdfn[y]<=mxdfn[x]){
add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
}
else if(dfn[y]<=dfn[x]&&mxdfn[x]<=mxdfn[y]){
add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
}
else{
add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
add[dfn[y]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
add[dfn[x]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
}
}
for(int i=1;i<=n;i++)d[i]+=d[i-1];
segt.init();
for(int i=1;i<=n;i++){
for(int j=0;j<add[i].size();j++){
int l=add[i][j].X.X,r=add[i][j].X.Y,v=add[i][j].Y;
segt.add(l,r,v);
}
ans[mng[i]]=n-segt.zero()-!!d[i];
}
for(int i=1;i<=n;i++)printf("%d%c",ans[i],"
"[i==n]);
return 0;
}
方法三
这是 rng 的一个很巧妙的方法。
继续使用上面那个性质。于是我们只要知道,最小值是多少,最小值有几个即可,判断最小值是否为 (0) 即可。
于是维护这两个信息,然后你会发现这是可以轻松懒标记和上传的。上传的时候,如果两边最小值不同就取小的,否则合并。
复杂度跟上面一个方法一样。
代码:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=100000,M=100000,DB_SZ=333;
int n,m;
vector<int> nei[N+1];
int dfn[N+1],mxdfn[N+1],mng[N+1],nowdfn;
void dfs(int x=1,int fa=0){
mng[dfn[x]=mxdfn[x]=++nowdfn]=x;
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i];
if(y==fa)continue;
dfs(y,x);
mxdfn[x]=mxdfn[y];
}
}
int d[N+2];
struct segtree{
struct node{int l,r,mn,cnt,lz;}nd[N<<2];
#define l(p) nd[p].l
#define r(p) nd[p].r
#define mn(p) nd[p].mn
#define cnt(p) nd[p].cnt
#define lz(p) nd[p].lz
void bld(int l=1,int r=n,int p=1){
l(p)=l;r(p)=r;mn(p)=lz(p)=0;cnt(p)=r-l+1;
if(l==r)return;
int mid=l+r>>1;
bld(l,mid,p<<1);bld(mid+1,r,p<<1|1);
}
void init(){bld();}
void sprup(int p){
if(mn(p<<1)==mn(p<<1|1))mn(p)=mn(p<<1),cnt(p)=cnt(p<<1)+cnt(p<<1|1);
else{
if(mn(p<<1)<mn(p<<1|1))mn(p)=mn(p<<1),cnt(p)=cnt(p<<1);
else mn(p)=mn(p<<1|1),cnt(p)=cnt(p<<1|1);
}
}
void tag(int p,int v){
mn(p)+=v;
lz(p)+=v;
}
void sprdwn(int p){
tag(p<<1,lz(p));tag(p<<1|1,lz(p));
lz(p)=0;
}
void add(int l,int r,int v,int p=1){
if(l<=l(p)&&r>=r(p))return tag(p,v);
sprdwn(p);
int mid=l(p)+r(p)>>1;
if(l<=mid)add(l,r,v,p<<1);
if(r>mid)add(l,r,v,p<<1|1);
sprup(p);
}
int zero(){return mn(1)==0?cnt(1):0;}
}segt;
vector<pair<pair<int,int>,int> > add[N+2];
int ans[N+1];
int main(){
freopen("b.in","r",stdin);freopen("b.out","w",stdout);
cin>>n>>m;
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
nei[x].pb(y);nei[y].pb(x);
}
dfs();
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
d[dfn[x]]++,d[mxdfn[x]+1]--,d[dfn[y]]++,d[mxdfn[y]+1]--;
if(dfn[x]<=dfn[y]&&mxdfn[y]<=mxdfn[x]){
add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
}
else if(dfn[y]<=dfn[x]&&mxdfn[x]<=mxdfn[y]){
add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
}
else{
add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
add[dfn[y]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
add[dfn[x]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
}
}
for(int i=1;i<=n;i++)d[i]+=d[i-1];
segt.init();
for(int i=1;i<=n;i++){
for(int j=0;j<add[i].size();j++){
int l=add[i][j].X.X,r=add[i][j].X.Y,v=add[i][j].Y;
segt.add(l,r,v);
}
ans[mng[i]]=n-segt.zero()-!!d[i];
}
for(int i=1;i<=n;i++)printf("%d%c",ans[i],"
"[i==n]);
return 0;
}
方法四
不难发现这题还有另一个关于操作时效性的性质:由于树可以表示成括号序列,所以任意一次减法(即对加法的消除影响)都是撤销操作。
ymx 傻乎乎的维护了一个主席树,区间赋 (1)、回到历史版本、区间求和。其实根本不需要(只是我不会写的说),用我自己发明的「可撤销线段树」即可。这个东西思想很简单,跟可撤销并查集一样,每一次操作在栈中压入一个修改序列,把所有有修改的节点的原样给记录下来,撤销的时候就还原就可以了。
不难发现,这样空间跟主席树一样,是 (mathrm O(mlog n)) 的(捂脸
代码:
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define mp make_pair
#define X first
#define Y second
const int N=100000,M=100000,DB_SZ=333;
int n,m;
vector<int> nei[N+1];
int dfn[N+1],mxdfn[N+1],mng[N+1],nowdfn;
void dfs(int x=1,int fa=0){
mng[dfn[x]=mxdfn[x]=++nowdfn]=x;
for(int i=0;i<nei[x].size();i++){
int y=nei[x][i];
if(y==fa)continue;
dfs(y,x);
mxdfn[x]=mxdfn[y];
}
}
int d[N+2];
struct segtree{
struct node{int l,r,sum;bool lz;}nd[N<<2];
#define l(p) nd[p].l
#define r(p) nd[p].r
#define sum(p) nd[p].sum
#define lz(p) nd[p].lz
stack<stack<pair<int,node> > > stk;
void bld(int l=1,int r=n,int p=1){
l(p)=l;r(p)=r;sum(p)=r-l+1;lz(p)=0;
if(l==r)return;
int mid=l+r>>1;
bld(l,mid,p<<1);bld(mid+1,r,p<<1|1);
}
void init(){bld();}
void sprup(int p){
stk.top().push(mp(p,nd[p]));
sum(p)=sum(p<<1)+sum(p<<1|1);
}
void tag(int p){
stk.top().push(mp(p,nd[p]));
sum(p)=0;lz(p)=true;
}
void sprdwn(int p){
stk.top().push(mp(p,nd[p]));
if(lz(p))tag(p<<1),tag(p<<1|1),lz(p)=false;
}
void chg(int l,int r,int p=1){
if(p==1)stk.push(stack<pair<int,node> >());
if(l<=l(p)&&r>=r(p))return tag(p);
sprdwn(p);
int mid=l(p)+r(p)>>1;
if(l<=mid)chg(l,r,p<<1);
if(r>mid)chg(l,r,p<<1|1);
sprup(p);
}
int zero(){return sum(1);}
void cancel(){
stack<pair<int,node> > s=stk.top();
stk.pop();
while(s.size())nd[s.top().X]=s.top().Y,s.pop();
}
}segt;
vector<pair<pair<int,int>,int> > add[N+2];
int ans[N+1];
int main(){
freopen("b.in","r",stdin);freopen("b.out","w",stdout);
cin>>n>>m;
for(int i=1;i<n;i++){
int x,y;
scanf("%d%d",&x,&y);
nei[x].pb(y);nei[y].pb(x);
}
dfs();
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
d[dfn[x]]++,d[mxdfn[x]+1]--,d[dfn[y]]++,d[mxdfn[y]+1]--;
if(dfn[x]<=dfn[y]&&mxdfn[y]<=mxdfn[x]){
add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
}
else if(dfn[y]<=dfn[x]&&mxdfn[x]<=mxdfn[y]){
add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
}
else{
add[dfn[x]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
add[dfn[y]].pb(mp(mp(dfn[x],mxdfn[x]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[x],mxdfn[x]),-1));
add[dfn[x]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[x]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
add[dfn[y]].pb(mp(mp(dfn[y],mxdfn[y]),1));
add[mxdfn[y]+1].pb(mp(mp(dfn[y],mxdfn[y]),-1));
}
}
for(int i=1;i<=n;i++)d[i]+=d[i-1];
segt.init();
for(int i=1;i<=n;i++){
for(int j=0;j<add[i].size();j++){
int v=add[i][j].Y;
if(v==-1)segt.cancel();
}
for(int j=0;j<add[i].size();j++){
int l=add[i][j].X.X,r=add[i][j].X.Y,v=add[i][j].Y;
if(v==1)segt.chg(l,r);
}
ans[mng[i]]=n-segt.zero()-!!d[i];
}
for(int i=1;i<=n;i++)printf("%d%c",ans[i],"
"[i==n]);
return 0;
}
对比
中间那次 WA 的不管,从下到上分别是第 (1sim4) 种方法。
可见,分块复杂度大,劣是正常的;中间两种方法比较好也是理所当然的;而最后一种「我发明」的数据结构,虽然复杂度要优于分块,但是时空却被其他三种方法全方位吊打,说明我是 sb!!!