高级数据结构 (初版)

第四单元

一. 并查集

1.普通并查集

const int N=1e5;
int s[N];
int height[N];

void init_set(){
    for(int i=1;i<=N;i++){
        s[i]=i;
        height[i]=0;
    }
}

int find_set(int x){
    if(x!=s[x])
        s[x]=find_set(s[x]);
   return s[x];
}
//防爆栈板find_set函数
/*
int find_set(int x){
    int r=x;
    while(s[r]!=r)
        r=s[r];
    int i=x,j;
    while(i!=r){
        j=s[i];
        s[i]=r;
        i=j;
    }
    return r;
}
*/


void merge_set(int x,int y){
    x=find_set(x);
    y=find_set(y);
    
    if(height[x]==height[y]){
        height[x]=height[x]+1;
        s[y]=x;
    }
    else{
        if(height[x]<height[y])
            s[x]=y;
        else
            s[y]=x;
    }
}

2.带权并查集

const int N = 200010;
int s[N];                         
int d[N];                         
int ans;
void init_set(){                  
    for(int i = 0; i <= N; i++) { s[i] = i; d[i] = 0;  }
}
int find_set(int x){              
    if(x != s[x]) {
         int t = s[x];            
         s[x] = find_set(s[x]);   
         d[x] += d[t];            
     }
    return s[x];
}
void merge_set(int a, int b,int v){    
    int roota = find_set(a), rootb = find_set(b);
    s[roota] = rootb; //前缀和               
    d[roota] = d[b]- d[a] + v;
}

二. 树状数组

前言:树状数组可以解决的,线段树也可以解决,树状数组只是在一些问题上有奇效,优先建议练习时还是优先使用线段树

本质上,解决的问题实际是 差分数组 − > 原始数组 − > 前缀和数组 差分数组->原始数组->前缀和数组 差分数组>原始数组>前缀和数组 这其中要使用其二或其三的一种使用树优化效率的一种方法

1. 单点修改+区间查询

const int N = 1000;
#define lowbit(x)  ((x) & - (x))
int tree[N]={0};

void update(int x, int d) {   //单点修改:修改元素a[x],  a[x] = a[x] + d
     while(x <= N) {
        tree[x] += d;
        x += lowbit(x);
     }
}

int sum(int x) {             //查询前缀和:返回前缀和sum = a[1] + a[2] +... + a[x]
    int ans = 0;
    while(x > 0){
        ans += tree[x];
        x -= lowbit(x);
    }
    return ans;
}

2. 区间修改+单点查询

在上面的基础上,每个节点由差分数组组成

对于区间,左端L节点 + d +d +d,右端R+1节点 − d -d d

用sum求得单点值

3. 区间修改+区间查询

在上面的基础上,每个节点由差分数组组成

s u m ( L , R ) = s u m ( 1 , R ) − s u m ( 1 , L − 1 ) sum(L,R)=sum(1,R)-sum(1,L-1) sum(L,R)=sum(1,R)sum(1,L1)

s u m ( 1 , R ) = ∑ i = 1 k D i − ∑ i = 1 k ( i − 1 ) D i sum(1,R)=\sum_{i=1}^{k}{D_i}-\sum_{i=1}^k{(i-1)D_i} sum(1,R)=i=1kDii=1k(i1)Di

从而实现从差分数组到前缀和的转变

4. 单点修改+区间最值

const int N = 2e5+10; 
int n,m,a[N],tree[N]; 
int lowbit(int x){return x&(-x);}

void update(int x,int value){          //更新tree[x]的最大值
    while(x<=n){
        tree[x]=value;
        for(int i=1;i<lowbit(x);i<<=1)
            tree[x]=max(tree[x],tree[x-i]);
        x+=lowbit(x);
    }
}

int query(int L,int R){               //关于区间[L,R]的最值
    int ans=;
    while(L<R){
        ans=max(ans,a[R]);
        R--;
        while(R-L>=lowbit(R)){
            ans=max(ans,tree[R]);
            R-=lowbit(R);
        }
    }
    return ans;
}

三. 线段树

1. 区间查询

int tree[N*4];

int ls(int p){
    return p<<1;
}
int rs(int p){
    return p<<1|1;
}

void push_up(int p){
    tree[p]=tree[ls(p)]+tree[rs(p)];
    //tree[p]=min(tree[ls(p)],tree[rs(p)]);
    /*
    注意这个地方决定了子区间与父区间之间的关系
    */
}

void build(int p,int pl,int pr){  //默认(1,1,n)建树,基本这个保持不变
    if(pl==pr){
        tree[p]=a[pl];
        return;
    }
    int mid=(pl+pr)>>1;
    build(ls(p),pl.mid);
    build(rs(p),mid+1,pr);
    push_up(p);
}

int quary(int L, int R, int p, int pl, int pr) { //找[L,R]区间内的值,后面三位默认1,1,n 
	if(L <= pl && pr <= R)
		return tree[P];
	ll mid = (pl + pr) >> 1;
	ll res  = 0;
	if(L <= mid)
		res += query(L, R, ls(p), pl, mid);
	if(R > mid)
		res += query(L, R, rs(p), mid + 1, pr);
	return res;
}

2. 区间修改+区间查询

ll a[N];
ll tree[N << 2];
ll tag[N << 2];
ll ls(ll p) {
	return p << 1;
}
ll rs(ll p) {
	return p << 1 | 1;
}
void push_up(ll p) {
	tree[p] = tree[ls(p)] + tree[rs(p)];
}
void build(ll p, ll pl, ll pr) {
	tag[p] = 0;
	if(pl == pr) {
		tree[p] = a[pl];
		return;
	}
	ll mid = (pl + pr) >> 1;
	build(ls(p), pl, mid);
	build(rs(p), mid + 1, pr);
	push_up(p);
}

void addtag(ll p, ll pl, ll pr, ll d) {//注意:如果想使用区间修改来优化,重点就是修改这一部分
	tag[p] += d;
	tree[p] += d * (pr - pl + 1);
}

void push_down(ll p, ll pl, ll pr) {
	if(tag[p]) {
		ll mid = (pl + pr) >> 1;
		addtag(ls(p), pl, mid, tag[p]);
		addtag(rs(p), mid + 1, pr, tag[p]);
		tag[p] = 0;
	}
}

void update(ll L, ll R, ll p, ll pl, ll pr, ll d) {
	if(L <= pl && pr <= R) {
		addtag(p, pl, pr, d);
		return;
	}
	push_down(p, pl, pr);
	ll mid = (pl + pr) >> 1;
	if(L <= mid)
		update(L, R, ls(p), pl, mid, d);
	if(R > mid)
		update(L, R, rs(p), mid + 1, pr, d);
	push_up(p);
}

ll query(ll L, ll R, ll p, ll pl, ll pr) {

	if(pl >= L && R >= pr)
		return tree[p];
	push_down(p, pl, pr);
	ll res = 0;
	ll mid = (pl + pr) >> 1;
	if(L <= mid)
		res += query(L, R, ls(p), pl, mid);
	if(R > mid)
		res += query(L, R, rs(p), mid + 1, pr);
	return res;
}

3.区间最值

主要是通过tag标签来简化区间运算的,重点和难点在于tag的定义

4. 区间合并

主要是通过tag标签来简化区间运算的,重点和难点在于tree数组的定义

5.扫描线

1. 面积
int ls(int p){ return p<<1;  }   
int rs(int p){ return p<<1|1;}    

const int N = 20005;

int Tag[N];            //标志:线段是否有效,能否用于计算宽度
double length[N];      //存放区间i的总宽度
double xx[N];          //存放矩形右边x坐标值,下标用lower_bound查找

struct ScanLine{       //定义扫描线
    double y;                       //边的y坐标
    double right_x,left_x;          //边的x坐标:右、左
    int inout;                      //入边为1,出边为-1
    ScanLine(){}
ScanLine(double y,double x2,double x1,int io):
y(y),right_x(x2),left_x(x1),inout(io){}
}line[N];

bool cmp(ScanLine &a,ScanLine &b) { return a.y<b.y; }   //y坐标排序

void pushup(int p,int pl,int pr){          //从下往上传递区间值
if(Tag[p])    length[p] = xx[pr]-xx[pl];            
       //结点的Tag为正,这个线段对计算宽度有效。计算宽度   
    else if(pl+1 == pr)  length[p] = 0;    //叶子结点没有宽度
    else length[p] = length[ls(p)] + length[rs(p)];
}

void update(int L,int R,int io,int p,int pl,int pr){
    if(L<=pl && pr<=R){                //完全覆盖
        Tag[p] += io;                  //结点的标志,用来判断能否用来计算宽度
        pushup(p,pl,pr);
        return;
    }
    if(pl+1 == pr)  return;            //叶子结点
    int mid = (pl+pr) >> 1;
    if(L<=mid)  update(L,R,io,ls(p),pl,mid);
    if(R>mid)   update(L,R,io,rs(p),mid,pr);   //注意不是mid+1
    pushup(p,pl,pr);
}

int main(){    
    int n, t = 0;
    while(scanf("%d",&n),n){
        int cnt = 0;        //边的数量,包括入边和出边
        while(n--){ 
            double x1,x2,y1,y2; scanf("%lf%lf%lf%lf",&x1,&y1,&x2,&y2);//输入一个矩形
            line[++cnt] = ScanLine(y1,x2,x1,1);      //给入边赋值
            xx[cnt] = x1;                            //记录x坐标
            line[++cnt] = ScanLine(y2,x2,x1,-1);     //给出边赋值
            xx[cnt] = x2;                            //记录x坐标
        }
        sort(xx+1,xx+cnt+1);                         //对所有边的x坐标排序
        sort(line+1,line+cnt+1,cmp);            //对扫描线按y轴方向从低到高排序
        int num = unique(xx+1,xx+cnt+1)-(xx+1); //离散化:用unique去重,返回个数
        memset(Tag,0,sizeof(Tag));
        memset(length,0,sizeof(length));              
        double ans = 0;
        for(int i=1;i<=cnt;++i) {                    //扫描所有入边和出边
            int L,R; 
            ans += length[1]*(line[i].y-line[i-1].y);//累加当前扫描线的面积=宽*高
            L = lower_bound(xx+1,xx+num+1,line[i].left_x)-xx; 
                                           //x坐标离散化:用相对位置代替坐标值
            R = lower_bound(xx+1,xx+num+1,line[i].right_x)-xx;   
            update(L,R,line[i].inout,1,1,num);
        }
        printf("Test case #%d\nTotal explored area: %.2f\n\n",++t,ans);
    }
    return 0;
}

