算法模板——高级数据结构(未完待更)

1. 树状数组

名曰树状数组,那么究竟它是树还是数组呢?数组在物理空间上是连续的,而树是通过父子关系关联起来的,而树状数组正是这两种关系的结合,首先在存储空间上它是以数组的形式存储的,即下标连续;其次,对于两个数组下标 x , y ( x < y ) x,y(x < y) x,y(x<y),如果 x + 2 k = y x + 2^k = y x+2k=y ( k k k等于 x x x的二进制表示中末尾0的个数),那么定义 ( y , x ) (y, x) (y,x)为一组树上的父子关系,其中 y y y为父结点, x x x为子结点。

在这里插入图片描述
然后我们来看树状数组上的结点Ci具体表示什么,这时候就需要利用树的递归性质了。我们定义Ci的值为它的所有子结点的值 和 Ai 的总和,之前提到当i为奇数时Ci一定为叶子结点,所以有Ci = Ai ( i为奇数 )。

C1 = A1
C2 = C1 + A2 = A1 + A2
C3 = A3
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
C5 = A5
C6 = C5 + A6 = A5 + A6
C7 = A7
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8

我们从中可以发现,其实Ci还有一种更加普适的定义,它表示的其实是一段原数组A的连续区间和。

树状数组(Binary Indexed Tree(B.I.T), Fenwick Tree)作为一个查询和修改复杂度都为 O ( l o g n ) O(logn) O(logn)的数据结构。下面我们就看一下这两个操作的具体实现:

求和操作

查询 [ l , r ] [l, r] [l,r]的和,即为 s u m ( r ) − s u m ( l − 1 ) sum(r)-sum(l-1) sum(r)sum(l1)

int sum(int x){
	int s = 0;
	for(int i=x;i;i-=lowbit(i))
		s += c[i];
	return s;
}

更新操作

void add(int x, int v){
	for(int i=x;i<=n;i+=lowbit(i)) 
		c[i] += v;
}

lowbit函数实现

int lowbit(int x){
	return x&(-x);
}

1.1 PUIQ模型

单点更新,区域查询(标准的树状数组)

1.2 降维

总的来说就是,保证树状数组只处理其中的一个维度,至于其他的维度根据题目做不同处理(但都不是利用树状数组进行处理)

1.3 二分模型

1.4 多维树状数组

简单来说就是,用一个树状数组同时处理多个维度

void add(int x, int y, int v)
{
	for(int i=x;i<=n;i+=lowbit(i))
		for(int j=y;j<=n;j+=lowbit(j))
			c[i][j] += v;
}

int sum(int x, int y)
{
	int s = 0;
	for(int i=x;i;i-=lowbit(i))
		for(int j=y;j;j-=lowbit(j))
			s += c[i][j];
	return s;
}

1.5 逆序模型

来看一个给定 n n n个元素的排列 X 0 , X 1 , X 2 , … , X n − 2 , X n − 1 X_0,X_1,X_2,…,X_{n-2},X_{n-1} X0,X1,X2,,Xn2,Xn1,对于某个 X i X_i Xi 元素,如果想知道以它为"首"的逆序对的对数( 形如 ( X i X j ) (X_iX_j) (XiXj) 的逆序对),就是需要知道 X i + 1 , … , X n − 2 , X n − 1 X_{i+1}, … ,X_{n-2}, X_{n-1} Xi+1,,Xn2,Xn1 这个子序列中小于 X i X_i Xi 的元素的个数。

那么我们只需要对这个排列从后往前枚举,每次枚举到 X i X_i Xi 元素时,执行 c n t + = s u m ( X i − 1 ) cnt += sum(X_i-1) cnt+=sum(Xi1),然后再执行 a d d ( X i , 1 ) add(X_i, 1) add(Xi,1) n n n个元素枚举完毕,得到的 c n t cnt cnt值就是我们要求的逆序数了。总的时间复杂度 O ( n l o g n ) O(nlogn) O(nlogn)

注意,有些OJ题目给出 X i X_i Xi的取值范围很大,而树状数组的树状部分 C [ . . . ] C[...] C[...]是建立在一个有点像位存储的数组的基础之上的,不是单纯的建立在输入数组之上。所以这时我们会用到离散化,离散化的过程一般为:将 a [ . . . ] a[...] a[...]升序排列, i n d e x [ i ] = j index[i]=j index[i]=j i i i为排序前元素的下标, j j j为排序后元素的下标。

