蓝桥杯训练day4

并查集,堆,哈希表

  • 1.并查集
    • (1)合并集合
    • (2)连通块中点的数量
    • (3)食物链
    • (4)亲戚
    • (5)银河英雄传说
  • 2.堆
    • (1)堆排序
    • (2)模拟哈希表
  • 3.哈希表
    • (1)模拟散列表
    • (2)字符串哈希
    • (3)笨拙的手指

1.并查集

并查集:有两个庞大的家族,每个家族里面关系都很复杂且明确。每个家族有唯一的一个族长,如果给出家族所有人之间的关系,要将所有的人归类到确定的家族(有什么意义?比如我知道a是b的姑妈,但是这并不知道a是哪个家族(A,B)的,如果知道a是A族长的女儿,那么间接就知道了b是A族长女儿的侄子,那么b就是A族的成员) 这就是并查集要做的第一件事情:根据元素之间的关系给元素分类,每一类元素都一个特有的标签(也就是一个族长)
第二件事情:如果将AB连两个族合并,不需要将A族的成员的全部转移到B族,只需要将A族族长和B族族长联系起来就可以。

(1)合并集合

蓝桥杯训练day4_第1张图片
题目看题解视频,讲的非常透彻

#include
using namespace std;

const int N=100010;


int p[N];  //p[i]=j表示i和j有关系


int n,m;

int find(int x)  //查询祖宗节点+路径压缩
{
    if(p[x]!=x)p[x]=find(p[x]);
    return p[x];
}

int main()
{
    cin>>n>>m;
    int a,b;
    
    for(int i=1;i<=n;i++)  //初始化,一开始所有的元素都是一个集合
        p[i]=i;
        
    while(m--)  //m次操作
    {
        char op;
        cin>>op;
        cin>>a>>b;  //a,b有联系
        
        if(op=='M')  //合并a,b
        {
            p[find(a)]=find(b);
        }
        if(op=='Q')  //查询
        {
            if(find(a)==find(b))puts("Yes");
            else puts("No");
        }
        
    }
    return 0;
}

(2)连通块中点的数量

蓝桥杯训练day4_第2张图片
看懂第一题,这个也懂,一模一样。

#include
#include
using namespace std;


const int N=1e5+10;

int p[N];
int cnt[N];

int n,m;


int find(int x)
{
    if(x!=p[x])p[x]=find(p[x]);  //递归,回溯,回溯的时候路径压缩
    return p[x];
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++)  //初始化,一开始所有的元素都是一个集合
    {
        p[i]=i;
        cnt[i]=1;
    }
        
    while(m--)
    {
        string op;
        int a,b;
        cin>>op;
        
        
        if(op=="C")  //a,b建立关系
        {
            cin>>a>>b;
            int pa,pb;
            pa=find(a);
            pb=find(b);
            if(pa!=pb)
            {
                p[pa]=pb;
                cnt[pb]+=cnt[pa];
            }
        }
        else if(op=="Q1")  //判断a,b是否在一个集合
        {
            cin>>a>>b;
            if(find(a)==find(b))puts("Yes");
            else puts("No");
        }
        else
        {
            cin>>a;
            cout<<cnt[find(a)]<<endl;
        }
        
    }
    return 0;
}

(3)食物链

蓝桥杯训练day4_第3张图片
大概思路:
带权并查集
将所有的关系都放入一个并查集,然后用d[n]来描述各个元素之间的关系。
即–建立关系,用d[n]判断关系。

食物链

#include
#include
using namespace std;

const int N=1e5+10;

int p[N];
int d[N];  //d[i]=j表示i到根节点的距离

int n,k;

int res=0;  //记录假话条数

int find(int x)
{
    if(x!=p[x])
    {
        int t=find(p[x]);
        d[x]+=d[p[x]];
        p[x]=t;
    }
    return p[x];
}

