暴力美学:替罪羊树
替罪羊树
简述
替罪羊树是一种体现代码暴力美学的数据结构。
虽然暴力,但它不是像分块、莫队那样的根号算法,它是一种 \(\log\) 算法。
多了解几个平衡树,会发现每棵树都有自己的特点,比如:
- Treap 是 BST 与堆的结合体;
- Splay 特有的伸展到根;
- Fhq_Treap 类似于拼图;
- 红黑树的节点染色;
- 替罪羊树暴力重构。
这里的暴力重构,就是指将一个子树转换为序列,再重新建树。
注:本文例题为 P3369,代码以 A 掉此题为目的。
定义与储存
和正常的 BST 差不多,只是多了一个实际节点个数 fac
。至于用处,等下再解释。
struct Tzys{
int lc,rc;
int val,cnt;
int siz,fac;
}T[inf];
然后是一些基本的辅助函数:
新建一个节点:
int new_(int k)
{
T[++cnt].val=k;T[cnt].cnt=1;
T[cnt].siz=T[cnt].fac=1;
return cnt;
}
更新子树大小:
void pushup(int i)
{
T[i].siz=T[T[i].lc].siz+T[T[i].rc].siz+T[i].cnt;
T[i].fac=T[T[i].lc].fac+T[T[i].rc].fac+T[i].cnt;
}
但是,有时候需要将某个点到根的路径上所有点的子树大小都更新一遍。因为没有维护父指针,所以需要一个递归实现:
void maintain(int now,int to)
{
if(now==to)return;
if(T[to].val<T[now].val)
maintain(T[now].lc,to);
else maintain(T[now].rc,to);
pushup(now);
}
插入、删除
插入和 BST 的插入方式相同,只是在操作之后需要检验是否平衡。
而删除操作并不是真正的删除,而是惰性删除,即只减小 fac
而不改变 siz
,等暴力重构的时候在重新统计 siz
。
void insert(int &now,int k)
{
if(!now)
{
now=new_(k);
check(rot,now);
return;
}
T[now].siz++,T[now].fac++;
if(k==T[now].val)
{
T[now].cnt++;
check(rot,now);
return;
}
if(k<T[now].val)
insert(T[now].lc,k);
else insert(T[now].rc,k);
}
void remove(int now,int k)
{
T[now].fac--;
if(T[now].val==k)
{
T[now].cnt--;
check(rot,now);
return;
}
if(k<T[now].val)
remove(T[now].lc,k);
else remove(T[now].rc,k);
}
检查平衡
注意到,重构是将整棵子树拍平,所以将深度较浅的点重构之后,深度较深的点绝对不需要再次重构;但深度较深的点重构之后,深度较浅的点可能需要再次重构。
所以每次重构需要找到最浅的需要重构的节点。
而且,可以发现,只有插入的点经过的路径可能需要重构,对其他的点的平衡没有影响。
所以就用一个递归,从根开始找。
void check(int &now,int to)
{
if(now==to)return;
if(pd(now))
{
h.clear();assign(now);
rebuild(now,0,h.size()-1);
maintain(rot,now);
return;
}
if(T[to].val<T[now].val)
check(T[now].lc,to);
else check(T[now].rc,to);
}
那么,怎么判断当前节点是否需要重构呢?
有两点:
-
当前节点的左右子树中存在一棵子树的节点个数大于当前节点子树的节点个数乘平衡因子。
-
当前节点的子树中非真实存在的节点个数大于子树节点个数的 \(30\%\)。
其中平衡因子常取 \(0.75\)。
bool pd(int i){return (max(T[T[i].lc].siz,T[T[i].rc].siz)>T[i].siz*0.75)||(T[i].siz-T[i].fac>T[i].siz*0.3);}
好像不是很可读……
bool pd(int i)
{
return (max(T[T[i].lc].siz,T[T[i].rc].siz)>T[i].siz*0.75)||
(T[i].siz-T[i].fac>T[i].siz*0.3);
}
应该能好点吧……
重构
重构比较简单,分为两个步骤:拍平和重建。
拍平很简单,按照中序遍历来就好:
vector<int>h;
void assign(int now)
{
if(!now)return;
assign(T[now].lc);
if(T[now].cnt)h.push_back(now);
assign(T[now].rc);
}
当然,被惰性删掉的点不需要再加进去了。
重建则和 Splay 实现区间平衡树的建树时一样的。
void rebuild(int &i,int l,int r)
{
if(l>r){i=0;return;}
int mid=(l+r)>>1;
i=h[mid];
rebuild(T[i].lc,l,mid-1);
rebuild(T[i].rc,mid+1,r);
pushup(i);
}
至此,替罪羊树的核心就结束了。
查询
查询操作就和普通的 BST 一样了,此处不在赘述。
但需要注意的是,替罪羊树的某棵子树真实存在的节点个数是 fac
个而不是 siz
个。
int ask_rnk(int k)
{
int now=rot,ans=1;
while(now)
{
if(k==T[now].val)return ans+T[T[now].lc].fac;
if(k<T[now].val)now=T[now].lc;
else ans+=T[T[now].lc].fac+T[now].cnt,now=T[now].rc;
}
return ans;
}
int ask_kth(int k)
{
int now=rot;
while(1)
{
if(T[T[now].lc].fac+1<=k&&k<=T[T[now].lc].fac+T[now].cnt)
return T[now].val;
if(k<=T[T[now].lc].fac)now=T[now].lc;
else k-=T[T[now].lc].fac+T[now].cnt,now=T[now].rc;
}
}
至于前驱后继,没必要在单独打函数,可以和权值线段树那里一样的实现方式。
if(op==3)wr(ask_rnk(k),'\n');
if(op==4)wr(ask_kth(k),'\n');
if(op==5)wr(ask_kth(ask_rnk(k)-1),'\n');
if(op==6)wr(ask_kth(ask_rnk(k+1)),'\n');
不过想自己打也不是不可以(笑。
完整代码
const int inf=1e5+7;
int n;
struct Tzys{
int lc,rc;
int val,cnt;
int siz,fac;
}T[inf];
int rot,cnt;
int new_(int k)
{
T[++cnt].val=k;T[cnt].cnt=1;
T[cnt].siz=T[cnt].fac=1;
return cnt;
}
void pushup(int i)
{
T[i].siz=T[T[i].lc].siz+T[T[i].rc].siz+T[i].cnt;
T[i].fac=T[T[i].lc].fac+T[T[i].rc].fac+T[i].cnt;
}
bool pd(int i){return (max(T[T[i].lc].siz,T[T[i].rc].siz)>T[i].siz*0.75)||(T[i].siz-T[i].fac>T[i].siz*0.3);}
vector<int>h;
void assign(int now)
{
if(!now)return;
assign(T[now].lc);
if(T[now].cnt)h.push_back(now);
assign(T[now].rc);
}
void rebuild(int &i,int l,int r)
{
if(l>r){i=0;return;}
int mid=(l+r)>>1;
i=h[mid];
rebuild(T[i].lc,l,mid-1);
rebuild(T[i].rc,mid+1,r);
pushup(i);
}
void maintain(int now,int to)
{
if(now==to)return;
if(T[to].val<T[now].val)
maintain(T[now].lc,to);
else maintain(T[now].rc,to);
pushup(now);
}
void check(int &now,int to)
{
if(now==to)return;
if(pd(now))
{
h.clear();assign(now);
rebuild(now,0,h.size()-1);
maintain(rot,now);
return;
}
if(T[to].val<T[now].val)
check(T[now].lc,to);
else check(T[now].rc,to);
}
void insert(int &now,int k)
{
if(!now)
{
now=new_(k);
check(rot,now);
return;
}
T[now].siz++,T[now].fac++;
if(k==T[now].val)
{
T[now].cnt++;
check(rot,now);
return;
}
if(k<T[now].val)
insert(T[now].lc,k);
else insert(T[now].rc,k);
}
void remove(int now,int k)
{
T[now].fac--;
if(T[now].val==k)
{
T[now].cnt--;
check(rot,now);
return;
}
if(k<T[now].val)
remove(T[now].lc,k);
else remove(T[now].rc,k);
}
int ask_rnk(int k)
{
int now=rot,ans=1;
while(now)
{
if(k==T[now].val)return ans+T[T[now].lc].fac;
if(k<T[now].val)now=T[now].lc;
else ans+=T[T[now].lc].fac+T[now].cnt,now=T[now].rc;
}
return ans;
}
int ask_kth(int k)
{
int now=rot;
while(1)
{
if(T[T[now].lc].fac+1<=k&&k<=T[T[now].lc].fac+T[now].cnt)
return T[now].val;
if(k<=T[T[now].lc].fac)now=T[now].lc;
else k-=T[T[now].lc].fac+T[now].cnt,now=T[now].rc;
}
}
int main()
{
n=re();
while(n--)
{
int op=re(),k=re();
if(op==1)insert(rot,k);
if(op==2)remove(rot,k);
if(op==3)wr(ask_rnk(k),'\n');
if(op==4)wr(ask_kth(k),'\n');
if(op==5)wr(ask_kth(ask_rnk(k)-1),'\n');
if(op==6)wr(ask_kth(ask_rnk(k+1)),'\n');
}
return 0;
}
指针实现
const int inf=1e5+7;
int n;
struct Tzy{
int val,cnt;
int siz,fac;
Tzy *lc,*rc;
Tzy(){}
Tzy(int val,int cnt,int siz,int fac,Tzy* lc,Tzy* rc):
val(val),cnt(cnt),siz(siz),fac(fac),lc(lc),rc(rc){}
}*rot,*nul;
void pushup(Tzy* &i)
{
i->siz=i->lc->siz+i->rc->siz+i->cnt;
i->fac=i->lc->fac+i->rc->fac+i->cnt;
}
bool pd(Tzy* i){return std::max(i->lc->siz,i->rc->siz)>i->siz*0.75||i->siz-i->fac>i->siz*0.3;}
std::vector<Tzy*>h;
void assign(Tzy* now)
{
if(now==nul)return ;
assign(now->lc);
if(now->cnt)h.push_back(now);
assign(now->rc);
}
void rebuild(Tzy* &i,int l,int r)
{
if(l>r){i=nul;return;}
int mid=(l+r)>>1;
i=h[mid];
rebuild(i->lc,l,mid-1);
rebuild(i->rc,mid+1,r);
pushup(i);
}
void maintain(Tzy* &now,Tzy* to)
{
if(now==to)return;
if(to->val<now->val)
maintain(now->lc,to);
else maintain(now->rc,to);
pushup(now);
}
void check(Tzy* &now,Tzy* to)
{
if(now==to)return;
if(pd(now))
{
h.clear();assign(now);
rebuild(now,0,h.size()-1);
maintain(rot,now);
return;
}
if(to->val<now->val)
check(now->lc,to);
else check(now->rc,to);
}
void insert(Tzy* &now,int k)
{
if(now==nul)
{
now=new Tzy(k,1,1,1,nul,nul);
check(rot,now);
return;
}
now->siz++,now->fac++;
if(k==now->val)
{
now->cnt++;
check(rot,now);
return;
}
if(k<now->val)
insert(now->lc,k);
else insert(now->rc,k);
}
void remove(Tzy* now,int k)
{
now->fac--;
if(now->val==k)
{
now->cnt--;
check(rot,now);
return;
}
if(k<now->val)
remove(now->lc,k);
else remove(now->rc,k);
}
int ask_rnk(int k)
{
Tzy* now=rot;int ans=1;
while(now!=nul)
{
if(k==now->val)return ans+now->lc->fac;
if(k<now->val)now=now->lc;
else ans+=now->lc->fac+now->cnt,now=now->rc;
}
return ans;
}
int ask_kth(int k)
{
Tzy* now=rot;
while(1)
{
if(now->lc->fac+1<=k&&k<=now->lc->fac+now->cnt)
return now->val;
if(k<=now->lc->fac)now=now->lc;
else k-=now->lc->fac+now->cnt,now=now->rc;
}
}
int main()
{
n=re();
rot=nul=new Tzy(0,0,0,0,0,0);
while(n--)
{
int op=re(),k=re();
if(op==1)insert(rot,k);
if(op==2)remove(rot,k);
if(op==3)wr(ask_rnk(k),'\n');
if(op==4)wr(ask_kth(k),'\n');
if(op==5)wr(ask_kth(ask_rnk(k)-1),'\n');
if(op==6)wr(ask_kth(ask_rnk(k+1)),'\n');
}
return 0;
}
时间复杂度证明
替罪羊树的优势
树套树都听说过吧。
但是你知道平衡树做外层树吗?
如果平衡树的每个节点都有套有一个内层树的话,那么用普通的旋转的平衡树的话,我们就不得不在旋转的时候合并子节点的内层树,而这样的单次操作时间复杂度为 \(O(n)\)。
但用替罪羊树就不一样了。它的结构基本稳定,只需要偶尔重构,且子树大小较大的节点重构一次之后很久才需要进行下一次重构,所以在树套树中可以作为外层树使用。
比如这题。
我不会啊,OI-wiki 上没写 qwq。