二叉堆+哈夫曼树小结

概念:大根堆:树中任意一个节点的权值都小于父节点的权值,小根堆反之:

根据完全二叉树的性质:我们储存节点时,左儿子的编号等于父节点编号*2,右儿子编号等于父节点编号*2+1;

c++的stl中的priority_queue(优先队列)为实现一个大根堆,支持push(insert),top( ),pop( ) ,不支持remove,所以感觉很方便,但还是要会手写堆的;

 

第一题:supermarket

贪心:最优解中,对于每个时间t,应该在保证不卖出过期的情况下尽量卖出利润前t大的商品,动态维护这样一个性质;

我们将商品时间排序,见一个小根堆,根节点为商品利润,对于每一个商品;若当前过期时间t大于堆中商品个数,直接插入,若等于,若此时商品利润大于堆顶,则替换堆顶;

#include<bits/stdc++.h>
using namespace std;
int n;
const int maxn = 1e4 + 100;
template<typename T>inline void read(T &x)
{
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch))  {if(ch=='-')  f=-1;  ch=getchar();}
    while(isdigit(ch))  {x=x*10+ch-'0';  ch=getchar();}
    x*=f;
}
pair<int,int> a[maxn];
int main() {
    while(cin>>n) {
        for(int i=1;i<=n;i++) 
            read(a[i].second),read(a[i].first);
        sort(a + 1, a + n + 1);
        priority_queue<int ,vector<int>,greater<int> > q;
        for(int i=1;i<=n;i++) {
            if(a[i].first>q.size()) 
                q.push(a[i].second);
            else if(a[i].first==q.size()&&a[i].second>q.top()) {
                int x=q.top();q.pop();
                q.push(a[i].second);
            }
        }
        int ans=0;
        while(q.size()) {
            ans+=q.top();q.pop();
        }
        cout<<ans<<endl;
    }
    return 0;
}
View Code

 

第二题:Sequence

一个M行N列的矩阵,每一行中选一个数,一共取M个,求和,共有NM中结果,输出前N小个数;

我们可以先做前两个序列一共取两个数得到的和,取前n小,构成一个长度为n序列,然后将得到的新序列与第三个序列合并,以此类推,一共合并M-1次,最后得到的数列就是前n小的和;

对于一个排好序的数列{an},{bn}(排不排序无所谓);

得到n个序列:

b1+a1,b1+a2,b1+a3.....,b1+an;

b2+a1,b2+a2,b2+a3.....,b2+an;

b3+a1,b3+a2,b3+a3.....,b3+an;

b4+a1,b4+a2,b4+a3.....,b4+an;

b5+a1,b5+a2,b5+a3.....,b5+an;

、、

bn+a1,bn+a2,bn+a3.....,bn+an;

我们可以开一个小根堆,加入两个元素,一个为权值,一个为当前a的编号;

对于上部分红色部分:我们可以发现它是当前n序列中的最小值,因为an是排好序的,我们将a1+b[i]先加入小根堆中;

假定我们选定b2+a1,为当前第二小,那么我们删掉b2+a1,将它所对应的下一个数b2+a2,加入堆中,进入第三小的选择中,从而保证小根堆中一直只有n个值

每次取出堆顶,那么当前的权值和赋给一个新的数组c,将下一个数加入堆中,我么可以发现下一个元素的取值和为当前的权值和-当前a[p]+a[p+1};

将得到的新数组c赋给a,接着向下一个数组合并;最后a数组存的n个数就是最后的答案;

#include<bits/stdc++.h>
using namespace std;
#define N 2010
typedef pair<int,int> PII;
template<typename T>inline void read(T &x) {
    x=0; T f=1,ch=getchar();
    while(!isdigit(ch)) {if(ch=='-') f=-1; ch=getchar();}
    while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48); ch=getchar();}
    x*=f;
}


int n,m,T,a[N],b[N],c[N];

inline void Union() {
    priority_queue<PII, vector<PII>, greater<PII> > heap;
    for(int i=1;i<=n;i++) heap.push({a[1]+b[i],1});
    for(int i=1;i<=n;i++) {
        PII t=heap.top();
        heap.pop();
        c[i]=t.first;
        heap.push({t.first+a[t.second+1]-a[t.second],t.second+1});
    }
    for(int i=1;i<=n;i++) a[i]=c[i];
}