int main()
{
    cin>>n>>k;
    for(int i=1;i<=n;i++)
        p[i]=i;
    while(k--)  //k句话
    {
        int D,a,b;
        cin>>D>>a>>b;
        if(a>n||b>n)
        {
            res++;
            continue;
        }
        int pa=find(a);
        int pb=find(b);
        if(D==1)  //a,b是同类
        {
            if(pa!=pb)  //a,b不在一个集合,说明a,b的关系是第一次被提到,这句是真的
            {
                p[pa]=pb;  
                //d[a]+d[pa]=d[b]
                d[pa]=d[b]-d[a];
            }
            else if(pa==pb&&(d[b]-d[a])%3!=0)  //表示a,b关系之前已经给出且现在最新的一句话是假的
            {
                res++;
            }
        }
        else  //a吃b 即(d[a]+d[pa]-1)=d[b]
        {
            if(pa!=pb)
            {
                p[pa]=pb;
                d[pa]=d[b]-d[a]+1;
            }
            else if(pa==pb)
            {
                if((d[a]-d[b]-1)%3!=0)res++;
            }
        }
        
    }
    
    cout<<res<<endl;
    return 0;
}

(4)亲戚

蓝桥杯训练day4_第4张图片

#include
using namespace std;


const int N=200010;


int p[N];


int n,m,k;


int find(int x)
{
    if(x!=p[x])p[x]=find(p[x]);
    return p[x];
}

int main()
{
    cin>>n>>m;
    
    for(int i=0;i<n;i++)  //初始化并查集,所有元素都是一个集合
        p[i]=i;
    
    while(m--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        int pa=find(a);
        int pb=find(b);
        p[pa]=pb;
    }
    
    cin>>k;
    while(k--)
    {
        int a,b;
        scanf("%d%d",&a,&b);
        int pa=find(a);
        int pb=find(b);
        if(pa==pb)puts("Yes");
        else puts("No");
    }
    return 0;
}


(5)银河英雄传说

蓝桥杯训练day4_第5张图片

#include
#include
using namespace std;
const int N=30010;

int p[N];
int d[N];  //d[i]=j表示i到祖宗节点的距离
int sz[N];  //sz记录集合的点的个数


int n;

int find(int x)
{
    if(x!=p[x])
    {
        int t=find(p[x]);  //t表示x的祖宗节点
        d[x]+=d[p[x]];  //回溯这句话,从祖宗节点开始回溯,那么d[x]会一直累加,1,2,3,4,,,
        p[x]=t;  //回溯的时候要用到还没更新的p[x],所以p[x]需要先保存,让d[x]完成使命再赋值给p[x]
    }
    return p[x];
}
int main()
{
    cin>>n;
    
    for(int i=0;i<N;i++)
        p[i]=i,sz[i]=1;
    
    while(n--)
    {
        char op;
        int a,b;
        cin>>op>>a>>b;
        int pa=find(a);
        int pb=find(b);
        if(op=='M')
        {
            if(pa!=pb)
            {
                p[pa]=pb;  
                d[pa]+=sz[pb];  //pa到pb的距离+节点个数
                sz[pb]+=sz[pa];  //pb列的节点个数
            } 
            
        }
        else
        {
            if(pa==pb)
            {
                cout<<max(0,abs(d[a]-d[b])-1)<<endl;   //如果两个点相邻,那么他们中间没有点
            }
            else
                cout<<"-1\n";
        }
    }
    return 0;
}

2.堆

对于堆的基础,推荐这篇博客:堆的简介
写的很基础,有基本的c基础都可以看懂。

堆是一个有特点的二叉树:对于任意一个节点,他的值大于等于他的所有孩子节点

对于堆的操作,简单介绍

  1. 插入元素:
    将元素放入末尾,然后向上进行比较和转移,维护二叉树的性质(对于任意节点,他大于等于他的左右孩子节点)

  2. 弹出根节点:将最后一个节点的值赋给根节点,然后从根节点开始向下比较和转换,维护二叉树。

//此代码是上述博客的,非本人的
#include
using namespace std;

#define DEFAULT_CAPACITY 128
typedef struct _Heap {
	int* arr;		//存储堆元素的数组
	int size;		//当前已存储的元素个数
	int capacity;	//当前的存储容量
}Heap;


bool initHeap(Heap& heap, int* original, int size);
static void buildHeap(Heap& heap);
static void adjustDown(Heap& heap, int index);
static bool insertHeap(Heap& heap, int value);
static void adjustUp(Heap& heap, int index);
static bool popMax(Heap& heap, int& value);

