L2-012 关于堆的判断 (模拟小根堆)

堆的定义及其建立

堆(Heap)是计算机科学中一类特殊的数据结构的统称,通常是一个可以被看做一棵完全二叉树的数组对象

堆满足下列性质:

  • 堆中某个节点的值总是不大于或不小于其父节点的值
  • 堆总是一棵完全二叉树

如下解释参考算法笔记
在这里插入图片描述

建堆的过程:

假设序列中元素的个数为 n n n,由于完全二叉树的叶子结点个数为 ⌈ n 2 ⌉ \lceil \dfrac{n}{2}\rceil 2n,因此数组下标在 [ 1 , ⌊ n 2 ⌋ ] [1,\lfloor \dfrac{n}{2}\rfloor] [1,2n]范围内的结点都是非叶子结点,于是可以从 ⌊ n 2 ⌋ \lfloor \dfrac{n}{2}\rfloor 2n号位开始倒着枚举结点,对每个遍历到的结点 i i i 进行 [ i , n ] [i,n] [i,n]范围的调整。

为什么要倒着枚举呢?这是因为每次调整完一个结点后,当前子树中权值最大的结点就会处在根结点的位置,这样当遍历到其父亲结点时,就可以直接使用这个结果。也就是说,这种做法保证每个结点都是以其为根结点的子树中的权值最大的结点。

建堆的代码如下,时间复杂度为 O ( n ) O(n) O(n)(证明可参考算法导论)

//建堆
void createHeap() {
    
    
    for (int i = n/2;i >= 1;i--){
    
    
        downAdjust(i, n);
    }
}

向下调整的代码,时间复杂度为 O ( l o g n ) O(logn) O(logn)

//对heap数组在[low,high]范围进行向下调整
//其中low为欲调整结点的数组下标,high一般为堆的最后一个元素的数组下标n

void downAdjust (int low,int high){
    
    
    int i=low,j=i*2;//i为欲调整结点,j为其左孩子
    while(j<=high){
    
    //存在孩子结点
        //如果右孩子存在,且右孩子的值大于左孩子
        if(j+1<=high && heap[j+1]>heap[j]) {
    
    
            j = j + 1;//让j存储右孩子下标
        }
        //如果孩子中最大的权值比欲调整结点i大
        if(heap[j]>heap[i]){
    
    
            swap(heap[j],heap[i]);//交换最大权值的孩子与欲调整结点i
            i=j;//保持i为欲调整结点、j为1的左孩子
            j=i*2;
        }
        else{
    
    
            break; //孩子的权值均比欲调整结点i小,调整结束
        }
    }
}

另外,如果要删除堆中的最大元素(也就是删除堆顶元素),并让其仍然保持堆的结构,那么只需要最后一个元素覆盖堆顶元素,然后对根结点进行调整即可。代码如下,时间复杂度为 O ( l o g n ) O(logn) O(logn):

扫描二维码关注公众号,回复: 13968305 查看本文章
//删除堆顶元素
void deleteTop() {
    
    
    heap[1] = heap[n--];//用最后一个元素覆盖堆顶元素,并让元素个数减1
    downAdjust(1, n);//向下调整堆顶元素
}

如果想要往堆里添加一个元素,可以把想要添加的元素放在数组最后(也就是完全二叉树的最后一个结点后面),然后进行向上调整操作。

向上调整总是把欲调整结点与父亲结点比较,如果权值比父亲结点大,那么就交换其与父亲结点,这样反复比较,直到达堆顶或是父亲结点的权值较大为止。向上调整的代码如下,时间复杂度为 O ( l o g n ) O(logn) O(logn):

//对heap数组在[low,high]范围进行向上调整
//其中low一般设置为1,high表示欲调整结点的数组下标
void upAdjust(int low,int high){
    
    
    int i=high,j=i/2;//i为欲调整结点,j为其父亲
    whi1e(j>=low){
    
    //父亲在[low,high]范围内
        //父亲权值小于欲调整结点i的权值
        if(heap[j]<heap[i]){
    
    
            swap(heap[j],heap[i]);//交换父亲和欲调整结点
            i=j;//保持1为欲调整结点、j为i的父亲
            j=i/2;
        }
        else{
    
    
            break;//父亲权值比欲调整结点i的权值大,调整结束
        }
    }
}

在此基础上实现添加元素的代码:

//添加元素x
void insert(int x){
    
    
    heap[++n]=x;//数组末位赋值为x
    upAdjust(1,n);//向上调整新加入的结点n
}

合并果子

首先通过一道题回顾一下堆的基本操作

