前言
这玩意儿也太抽象了,均摊的艺术。
一、原理
首先,替罪羊树也是一个二叉搜索树,所以每次插入节点就是按照二叉搜索树的方法插入。
替罪羊树的原理非常简单,就是设置一个平衡因子 alpha,每次检查以当前节点为根的子树是否平衡。方法就是,令当前节点为 h,子树大小为 n,一旦 h 的左子树或右子树的大小大于 alpha*n,此时就认为该子树不平衡。
调整的方法也很简单粗暴,就是先暴力中序遍历整棵树,此时因为是二叉搜索树,所以收集上来的必然是个有序数组。之后直接按照二分的方法暴力重构这棵树,即每次以二分的中点为头,然后去左侧和右侧重构子树。此时也可以想到,当发生不平衡时,因为反正要重构,所以肯定是选择最上方不平衡的位置,重构一次即可。
可以发现,其复杂度完全取决于这个平衡因子。当平衡因子取到小于等于 0.5 时,此时即使是非常平衡的二叉搜索树,由于左右子树都是整体的一半,也会触发重构,那么每插入一个节点就需要重构了。而当平衡因子取到大于等于 1 时,此时不管左子树还是右子树,其大小都不可能比整棵树还大,那么就永远都不会重构了。所以,为了保证重构次数和树高取到一个相对平衡的点,考虑选择平衡因子 0.7。
二、题目------普通平衡树
这也太像线段树了......
cpp
#include <bits/stdc++.h>
using namespace std;
/* /\_/\
* (= ._.)
* / > \>
*/
/*
*想好再写
*注意审题 注意特判
*不要红温 不要急躁 耐心一点
*WA了不要立马觉得是思路不对 先耐心找反例
*/
#define endl '\n'
#define dbg(x) cout<<#x<<endl;cout<<x<<endl;
#define vdbg(a) cout<<#a<<endl;for(auto x:a)cout<<x<<" ";cout<<endl;
#define YES cout<<"YES"<<endl;return ;
#define Yes cout<<"Yes"<<endl;return ;
#define NO cout<<"NO"<<endl;return ;
#define No cout<<"No"<<endl;return ;
#define popcount __builtin_popcount
using ll=long long;
using ld=long double;
using pii=pair<int,int>;
using pll=pair<ll,ll>;
const int INF=1e9;
const ll INFLL=1e18;
const int dx[]={-1,1,0,0};
const int dy[]={0,0,-1,1};
const int ddx[]={-2,-1,1,2,2,1,-1,-2};
const int ddy[]={1,2,2,1,-1,-2,-2,-1};
template<typename T>
struct ScapeGoat
{
private:
double ALPHA=0.7;
vector<T>key;
vector<int>count;
vector<int>left;
vector<int>right;
vector<int>size;
vector<int>node;
int head=0;
int cnt;
vector<int>tmp;
int ci;
int top; //highest unbalanced node
int father; //father of top
int side; //top in which side of father
int init(T num){
key[++cnt]=num;
left[cnt]=right[cnt]=0;
count[cnt]=size[cnt]=node[cnt]=1;
return cnt;
}
void up(int i){
size[i]=size[left[i]]+size[right[i]]+count[i];
node[i]=node[left[i]]+node[right[i]]+(count[i]>0);
}
void inorder(int i){
if(i!=0){
inorder(left[i]);
if(count[i]>0){
tmp[++ci]=i;
}
inorder(right[i]);
}
}
int build(int l,int r){
if(l>r){
return 0;
}
int m=l+r>>1;
int h=tmp[m];
left[h]=build(l,m-1);
right[h]=build(m+1,r);
up(h);
return h;
}
void rebuild(){
if(top!=0){ //need rebuild
ci=0;
inorder(top);
if(ci>0){
if(father==0){ //top is head
head=build(1,ci);
}else if(side==1){ //top is on the left of father
left[father]=build(1,ci);
}else{
right[father]=build(1,ci);
}
}
}
}
bool balance(int i){
return ALPHA*node[i]>=max(node[left[i]],node[right[i]]);
}
void insert(int i,int f,int s,T num){
if(i==0){
if(f==0){
head=init(num);
}else if(s==1){
left[f]=init(num);
}else{
right[f]=init(num);
}
}else{
if(key[i]==num){
count[i]++;
}else if(key[i]>num){
insert(left[i],i,1,num);
}else{
insert(right[i],i,2,num);
}
up(i);
if(!balance(i)){
top=i,father=f,side=s;
}
}
}
void erase(int i,int f,int s,T num){
if(key[i]==num){
count[i]--;
}else if(key[i]>num){
erase(left[i],i,1,num);
}else{
erase(right[i],i,2,num);
}
up(i);
if(!balance(i)){
top=i,father=f,side=s;
}
}
int small(int i,T num){
if(i==0){
return 0;
}
if(key[i]>=num){
return small(left[i],num);
}
return size[left[i]]+count[i]+small(right[i],num);
}
int index(int i,int x){
if(size[left[i]]>=x){
return index(left[i],x);
}else if(size[left[i]]+count[i]<x){
return index(right[i],x-size[left[i]]-count[i]);
}
return key[i];
}
public:
ScapeGoat(int n):key(n),count(n),left(n),right(n),size(n),node(n),tmp(n){
head=cnt=0;
}
void insert(T num){
top=father=side=0;
insert(head,0,0,num);
rebuild();
}
void erase(T num){
if(rank(num)!=rank(num+1)){
top=father=side=0;
erase(head,0,0,num);
rebuild();
}
}
int rank(T num){
return small(head,num)+1;
}
int index(int x){
return index(head,x);
}
int pre(T num){
int kth=rank(num);
if(kth==1){
return -INF;
}
return index(kth-1);
}
int post(T num){
int kth=rank(num+1);
if(kth==size[head]+1){
return INF;
}
return index(kth);
}
};
void solve()
{
int q;
cin>>q;
ScapeGoat<int>tree(q+1);
int op,x;
while(q--)
{
cin>>op;
if(op==1)
{
cin>>x;
tree.insert(x);
}
else if(op==2)
{
cin>>x;
tree.erase(x);
}
else if(op==3)
{
cin>>x;
cout<<tree.rank(x)<<endl;
}
else if(op==4)
{
cin>>x;
cout<<tree.index(x)<<endl;
}
else if(op==5)
{
cin>>x;
cout<<tree.pre(x)<<endl;
}
else if(op==6)
{
cin>>x;
cout<<tree.post(x)<<endl;
}
}
}
void init()
{
}
signed main()
{
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
int t=1;
//cin>>t;
init();
while(t--)
{
solve();
}
return 0;
}
在替罪羊树中,当一个节点的词频降到 0 了,此时不立即删除这个节点,因为可能导致重构的次数增多,所以在统计的时候需要判断词频信息。
由于每次会重构树,所以需要亿点分讨来判断修改整个头节点和子树的情况。除此之外,此时可以考虑使用 rank 和 index 方法来方便求解 pre 和 post。
总结
马上重量级就要来了()