//初始化堆
bool initHeap(Heap& heap, int* original, int size) {
	//如果默认大小比size小,则申请默认大小的空间,否则申请size大小的空间
	int capacity = DEFAULT_CAPACITY > size ? DEFAULT_CAPACITY : size;
	heap.arr = new int[capacity];
	if (!heap.arr)return false;
	heap.capacity = capacity;
	heap.size = 0;
	//如果存在原始数据,则拷贝过来
	if (size > 0) {
		memcpy(heap.arr, original, size * sizeof(int));
		heap.size = size;
		buildHeap(heap);
	}
	return true;
}

/*从最后一个父节点开始(heap.size) - 1 / 2)(因为size是从1开始,所以要先减去1)
逐个调整所有的父节点(直到根节点),确保每一个父节点都是最大堆,最后
整体上形成一个最大堆*/
void buildHeap(Heap& heap) {
	for (int i = (heap.size - 1) / 2; i >= 0; i--) {
		adjustDown(heap, i);
	}
}

void adjustDown(Heap& heap, int index) {
	int cur = heap.arr[index];  //记录父节点值
	int parent, child;

	/*怕段是否存在大于当前结点的子节点,如果不存在,则堆本身平衡,不需要
	调整,如果存在,则将最大子节点与之交换,交换后,如果这个子节点还有
	子节点(即parent*2+1
	for (parent = index; (parent * 2 + 1) < heap.size; parent = child) {
		child = parent * 2 + 1; //左子节点

		//取两个子节点最大结点
		if (((child + 1) < heap.size) && (heap.arr[child + 1] > heap.arr[child])) {
			child++;
		}
		if (cur >= heap.arr[child])break;//不大于,跳出循环
		else {
			/*大于当前父节点,进行交换,然后从子节点位置继续向下调整,
			即for从第二次循环开始,初始值都为上一次的子节点位置*/
			heap.arr[parent] = heap.arr[child];
			heap.arr[child] = cur;
		}
	}
}
static bool popMax(Heap& heap, int& value);
bool popMax(Heap& heap, int& value) {
	if (heap.size < 1)return false;
	value = heap.arr[0];

	//将size-1的位置值赋给根节点,size本身又--
	/*相当于
		heap.arr[0] = heap.arr[heap.size-1];
		heap.size--;
	*/
	heap.arr[0] = heap.arr[--heap.size];
	adjustDown(heap, 0);  //向下执行堆调整
	return true;
}


bool insertHeap(Heap& heap, int value) {
	if (heap.size == heap.capacity) {
		fprintf(stderr, "栈空间耗尽!\n");
		return false;
	}

	int index = heap.size;
	heap.arr[heap.size++] = value;//先赋值value,再size++
	adjustUp(heap, index);
}

void adjustUp(Heap& heap, int index) {
	if (index < 0 || index >= heap.size) {
		//如果只有一个结点(插入的结点)inedx<0,或者大于堆的最大值,return掉
		return;
	}
	int temp = heap.arr[index];//temp为插入的值
	while (index > 0) {

		int parent = (index - 1) / 2;
		if (parent >= 0) {   //如果索引没有出界,就执行想要操作
			if (heap.arr[parent] < temp) {
				heap.arr[index] = heap.arr[parent];
				heap.arr[parent] = temp;
				index = parent;
			}
			else break;    //如果没有比父结点大,则跳出
		}
		else break;  //越界,结束循环
	}
}



int main() {

	Heap hp;
	int orignArry[] = { 1,2,3,87,93,82,92,86,95 };

	if (!initHeap(hp, orignArry, sizeof(orignArry) / sizeof(orignArry[0]))) {
		fprintf(stderr, "初始化堆失败!\n"); //输出到控制台
		exit(-1);
	}

	for (int i = 0; i < hp.size; i++) {
		cout << "第" << i << "个数为:" << hp.arr[i] << endl;
	}

	//向堆中插入元素
	insertHeap(hp, 99);
	printf("在堆中插入新的元素99,插入结果:\n");
	for (int i = 0; i < hp.size; i++) {
		cout << "第" << i << "个数为:" << hp.arr[i] << endl;
	}

	int value;
	while (popMax(hp, value)) {
		cout << "依次出列最大元素:" << value << endl;
	}

	system("pause");
	return 0;
}