2. 周长
int ls(int p){ return p<<1;  }   
int rs(int p){ return p<<1|1;}  
const int N = 200005;
struct ScanLine {
int l, r, h, inout;  //inout=1 下边, inout=-1 上边
ScanLine() {}
ScanLine(int a, int b, int c, int d) :l(a), r(b), h(c), inout(d) {}
}line[N];
bool cmp(ScanLine &a, ScanLine &b) { return a.h<b.h; }   //y坐标排序
bool lbd[N<<2], rbd[N<<2]; //标记这个结点的左右两个端点是否被覆盖(0表示没有,1表示有)
int num[N << 2];           //这个区间有多少条独立的边
int Tag[N << 2];           //标记这个结点是否有效 
int length[N << 2];        //这个区间的有效宽度
void pushup(int p, int pl, int pr) {
	if (Tag[p]) {                  //结点的Tag为正,这个线段对计算宽度有效  
		lbd[p] = rbd[p] = 1;
		length[p] = pr - pl + 1;
		num[p] = 1;               //每条边有两个端点
	}
	else if (pl == pr) length[p]=num[p]=lbd[p]=rbd[p]=0;//叶子结点 
	else {     
		lbd[p] = lbd[ls(p)];      //和左儿子共左端点
		rbd[p] = rbd[rs(p)];      //和右儿子共右端点
		length[p] = length[ls(p)] + length[rs(p)];
		num[p] = num[ls(p)] + num[rs(p)];
		if (lbd[rs(p)] && rbd[ls(p)]) num[p] -= 1;   //合并边
	}
}
void update(int L, int R, int io, int p, int pl, int pr) {
    if(L<=pl && pr<=R){    //完全覆盖
		Tag[p] += io;
		pushup(p, pl, pr);
		return;
	}
	int mid  = (pl + pr) >> 1;
	if (L<= mid) update(L, R, io, ls(p), pl, mid);
	if (mid < R) update(L, R, io, rs(p), mid+1, pr);
	pushup(p, pl, pr);
}
int main() {
int n;
while (~scanf("%d", &n)) {
int cnt  = 0;
int lbd = 10000, rbd = -10000;
for (int i = 0; i < n; i++) {
		 	 int x1,y1,x2,y2; scanf("%d%d%d%d", &x1,&y1,&x2,&y2);   //输入矩形
			 lbd = min(lbd, x1);                      //横线最小x坐标
			 rbd = max(rbd, x2);                      //横线最大x坐标
			 line[++cnt] = ScanLine(x1, x2, y1, 1);   //给入边赋值
			 line[++cnt] = ScanLine(x1, x2, y2, -1);  //给出边赋值
		 }
		 sort(line+1, line + cnt+1, cmp);    	        //排序。数据小,不用离散化 
		 int ans = 0, last = 0;                       //last:上一次总区间被覆盖长度
		 for (int i = 1; i <= cnt ; i++){             //扫描所有入边和出边
		      if (line[i].l < line[i].r) 
                  update(line[i].l, line[i].r-1, line[i].inout, 1, lbd, rbd-1);
			 ans += num[1]*2 * (line[i + 1].h - line[i].h);  //竖线
			 ans += abs(length[1] - last);                   //横线
			 last = length[1];                 
		 }
		 printf("%d\n", ans);
	}
	return 0;
}

四. 可持久化线段树(主席树)

一种记录多个历史时期的线段树的算法,一般用于求第k大/小问题,虽然在空间复杂度上已经有很好的优化,但是时间复杂度相较于分块还是太大,建议使用莫队算法解决这类问题

第k小/大问题
const int N = 200010;
int cnt = 0;        //用cnt标记可以使用的新结点
int a[N], b[N], root[N]; //a[]是原数组,b[]是排序后数组,root[i]记录第i棵线段树的根结点编号
struct{             //定义结点
int L, R, sum;  //L左儿子, R右儿子,sum[i]是结点i的权值(即图中圆圈内的数字)
}tree[N<<5];        //  <<4是乘16倍,不够用;<<5差不多够用   
int build(int pl, int pr){        //初始化一棵空树,实际上无必要
    int rt = ++ cnt;              //cnt为当前结点编号
    tree[rt].sum = 0;
    int mid=(pl+pr)>>1;
    if (pl < pr){
        tree[rt].L = build(pl, mid);
        tree[rt].R = build(mid+1, pr);
    }
    return rt;      //返回当前结点的编号
}
int update(int pre, int pl, int pr, int x){   //建一棵只有logn个结点的新线段树
    int rt = ++cnt;           //新的结点,下面动态开点
    tree[rt].L = tree[pre].L; //该结点的左右儿子初始化为前一棵树相同位置结点的左右儿子
    tree[rt].R = tree[pre].R; 
    tree[rt].sum = tree[pre].sum + 1;  //插了1个数,在前一棵树的相同结点加1
    int mid = (pl+pr)>>1;
    if (pl < pr){            //从根结点往下建logn个结点
        if (x <= mid)        //x出现在左子树,修改左子树 
            tree[rt].L = update(tree[pre].L, pl, mid, x);
        else                 //x出现在右子树,修改右子树
            tree[rt].R = update(tree[pre].R, mid+1, pr, x);
    }
    return rt;               //返回当前分配使用的新结点的编号
}
int query(int u, int v, int pl, int pr, int k){    //查询区间[u,v]第k小
    if (pl == pr) return pl;  //到达叶子结点,找到第k小,pl是结点编号,答案是b[pl] 
    int x = tree[tree[v].L].sum - tree[tree[u].L].sum;   //线段树相减
    int mid = (pl+pr)>>1;
    if (x >= k)     //左儿子数字大于等于k时,说明第k小的数字在左子树
        return query(tree[u].L, tree[v].L, pl, mid, k);
    else            //否则在右子树找第k-x小的数字 
        return query(tree[u].R, tree[v].R, mid+1, pr, k-x);
}
int main(){
    int n, m;    scanf("%d%d", &n, &m);
    for (int i=1; i<=n; i++){ scanf("%d", &a[i]);  b[i]=a[i]; }
    sort(b+1, b+1+n);    //对b排序
    int size = unique(b+1, b+1+n)-b-1; //size等于b数组中不重复的数字的个数
  //root[0] = build(1, size);   //初始化一棵包含size个元素的空树,实际上无必要
    for (int i = 1; i <= n; i ++){     //建n棵线段树
        int x = lower_bound(b+1, b+1+size, a[i]) - b;
                                  //找等于a[i]的b[x]。x是离散化后a[i]对应的值
        root[i] = update(root[i-1], 1, size, x);  
                                  //建第i棵线段树,root[i]是第i棵线段树的根结点
    }
    while (m--){
        int x, y, k;      scanf("%d%d%d", &x, &y, &k);
        int t = query(root[x-1], root[y], 1, size, k); 
                          //第y棵线段树减第x-1棵线段树,就是区间[x,y]的线段树
        printf("%d\n", b[t]);
    }
    return 0;
}

五. 分块与莫队算法

莫队算法=离线+暴力+分块

typedef long long ll;
const ll maxn = 5e5 + 5;
ll a[maxn];
ll partLen;       //块长
ll partID[maxn];  //块号
ll partSum[maxn]; //块和
ll tag[maxn];     //块标记

void add(ll l, ll r, ll c)
{
    ll startID = partID[l], endID = partID[r];
    if (startID == endID) //l和r在同一个块,直接暴力
    {
        for (ll i = l; i <= r; i++)
        {
            a[i] += c;
            partSum[startID] += c;
        }
        return;
    }
    //不在同一个块,分成三段处理
    for (ll i = l; partID[i] == startID; i++)//起始段
    {
        a[i] += c;
        partSum[startID] += c;
    }
    for (ll i = startID + 1; i < endID; i++)//中间若干个整块
    {
        tag[i] += c;
        partSum[i] += c * partLen;
    }
    for (ll i = r; partID[i] == endID; i--)//末尾段
    {
        a[i] += c;
        partSum[endID] += c;
    }
}
//查询的思想和修改基本相同
ll query(ll l, ll r, ll c)
{
    ll ans = 0;
    ll startID = partID[l], endID = partID[r];
    if (startID == endID)
    {
        for (ll i = l; i <= r; i++)
        {
            ans += a[i] + tag[endID];
            ans %= c;
        }
        return ans;
    }
    for (ll i = l; partID[i] == startID; i++)
    {
        ans += a[i] + tag[startID];
        ans %= c;
    }
    for (ll i = startID + 1; i < endID; i++)
    {
        ans += partSum[i];
        ans %= c;
    }
    for (ll i = r; partID[i] == endID; i--)
    {
        ans += a[i] + tag[endID];
        ans %= c;
    }
    return ans;
}
int main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    ll n;
    cin >> n;
    partLen = sqrt(n);
    memset(partID, -1, sizeof(partID));
    for (ll i = 0; i < n; i++)
    {
        cin >> a[i];
        partID[i] = i / partLen + 1;// 第i个应该在那个位置
        partSum[partID[i]] += a[i];
    }
    for (ll i = 0; i < n; i++)
    {
        ll opt, l, r, c;
        cin >> opt >> l >> r >> c;
        l--, r--;
        if (opt)
            cout << query(l, r, c + 1) << '\n';
        else
            add(l, r, c);
    }
    return 0;
}

六. 块状链表

块状链表=分块+链表

注意:莫队算法跟这个都是分块算法,莫队算法不修改数据的规模,块状链表修改数据的规模

int block = 2500;                       //一个块的标准大小 = sqrt(n)
list<vector<char> > List;               //整体是链表,链表的每个元素是一个块
typedef list<vector<char> >::iterator it;