在合并果子(传送门)中,我们可以增加限制条件,把可以合并2堆果子改为:可以将不超过K堆果子合并到一起,那么可以贪心地想到,我们第一次合并的次数不一定是K,但后面每一次合并的次数一定是K

代码:

#include <bits/stdc++.h>
#define f(i,a,b) for(int i=a;i<b;i++)
#define ff(i,a,b) for(int i=a;i<=b;i++)
using namespace std;
const int N=1e4+5;
int n,k;
int a[N],num=0;

void add(int x){
    
    
    a[++num]=x;
    int t=num;
    while (t>1 && a[t/2]>a[t]){
    
    
        swap(a[t/2],a[t]);
        t/=2;
    }
}

void del_top(int x){
    
    
    a[1]=a[num--];

    //只要有一个儿子即可,所以看左儿子就好(先有左儿子,再有右儿子)
    while(x*2<=num && (a[x]>a[x*2] || a[x]>a[x*2+1])){
    
    
        //右子大或者右儿子不存在
        if(a[x*2+1]>a[x*2] || x*2+1>num){
    
    
            swap(a[x*2],a[x]);
            x=x*2;
        }
        else{
    
    
            swap(a[x*2+1],a[x]);
            x=x*2+1;
        }
    }

}

int main(){
    
    
    cin>>n>>k;
    f(i,0,n){
    
    
        int t;cin>>t;
        add(t);
    }
    int len=((n-1)%(k-1)==0)?k:(n-1)%(k-1)+1;
    int res=0;
    while(num>1){
    
    
        int sum=0;
        ff(i,1,len){
    
    
            sum+=a[1];
            if(num==0) break;
            del_top(1);
        }
        res+=sum;
        add(sum);
        len=k;
    }
    cout<<res<<endl;
}

L2-012 关于堆的判断

传送门

代码:

#include <bits/stdc++.h>
#define debug(x) cout<<#x<<":"<<x<<endl;
#define f(i,a,n) for(int i=a;i<n;++i)
#define ff(i,a,n) for(int i=a;i<=n;++i)
const int INF=0x3f3f3f3f;
using namespace std;
typedef long long ll;
//
const int N=25;
int a[1<<(N/2)];
int n,m,num=0;
map<int,int> mp;

//以加点方式建立堆
void add(int i){
    
    
    while(i>1 && a[i/2]>a[i]){
    
    
        swap(a[i/2],a[i]);
        i/=2;
    }
}

int main(){
    
    
    cin>>n>>m;
    
    ff(i,1,n){
    
    
        scanf("%d",&a[i]);
        add(i);
    }
    
    ff(i,1,n)//记录堆下标
        mp[a[i]]=i;
    
    f(i,0,m){
    
    
        int t;string s;
        cin>>t>>s;
        if(s[0]=='a'){
    
    
            int t1;cin>>t1>>s>>s;
            if(mp[t]/2==mp[t1]/2)cout<<"T";
            else cout<<"F";
        }
        else{
    
    
            int t1;
            cin>>s>>s;
            if(s[0]=='r'){
    
    
                if(mp[t]==1)cout<<"T";
                else cout<<"F";
            }
            else if(s[0]=='p'){
    
    
                cin>>s>>t1;
                if(mp[t1]/2==mp[t])cout<<"T";
                else cout<<"F";
            }
            else{
    
    
                cin>>s>>t1;
                if(mp[t]/2==mp[t1])cout<<"T";
                else cout<<"F";
            }
        }
        cout<<endl;
    }
    return 0;
}

7-5 堆中的路径

传送门

代码:

#include <bits/stdc++.h>
#define debug(x) cout<<#x<<":"<<x<<endl;
#define f(i,a,n) for(int i=a;i<n;++i)
#define ff(i,a,n) for(int i=a;i<=n;++i)
const int INF=0x3f3f3f3f;
using namespace std;
typedef long long ll;
//
const int N=10010;
int a[N];
int n,m;

//向上调整
void add(int i){
    
    
    while(i>1 && a[i/2]>a[i]){
    
    
        swap(a[i/2],a[i]);
        i/=2;
    }
}

void printHeap(int i){
    
    
    while(i>1){
    
    
        printf("%d ", a[i]);
        i=i/2;
    }
    printf("%d\n", a[1]);
}

int main(){
    
    
    while(scanf("%d%d", &n, &m) != EOF){
    
    
        for(int i=1; i <= n; i++){
    
    
            scanf("%d",&a[i]);
            add(i);
        }
        for(int i=0; i < m; i++){
    
    
            int t;
            scanf("%d",&t);
            printHeap(t);
        }
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_45550375/article/details/122797525