(1)堆排序

蓝桥杯训练day4_第6张图片

#include
#include
using namespace std;

const int N=1e6+10;
typedef long long ll;

ll h[N];


int n,m;

int hc;

void down(int father)  //对father进行转移(向下)
{
    int t=h[father];
    int child=father*2+1;
    //将当前节点的左右孩子节点与它进行比较,将最小的放到father的位置
    if(child<=hc)  //左孩子
    {
        if(h[child]<t)
            t=h[child];
    }
    if(child+1<=hc)//右孩子
    {
        if(h[child+1]<t)
        {
            t=h[child+1];
            child=child+1;
        }
    }
    
    if(t<h[father]) //更新父亲节点
    {
        h[child]=h[father];
        h[father]=t;
        down(child);  //由于child变动,所以继续调整
    }
}


int main()
{
    cin>>n>>m;
    for(int i=0;i<n;i++)  //输入堆数组,但没有排序
        cin>>h[i];
        
    hc=n-1;   //hc表示堆元素个数,必须在这里赋初值,否则down函数里面的hc是随机的
    //要对堆排序,然后输出前面m个数
    for(int i=(n-1)/2;i>=0;i--)  //i*2+1表示i的左孩子,i*2+2表示i的右孩子,(i-1)/2表示i的父节点
    //n/2表示最后一层有孩子的根节点中的最右边一个。
    {
        down(i);
    }
    
    while(m--)
    {
        cout<<h[0]<<" ";
        //输入堆顶元素,然后要弹出继续更新
        h[0]=h[hc];  //将堆最后一个元素赋值给堆顶元素,(相当于堆顶弹出)然后从堆顶开始更新
        hc--;
        down(0);
    }
    return 0;
}

(2)模拟哈希表

蓝桥杯训练day4_第7张图片模拟。这里的hp和ph数组的相互配合非常巧妙

#include
#include
using namespace std;
const int N=1e5+10;
int n,m=0;//输入需要操作的次数
int h[N],ph[N],hp[N],cnt;//h代表heap(堆),ph(point->heap)可以获得第几个插入的元素现在在堆的那个位置
//hp(heap->point)可以获得在堆的第n个元素存的是第几个插入的元素
void heap_swap(int a,int b){//交换在heap中位置分别为a,b的两个元素
    swap(ph[hp[a]],ph[hp[b]]);//根据a和b的位置找到它们分别是第几个插入的元素,然后将其(在h数组中的)下标转换
    swap(hp[a],hp[b]);//将两个位置存的是第几号元素转换
    swap(h[a],h[b]);//最后再转换值(这三个语句位置可以换,但是从上到下逐渐变短的话比较美观)
}
void down(int u){//当前堆的元素下沉
    int t=u;//让t代指u以及其两个儿子(三个点)中的最大值
    if(u*2<=cnt and h[u*2]<h[t])t=u*2;
    if(u*2+1<=cnt and h[u*2+1]<h[t])t=u*2+1;//注意此处为d[t]
    if(u!=t){//最小值不是t,那么下沉,并且继续down操作
        heap_swap(u,t);
        down(t);
    }
}
void up(int u){
    while(u/2 and h[u/2]>h[u]){//第一个u/2是防止当u冲到顶然后陷入死循环
        heap_swap(u/2,u);
        u/=2;
    }
}
int main(){
    cin>>n;
    while(n--){
        string op;
        int k,x;
        cin>>op;
        if(op=="I"){
            cin>>x;
            cnt++,m++;
            ph[m]=cnt;//表示第m个插入的数的下标是cnt
            hp[cnt]=m;//表示下标是cnt的数是第m个插入的数
            h[cnt]=x;
            up(cnt);
        }else if(op=="PM"){//Print Min 打印最小
            cout<<h[1]<<endl;
        }else if(op=="DM"){
            heap_swap(1,cnt);//将底部一个元素放上来
            cnt--;//所有元素数量减一
            down(1);//将放上来的元素沉下去
        }else if(op=="D"){
            cin>>k;//k存储拿到第几个输入的数字
            k=ph[k];//k从储存第几个输入的数字变换为储存那个数字存放在h的哪个位置
            heap_swap(k,cnt);//将底部一个元素放上来
            cnt--;//所有元素数量减一
            down(k);
            up(k);
        }else{//剩下来还没有操作的就是C(change)了,不必多谢一个if判断
            cin>>k>>x;
            k=ph[k];//k从储存第几个输入的数字变换为储存那个数字存放在h的哪个位置
            h[k]=x;
            down(k);
            up(k);
        }
    }
    return 0;
}