it Find(int &pos) {                     //返回块,并更新x为这个块中的位置
    for (it i = List.begin(); ;i++) {   //逐个找链表上的每个块
        if(i == List.end() || pos <= i->size())  return i;
        pos -= i->size();               //每经过一个块,就更新x
    }
}
void Output(int L, int R)  {            // [L, R)
    it L_block = Find(L), R_block = Find(R);
    for (it it1 = L_block;  ; it1++){               //打印每个块
         int a;  it1 == L_block ? a=L : a=0;        //一个块的起点
         int b;  it1 == R_block ? b=R : b=it1->size();    //块的终点
         for (int i = a; i < b; i++)  putchar(it1->at(i));
         if(it1 == R_block) break;   //迭代器it不能用 <= ,只有 == 和 !=
    }
    putchar('\n');
}
it Next(it x){return ++x; }  //返回下一个块
void Merge(it x) {           //合并块x和块x+1
    x->insert(x->end(), Next(x)->begin(), Next(x)->end());
    List.erase(Next(x));
}
void Split(it x, int pos){   //把第x个块在这个块的pos处分成2块
    if (pos == x->size())  return;         //pos在这个块的末尾
    List.insert(Next(x), vector<char>(x->begin() + pos, x->end()));
                             //把pos后面的部分划给下一个块
    x->erase(x->begin() + pos, x->end());   //删除划出的部分
}
void Update(){                //把每个块重新划成等长的块
    for (it i = List.begin(); i != List.end(); i++){
        while (i->size() >= (block << 1))   //如果块大于2个block,分开
            Split(i, i->size() - block);
        while (Next(i) != List.end() && i->size() + Next(i)->size() <= block)                           
            Merge(i);                       //如果块+下一个块小于block,合并
        while (Next(i) != List.end() && Next(i)->empty()) //删除最后的空块
            List.erase(Next(i));
    }
}
void Insert(int pos, const vector<char>& ch){
    it curr = Find(pos);
    if (!List.empty())     Split(curr, pos);  //把一个块拆为两个
    List.insert(Next(curr), ch);              //把字符串插到两个块中间
    Update();
}
void Delete(int L, int R) {                   // [L, R)区间的元素
    it L_block, R_block;
    L_block = Find(L); Split(L_block, L);
    R_block = Find(R); Split(R_block, R);
    R_block++;
    while(Next(L_block) != R_block)   List.erase(Next(L_block));
    Update();
}
int main(){
    vector<char> ch; int len, pos, n;
    cin >> n;
    while (n--) {
        char opt[7];   cin >> opt;
        if(opt[0]=='M') cin >> pos;
        if(opt[0]=='I'){
            ch.clear();  cin >> len;  ch.resize(len);
            for (int i = 0; i < len; i++){
                ch[i] = getchar(); 
                while(ch[i]<32||ch[i]>126)  ch[i]=getchar();
            }                   //读一个合法字符
            Insert(pos, ch);    //把字符串插入到链表中
        }
        if(opt[0]=='D'){  cin >> len;    Delete(pos, pos + len); }
        if(opt[0]=='G'){  cin >> len;    Output(pos, pos + len); }
        if(opt[0]=='P')  pos--;
        if(opt[0]=='N')  pos++;
    }
    return 0;
}

七. 简单树上问题

1. 树的重心

重心的定义:以某一节点为根,如果该节点的最大子树的节点最小,则u为树的重心(也就是子树最均衡)

const int N = 50005;          //最大结点数
struct Edge{ int to, next;} edge[N<<1];      //两倍:u-v, v-u
int head[N], cnt = 0;
void init(){                  //链式前向星:初始化
    for(int i=0; i<N; ++i){
        edge[i].next = -1; 
        head[i] = -1;
    }
    cnt = 0;
}
void addedge(int u,int v){    //链式前向星:加边u-v
    edge[cnt].to = v;
    edge[cnt].next = head[u];
    head[u] = cnt++;
}
int n;
int d[N], ans[N], num=0, maxnum=1e9;        //d[u]: 以u为根的子树的结点数量
void dfs(int u,int fa){                        
    d[u] = 1;                               //递归到最底层时,结点数加1
    int tmp = 0;
    for(int i=head[u]; ~i; i=edge[i].next){ //遍历u的子结点。~i也可以写成i!=-1
        int v = edge[i].to;                 //v是一个子结点
        if(v == fa) continue;               //不递归父亲
        dfs(v,u);                           //递归子结点,计算v这个子树的结点数量
        d[u] += d[v];                       //计算以u为根的结点数量
        tmp = max(tmp,d[v]);                //记录u的最大子树的结点数量
    }
    tmp = max(tmp, n-d[u]);                 //tmp = u的最大连通块的结点数
    //以上计算出了u的最大连通块
    //下面统计疑似教父。如果一个结点的最大连通块比其他结点的都小,它是疑似教父
    if(tmp < maxnum){                       //一个疑似教父
        maxnum = tmp;                       //更新“最小的”最大连通块 
        num = 0;
        ans[++num] = u;                     //把教父记录在第1个位置
    }
    else if(tmp == maxnum)  ans[++num] = u; //疑似教父有多个,记录在后面            
}
int main(){
    scanf("%d",&n);
    init();
    for(int i=1; i<n; i++){
        int u, v;      scanf("%d %d", &u, &v);
        addedge(u,v);  addedge(v,u);
    }
    dfs(1,0);
    sort(ans+1, ans+1+num);
    for(int i=1;i<=num;i++)   printf("%d ",ans[i]);
}

2. 树的直径

树的直径是树上最远的两点间的距离,又被称为树的最远点对。

1. 两次DFS
  1. 需要完整的路径
  2. 不可以有负权边
const int N=1e5+10;
struct edge{ int to,w;};    //to: 边的终点  w:权值
vector<edge> e[N];          //用邻接表存边
int dist[N];                //记录距离
void dfs(int u,int father,int d){   //用dfs计算从u到每个子结点的距离
    dist[u]=d;
    for(int i=0;i<e[u].size();i++)
        if(e[u][i].to != father)    //很关键,这一句保证不回头搜父结点
            dfs(e[u][i].to, u, d + e[u][i].w);   
}
int main(void){
    int n;   cin>>n;
    for(int i=0;i<n-1;i++){
        int a,b,w; cin>>a>>b>>w;
        e[a].push_back({b,w});       //a的邻居是b,边长w
        e[b].push_back({a,w});       //b的邻居是a
    }
    dfs(1,-1,0);                     //计算从任意点(这里用1号点)到树上每个结点的距离
    int s = 1;
    for(int i=1;i<=n;i++)            //找最远的结点s, s是直径的一个端点
        if(dist[i]>dist[s])  s = i;
    dfs(s,-1,0);                     //从s出发,计算以s为起点,到树上每个结点的距离
    int t = 1;
    for(int i=1;i<=n;i++)            //找直径的另一个端点t
        if(dist[i]>dist[t])  t = i;
    cout << dist[t]<<endl;           //打印树的直径的长度
    return 0;
}

2. 树形DP
  1. 只可以求长度

  2. 可以求负权边

const int N=1e5+10;
struct edge{int to,w; };           //to: 边的终点  w:权值
vector<edge> e[N];
int dp[N];
int maxlen = 0;
bool vis[N];
void dfs(int u){
    vis[u] = true;
    for(int i = 0; i < e[u].size(); ++ i){
        int v = e[u][i].to,  edge = e[u][i].w;
        if(vis[v])   continue;     //v已经算过
        dfs(v);
        maxlen = max(maxlen, dp[u]+ dp[v]+ edge);  
               //计算max{f[u]}。注意此时dp[u]不包括v这棵子树,下一行才包括
        dp[u] = max(dp[u], dp[v] + edge); //计算dp[u],此时包括了v这棵子树
    }
    return ;
}
int main(){
    int n;    cin >> n;
    for(int i = 0; i < n-1; i++){
        int a, b, w;     cin >> a >> b >> w;
        e[a].push_back({b,w});  //a的邻居是b,路的长度w
        e[b].push_back({a,w});  //b的邻居是a
    }
    dfs(1);                     //从点1开始DFS
    cout << maxle
        n << endl;
    return 0;
}

八. LCA

即最近公共祖先,在x,y的公共祖先中,深度最大的被称为最近公共祖先

1. 基础算法

1. 倍增法

在线算法,可以单独处理每个查询

const int N=500005;
struct Edge{int to, next;}edge[2*N];     //链式前向星
int head[2*N], cnt;
void init(){                             //链式前向星:初始化
    for(int i=0;i<2*N;++i){ edge[i].next = -1;   head[i] = -1; }
    cnt = 0;
}
void addedge(int u,int v){               //链式前向星:加边
	edge[cnt].to = v;  edge[cnt].next = head[u];  head[u] = cnt++;
} //以上是链式前向星
int fa[N][20], deep[N];
void dfs(int x,int father){        //求x的深度deep[x]和fa[x][]。father是x的父结点。
    deep[x] = deep[father]+1;      //深度:比父结点深度多1
    fa[x][0] = father;             //记录父结点
    for(int i=1; (1<<i) <= deep[x]; i++)    //求fa[][]数组,它最多到根结点
    	    fa[x][i] = fa[fa[x][i-1]][i-1];
    for(int i=head[x]; ~i; i=edge[i].next)  //遍历结点i的所有孩子。~i可写为i!=-1
        if(edge[i].to != father)            //邻居:除了父亲,都是孩子
           dfs(edge[i].to, x);
}
int LCA(int x,int y){
    if(deep[x]<deep[y])  swap(x,y);  //让x位于更底层,即x的深度值更大
    //(1)把x和y提到相同的深度
    for(int i=19;i>=0;i--)           //x最多跳19次:2^19 > 500005
        if(deep[x]-(1<<i)>=deep[y])  //如果x跳过头了就换个小的i重跳
            x = fa[x][i];            //如果x还没跳到y的层,就更新x继续跳
    if(x==y)  return x;              //y就是x的祖先
    //(2)x和y同步往上跳,找到LCA
    for(int i=19;i>=0;i--)           //如果祖先相等,说明跳过头了,换个小的i重跳
        if(fa[x][i]!=fa[y][i]){      //如果祖先不等,就更新x、y继续跳
            x = fa[x][i];   y = fa[y][i];
        }
    return fa[x][0];                 //最后x位于LCA的下一层,父结点fa[x][0]就是LCA
}
int main(){    
    init();                          //初始化链式前向星
    int n,m,root;  scanf("%d%d%d",&n,&m,&root); 
    for(int i=1;i<n;i++){            //读一棵树,用链式前向星存储
        int u,v;       scanf("%d%d",&u,&v); 
        addedge(u,v);  addedge(v,u);
    }
    dfs(root,0);                      //计算每个结点的深度并预处理fa[][]数组
    while(m--){
        int a,b;   scanf("%d%d",&a,&b); 
        printf("%d\n", LCA(a,b));
    }
    return 0;
}
2. Tarjan算法

离线算法,对查询进行排序后再计算,可以得到很好的效率