int main() {
    read(T);
    while(T--) {
        read(m); read(n);
        for(int i=1;i<=n;i++) read(a[i]);
        sort(a+1,a+n+1);
        for(int i=1;i<m;i++) {
            for(int j=1;j<=n;j++) read(b[j]);
            sort(b+1,b+n+1);
            Union();
        }
        for(int i=1;i<=n;i++) printf("%d ",a[i]);
        puts(" ");
    }
    return 0;
}
View Code

 

哈夫曼树:

名字挺高级哈,对于一个有i个子节点的k叉树,每个节点对应一个权值wi,要求最小化WPL=Σwi * Li,Li表示该节点到根节点的距离;

为了最小化,我们对于权值大的,尽可能深度较小;

当k==2,我们可以用贪心建出这样一棵树;

1.建一个小根堆,存进去n个节点的权值;

2.取出最小的两个w1,w2,ans+=w1+w2;

3.建一个权值为w1+w2的节点p,并将p插入小根堆中;

4.重复直至堆大小为1,此时ans=Σwi * Li,并且为最小值;

对于k>2时,我们考虑当最后一轮节点数不足以选出k个,这不是最优解,所以我们可以补充添加权值为0的节点,使满足(n-1)mod(k-1)==0,这样执行每次从堆中取出k个的贪心就是正确的;

 

第三题:合并果子

最终消耗的的体力为果子的质量*合并它的次数,这是一个哈夫曼问题;

对于k==2的情况下,按上述操作即可;

 

#include<bits/stdc++.h>
using namespace std;
priority_queue< int,vector< int >,greater< int > > q;
int n,a,sum;
template<typename T>inline void read(T &x)
{
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch))  {if(ch=='-')  f=-1;  ch=getchar();}
    while(isdigit(ch))  {x=(x<<1)+(x<<3)+(ch^48);  ch=getchar();}
    x*=f;
}
int main() {
    read(n);
    for(int i=1;i<=n;i++)
        read(a),q.push(a);
    for(int i=1;i<n;i++) {
        int x1=q.top();q.pop();
        int x2=q.top();q.pop();
        sum+=x1+x2;
        q.push(x1+x2);
    }
    cout<<sum<<endl;
    return 0;
}
View Code

 

第四题:荷马史诗

为了使任意si不是sj的前缀,为满足这个性质:

我们可以使用哈弗曼编码( n个节点的哈夫曼树含有2n-1个节点,没有度为1的节点 编码从叶子节点到根节点,译码从根节点到叶子节点。),也称为前缀编码;

引例:如果有一需传送的电文为 ‘ABACCDA’,我们可以尝试构建一个二进制串表示,为了保证唯一性,我们需要任意si不是sj的前缀;

我们记录A,B,C,D的出现次数{3,1,2,1};

采用哈夫曼编码;

 

编码: A:0, C:10,  B:110, D:111 。电文 ‘ABACCDA’ 便为 ‘0110010101110’(共 13 位)。

对于本题:我们将每个单词出现的次数作为wi,求出一个k叉哈夫曼树,

那么第一问就是要求出所有叶子结点的WPL,第二问求构造的哈夫曼树最深的深度最小;

第一问就类似合并果子做就可以了,对于第二问,我们只需要在求哈夫曼树时,对于权值相同的点,优先考虑当前深度最小的即可;

 

#include<bits/stdc++.h>
using namespace std;
#define ll long long 
typedef pair<ll,int> PII;
priority_queue<PII,vector<PII>,greater<PII> > heap;
template<typename T>inline void read(T &x) {
    x=0;T f=1,ch=getchar();
    while(!isdigit(ch)) {if(ch=='-') f=-1;ch=getchar();}
    while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();}
    x*=f;
}
int n,k;
ll x;
int main() {
    read(n); read(k);
    for(int i=1;i<=n;i++) {
        read(x);
        heap.push(make_pair(x,0));
    }
    while((n-1)%(k-1)) heap.push(make_pair(0,0)),n++;
    ll res=0;
    while(heap.size()>1) {
        int depth=0;
        ll s=0;
        for(int i=1;i<=k;i++) {
            PII t=heap.top();
            heap.pop();
            s+=t.first;
            depth=max(depth,t.second);    
        }
        res+=s;  
        heap.push(make_pair(s,depth+1));
    }
    cout<<res<<endl<<heap.top().second<<endl;
    return 0;
}
View Code

 

还是要学习:

你的问题在于读书不多而想的太多。——杨绛。

posted @ 2019-06-10 13:30  Tyouchie  阅读(463)  评论(0编辑  收藏  举报