3.哈希表

哈希表
a[x]=y

x叫关键字,y叫值。(key–value)
通过一系列的变换由x找到y
比如ax+by=c,则ax=c-by,x=(c-by)/a

哈希表有两个东西:

  1. 一个映射关系(函数)(自己定义,隐藏的逻辑)
  2. (key–value)(实际存储的东西,可以被看到的)

(1)模拟散列表

蓝桥杯训练day4_第8张图片

两种方法:拉链法和开放寻址法
两种方法的本质区别就是对于哈希冲突的解决。
什么是哈希冲突?
就是不同的x通过函数映射得到一样的y。

#include
#include
using namespace std;

const int N=1e5+10;
const int mod=1e5+7;

int h[N],e[N],ne[N],idx;

void insert(int x)  //插入元素。
{
    int k=(x%N+N)%mod;
    
    e[idx]=x;
    ne[idx]=h[k];
    h[k]=idx++;
}

bool find(int x)
{
    int k=(x%N+N)%mod;
    for(int i=h[k];i!=-1;i=ne[i])
    {
        int j=e[i];
        if(e[i]==x)
        {
            return true;
        }
    }
    return false;
}

int main()
{
    memset(h,-1,sizeof(h));
    int n;
    cin>>n;
    while(n--)  //n次操作
    {
        char op[2];
        cin>>op;
        int x;
        cin>>x;
        if(*op=='I')
            insert(x);
        else
        {
            if(find(x))puts("Yes");
            else puts("No");
        }
    }
    return 0;
}

(2)字符串哈希

蓝桥杯训练day4_第9张图片
核心:将字符串映射成一个数值(唯一)

#include
#include
#include
using namespace std;
typedef unsigned long long ULL;
const int N = 1e5+5,P = 131;//131 13331
ULL h[N],p[N];

// h[i]前i个字符的hash值
// 字符串变成一个p进制数字,体现了字符+顺序,需要确保不同的字符串对应不同的数字
// P = 131 或  13331 Q=2^64,在99%的情况下不会出现冲突
// 使用场景: 两个字符串的子串是否相同
ULL query(int l,int r)
{
    return h[r] - h[l-1]*p[r-l+1];
}
int main()
{
    int n,m;
    cin>>n>>m;
    string x;
    cin>>x;

    //字符串从1开始编号,h[1]为前一个字符的哈希值
    p[0] = 1;
    h[0] = 0;
    for(int i=0;i<n;i++)
    {
        p[i+1] = p[i]*P;            
        h[i+1] = h[i]*P +x[i];      //前缀和求整个字符串的哈希值
    }

    while(m--)
    {
        int l1,r1,l2,r2;
        cin>>l1>>r1>>l2>>r2;
        if(query(l1,r1) == query(l2,r2)) printf("Yes\n");
        else printf("No\n");

    }
    return 0;
}

(3)笨拙的手指

蓝桥杯训练day4_第10张图片

#include 
#include 
#include 
#include 

using namespace std;

int base(string s, int b)
{
    int res = 0;
    for (auto x: s)
        res = res * b + x - '0';
    return res;
}

int main()
{
    string x, y;
    cin >> x >> y;

    unordered_set<int> hash;
    for (int i = 0; i < x.size(); i ++ )
    {
        string s = x;
        s[i] ^= 1;
        if (s.size() > 1 && s[0] == '0') continue;
        hash.insert(base(s, 2));
    }

    for (int i = 0; i < y.size(); i ++ )
        for (int j = 0; j < 3; j ++ )
            if (y[i] - '0' != j)
            {
                string s = y;
                s[i] = j + '0';
                if (s.size() > 1 && s[0] == '0') continue;
                int n = base(s, 3);
                if (hash.count(n))
                    cout << n << endl;
            }

    return 0;
}


你可能感兴趣的:(蓝桥杯,算法,c++)