寒假集训总结

分块

基本已经比较熟悉了,虽然也没什么能写出来都题,众数都还不太熟悉,更别说一些其他都复杂操作来。

蒲公英

就是分块众数模版了。

莫队

普通莫队

思想大概就是利用上一个询问可以快速修改到下一个询问来优化。

带修莫队

添加一维时间轴,类似于 \(CDQ\) 分治的操作,变为 \(3\) 维的操作,并套一个数据结构如 \(bit\) 维护第三维。

回滚莫队

在答案区间只能扩充而无法减少时,通过回滚莫队先对右端点排序,再对左端点排序。

珂朵莉树

待学。

线段树

树剖

似乎没啥可写的了。

主席树

利用差分的思想记录每一次修改后的权值线段树的值,每个值表示出现次数 \(cnt\),用第 \(r\) 次的值减去 \(l-1\) 次的值得到出现的数的结果。

吉司机线段树

主要可以处理区间赋值为 \(\min(a_i,k)\) 这个正常线段树下略显毒瘤的操作。
暴力修改显然是不行的,即使维护一个 \(maxn\) 值表示是否修改,也对实际复杂度毫无实质优化,甚至在最劣情况下会达到 \(O(n^2\log{n})\)。我们可以做一个看上去玄学的操作,记录一个 \(second\) 表示次大值,如果 \(maxn < k\) 则操作无意义,如果 \(second<k<maxn\),修改最大值就变得十分容易了,但是如果 \(second>k\),那么就继续二分两个区间,直到可以操作为止,因此需要记录 \(cnt\) 表示最大值的个数,\(maxn,second\) 分别表示最大值和次大值。
吉司机的时间复杂度可以这样证明:

我们可以认为每个树上节点的最大值视作区间取 \(min\) 标记,也因为只有最大值会在取 \(min\) 操作中受到影响,次大值作为子树标记中的最大值,至于子树中原本作为最大值上传到父节点的值,实际上对答案统计是无效的。因此,我们可以得到一个从任意叶节点向根节点单调递增的标记。并且存在这样一个性质:每一个点的实际标记值就是从这个点出发,向根节点路径上的第一个值。因为子节点等于父节点的值都被略去了,所以只需一直访问父节点就可以得到原有标记值。而每一次 \(dfs\) 取区间 \(\min(a_i,k)\) 时,进行一次 \(dfs\) 对标记中大于 \(k\) 的进行了回收。可以发现,回收标记的次数不会超过标记下放的次数与打标记的次数之和,而后二者的时间复杂度均为 \(O(n\log n)\) 的,因此回收标记的时间复杂度实际也为 \(O(n\log n)\)

\(zkw\) 线段树

思路

思路

\(zkw\) 线段树是线段树的之中变体,常数更小,空间更小(事实上普通线段树也不一定要 \(4\) 倍空间,可以认为二者的空间复杂度是相近的),码量更小,但是适用性仅在于没有运算优先级的问题,也就与树状数组的实际功能相去无几了,其维护的特点是从叶向上的维护,与递归线段树相反;时间复杂度是 \(\log\) 级别的,与递归线段树相同。

事实上,\(zkw\) 线段树与树状数组极为类似的原因,正是树状数组就是一棵省掉一半空间后中序遍历下的线段树。

\(zkw\) 线段树的维护方式极为巧妙,给定两个指针 \(l=l+N-1,r=r+N+1\),也就是区间左端点左一个与区间右端点右一个。那么对于左指针而言只有当它为左儿子时则修改此时的右儿子,反之对右指针同理,并对路径上修改的打上懒标记,懒标记表示的是当前点的修改,因为懒标记不曾下放,也就同时意味着儿子中未修改的。查询时同理,如果当前点有懒标记那么加上当前的 \(cnt\times lazy\),并加上另一半子树的贡献。

可以发现事实上并不需要使用 \(tree\),因为 \(lazy\) 套上差分就可以完成所有的事,当然这时候常数会略大,实践表面这样时间复杂度已经与递归线段树相近,因此实际意义并不是很大。

例题一:线段树1

思路

基本操作看看就行。

\(Code\)
#include<bits/stdc++.h>
using namespace std;

int n, m, N = 1;
int op, x, y, z;
long long tree[400005], lazy[400005];

template<typename T>
inline void read(T&x){
	x = 0; char q; bool f = 1;
	while(!isdigit(q = getchar()))	if(q == '-')	f = 0;
	while(isdigit(q)){
		x = (x<<1) + (x<<3) + (q^48);
		q = getchar();
	}
	x = f?x:-x;
}

template<typename T>
inline void write(T x){
	if(x < 0){
		putchar('-');
		x = -x;
	}
	if(x > 9)	write(x/10);
	putchar(x%10^48);
}

inline void modify(int l, int r, int num){
    int cntl = 0, cntr = 0, len = 1;
    l = N+l-1, r = N+r+1;
    while(l^r^1){
        tree[l] += num*cntl, tree[r] += num*cntr;
        if(~l&1)    tree[l^1] += num*len, lazy[l^1] += num, cntl += len;
        if(r&1) tree[r^1] += num*len, lazy[r^1] += num, cntr += len;
        l >>= 1, r >>= 1, len <<= 1;
    }
    while(l){
        tree[l] += num*cntl, tree[r] += num*cntr;
        l >>= 1, r >>= 1;
    }
}

inline long long query(int l, int r){
    int cntl = 0, cntr = 0, len = 1;
    long long ans = 0;
    l = N+l-1, r = N+r+1;
    while(l^r^1){
        if(lazy[l]) ans += lazy[l]*cntl;
        if(lazy[r]) ans += lazy[r]*cntr;
        if(~l&1)    ans += tree[l^1], cntl += len;
        if(r&1) ans += tree[r^1], cntr += len;
        l >>= 1, r >>= 1, len <<= 1;
    }
    while(l){
        ans += lazy[l]*cntl+lazy[r]*cntr;
        l >>= 1, r >>= 1;
    }
    return ans;
}

int main(){
    read(n), read(m);
    while(N <= n+1) N <<= 1;
    for(register int i = N+1; i <= N+n; ++i)    read(tree[i]);
    for(register int i = N-1; i >= 1; --i)  tree[i] = tree[i<<1]+tree[i<<1|1];
    for(register int i = 1; i <= m; ++i){
        read(op);
        if(op == 1){
            read(x), read(y), read(z);
            modify(x, y, z);
        }
        if(op == 2){
            read(x), read(y);
            write(query(x, y));
            puts("");
        }
    }
    return 0;
}

平衡树

\(splay\)

经典平衡树,主要还是怎么转了,还是理解了几个问题。

  1. 非单旋而是双旋:单旋并不能改变树的结构,约等于写了一个复杂一些的二叉搜索树。
  2. 关于前驱和后继的奇妙求法:\(find\) 函数如果找不到值的话,一定会找到一个它的前驱或者后继,所以如果求后继不是后继直接跳父亲并跳向另一半儿子,前驱同理。
  3. \(delete\) 操作后如果点存在则 \(splay\),否则无法维护 \(size\) 大小。

树套树

待学。

posted @ 2022-02-18 18:48  Zzzzzzzm  阅读(63)  评论(0)    收藏  举报