高级数据结构
最近开始看,《算法竞赛从入门到进阶》。这本书是罗老师写的,简称罗书。
觉得这本书比起大众会难一些,同时又会比那本经典的《算法导论》教材更加通俗一些。
并查集
并查集思想常用“帮派”思想来转换,显得更加通俗易懂。
比如,如下是自己做的一张图:
其中,我们含有一个数组a,一开始初始化a[i] = i。
可以这么理解,帮派1只有一个以1为代表的老大,以此类推……
当我们要合并时,就会有这么一个变换,即某个帮派的老大带领自己的小弟们一起加入另一个帮派了。【过程二->过程三,老大3带领自己的小弟1一起加入5。】
不难发现有这么一个问题,加入我们下一个过程要执行的一个合并操作为
Union(1, 4),那么实际上我们的操作应该是找到1的老大,让这个老大改为4的小弟才算完。那么我们从1开始找老大就会往上找两次,下次再有1的合并操作的话找的次数也会逐渐增加!
所以我们需要在找老大的过程中做一个优化,即让不是老大的这些个结点的a值都直接指向当前老大(无法理解的话可以看代码)。
同时做的一个优化是,我们一开始Union操作都默认把前边那个“帮派”归并到后边的帮派了,于是会导致“树”一味长高。(可见上图)
那么这样一来我们就维护一个height,一开始都初始化为0。
最后要合并的时候我们判断树的高度,根据树的高度做不一样的合并操作。
代码如下所示:
#include <iostream>
#include <algorithm>
using namespace std;
// 并查集的思想在于压缩路径,通过Debug的方式来加深理解
class Disjoint_Set{
int *a;
int *height;
int num;
public:
Disjoint_Set(int n) : num(n) {}
void Init();
int Find(int x);
int Optimized_Find(int x);
void Union(int x, int y);
void Optimized_Union(int x, int y); // 基础上进行的优化
int Howmany();
~Disjoint_Set() { delete[] a, delete[] height; }
};
void Disjoint_Set::Init(){
a = new int[num + 1];
height = new int[num + 1];
for (int i = 1; i <= num; i ++){
a[i] = i; // 将每个集合的代表元素设置为自身
height[i] = 0; // 将各个集合视为一棵树的话,默认一开始高为0
}
}
int Disjoint_Set::Find(int x){ // 实际上是找祖先的一个过程,当某个元素之值==集合代表值时称为根
return x == a[x] ? x : Find(a[x]);
}
int Disjoint_Set::Optimized_Find(int x){
// 相较于基础的寻根,这儿仅仅多加了一个操作就是把路径上的那些都改为新根值
if(x != a[x]){
a[x] = Optimized_Find(a[x]);
}
return x;
}
void Disjoint_Set::Union(int x, int y){
int a_x = Find(x);
int a_y = Find(y);
if(a_x != a_y){
a[a_x] = a_y;
}
}
void Disjoint_Set::Optimized_Union(int x, int y){
int a_x = Optimized_Find(x);
int a_y = Optimized_Find(y);
if(height[a_x] == height[a_y]){
a[a_x] = a[a_y]; // 等高时反过来也无所谓
height[y]++;
}
else{ // 把矮树挂到高树上高度就不会变,否则总高会加1
if(height[a_x] > height[a_y]){
a[a_y] = a[a_x];
}
else{
a[a_x] = a[a_y];
}
}
}
int Disjoint_Set::Howmany(){ // 计算有几个不同的集合
int sum = 0;
for (int i = 1; i <= num; i ++){
if(a[i] == i){
sum++;
}
}
return sum;
}
int main(){
int num;
cin >> num;
Disjoint_Set DS(num);
DS.Init();
}
课本例题,HDU1213,链接How Many Tables
Input
2
5 3
1 2
2 3
4 5
5 1
2 5
Output
2
4
题目含义简译:
一个生日上有N个人,他们想和“自己认识的人”坐一张桌子。问需要几张桌子。
(假如A认识B,B认识C,那么默认A认识C)
输入T个案例,N个人,M是关系数量,即某某和某某认识(用以合并操作)
理解上面的代码后,可以直接套:
如下是AC代码:`
#include <iostream>
using namespace std;
int T, N, M;
int p1, p2;
int a[1001];
int Optimized_Find(int x){
// 相较于基础的寻根,这儿仅仅多加了一个操作就是把路径上的那些都改为新根值
return x == a[x] ? x : Optimized_Find(a[x]);
}
void Optimized_Union(int x, int y){
int a_x = Optimized_Find(x);
int a_y = Optimized_Find(y);
a[a_x] = a[a_y];
}
int Howmany(){ // 计算有几个不同的集合
int sum = 0;
for (int i = 1; i <= N; i ++){
if(a[i] == i){
sum++;
}
}
return sum;
}
int main(){
cin >> T;
while(T --){
cin >> N >> M;
for (int i = 1; i <= N; i ++){
a[i] = i;
}
for (int i = 0; i < M; i ++){
cin >> p1 >> p2;
Optimized_Union(p1, p2);
}
cout << Howmany() << endl;
}
}
实际上很曲折
【一直内存超限。我先删除了height数组,仍旧超限,然后取消了类,还是超限。最后是把寻根优化那部分重新打回原样,才得以AC。】
但实际上,路径压缩还是很重要的!尤其是大数据量时,如果不优化“寻根”,最后递归层级会非常深。但每一轮都改变一下就会好很多。
二叉树
详细内容可以查看Blog的“树”章节。
这里主要针对hdu 1710,由一棵树的先序和中序遍历求一棵树的后序遍历习题展开。链接hdu 1710
样例:
Input
9
1 2 4 7 3 5 8 9 6
4 7 2 1 8 5 9 3 6
Output
7 4 2 8 9 5 6 3 1
首先,要知道先序是“根->左->右”,而中序是“左->根->右”。
因此我们知道在中序遍历中,只要在根节点左边的就是左子树部分,右边的就是右子树部分,而这正好是先序遍历所缺失的。
我们可以把先序遍历的结果,每一个结点都单独看做一个根节点。然后,我们就去中序中找到此结点所在位置,此结点右边部分是根节点右子树,反之左子树。如果此结点的左右部分为空,即已经到叶子结点了,递归返回,返回到此叶子的父结点继续操作。
#include <iostream>
using namespace std;
const int N = 1010;
int num;
int pre[N], mid[N], post[N];
int k = 0;
struct TreeNode{
int val;
TreeNode* left;
TreeNode *right;
TreeNode(int value = 0, TreeNode* l = NULL, TreeNode* r = NULL):val(value), left(l), right(r){}
};
void buildtree(TreeNode* &root, int l, int r, int& num){
int flag = -1;
for (int i = l; i <= r; i ++){
if(mid[i] == pre[num]){
flag = i;
break;
}
}
if(flag == -1)
return;
root = new TreeNode(mid[flag]);
num++;
buildtree(root->left, l, flag - 1, num);
buildtree(root->right, flag + 1, r, num);
}
void posttraversetree(TreeNode* root){
if(root){
posttraversetree(root->left);
posttraversetree(root->right);
post[k++] = root->val;
}
}
int main(){
cin >> num;
for (int i = 1; i <= num; i ++){
cin >> pre[i];
}
for (int i = 1; i <= num; i ++){
cin >> mid[i];
}
// 左 -> 右 -> 根的顺序下,整棵树的根节点和中序是一样的
TreeNode *root;
int t = 1;
buildtree(root, 1, num, t);
posttraversetree(root);
for (int i = 0; i < num; i ++){
if(i != num - 1)
cout << post[i] << ' ';
else
cout << post[i] << endl;
}
return 0;
}
二叉搜索树
可以参考之前写过的一份代码【其中的BSTree是,AVLTree难度较高】
#include<iostream>
#include<algorithm>
#include<string>
#include<vector>
#include<stack>
#include<queue>
#include<deque>
#include<map>
#include<cmath>
#include<string.h>
using namespace std;
const int LH = 1; // 左高
const int EH = 0; // 等高
const int RH = -1; // 右高
const int flagl = 1;
const int flagr = 0;
struct BSTreeNode{
BSTreeNode *left;
BSTreeNode *right;
int data;
};
class BINARY_SEARCH_TREE{
private:
BSTreeNode *root;
BSTreeNode *loc;
BSTreeNode *q;
public:
BINARY_SEARCH_TREE() { root = NULL; }
BSTreeNode *GetRoot() { return root; }
void CreateTree(vector<int> &p);
bool Search(BSTreeNode *p);
void Insert(BSTreeNode *p);
void Delete(BSTreeNode* p);
void Traverse(BSTreeNode* t);
};
void BINARY_SEARCH_TREE::CreateTree(vector<int> &p){
// 创建二叉搜索树用到的是插入创建法
// 根据教材所给方案,始终用search的方式来查到一个结点所应该在的位置
for (size_t i = 0; i < p.size(); i ++){
BSTreeNode* temp = new BSTreeNode;
temp->left = temp->right = NULL;
temp->data = p.at(i);
Insert(temp);
}
}
bool BINARY_SEARCH_TREE::Search(BSTreeNode* p){
// 递归搜索此结点代表的值是否存在于二叉树中
if(!q) return false;
else if(p->data > q->data){
loc = q;
q = q->right;
return Search(p);
}
else if(p->data < q->data){
loc = q;
q = q->left;
return Search(p);
}
return true;
}
void BINARY_SEARCH_TREE::Insert(BSTreeNode *p){
if(!root){
root = p;
}
else if(!Search(p)){
if(loc->data < p->data) loc->right = p;
else loc->left = p;
}
q = root;
}
void BINARY_SEARCH_TREE::Traverse(BSTreeNode* t){
// 利用中序遍历的形式来得到排序好的序列
if(t){
Traverse(t->left);
cout << t->data << " ";
Traverse(t->right);
}
}
void BINARY_SEARCH_TREE::Delete(BSTreeNode* p){
// 删除要考虑情况
// 如果删除的是叶子结点,或者说只有左子树抑或右子树的话,那么直接链到另一边即可
// 如果删除的结点有左右孩子的话
//
if(!Search(p)){
cout << "不存在这个数" << endl;
}
else{
BSTreeNode *temp = q;
if(!q->left){
q = q->right;
if(loc->left == temp) loc->left = q;
else loc->right = q;
delete[] temp;
}
else if(!q->right){
q = q->left;
if(loc->left == temp) loc->left = q;
else loc->right = q;
delete[] temp;
}
else{
// 用教材中的法2
// 即删除节点的直接后继,即左孩子的最右孩子作为新根
// 这个最右孩子的左子树全部挂到 这个最右孩子的根节点的右孩子上!!
// 稍微有丢复杂
BSTreeNode *t = q->left;
while(t->right){
temp = t; // 保存那个最右结点的根
t = t->right; // 寻找到最右结点
}
q->data = t->data;
if(temp!=q) temp->right = t->left;
else temp->left = t->left;
delete[] t;
}
}
}
struct ATREE{
ATREE* left;
ATREE* right;
int data;
int bf;
};
int main(){
/*
BINARY_SEARCH_TREE tree;
vector<int> vec{30, 23, 7, 13, 2, 0, 99};
tree.CreateTree(vec);
BSTreeNode *t = tree.GetRoot();
BSTreeNode *temp = new BSTreeNode;
temp->left = temp->right = NULL;
temp->data = 55;
tree.Insert(temp);
tree.Traverse(t);
delete[] temp;
cout << endl;
BSTreeNode *case3 = new BSTreeNode;
case3->left = case3->right = NULL;
case3->data = 7;
tree.Delete(case3);
t = tree.GetRoot();
tree.Traverse(t);
cout << endl;
*/
/*
// 二叉平衡树
AVLTREE tree;
ATREE* r = tree.GetRoot();
vector<int> vec{30, 23, 7, 13, 2, 0, 99};
for(size_t i = 0 ; i < vec.size() ; i ++){
tree.Insertion(r, vec[i]);
if(!r)
r = tree.GetRoot();
}
tree.LayerTraverse();
*/
return 0;
}
Treap树
这也是一种较为容易的平衡树。
【实际上学习过AVL树或者说红黑树的话可以看出此树并不是严格平衡的,它是存在左右子树高度差超过1的情况的。】
简单介绍平衡树:
这是对二叉搜索树很有可能退化成链表的一种有效改进。试想一个数组[5, 4, 3, 2, 1]变为二叉搜索树的话,可能是以1为根其余逐一挂在右子树上;同样可能是5位根,其余挂在左子树上,实际上就像是链表关系。
平衡在于调整左右子树的高度,让其看起来不那么极端,使得所有数的搜索路径长度都比较平缓。
Treap树的结点类型含有一个“优先级”元素。我们不仅遵循BST原则,即:右子树上任何值永远比根及左子树大。
那么定义此树时还有一个重要原则就是,子结点的优先级要低于根结点。
其实此树有点不三不四的感觉,不是正宗的二叉平衡树但又带有平衡法的一些基本操作,左旋右旋等……竞赛中,常与“名次树”综合考评。
接下来就仔细研究一下源码看看实现流程及一些注意点:
// hdu-4585 名次树
// hdu-3726
#include <iostream>
using namespace std;
struct TreeNode{
int size; // 用于统计以某结点为根的子树的总结点数,可以很快统计出名次
int val; // 基本元素值
int priority; // 优先级
TreeNode *son[2]; // 用son[0]代表左孩子 1为右孩子
int cmp(int x) const { // 实际上就是搜索方式,帮助我们判断往左还是往右
if(x == val)
return -1;
return x < val ? 0 : 1;
}
void update(){
size = 1; // 调整时每次都需要从1开始加,因为左旋右旋后原先那一部分丢失
if(son[0] != NULL)
size += son[0]->size;
if(son[1] != NULL)
size += son[1]->size;
}
};
void rotate(TreeNode* &o, int x){
// 先把优先级高的孩子结点挑出来
TreeNode *p = o->son[x];
/*
假如挑出来的是右孩子,那么右孩子变为根的同时,右孩子的左子树挂到原根的右子树上
假如挑出来的是左孩子,那么左孩子变为根的同时,左孩子的右子树挂在原根的左子树上
不难发现,右孩子(选左子树),左孩子(选右子树)
*/
o->son[x] = p->son[x ^ 1]; // 根据教材所示,且有理论基础的支撑,异或会更快
p->son[x ^ 1] = o;
o->update(); // 不能交换顺序,此时原根o已经是变为p的子节点了
p->update(); // 要先更新子节点的值,根结点最后才会正确
o = p; // 换回来,前边的递归可能还未结束,新的根结点可能还要往上爬!
}
int kth(TreeNode* &o, int k){ // 此函数用于返回第k名是谁
if(o == NULL || k <= 0 || k > o->size){
cout << "不合法值" << endl;
return -1;
}
else{
TreeNode *p = o->son[1];
if(k == p->size + 1)
return o->val;
else if(k <= p->size)
return kth(p, k);
else
return kth(o->son[0], k - p->size - 1);
}
}
int find(TreeNode* &o, int x){ // 此函数用于返回某个数为第几名
if(o == NULL)
return -1;
else{
int d = o->cmp(x); // 助力我们判断往左还是往右
if(d == -1)
return o->son[1] == NULL ? 1 : o->son[1]->size + 1;
else if(d == 1)
return find(o->son[1], x);
else{
int tmp = find(o->son[0], x);
if(tmp == -1)
return -1;
else
return o->son[1] == NULL ? tmp + 1 : tmp + 1 + o->son[1]->size;
}
}
}
// 插入结点
void insert(TreeNode* &o, int x){ // 把x插入到树中
if(o == NULL){ // 空树很好处理,开辟一个根结点后按规定指定值即可
o = new TreeNode;
o->son[0] = o->son[1] = NULL;
o->val = x;
o->priority = rand(); // 每个结点的优先级不是靠输入的,而是随机生成的!!
o->size = 1;
}
else{
int ret = o->cmp(x); // 先要判断此结点应该成为左还是右孩子
if(ret != -1){
insert(o->son[ret], x); // 因为我们仅仅是与根结点作比较,不是说直接就找到插入位置的,要递归搜索直到叶子
o->update(); // 更新子树总结点数
if(o->priority < o->son[ret]->priority){ // 根据规则假如优先级比根结点高就要进行调整(利用重载函数)
rotate(o, ret); // 依靠左旋和右旋整
}
}
}
}
void traverse(TreeNode* &o){
// 利用树的中序遍历 从小到大
if(o){
traverse(o->son[0]);
cout << "val: " << o->val << endl
<< "priority: " << o->priority << endl
<< "size: " << o->size << endl;
traverse(o->son[1]);
}
}
void pretraverse(TreeNode* &o){
// 利用树的中序遍历 从小到大
if(o){
cout << "val: " << o->val << endl
<< "priority: " << o->priority << endl
<< "size: " << o->size << endl;
pretraverse(o->son[0]);
pretraverse(o->son[1]);
}
}
int main(){
int n;
scanf("%d", &n);
int k;
scanf("%d", &k);
TreeNode* root = new TreeNode;
root->son[0] = root->son[1] = NULL;
root->size = 1;
root->priority = rand();
root->val = k;
for (int i = 1; i < n; i ++){
scanf("%d", &k);
insert(root, k);
}
// 我用的测试为
// 6
// 30 23 7 13 3 2
traverse(root);
printf("\n");
pretraverse(root);
printf("\n");
printf("%d", kth(root, 3));
printf("\n");
printf("%d", find(root, 23));
}
以上看懂即可,但一定要看懂,我之前看源代码就喜欢一知半解,一半靠看懂另一半靠自己内心猜。
总体来看Treap树会比AVL树和红黑树简单不少,正如简述里说的其在题目中常作为名次树出现。主要功能在于快速找到一个元素排第几或者说找到排第几的元素。
因此其作为一棵树多维护的一些量是size,即子树的大小。
HDU-4585shaolin
Input
3
2 1
3 3
4 2
0
Output
2 1
3 2
4 2
首先声明的是,此题用map是一种很不错的解法,但我们目的在于训练自己对于Treap树是否熟悉了,因此用Treap树来提交。
先解释一下题目含义:
少林寺有武僧,每个僧人都有自己独特的编号和武力值。一个僧人刚进入时会选择和自己武力值最接近的老僧人进行一场比试,如果有两个老僧人和自己的差值的绝对值相同那么和武力值低一点那个比试。
方丈是一位编号为1,武力值1e9的僧人,他现在手上有各位僧人进入少林的时间记录但丢失了每一场比试的记录,要根据前者恢复后者。
输入的意思:
第一行代表方丈之后进入少林寺的僧人个数。此后,根据个数会按进入少林从早到晚时间顺序打出此僧人的编号和武力值。最后以0结尾。
输出含义:
当2僧人进入时,只能和方丈打,于是2-1。
3号僧人进入时,明显和2差距更小,选择2,于是3-2。
4号进入时,也是和2差距更小,也选择了2。于是4-2。
如何利用Treap树?
根结点确定了,就是第一个武僧。
然后我们每插入一个僧人结点就判断他的名次。然后我们去找排名在其前后一位的僧人,并且返回武力值。找到差距较小,相等情况下就找比自己小的。然后按那个武力值再返回找到对应的僧人编号。
在此还需维护一个武力值和僧人编号的对应数组。
这个时候到了一个困惑的时候,我要是维护这么一个数组的话为什么不直接遍历数组?
试想我们用一个 id[g] = k来表示如下含义:
武力值为g的僧人对应id为k。当然了你想反过来也无所谓!
题目中给出g,k都是 < = 5000000,但题目可没告诉你数值是连续的,只是给出的示例是连续的。
这意味着什么,你可能得从 0 一直遍历到 5000000才会找到最接近的武力值,更何况中间一些武力值可能根本没有对应的僧人编号。这个虽然说看起来是O(n)的时间规模是一个较为优秀的算法,但细思极恐!
如果你用一棵树,就不会存在去访问空结点的问题,冗余的比较次数也会消除。
而如果用STL的map的话就更佳了,map的底层实现是一棵红黑树,具有极其优良的性质。
我们不断insert键值对入map中,然后每次插入前,用迭代器对map已有僧人进行访问查找到武力值最接近的那个点,用变量来维护找到的僧人编号即可。
map是O(logn)级别的,而vector显然是O(n)级别的,改选谁做容器显而易见。
上面权当是废话,主要还是树的实现。
代码如下:(仔细阅读过上面的话应该没什么大问题)
【注:后面加入的僧人武力值都在0 - 5000000,除了第一位进来的要和方丈打,后面的无论从哪种情况考虑都不会再和方丈打了,因此无需考虑方丈这个大BUG!】
#include <iostream>
using namespace std;
const int nums = 5e6 + 1;
int n;
int g, k;
int id[nums]; // 用于记录武力值与id号一一对应,等我们找到武力值后直接索引到id号
struct TreaptreeNode{
int size;
int priority;
int kongfu;
TreaptreeNode *son[2];
int cmp(int _kongfu){
if(kongfu == _kongfu)
return -1;
else{
if(_kongfu > kongfu)
return 1;
return 0;
}
}
void update(){
size = 1;
if(son[0] != NULL)
size += son[0]->size;
if(son[1] != NULL)
size += son[1]->size;
}
};
void rotate(TreaptreeNode* &o, int ret){
TreaptreeNode *q = o->son[ret];
o->son[ret] = q->son[ret ^ 1];
q->son[ret ^ 1] = o;
o->update();
q->update();
o = q;
}
void insert(TreaptreeNode* &o, int kongfu){
if(o == NULL){
o = new TreaptreeNode;
o->kongfu = kongfu;
o->son[0] = o->son[1] = NULL;
o->priority = rand();
o->update();
}
else{
int ret = o->cmp(kongfu);
insert(o->son[ret], kongfu);
o->update();
if(o->priority < o->son[ret]->priority){
rotate(o, ret);
}
}
}
int kth(TreaptreeNode* &o, int k){
if(o == NULL || o->size < k || k <= 0){
return -1;
}
else{
int s = o->son[1] == NULL ? 0 : o->son[1]->size;
if(s + 1 == k){
return o->kongfu;
}
else if(s >= k){
return kth(o->son[1], k);
}
else{
return kth(o->son[0], k - s - 1);
}
}
}
int find(TreaptreeNode* &o, int k){
if(o == NULL){
return -1;
}
else{
if(k == o->kongfu){
return o->son[1] == NULL ? 1 : o->son[1]->size + 1;
}
else if(k > o->kongfu){
return find(o->son[1], k);
}
else{
int ans = find(o->son[0], k);
if(ans == -1)
return -1;
else
return o->son[1] == NULL ? ans + 1 : ans + 1 + o->son[1]->size;
}
}
}
int main(){
while(~scanf("%d", &n) && n){
scanf("%d %d", &g, &k);
id[k] = g;
TreaptreeNode* root = new TreaptreeNode;
root->son[0] = root->son[1] = NULL;
root->priority = rand();
root->update();
root->kongfu = k;
printf("%d 1\n", g);
for (int i = 1; i < n ; i ++){
scanf("%d %d", &g, &k);
id[k] = g;
insert(root, k);
int _k;
int ans = find(root, k);
int ans1 = kth(root, ans - 1);
int ans2 = kth(root, ans + 1);
if(ans1 != -1 && ans2 != -1){
_k = ans1 - k < k - ans2 ? ans1 : ans2;
}
else if(ans1 == -1)
_k = ans2;
else if(ans2 == -1)
_k = ans1;
_k = id[_k];
printf("%d %d\n", g, _k);
}
}
return 0;
}