同样是用树状数组求逆序数,如果对排列从前往后枚举,则算法过程会有些不同。如果数据不是很大,我们可以将数据一个个插入到树状数组中,每插入一个数( a d d ( X i , 1 ) add(X_i, 1) add(Xi,1)),就统计比它小的数的个数 s u m ( X i ) sum(X_i) sum(Xi),那么 i − s u m ( X i ) i-sum(X_i) isum(Xi)即为比 X i X_i Xi大的个数,也就是 X i X_i Xi的逆序数, c n t + = i − s u m ( X i ) cnt+=i-sum(X_i) cnt+=isum(Xi). 同时需要注意是否需要先用到离散化。

1.6 多重树状数组

不同于多维树状数组,这里,我们用到了多个树状数组进行处理

void add(int x, int v, int op){
    for(int i=x;i<=N;i+=lowbit(i))
        c[op][i] += op?x:v;
}

int sum(int x, int op){
    int s = 0;
    for(int i=x;i;i-=lowbit(i))
        s += c[op][i];
    return s;
}

在多重树状数组下面,有个小分支的情况:有些时候不一定要把 c [ . . . ] c[...] c[...] 数组增加一个维度(即开出两个树状数组来),可以清零后再复用一次,至于这种情况要仔细判断是否真的两个维度是前后执行的,如果是并行执行的,那么还是老老实实的把 c [ . . . ] c[...] c[...] 数组增加一个维度

2. 单调队列

在这里插入图片描述
举个例子,
在这里插入图片描述

class MonotonicQueue { //单调队列(从大到小)
public:
    deque<int> que; // 使用deque来实现单调队列
    // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。
    // 同时pop之前判断队列当前是否为空。
    void pop (int value) {
        if (!que.empty() && value == que.front()) {
            que.pop_front();
        }
    }
    // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 
    // 这样就保持了队列里的数值是单调从大到小的了。
    void push (int value) {
        while (!que.empty() && value > que.back()) {
            que.pop_back();
        }
        que.push_back(value);

    }
    // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。
    int front() {
        return que.front();
    }
};

3 并查集

class DisjoinSetUnion{
private:
	vector<int> father, rank;
	int n;
	
public:
	DisjoinSetUnion(int _n){
		n = _n;
		rank.resize(n, 1);
		father.resize(n);
		for(int i=0;i<n;i++) father[i] = 1; 
	}
	
	int find(x){
		return father[x]==x?x:father[x]=find(f[x]);
	}
	
	void merge(int x, int y){
		int father_x = find(x), father_y = find(y);
		if(father_x==father_y) return;
		if(rank[father_x]<rank[father_y]) swap(father_x, father_y);
		rank[father_x] += rank[father_y];
		father[father_y] = fatehr_x;
	}

};

字典树

Trie,又称前缀树或字典树,是一棵有根树,其每个节点包含以下字段:

  • 指向子节点的指针数组 next \textit{next} next。对于本题而言,数组长度为 26 26 26,即小写英文字母的数量。此时 next [ 0 ] \textit{next}[0] next[0] 对应小写字母 a a a next [ 1 ] \textit{next}[1] next[1] 对应小写字母 b b b,…, next [ 25 ] \textit{next}[25] next[25] 对应小写字母 z z z
  • 布尔字段 isEnd \textit{isEnd} isEnd,表示该节点是否为字符串的结尾

如下图所示是一个包含 sea,   sells,   she \textit{sea, sells, she} sea, sells, she 的字典树

在这里插入图片描述
字典树的数据结构如下,包含了三种常用的操作 insertsearchstartsWith

class Trie {
private:
    bool isEnd;
    Trie* next[26];

public:
    /** Initialize your data structure here. */
    Trie() {
        isEnd = false;
        memset(next, 0, sizeof(next));
    }
    
    /** Inserts a word into the trie. */
    void insert(string word) {
        Trie* node = this;
        for(auto c: word)
        {
            if(node->next[c-'a']==NULL) node->next[c-'a'] = new Trie();
            node  = node->next[c-'a'];
        }
        node->isEnd = true;
    }
    
    /** Returns if the word is in the trie. */
    bool search(string word) {
        Trie* node = this;
        for(auto c: word)
        {
            node = node->next[c-'a'];
            if(node==NULL) return false;
        }
        return node->isEnd;

    }
    
    /** Returns if there is any word in the trie that starts with the given prefix. */
    bool startsWith(string prefix) {
        Trie* node = this;
        for(auto c: prefix)
        {
            node = node->next[c-'a'];
            if(node==NULL) return false;
        }
        return true;
    }
};

/**
 * Your Trie object will be instantiated and called as such:
 * Trie* obj = new Trie();
 * obj->insert(word);
 * bool param_2 = obj->search(word);
 * bool param_3 = obj->startsWith(prefix);
 * 
 * or:
 * Trie trie;
 * bool param_4 = trie.search(word)
 */
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值