const int N=500005;
int fa[N], head[N], cnt, head_query[N], cnt_query, ans[N];
bool vis[N];
struct Edge{ int to, next, num;}edge[2*N], query[2*N];  //链式前向星
void init(){              //链式前向星:初始化
    for(int i=0;i<2*N;++i){
        edge[i].next = -1;  head[i] = -1;
        query[i].next = -1; head_query[i] = -1;
    }
    cnt = 0; cnt_query = 0;
}
void addedge(int u,int v){             //链式前向星:加边
edge[cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt++;
}
void add_query(int x, int y, int num) { //num 第几个查询
query[cnt_query].to = y;
query[cnt_query].num = num;	   //第几个查询
query[cnt_query].next = head_query[x];
head_query[x] = cnt_query++;
}
int find_set(int x) {	                //并查集查询
return fa[x] == x ? x : find_set(fa[x]);
}
void tarjan(int x){                    //tarjan是一个DFS
vis[x] = true;
for(int i=head[x]; ~i; i=edge[i].next){   // ~i可以写为i!=-1
int y = edge[i].to;
if( !vis[y] ) {     //遍历子结点
tarjan(y);
fa[y] = x;      //合并并查集:把子结点y合并到父结点x上
		}
	}
	for(int i = head_query[x]; ~i; i = query[i].next){ //查询所有和x有询问关系的y
	    int y = query[i].to;
	    if( vis[y])                                   //如果to被访问过
	        ans[query[i].num] = find_set(y);          //LCA就是find(y)
	}
}
int main () {
    init();
memset(vis, 0, sizeof(vis));
int n,m,root;  scanf("%d%d%d",&n,&m,&root);
for(int i=1;i<n;i++){                //读n个结点
fa[i] = i;                       //并查集初始化
int u,v;   scanf("%d%d",&u,&v);
        addedge(u,v);  addedge(v,u);     //存边
}
fa[n] = n;                           //并查集的结点n
for(int i = 1; i <= m; ++i) {        //读m个询问
int a, b; scanf("%d%d",&a,&b);
add_query(a, b, i); add_query(b, a, i);  //存查询
}
tarjan(root);
for(int i = 1; i <= m; ++i)	printf("%d\n",ans[i]);
}

2. 树上两点之间的最短距离

两点深度之和减去两倍的LCA深度

d i s t ( x , y ) = d e e p [ x ] + d e e p [ y ] − 2 ∗ d e p p [ L C A ( x , y ) ] dist(x,y)=deep[x]+deep[y]-2*depp[LCA(x,y)] dist(x,y)=deep[x]+deep[y]2depp[LCA(x,y)]

3. 树上差分

树上两点 u − > v u->v u>v可看做 u − > L C A ( x , y ) u->LCA(x,y) u>LCA(x,y) v − > L C A ( x , y ) v->LCA(x,y) v>LCA(x,y),设x为L,LCA(x,y)的父节点为R+1,D[L]++,D[R+1]–,对另一条线路同理

//洛谷P3128,LCA + 树上差分
#include <bits/stdc++.h>
using namespace std;
#define N 50010
struct Edge{int to,next;}edge[2*N]; //链式前向星
int head[2*N],D[N],deep[N],fa[N][20],ans,cnt;
void init();                      
void addedge(int u,int v); 
void dfs1(int x,int father);       
int LCA(int x,int y); //以上4个函数和“树上的倍增”中洛谷P3379的倍增代码完全一样
void dfs2(int u,int fath){
	for (int i=head[u];~i;i=edge[i].next){   //遍历结点i的所有孩子。~i可以写为i!=-1
		int e=edge[i].to;
		if (e==fath) continue;
		dfs2(e,u);
		D[u]+=D[e];
	}
	Ans = max(ans,D[u]);
}
int main(){
    init();        //链式前向星初始化
int n,m;  scanf("%d%d",&n,&m);
for (int i=1;i<n;++i){
         int u,v; scanf("%d%d",&u,&v);
         addedge(u,v); addedge(v,u);
	}
dfs1(1,0);     //计算每个结点的深度并预处理fa[][]数组
for (int i=1; i<=m; ++i){
         int a,b; scanf("%d%d",&a,&b);
         int lca = LCA(a,b);
         D[a]++;  D[b]++;  D[lca]--;  D[fa[lca][0]]--;    //树上差分
	} 
	dfs2(1,0);     //用差分数组求每个结点的权值
	printf("%d\n",ans);
	return 0;
}

九. 树上的分治

点分治:核心是让子树的节点数接近,也就是使最大子树的子节点树最少

1. 静态分治点

只查询不修改,把树上内容分解为一个个独立的子树,进行求解

[例题](P3806 【模板】点分治 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

2. 动态分支点

查改+修改+强制在线,即点分树

[例题](ZJOI 2007 Hide 捉迷藏 题解-CSDN博客)

十. 树链剖分

  1. 修改点x到点y的路径上的个点权值
  2. 查询点x到点y的路径上节点权值之和
  3. 修改点x子树上各点的权值
  4. 查询点x子树上所有节点的权值之和
1. 点权值
#include<bits/stdc++.h>
using namespace std;
const int N=100000+10;
int n,m,r,mod;
//以下是链式前向星
struct Edge{int to, next;}edge[2*N];   
int head[2*N], cnt;
void init();                 //与前一小节“洛谷P3379树链剖分”的init()一样
void addedge(int u,int v);   //与前一小节“洛谷P3379树链剖分”的addedge()一样
//以下是线段树
int ls(int x){ return x<<1;  }     //定位左儿子:x*2
int rs(int x){ return x<<1|1;}     //定位右儿子:x*2 + 1
int w[N],w_new[N];                 //w[]、w_new[]初始点权
int tree[N<<2], tag[N<<2];         //线段树数组、lazy-tag操作
void addtag(int p,int pl,int pr,int d){      //给结点p打tag标记,并更新tree
    tag[p]  += d;                            //打上tag标记
    tree[p] += d*(pr-pl+1); tree[p] %= mod;  //计算新的tree
}
void push_up(int p){                         //从下往上传递区间值
    tree[p] = tree[ls(p)] + tree[rs(p)];  tree[p] %= mod;
}
void push_down(int p,int pl, int pr){
    if(tag[p]){
        int mid = (pl+pr)>>1;
        addtag(ls(p),pl,mid,tag[p]);    //把tag标记传给左子树
        addtag(rs(p),mid+1,pr,tag[p]);  //把tag标记传给右子树
        tag[p] = 0;
    }
}
void build(int p,int pl,int pr){        //建线段树
    tag[p] = 0;
    if(pl==pr){
        tree[p] = w_new[pl];    tree[p] %= mod;
        return;
    }
    int mid = (pl+pr) >> 1;
    build(ls(p),pl,mid);
    build(rs(p),mid+1,pr);
    push_up(p);
}
void update(int L,int R,int p,int pl,int pr,int d){
    if(L<=pl && pr<=R){  addtag(p, pl, pr,d);  return; }
    push_down(p,pl,pr);    
    int mid = (pl+pr) >> 1;
    if(L<=mid)   update(L,R,ls(p),pl,mid,d);
    if(R> mid)   update(L,R,rs(p),mid+1,pr,d);
    push_up(p);
}
int query(int L,int R,int p,int pl,int pr){
    if(pl>=L && R >= pr)   return tree[p] %= mod;
    push_down(p,pl,pr);
    int res =0;
    int mid = (pl+pr) >> 1;
    if(L<=mid)   res += query(L,R,ls(p),pl,mid);
    if(R> mid)   res += query(L,R,rs(p),mid+1,pr);
    return res;
}
//以下是树链剖分
int son[N],id[N],fa[N],deep[N],siz[N],top[N];
void dfs1(int x, int father); //与前一小节“洛谷P3379树链剖分”dfs1()一样
int num = 0;
void dfs2(int x,int topx){    //x当前结点,topx当前链的最顶端的结点
    id[x] = ++num;            //对每个结点新编号
    w_new[num] = w[x];        //把每个点的初始值赋给新编号
    top[x]=topx;              //记录x的链头
    if(!son[x])   return;     //x是叶子,没有儿子,返回
    dfs2(son[x],topx);                     //先dfs重儿子
    for(int i=head[x];~i;i=edge[i].next){  //再dfs轻儿子
        int y=edge[i].to;
        if(y!=fa[x] && y!=son[x]) dfs2(y,y);//每个轻儿子都有一条从它自己开始的链
    }
}
void update_range(int x,int y,int z){        //和求LCA(x, y)的过程差不多
    while(top[x]!=top[y]){
        if(deep[top[x]]<deep[top[y]])   swap(x,y);
        update(id[top[x]],id[x],1,1,n,z);    //修改一条重链的内部
        x = fa[top[x]];
    }
    if(deep[x]>deep[y])    swap(x,y);
    update(id[x],id[y],1,1,n,z);             //修改一条重链的内部
}
int query_range(int x,int y){                //和求LCA(x,y)的过程差不多
    int ans=0;
    while(top[x]!=top[y]){                   //持续往上跳,直到若x和y属于同一条重链
        if(deep[top[x]]<deep[top[y]]) swap(x,y); //让x是链头更深的重链
        ans += query(id[top[x]],id[x],1,1,n);    //加上x到x的链头这一段区间
        ans %= mod;
        x = fa[top[x]];                          //x穿过轻边,跳到上一条重链
    }
if(deep[x]>deep[y])  swap(x,y); 
//若LCA(x, y) = y,交换x,y,让x更浅,使得id[x] <= id[y]
    ans += query(id[x],id[y],1,1,n);             //再加上x, y的区间和
    return ans % mod;
}
void update_tree(int x,int k){  update(id[x],id[x]+siz[x]-1,1,1,n,k); }
int query_tree(int x){   return query(id[x],id[x]+siz[x]-1,1,1,n) % mod; }
int main(){
    init();      //链式前向星初始化
    scanf("%d%d%d%d",&n,&m,&r,&mod);
    for(int i=1;i<=n;i++)   scanf("%d",&w[i]);
    for(int i=1;i<n;i++){
        int u,v;            scanf("%d%d",&u,&v);
        addedge(u,v);       addedge(v,u);
    }
    dfs1(r,0);
    dfs2(r,r);
    build(1,1,n);         //建线段树
    while(m--){
        int k,x,y,z;   scanf("%d",&k);
        switch(k){
           case 1:scanf("%d%d%d",&x,&y,&z);update_range(x,y,z);            break; //修改点x到点y的路径上的个点权值
           case 2:scanf("%d%d",&x,&y);     printf("%d\n",query_range(x,y));break; //查询点x到点y的路径上节点权值之和
           case 3: scanf("%d%d",&x,&y);    update_tree(x,y);               break; //修改点x子树上各点的权值
           case 4: scanf("%d",&x);         printf("%d\n",query_tree(x));   break; //查询点x子树上所有节点的权值之和
        }
    }
}
2. 边权值

将上述边权转化为点权,每条边上的权值赋给这条边下层的节点(根节点为0)

1. 区间求和

区间求和,多算一个LCA点的值

2. 区间查询

区间查询,多算一个LCA点的值

同上

#include<bits/stdc++.h>
using namespace std;
const int N=100000+10;
int n,m,r,mod;

struct Edge{int to, next;}edge[2*N];   
int head[2*N], cnt;
void init();                 
void addedge(int u,int v);   

int ls(int x){ return x<<1;  }     
int rs(int x){ return x<<1|1;}     
int w[N],w_new[N];                 
int tree[N<<2], tag[N<<2];         
void addtag(int p,int pl,int pr,int d){      
    tag[p]  += d;                            
    tree[p] += d*(pr-pl+1); tree[p] %= mod;  
}
void push_up(int p){                         
    tree[p] = (tree[ls(p)] + tree[rs(p)]) % mod;
}
void push_down(int p,int pl, int pr){
    if(tag[p]){
        int mid = (pl+pr)>>1;
        addtag(ls(p),pl,mid,tag[p]);    
        addtag(rs(p),mid+1,pr,tag[p]);  
        tag[p] = 0;
    }
}
void build(int p,int pl,int pr){        
    tag[p] = 0;
    if(pl==pr){
        tree[p] = w_new[pl];    tree[p] %= mod;
        return;
    }
    int mid = (pl+pr) >> 1;
    build(ls(p),pl,mid);
    build(rs(p),mid+1,pr);
    push_up(p);
}
void update(int L,int R,int p,int pl,int pr,int d){
    if(L<=pl && pr<=R){  addtag(p, pl, pr,d);  return; }
    push_down(p,pl,pr);    
    int mid = (pl+pr) >> 1;
    if(L<=mid)   update(L,R,ls(p),pl,mid,d);
    if(R> mid)   update(L,R,rs(p),mid+1,pr,d);
    push_up(p);
}
int query(int L,int R,int p,int pl,int pr){
    if(pl>=L && R >= pr)   return tree[p] %= mod;
    push_down(p,pl,pr);
    int res =0;
    int mid = (pl+pr) >> 1;
    if(L<=mid)   res += query(L,R,ls(p),pl,mid);
    if(R> mid)   res += query(L,R,rs(p),mid+1,pr);
    return res % mod;
}

int son[N],id[N],fa[N],deep[N],siz[N],top[N];
void dfs1(int x, int father); 
int num = 0;
void dfs2(int x,int topx){    
    id[x] = ++num;            
    w_new[num] = w[x];        
    top[x]=topx;              
    if(!son[x])   return;     
    dfs2(son[x],topx);                     
    for(int i=head[x];~i;i=edge[i].next){  
        int y=edge[i].to;
        if(y!=fa[x] && y!=son[x]) dfs2(y,y);
    }
}
int lca(int x, int y) {
    while(top[x] != top[y]) {
        if(deep[top[x]] < deep[top[y]]) swap(x, y);
        x = fa[top[x]];
    }
    return deep[x] < deep[y] ? x : y;
}
void update_range(int x,int y,int z){        
    int LCA = lca(x, y);
    while(top[x]!=top[y]){
        if(deep[top[x]]<deep[top[y]])   swap(x,y);
        if(top[x] != top[LCA])
            update(id[top[x]],id[x],1,1,n,z);
        else
            update(id[LCA]+1,id[x],1,1,n,z); // avoid updating LCA
        x = fa[top[x]];
    }
    if(deep[x]>deep[y])    swap(x,y);
    if(x != LCA)
        update(id[x],id[y],1,1,n,z);             // update range except LCA
    else
        update(id[x]+1,id[y],1,1,n,z);           // avoid updating LCA
}
int query_range(int x,int y){                
    int ans=0;
    while(top[x]!=top[y]){                   
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        ans += query(id[top[x]],id[x],1,1,n);    
        ans %= mod;
        x = fa[top[x]];                          
    }
    if(deep[x]>deep[y])  swap(x,y); 
    ans += query(id[x],id[y],1,1,n);             
    return ans % mod;
}
void update_tree(int x,int k){  update(id[x],id[x]+siz[x]-1,1,1,n,k); }
int query_tree(int x){   return query(id[x],id[x]+siz[x]-1,1,1,n) % mod; }
int main(){
    init();      
    scanf("%d%d%d%d",&n,&m,&r,&mod);
    for(int i=1;i<=n;i++)   scanf("%d",&w[i]);
    for(int i=1;i<n;i++){
        int u,v;            scanf("%d%d",&u,&v);
        addedge(u,v);       addedge(v,u);
    }
    dfs1(r,0);
    dfs2(r,r);
    build(1,1,n);        
    while(m--){
        int k,x,y,z;   scanf("%d",&k);
        switch(k){
           case 1:scanf("%d%d%d",&x,&y,&z);update_range(x,y,z);            break;
           case 2:scanf("%d%d",&x,&y);     printf("%d\n",query_range(x,y));break;
           case 3: scanf("%d%d",&x,&y);    update_tree(x,y);               break;
           case 4: scanf("%d",&x);         printf("%d\n",query_tree(x));   break;
        }
    }
}

十一. 二叉查找树

感觉不如set

或者用lower_bound等当代餐更方便

以下11-16这部分要求对原理理解即可

十二. 替罪羊树

不平衡->中序遍历列出字数上所有元素,删除子树->中间元素为根,重建子树

//洛谷P3369  替罪羊树
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
const double alpha = 0.75;  //不平衡率。一般用alpha来表示
struct Node{
  int ls,rs; //左右儿子
  int val;   //结点存的数字
  int tot;   //当前子树占用的空间数量,包括实际存储的结点和被标记删去的点
  int size;  //子树上实际存储数字的数量
  int del;   //=1表示这个结点存有数字,=0表示这个点存的数字被删了
}t[N];
int order[N],cnt; //order[]记录拍扁后的结果,即那些存有数字的结点。cnt是数量
int tree_stack[N],top = 0;  //用一个栈来回收和分配可用的结点
int root = 0;               //根结点,注意重建过程中根结点会变化
void inorder(int u){        //中序遍历,“拍平”摧毁这棵子树
    if(!u)  return;         //已经到达叶子,退出
    inorder(t[u].ls);       //先遍历左子树
    if(t[u].del)  order[++cnt] = u;      //如果该结点存有数字,读取它
    else          tree_stack[++top] = u; //回收该结点,等待重新分配使用
    inorder(t[u].rs);       //再遍历右子树
}
void Initnode(int u){       //重置结点的参数
    t[u].ls = t[u].rs = 0;
    t[u].size = t[u].tot = t[u].del = 1;
}
void Update(int u){
    t[u].size = t[t[u].ls].size + t[t[u].rs].size + 1;
    t[u].tot = t[t[u].ls].tot + t[t[u].rs].tot + 1;
}
//int rebuild_num=0;                      //测试:统计重建次数
void build(int l,int r,int &u){           //把拍扁的子树拎起来,重建
//  rebuild_num++;                        //测试:统计重建次数
    int mid = (l + r) >> 1;               //新的根设为中点,使重构出的树尽量平衡
    u = order[mid];
    if(l == r){Initnode(u); return;}      //如果是叶子,重置后返回
    if(l < mid)  build(l,mid - 1,t[u].ls);//重构左子树
    if(l == mid) t[u].ls = 0;             //注意这里,不要漏了
    build(mid + 1,r,t[u].rs);             //重构右子树
    Update(u);                            //更新
}
void rebuild(int &u){                     //重建。注意是&u
    cnt = 0;
    inorder(u);                           //先拍平摧毁
    if(cnt)   build(1,cnt,u);             //再拎起,重建树
    else      u = 0;                      //特判该子树为空的情况
}
bool notbalance(int u){                   //判断子树u是否平衡
    if((double)t[u].size*alpha <=(double)max(t[t[u].ls].size,t[t[u].rs].size))
        return true;                      //不平衡了
    return false;                         //还是平衡的
}
void Insert(int &u,int x){                //插入数字x。注意是&u,传回了新的u
    if(!u){                               //如果结点u为空,直接将x插到这里
        u = tree_stack[top--];            //从栈顶拿出可用的空结点
        t[u].val = x;                     //结点赋值
        Initnode(u);                      //其他参数初始化
        return;
    }
    t[u].size++;
    t[u].tot++;
    if(t[u].val >= x)  Insert(t[u].ls,x);  //插到右子树
    else               Insert(t[u].rs,x);  //插到左子树
    if(notbalance(u))  rebuild(u);         //如果不平衡了,重建这棵子树
}
int Rank(int u,int x){                     //排名,x是第几名
	if(u==0)	   return 0;
	if(x>t[u].val) return t[t[u].ls].size+ t[u].del + Rank(t[u].rs, x);
	return Rank(t[u].ls,x);
}
int kth(int k){                            //第k大数是几?
    int u = root;
    while(u){
        if(t[u].del && t[t[u].ls].size + 1 == k) return t[u].val;
        else if(t[t[u].ls].size >= k)  u = t[u].ls;
        else{
            k -= t[t[u].ls].size + t[u].del;
            u = t[u].rs;
        }
    }
    return t[u].val;
}
void Del_k(int &u,int k){       //删除排名为k的数
    t[u].size--;                //要删除的数肯定在这棵子树中,size减1
    if(t[u].del && t[t[u].ls].size + 1 == k){
       t[u].del = 0;            //del=0表示这个点u被删除了,但是还保留位置
       return;
    }
    if(t[t[u].ls].size + t[u].del >= k)  Del_k(t[u].ls,k); //在左子树上
    else   Del_k(t[u].rs,k - t[t[u].ls].size - t[u].del);  //在右子树上
}
void Del(int x){                 //删除值为k的数
    Del_k(root,Rank(root,x)+1);  //先找x的排名,然后用Del_k()删除
    if(t[root].tot * alpha >= t[root].size)
        rebuild(root);           //如果子树上被删除的结点太多,就重构
}
/*
void print_tree(int u){          //测试:打印二叉树,观察
    if(u){
        cout<<"v="<<t[u].val<<",l="<<t[u].ls<<",r="<<t[u].rs<<endl;
        print_tree(t[u].ls);
        print_tree(t[u].rs);
    }
}
int tree_deep[N]={0},deep_timer=0,max_deep=0;    //测试
void cnt_deep(int u){                            //测试:计算二叉树的深度
    if(u){
        tree_deep[u]=++deep_timer;               //结点u的深度
        max_deep = max(max_deep,tree_deep[u]);   //记录曾经的最大深度
        cnt_deep(t[u].ls);
        cnt_deep(t[u].rs);
        deep_timer--;
    }
}   */
int main(){
    for(int i=N-1;i>=1;i--) tree_stack[++top] = i;   //把所有可用的t[]记录在这个栈里面
    int q;	cin>>q;
//  rebuild_num = 0;deep_timer=0;max_deep=0;         //测试
    while(q--){
        int opt,x; cin >> opt >> x;
        switch (opt){
            case 1: Insert(root,x);    break;
            case 2: Del(x);            break;
            case 3: printf("%d\n",Rank(root,x)+1);         break;
            case 4: printf("%d\n",kth(x));                 break;
            case 5: printf("%d\n",kth(Rank(root,x)));      break;
            case 6: printf("%d\n",kth(Rank(root,x+1)+1));  break;
        }
//      cout<<">>"<<endl;print_tree(root);cout<<endl<<"<<"<<endl;  //测试:打印二叉树
//      cnt_deep(root);                 //测试:计算曾经的最大深度
    }
//  cout<<"rebuild num="<<rebuild_num<<endl;  //测试:打印重建次数
//  cout<<"deep="<<max_deep<<endl;            //测试:打印替罪羊树的最大深度
    return 0;
}

十三. Treap(树堆)

Treap树:

(1) 键值 满足 BST要求 l s . k e y < = f a . k e y < = r s . k e y ls.key <= fa.key <=rs.key ls.key<=fa.key<=rs.key

(2) 优先级 满足 堆的要求 f a . p r i > = m a x { l s . p r i , r e . p r i } fa.pri>=max {\{ls.pri,re.pri\}} fa.pri>=max{ls.pri,re.pri}

特性:

每个键值和优先级确定且不同,BST形式唯一

const int M=1e6+10;
int cnt = 0;             //t[cnt]: 最新结点的存储位置
struct Node{
int ls,rs;           //左右儿子
int key,pri;         // key:键值;pri:随机的优先级
int size;            //当前结点为根的子树的结点数量,用于求第k大和rank
}t[M];                   //tree[],存树
void newNode(int x){     //初始化一个新结点
cnt++;        //从t[1]开始存储结点,t[0]被放弃。若子结点是0,表示没有子结点
t[cnt].size = 1;
t[cnt].ls = t[cnt].rs = 0;  //0表示没有子结点
t[cnt].key = x;             //key: 键值
t[cnt].pri = rand();        //pri:随机产生,优先级
}
void Update(int u){             //更新以u为根的子树的size
t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;
}
void rotate(int &o,int d){      //旋转,参考图示理解。 d=0右旋,d=1左旋
int k;
    if(d==1) {                  //左旋,把o的右儿子k旋到根部
       k=t[o].rs;
       t[o].rs=t[k].ls;//图中的x
       t[k].ls=o;
    }
    else {                      //右旋,把o的左儿子k旋到根部
       k=t[o].ls;
       t[o].ls=t[k].rs; //图中的x
       t[k].rs=o;
    }
t[k].size=t[o].size;
Update(o);
o=k;                       //新的根是k
}
void Insert(int &u,int x){
if(u==0){newNode(x);u=cnt;return;}    //递归到了一个空叶子,新建结点
t[u].size++;
if(x>=t[u].key)    Insert(t[u].rs,x); //递归右子树找空叶子,直到插入
else               Insert(t[u].ls,x); //递归左子树找空叶子,直到插入
if(t[u].ls!=0 && t[u].pri>t[t[u].ls].pri) rotate(u,0);
if(t[u].rs!=0 && t[u].pri>t[t[u].rs].pri) rotate(u,1);
Update(u);
}
void Del(int &u,int x){
t[u].size--;
if(t[u].key==x){
if(t[u].ls==0&&t[u].rs==0){u=0; return;}
if(t[u].ls==0||t[u].rs==0){u=t[u].ls+t[u].rs; return;}
if(t[t[u].ls].pri < t[t[u].rs].pri)
{     rotate(u,0); Del(t[u].rs, x); return;}
		 else{ rotate(u,1); Del(t[u].ls, x); return;}
	}
	if(t[u].key>=x) Del(t[u].ls,x);
	else            Del(t[u].rs,x);
	Update(u);
}
int Rank(int u,int x){   //排名,x是第几名
	if(u==0)	return 0;
	if(x>t[u].key) return t[t[u].ls].size+1+Rank(t[u].rs, x);
	return Rank(t[u].ls,x);
}
int kth(int u,int k){    //第k大数是几?
	if(k==t[t[u].ls].size+1) return t[u].key;   //这个数为根
	if(k> t[t[u].ls].size+1) return kth(t[u].rs,k-t[t[u].ls].size-1);//右子树
	if(k<=t[t[u].ls].size)   kth(t[u].ls,k);    //左子树
}
int Precursor(int u,int x){
	if(u==0) 	return 0;
	if(t[u].key>=x)	return Precursor(t[u].ls,x);
	int tmp = Precursor(t[u].rs,x);
	if(tmp==0)	return t[u].key;
	return tmp;
}
int Successor(int u,int x){
	if(u==0) 	return 0;
	if(t[u].key<=x)	return Successor(t[u].rs,x);
	int tmp = Successor(t[u].ls,x);
	if(tmp==0) 	return t[u].key;
	return tmp;
}
int main(){
	srand(time(NULL));
	int root = 0;           //root是整棵树的树根,0表示初始为空
	int n;	cin>>n;
	while(n--){
        int opt,x; cin >> opt >> x;
        switch (opt){
            case 1: Insert(root,x);    break;
            case 2: Del(root,x);       break;
            case 3: printf("%d\n",Rank(root,x)+1);    break;
            case 4: printf("%d\n",kth(root,x));       break;
            case 5: printf("%d\n",Precursor(root,x)); break;
            case 6: printf("%d\n",Successor(root,x)); break;
        }
	}
	return 0;
}

十四. FHQ Treap

//洛谷P3369,FHQ Treap
#include<bits/stdc++.h>
using namespace std;
const int M=1e6+10;
int cnt=0, root=0;        //t[cnt]: 最新结点的存储位置;root:整棵树的根,用于访问树
struct Node{
	int ls,rs;           //左儿子lson,右儿子rson
	int key,pri;         //key:键值,pri:随机的优先级
	int size;            //当前结点为根的子树的结点数量,用于求第k大和rank
}t[M];                   //tree[],存树
void newNode(int x){     //建立只有一个点的树
	cnt++;               //从t[1]开始存储结点,t[0]被放弃
	t[cnt].size = 1;
	t[cnt].ls = t[cnt].rs = 0;  //0表示没有子结点
	t[cnt].key = x;             //key: 键值
	t[cnt].pri = rand();        //pri:随机产生,优先级
}
void Update(int u){       //更新以u为根的子树的size
	t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;
}
void Split(int u,int x,int &L,int &R) {   //权值分裂。返回以L和R为根的2棵树
	if(u == 0){L = R = 0;	return;}      //到达叶子,递归返回
	if(t[u].key <= x){                   //本结点比x小,那么到右子树上找x
		L = u;                           //左树的根是本结点
		Split(t[u].rs, x, t[u].rs, R);   //通过rs传回新的子结点
	}
	else{                                 //本结点比x大,继续到左子树找x
		R = u;                           //右数的根是本结点
		Split(t[u].ls,x,L,t[u].ls);
	}
	Update(u);//更新当前结点的size
}
int Merge(int L,int R){                   //合并以L和R为根的两棵树,返回一棵树的根
	if(L==0 || R==0)  return L+R;        //到达叶子。若L==0,就是返回L+R=R
	if(t[L].pri > t[R].pri){             //左树L优先级大于右树R,则L节点是父结点
		t[L].rs = Merge(t[L].rs,R);      //合并R和L的右儿子,并更新L的右儿子
		Update(L);
		return L;                        //合并后的根是L
	}
	else{	                            //合并后R是父结点
		t[R].ls = Merge(L,t[R].ls);      //合并L和R的左儿子,并更新R的左儿子
        Update(R);
		return R;                        //合并后的根是R
	}
}
int Insert(int x){                        //插入数字x
    int L,R;
    Split(root,x,L,R);
    newNode(x);                           //新建一棵只有一个点的树t[cnt]
    int aa = Merge(L,cnt);
    root = Merge(aa,R);
}
int Del(int x){        //删除数字x。请对比后面例题洛谷P5055的排名分裂的删除操作
    int L,R,p;
    Split(root,x,L,R);                    //<=x的树和>x的树
    Split(L,x-1,L,p);                     //<x的树和==x的树
    p = Merge(t[p].ls,t[p].rs);           //合并x=p的左右子树,也就是删除了x
    root = Merge(Merge(L,p),R);
}
void Rank(int x){                         //查询数x的排名
    int L,R;
    Split(root,x-1,L,R);                  // <x的树和>=x的树
    printf("%d\n",t[L].size+1);
    root = Merge(L,R);                    //恢复
}
int kth(int u,int k){                     //求排名第k的数
	if(k==t[t[u].ls].size+1) return u;              //这个数为根
	if(k<=t[t[u].ls].size)   return kth(t[u].ls,k); //在左子树
	if(k>t[t[u].ls].size)    return kth(t[u].rs,k-t[t[u].ls].size-1);  //在右子树
}
void Precursor(int x){                    //求x的前驱
    int L,R;
    Split(root,x-1,L,R);
    printf("%d\n",t[kth(L,t[L].size)].key);
    root = Merge(L,R);                   //恢复
}
void Successor(int x){                   //求x的后继
    int L,R;
    Split(root,x,L,R);                   //<=x的树和>x的树
    printf("%d\n",t[kth(R,1)].key);
    root = Merge(L,R);                   //恢复
}
int main(){
    srand(time(NULL));
    int n;	cin >> n;
    while(n--){
        int opt,x; cin >> opt >> x;
        switch (opt){
            case 1: Insert(x);     break;
            case 2: Del(x);        break;
            case 3: Rank(x);       break;
            case 4: printf("%d\n",t[kth(root,x)].key); break;  //排名x的结点
            case 5: Precursor(x);  break;
            case 6: Successor(x);  break;
        }
	}
	return 0;
}

应用

这个分裂又整合的底层更新逻辑,和线段是相似,可以做一些线段树的操作

如:

快操作,区间和等等

1.luogu 4008
//洛谷P4008的FHQ treap代码
#include<bits/stdc++.h>
using namespace std;
const int M = 2e6+10;
int root = 0,cnt = 0;
struct Node{int ls,rs; char val; int pri; int size;}t[M];              //tree[]存树
void Update(int u){t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;}   //用于排名分裂
int newNode(char x){     //建立只有一个点的树
	cnt++;
	t[cnt].size = 1;  t[cnt].pri = rand();  t[cnt].ls = t[cnt].rs = 0;
	t[cnt].val = x;      //一个字符	
	return cnt;
}
void Split(int u,int x,int &L,int &R){   //排名分裂,不是权值分裂
    if(u == 0){L = R = 0; return ;}
    if(t[t[u].ls].size+1 <= x){          //第x个数在u的右子树上
        L = u;   Split(t[u].rs, x-t[t[u].ls].size-1, t[u].rs, R);
    }
    else{R = u; Split(t[u].ls,x,L,t[u].ls); }     //第x个数在左子树上
    Update(u);
}
int Merge(int L,int R){                  //合并
	if(L==0 || R==0)  return L+R;
	if(t[L].pri > t[R].pri){ t[L].rs = Merge(t[L].rs,R); Update(L);  return L; }
	else{ t[R].ls = Merge(L,t[R].ls);  Update(R);	return R;}
}
void inorder(int u){                     //中序遍历,打印结果
    if(u == 0) return;
    inorder(t[u].ls);    cout << t[u].val;   inorder(t[u].rs);
}
int main(){
    srand(time(NULL));
    int len, L, p, R, pos = 0;      //pos是光标的当前位置
    int n;   cin>>n;
    while(n--){
        char opt[10];    cin>>opt;
        if(opt[0]=='M')  cin>>pos;  //移动光标
        if(opt[0]=='I'){            //插入len个字符
            cin>>len;
            Split(root,pos,L,R);
            for(int i=1;i<=len;i++)  {     //逐个读入字符
                char ch=getchar();   while(ch<32||ch>126)  ch=getchar();
                L = Merge(L,newNode(ch));  //把字符加到树中
            }
            root = Merge(L,R);
        }
        if(opt[0]=='D'){             //删除光标后len个字符
            cin>>len;     Split(root,pos+len,L,R); Split(L,pos,L,p);
            root = Merge(L,R);
        }
        if(opt[0]=='G'){              //打印len个字符
            cin>>len;     Split(root,pos+len,L,R); Split(L,pos,L,p);
            inorder(p); cout<<"\n";   //打印
            root = Merge(Merge(L,p),R);
        }
        if(opt[0]=='P') pos--;     
        if(opt[0]=='N') pos++;
    }
    return 0;
}
2. 区间翻转

十五. 笛卡尔树

简化的Treap树,有键值和随机的优先级

主要目标是一个优先级和键值确定的数列

//改写自:https://blog.csdn.net/qq_40679299/article/details/80395824
#include<cstdio>
#include<algorithm>
#include<cstring>
#include <stack>
using namespace std;
const int N = 50005;
const int INF = 0x7fffffff;
struct Node{
    char s[100];   int ls,rs,fa,pri;
    friend bool operator<(const Node& a,const Node& b){
        return strcmp(a.s,b.s)<0;}
}t[N];
void buildtree(int n){         //不用栈,直接查最右链
    for(int i=1;i<=n;i++){
        int pos = i-1;         //从前一个结点开始比较,前一个结点在最右链的末端
        while(t[pos].pri < t[i].pri) 
             pos = t[pos].fa;  //沿着最右链一直找,直到pos的优先级比i大
        t[i].ls = t[pos].rs;   //图(4):i是19,pos是24, 15调整为19的左儿子
        t[t[i].ls].fa = i;     //图(4):15的父亲是19
        t[pos].rs = i;         //图(4):24的右儿子是19
        t[i].fa = pos;         //图(4):19的父亲是24
    }
}
void buildtree2(int n){             //用栈来辅助建树
    stack <int> ST;  ST.push(0);    //t[0]进栈,它一直在栈底
    for(int i=1;i<=n;i++){
        int pos = ST.top();
        while(!ST.empty() && t[pos].pri < t[i].pri){
            pos = t[ST.top()].fa;
            ST.pop();               //把比i优先级小的弹出栈
        }
        t[i].ls = t[pos].rs;
        t[t[i].ls].fa = i;
        t[pos].rs = i;
        t[i].fa = pos;
        ST.push(i);                 //每个结点都一定要进栈
    }
}
void inorder(int x){                //中序遍历打印
    if(x==0)     return;
    printf("(");
    inorder(t[x].ls);  printf("%s/%d",t[x].s,t[x].pri);  inorder(t[x].rs); 
    printf(")");
}
int main(){
    int n;
    while(scanf("%d",&n),n){
        for(int i=1;i<=n;i++){
            t[i].ls = t[i].rs = t[i].fa = 0;        //有多组测试,每次要清零
            scanf(" %[^/]/%d", t[i].s, &t[i].pri);  //注意输入的写法
        }
        t[0].ls = t[0].rs = t[0].fa = 0;            //t[0]不用,从t[1]开始插结点
        t[0].pri = INF;    //t[0]的优先级无穷大
        sort(t+1,t+1+n);   //对标签先排序,非常关键。这样建树时就只需要考虑优先级pri
        buildtree(n);      //buildtree2(n);        //两种建树方法
        inorder(t[0].rs);  //t[0]在树的最左端,第一个点是t[0].rs
        printf("\n");
    }
    return 0;
}

十六. Splay树

和FHQ Treap功能相似

//改写自 https://www.luogu.com.cn/blog/dedicatus545/solution-p4008
#include<bits/stdc++.h>
using namespace std;
const int M = 2e6+10;
int cnt, root;
struct Node{int fa,ls,rs,size; char val;}t[M];   //tree[]存树;
void Update(int u){t[u].size = t[t[u].ls].size + t[t[u].rs].size+1;}  //用于排名
char str[M]={0};                //一次输入的字符串
int build(int L,int R,int f){   //把字符串str[]建成平衡树
    if(L>R) return 0;
    int mid = (L+R)>>1;
    int cur = ++cnt;
    t[cur].fa = f;
    t[cur].val= str[mid];
    t[cur].ls = build(L,mid-1,cur);
    t[cur].rs = build(mid+1,R,cur);
    Update(cur);
    return cur;                 //返回新树的根
}
int get(int x){return t[t[x].fa].rs == x;} //如果u是右儿子,返回1;左儿子返回0
void rotate(int x){                        //单旋一次
    int f=t[x].fa, g=t[f].fa, son=get(x);  //f:父亲;g:祖父
    if(son==1) {                           //x是右儿子,左旋zag
        t[f].rs = t[x].ls;                 //图“单旋”中的结点b
        if(t[f].rs)  t[t[f].rs].fa = f;
    }
    else {                                 //x是左儿子,右旋zig
        t[f].ls = t[x].rs;
        if(t[f].ls)  t[t[f].ls].fa = f;
    }
    t[f].fa=x;                  //x旋为f的父结点
    if(son == 1) t[x].ls=f;     //左旋,f变为x的左儿子
    else t[x].rs=f;             //右旋,f变为x的右儿子
    t[x].fa = g;                //x现在是祖父的儿子
    if(g){                      //更新祖父的儿子
        if(t[g].rs==f)  t[g].rs = x;
        else  t[g].ls = x;
    }
    Update(f);   Update(x);
}
void splay(int x,int goal){     //goal=0,新的根是x;goal!=0,把x旋为goal的儿子
    if(goal == 0) root=x;
    while(1){
        int f = t[x].fa, g=t[f].fa;        //一次处理x,f,g三个点
        if(f == goal) break;
        if(g != goal) {                    //有祖父,分一字旋和之字旋两种情况
            if(get(x)==get(f)) rotate(f);  //一字旋,先旋f、g
            else rotate(x);}               //之字旋,直接旋x
        rotate(x);
    }
    Update(x);
}
int kth(int k,int u){            //第k大数的位置
    if( k == t[t[u].ls].size + 1) return u;
    if( k <= t[t[u].ls].size )    return kth(k,t[u].ls);
    if( k >= t[t[u].ls].size + 1) return kth(k-t[t[u].ls].size-1,t[u].rs);
}
void Insert(int L,int len){                  //插入一段区间
    int x = kth(L,root), y = kth(L+1,root);  //x:第L个数的位置,y:第L+1个数的位置
    splay(x,0);   splay(y,x);                //分裂
     //先把x旋到根,然后把y旋到x的儿子,此时y是x的右儿子,且y的左儿子为空
    t[y].ls = build(1,len,y);                //合并:建一棵树,挂到y的左儿子上
    Update(y);    Update(x);
}
void del(int L,int R){             //删除区间[L+1,R]
    int x=kth(L,root), y=kth(R+1,root);
    splay(x,0); splay(y,x);        //y是x的右儿子,y的左儿子是待删除的区间
    t[y].ls=0;                 //剪断左子树,等于直接删除。这里为了简单,没有释放空间
    Update(y);    Update(x);
}
void inorder(int u){           //中序遍历
    if(u==0) return;
    inorder(t[u].ls);cout<<t[u].val;inorder(t[u].rs);
}
int main(){
    t[1].size=2;    t[1].ls=2;  //小技巧:虚拟祖父,防止旋转时越界而出错
    t[2].size=1;    t[2].fa=1;  //小技巧:虚拟父亲
    root=1; cnt=2;      //在操作过程中,root将指向字符串的根
    int pos=1;          //光标位置
    int n;   cin>>n;
    while(n--){
        int len;  char opt[10];  cin>>opt;
        if(opt[0]=='I'){
            cin>>len;
            for(int i=1;i<=len;i++){
                char ch=getchar();   while(ch<32||ch>126)  ch=getchar();
                str[i] = ch;
            }
            Insert(pos,len);
        }
        if(opt[0]=='D'){ cin>>len; del(pos,pos+len);}    //删除区间[pos+1,pos+len]
        if(opt[0]=='G'){
            cin>>len;     int x=kth(pos,root), y=kth(pos+len+1,root);
            splay(x,0);   splay(y,x);
            inorder(t[y].ls);    cout<<"\n";
        }
        if(opt[0]=='M'){ cin>>len; pos=len+1;}
        if(opt[0]=='P') pos--;
        if(opt[0]=='N') pos++;
    }
}

十七. K-D树

按x-y-…这个顺序交替轮流划分

1. 寻找最小值

#include<bits/stdc++.h>
using namespace std;
const int N = 100005;
const int K = 2;
#define ll long long
struct Point{int dim[K];};   //K维数据。本题是二维坐标x=dim[0],y=dim[1]
Point q[N];     //记录输入的n个坐标点
Point t[N];     //存二叉树,用最简单的方法:数组存二叉树
int  now;       //当前处于第几维,用于cmp()函数的比较
bool cmp(Point a,Point b){return a.dim[now] < b.dim[now];} //第now维数的比较
ll square(int x){return (ll)x*x;}  
ll dis(Point a,Point b){            //点a、b距离的平方
return square(a.dim[0]-b.dim[0])+square(a.dim[1]-b.dim[1]);
}
void build(int L,int R,int dep){    //建树
	if(L>=R) return;
	int d = dep % K;                //轮转法。dep是当前层的深度,d是当前层的维度
	int mid = (L+R) >> 1;
	now = d;
	nth_element(t+L,t+mid,t+R,cmp); //找中值
	build(L,mid,dep+1);             //继续建二叉树的下一层
	build(mid+1,R,dep+1);
}
ll ans;                             //答案:到最近点距离的平方值
void query(int L,int R,int dep,Point p){ //查找点p的最近点
     if(L >= R)return;
     int mid=(L+R)>>1;
     int d = dep % K;                 //轮转法
     ll mindis = dis(t[mid],p);       //这棵子树的根到p的最小距离
     if(ans == 0)  ans = mindis;      //赋初值
     if(mindis!=0 && ans > mindis) ans = mindis;  //需要特判t[mid]和p重合的情况
     if(p.dim[d] > t[mid].dim[d]) {  //在这个维度,p大于子树的根,接下来查右子树
        query(mid+1,R,dep+1,p);
        if(ans > square(t[mid].dim[d]-p.dim[d])) query(L,mid,dep+1,p);
//如果以ans为半径的圆与左子树相交,那么左子树也要查            
	}
     else{
         query(L,mid,dep+1,p);        //在这个维度,p小于子树的根,接下来查左子树
         if(ans > square(t[mid].dim[d]-p.dim[d])) //右子树也要查
             query(mid+1,R,dep+1,p);
    }
}
int main(){
     int T; scanf("%d",&T);
     while(T--){
		int n; scanf("%d",&n);
		for(int i=0;i<n;i++)
	  	    scanf("%d%d",&(q[i].dim[0]),&(q[i].dim[1])), t[i] = q[i];
		build(0,n,0);     //建树
		for(int i=0;i<n;i++){
		    ans=0;
		    query(0,n,0,q[i]);
			printf("%lld\n",ans);
		}
	}
	return 0;
}

2. 区间查询

#include<bits/stdc++.h>
using namespace std;
const int  N = 200010;
const double alpha = 0.75; //替罪羊树的不平衡率
#define lc t[u].ls
#define rc t[u].rs
struct Point{
	int dim[2],val;         //dim[0]即x,dim[1]即y
	Point(){};
	Point(int x,int y,int vall){dim[0]=x,dim[1]=y,val=vall;}
};
Point order[N]; int cnt;    //替罪羊树:用于拍平后存数据
struct kd_tree{
	int ls,rs;
	int mi[2],ma[2]; //mi[i]: 第i维上区间的下界; ma[i]:第i维上区间的上界
	int sum;         //以该点为根的子树权值之和
	int size;
	Point p;
}t[N];
int tot,root;
int top,tree_stack[N];    //替罪羊树:回收
int now;
bool cmp(Point a,Point b){return a.dim[now]<b.dim[now];}
void update(int u){
	for(int i=0;i<2;i++){
		t[u].mi[i] = t[u].ma[i] = t[u].p.dim[i];
		if(lc){
			t[u].mi[i] = min(t[u].mi[i],t[lc].mi[i]);
			t[u].ma[i] = max(t[u].ma[i],t[lc].ma[i]);
		}
		if(rc){
			t[u].mi[i] = min(t[u].mi[i],t[rc].mi[i]);
			t[u].ma[i] = max(t[u].ma[i],t[rc].ma[i]);
		}
	}
	t[u].sum = t[lc].sum + t[u].p.val+t[rc].sum;
	t[u].size = t[lc].size + t[rc].size+1;
}
void slap(int u)   {            //替罪羊树:拍平
	if(!u) return;
	slap(lc);                   //这里用中序遍历。其实先序、后序也行
	order[++cnt] = t[u].p;
	tree_stack[++top] = u;      //回收结点
	slap(rc);
}
int build(int l,int r,int d) {   //替罪羊树:建树
if(l>r) return 0;
int u;
if(top)   u = tree_stack[top--];
    else      u = ++tot;
int mid=(l+r)>>1;
    now = d;
nth_element(order+l, order+mid, order+r+1, cmp);
t[u].p = order[mid];
lc = build(l,mid-1,d^1);    //奇偶轮转法。没有用例题hdu2966的一般轮转法
rc = build(mid+1,r,d^1);
update(u);
return u;
}
bool notbalance(int u){          //替罪羊树:判断子树u是否平衡
    if(t[lc].size>alpha*t[u].size || t[rc].size>alpha*t[u].size)
        return true;             //不平衡了
    return false;                //还是平衡的
}
void Insert(int &u,Point now,int d){
	if(!u)	{
		if(top)   u=tree_stack[top--];
		else      u = ++tot;
		lc = rc = 0,t[u].p = now;
		update(u);
		return;
	}
	if(now.dim[d] <= t[u].p.dim[d])  Insert(lc,now,d^1);//按第d维的坐标比较
	else                             Insert(rc,now,d^1);
	update(u);
	if(notbalance(u)){                //不平衡
        cnt = 0;
		slap(u);                     //拍平
		u = build(1,t[u].size,d);    //重建
	}
}
int query(int u,int x1,int y1,int x2,int y2){
	if(!u) return 0;
	int X1=t[u].mi[0], Y1=t[u].mi[1], X2=t[u].ma[0], Y2=t[u].ma[1];
if(x1<=X1 && x2>=X2 && y1<=Y1 && y2>=Y2)   return t[u].sum; 
//子树表示的矩形完全在询问矩形范围内
	if(x1>X2 || x2<X1 || y1>Y2 || y2<Y1)	 return 0;        
//子树表示的矩形完全在询问矩形范围外
	int ans=0;
	X1=t[u].p.dim[0], Y1=t[u].p.dim[1], X2=t[u].p.dim[0], Y2=t[u].p.dim[1];
    if(x1<=X1 && x2>=X2 && y1<=Y1 && y2>=Y2)  ans+=t[u].p.val;//根在询问矩形内
	ans += query(lc,x1,y1,x2,y2) + query(rc,x1,y1,x2,y2);     //递归左右子树
	return ans;
}
int main(){
	int n; cin >> n;
	int ans=0;
	while(1){
        int opt;scanf("%d",&opt);
		if(opt==1){
			int x,y,val; scanf("%d%d%d",&x,&y,&val);
			x^=ans,y^=ans,val^=ans;
			Insert(root,Point(x,y,val),0);
		}
		if(opt==2){
			int x1,y1,x2,y2; scanf("%d%d%d%d",&x1,&y1,&x2,&y2);
			x1^=ans,y1^=ans,x2^=ans,y2^=ans;
            ans = query(root,x1,y1,x2,y2);
			printf("%d\n",ans);
		}
		if(opt==3) break;
	}
}

十八. 动态树与LCT

作用对象:动态规划的树和森林

//代码改写自:https://www.luogu.com.cn/blog/ecnerwaIa/dai-ma-jiang-xie
#include<bits/stdc++.h>
using namespace std;
const int N=3e5+5;
struct node{ int fa,ch[2],sum,val,lazy; }t[N];     //lazy用来标记reverse()的左右翻转
#define lc t[x].ch[0]    //左儿子
#define rc t[x].ch[1]    //右儿子
bool isRoot(int x){      //判断是否是splay根节点
    int g=t[x].fa;
    return t[g].ch[0]!=x && t[g].ch[1]!=x;//若为根,则父结点不应该有这个儿子
}
void pushup(int x){      //本题的求路径异或和。上传信息
    t[x].sum=t[x].val^t[lc].sum^t[rc].sum;
}
void reverse(int x){     
    if(!x)return;
    swap(lc,rc);         //翻转x的左右儿子
    t[x].lazy^=1;        //懒惰标记,先不翻转儿子的后代,后面再翻转
}
void pushdown(int x){    //递归翻转x的儿子的后代,并释放懒标记。
    if(t[x].lazy){
        reverse(lc);
        reverse(rc);
        t[x].lazy=0;
    }
}
void push(int x){ 
    if(!isRoot(x))  push(t[x].fa);  //从根到x全部pushdown
    pushdown(x);
}
void rotate(int x){
    int y=t[x].fa;
    int z=t[y].fa;
    int k=t[y].ch[1]==x;
    if(!isRoot(y)) t[z].ch[t[z].ch[1]==y]=x; 
    t[x].fa=z;
    t[y].ch[k]=t[x].ch[k^1];
    if(t[x].ch[k^1])t[t[x].ch[k^1]].fa=y;
    t[y].fa=x;
    t[x].ch[k^1]=y;
    pushup(y);
}
void splay(int x){     //提根:把x旋转为它所在的Splay树的根
    int y,z;
push(x);           //先pushdown处理x的所有子孙的lazy标记
while(!isRoot(x)){
        y=t[x].fa,z=t[y].fa;
        if(!isRoot(y))
            (t[z].ch[0]==y)^(t[y].ch[0]==x)?rotate(x):rotate(y);
        rotate(x);
    }
    pushup(x);
}
void access(int x){    //在原树上建一条实链,起点是根,终点是x
    for(int child=0; x; child=x, x=t[x].fa){  //从x往上走,沿着虚边走到根 
        splay(x);
        rc = child;     //右孩子是child,建立了一条实边
        pushup(x); 
    }                       
}
void makeroot(int x){  //把x在原树上旋转到根的位置
    access(x);    splay(x);     reverse(x);       
}
void split(int x,int y){   //把原树上以x为起点、y为终点的路径,生成一条实链
    makeroot(x);
    access(y);
    splay(y);    
}
void link(int x,int y){   //在结点x和y之间连接一条边
    makeroot(x);    t[x].fa=y;
}
void cut(int x,int y){  //将x,y的边切断
    split(x,y);
    if(t[y].ch[0]!=x||rc)  return;
    t[x].fa=t[y].ch[0]=0;
    pushup(x);
}
int findroot(int x){  //查找x在原树上的根
	access(x);  splay(x);
	while(lc)  pushdown(x),x=lc;    //找Splay树最左端的结点
	return x;
}
int main(){
    int n,m; scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){ scanf("%d",&t[i].val); t[i].sum = t[i].val; }
    while(m--){
        int opt,a,b;  scanf("%d%d%d",&opt,&a,&b);
        switch(opt){
            case 0:  split(a,b);   printf("%d\n",t[b].sum);  break;
            case 1:  if(findroot(a) != findroot(b))link(a,b);  break;
            case 2:  cut(a,b);   break;
            case 3:  splay(a);   t[a].val=b;   break;
        }
    }
    return 0;
}

应用

  1. 判断连通性:findroot(a)==findroot(b)
  2. 求两点之间距离 先执行split(x,y),然后累加这课Splay树的边权
  1. 求LCA
//洛谷P3379的部分代码
const int N=500000+5;
int query_lca(int x, int y) {     //求LCA(x, y)
access(x);
int ans;
for (int child = 0; y; child = y, y = t[y].fa) {  //模拟access(), y==0时退出
    splay(y); //若y在从根出发的路径p上,splay(y)后y是Splay的根,y没有父结点,t[y].fa=0
    t[y].ch[1] = child; 
    ans = y;
}
return ans;
}
int main() {
int n,m,rt;  scanf("%d%d%d",&n,&m,&rt);
for (int i=1; i<n; ++i) { int x,y; scanf("%d%d",&x,&y); link(x, y);}
makeroot(rt);           //定义根结点是rt
for (int i=1; i<=m; ++i){ int x,y; scanf("%d%d",&x,&y); printf("%d\n", query_lca(x, y));}
return 0;
}
  • 19
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值