算法竞赛刷题笔记vii

二叉堆

POJ 1456 Supermarket

在这里插入图片描述
这个题做过的,是在算法竞赛刷题笔记iv里面的洛谷P2949 Work Scheduling 当时这题不会做,现在做有点思路 但是是个wrong answer WA 代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
#include<utility>

using namespace std;

const int N = 10004;
typedef pair<int,int> P;
P a[N];

int main()
{
    int n,t;
    while(scanf("%d",&n) == 1){
        int ans = 0,ind = 1,time = 1;
        priority_queue<int,vector<int>,greater<int> > pq;
        for(int i=1;i<=n;i++){
            scanf("%d%d",&t,&a[i].first);//第一关键字是时间,第二关键字是价值
            a[i].second = -t;//存成负数方便sort排序
        }

        sort(a,a+n);//排成 时间从小到大,相同时间价值从大到小
        while(time <= 10000 && ind <= n){
            if(time <= a[ind].first){//time感觉可以想象成有多少个位置
                ans -= a[ind].second;
                pq.push(-a[ind].second);
                time++;
                ind++;
            }
            else{
                while(time > a[ind].first && pq.top() < -a[ind].second){
                    ans -= a[ind].second;
                    ans -= pq.top();
                    pq.pop();
                    pq.push(-a[ind].second);
                    if(++ind > n) break;
                }
                if(time > a[ind].first) ++ind;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

为啥是wrong answer,想了半天 看题发现n可以为0 那么我就把n为0的情况加了上去,又发现当else里面由于++ind > n 就break了 下面一个a[ind] 会越界 所以我把if(time>a[ind].first) 改成if(ind <= n && time > a[ind].first)
后来发现 我的sort的下标好像错了 = =,改了以后改成sort(a+1,a+n+1)就AC了
AC代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
#include<utility>

using namespace std;

const int N = 10004;
typedef pair<int,int> P;
P a[N];

int main()
{
    int n,t;
    while(scanf("%d",&n) == 1){
        if(n == 0) {printf("%d\n",0);continue;}
        int ans = 0,ind = 1,time = 1;
        priority_queue<int,vector<int>,greater<int> > pq;
        for(int i=1;i<=n;i++){
            scanf("%d%d",&t,&a[i].first);
            a[i].second = -t;
        }

        sort(a+1,a+n+1);
        while(time <= 10000 && ind <= n){
            if(time <= a[ind].first){
                ans -= a[ind].second;
                pq.push(-a[ind].second);
                time++;
                ind++;
            }
            else{
                while(time > a[ind].first && pq.top() < -a[ind].second){
                    ans -= a[ind].second;
                    ans -= pq.top();
                    pq.pop();
                    pq.push(-a[ind].second);
                    if(++ind > n) break;
                }
                if(ind <= n && time > a[ind].first) ++ind;
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

316K 63MS
看了一下之前写的代码 https://blog.csdn.net/suewiq/article/details/104121768
发现循环里面 原来写的好一些,原来是对所有的job进行循环 也就是这里的商品,时间复杂度就是 O ( n ) O(n) O(n)
在这里插入图片描述
光盘里面的代码 没有按time来限制,而是用a[i].first == q.size() 相等,比如a[i].first为3 那么q.size()也为3 说明必须要从堆里踢一个,后面的continue 表示后面的所有a[i].first为3 都会依次进行这个操作,位置够不用踢那么就直接push到堆里,看我之前在算法竞赛笔记里写的代码 其实time和堆的大小是相等的

POJ2442 Sequence

在这里插入图片描述
假设这m个序列都是有序的 那么最小的数就是m个序列第一个元素之和,m个指针都指向的是1,次小数就是其中一个指针往后走一格后m个数的和,我猜想得到下一个次小数就是m个指针中选一个向后移动一格,啪啪啪的打脸,序列 1 2 5和序列 1 3 7的最小3个数为2 3 4 得到第三个数的时候序列1指向2的指针向前移了!
我把这个想象成m个推钮的音量控制器,调最小音量,能把推钮往下压就往下压。
那么也就是说下一个次小数就是将m个指针中的一个指针往后推一格,然后其余所有指针都往下压
按照这个思路 代码:(wrong answer)

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
#include<utility>
using namespace std;
const int N = 104;
priority_queue<int,vector<int>,greater<int> > pq[N];
int mn[N];
int main()
{
    int n,t,m,x;
    scanf("%d",&t);
    for(int i=1;i<=t;i++){
        int sum = 0;
        scanf("%d%d",&m,&n);
        for(int k=1;k<=m;k++){
            for(int w=1;w<=n;w++){
                scanf("%d",&x);
                pq[k].push(x);//将每个序列中的数都push到相应的堆中
            }
        }
        for(int i=1;i<=m;i++){
            mn[i] = pq[i].top();//mn[i]存的是每个序列中最开始的最小数
            pq[i].pop();
            sum  += mn[i];//sum 为最开始的m个序列最小值之和
        }
        printf("%d ",sum);
        for(int i=1;i<n;i++){
            int ans = INT_MAX,ind,temp_sum;
            for(int k=1;k<=m;k++){
                temp_sum = sum + pq[k].top() - mn[k];//第k个按钮推到pq[k].top() 其余按钮都在相应的mn位置
                if(temp_sum < ans){//如果这个和小于当前最小值 更新ans 和 ind
                    ans = temp_sum;
                    ind = k;
                }
            }
            pq[ind].pop();//ind为取最小值,就把ind弹出去
            printf("%d ",ans);
        }
        puts("");
        for(int k=1;k<=m;k++)//清空堆,为下一次输入做准备
            while(!pq[k].empty()) pq[k].pop();
    }
    return 0;
}

我发现我为啥错了,因为不一定是每次都是只有一个推到上面,其余的都在最下面,有可能有两个推到第二格之类的,那我这个方法就不行了,康康书上怎么写的:
书上的想法并非是一口气来处理m个序列的,而是用一种递推的方法,2个序列的最小N个和与三个序列形成的最小N个和就是3个序列形成的最小N个和。
证明: 下面来证K(K<=N-1)个序列最小和 与 1个序列最小和 形成的最小和 是 K+1个序列直接形成的最小和。以合并的单个序列为视角,形成的K+1个序列的K+1个最小和中 来自另外K个序列的K个数的和一定在K个序列的最小和中,如果有一个不在(不妨令K+1个最小和中的数x是由单个序列的y和另外K个序列的K个数的和z相加),那么我能用N个K个序列最小和替换这个不在K个序列最小和中的数,得到的N个K+1最小和都小于x
那么与x是K+1个最小和 矛盾

我试着按照他的解析思路自己来实现一下,又是sample过了的wrong answer,内心有点沮丧啊

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
#include<utility>

using namespace std;

const int M = 104;
const int N = 2004;
int u[N],v[N],z[N];

struct A{
    int i,j,k;
    bool operator <(const A& a)const{
        return u[i] + v[j] - u[a.i] - v[a.j];
    }
};
priority_queue<A,vector<A> > pq;

int main()
{
    int n,t,m,x;
    scanf("%d",&t);
    for(int i=1;i<=t;i++){
        int flag = 0;
        scanf("%d%d",&m,&n);
        for(int k=1;k<=m;k++){
            for(int w=1;w<=n;w++){
                scanf("%d",&x);
                if(flag == 0)//第一次输入序列的时候放入u数组中
                    u[w] = x;
                else//之后输入的序列放入v数组中
                    v[w] = x;
            }
            flag++;
            if(flag > 1){//表示这至少是第二次输入数组
                A a;
                a.i = a.j = 1;
                a.k = 0;//为了防止A[2] + B[1] 和 A[1] + B[2]重复引入 (i,j)为(2,2)
                pq.push(a);
                for(int w=1;w<=n;w++){
                    A tp = pq.top(),tp0;
                    pq.pop();
                    tp0.i = tp.i;
                    tp0.j = tp.j;
                    tp0.k = tp.k;
                    z[w] = u[tp.i] + v[tp.j];
                    if(a.k == 0){//k为0的时候将(i+1,j,0)压入堆
                        tp0.i++;
                        pq.push(tp0);
                    }
                    tp.j++;//每次都压入(i,j+1,1)
                    tp.k = 1;
                    pq.push(tp);
                }
                for(int w=1;w<=n;w++)
                    u[w] = z[w];
            }
        }
        for(int i=1;i<=n;i++){//还原u v z三个数组
            printf("%d ",z[i]);
            u[i] = v[i] = z[i] = 0;
        }
    }
    return 0;
}

吸吸光盘里的神仙代码,看了一下书,书中是要将u v排序,加了sort还是不对,跟这题杠上了,肉眼debug,debug了好久 终于AC了 de出了好多个错误, 192K 547MS,

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
#include<utility>

using namespace std;

const int M = 104;
const int N = 2004;
int u[N],v[N],z[N];

struct A{
    int i,j,k;
    A(){i=0;j=0;k=0;}
    A(const A& w){
        i = w.i;
        j = w.j;
        k = w.k;
    }
    bool operator <(const A& a)const{
        return u[i] + v[j] > u[a.i] + v[a.j];//要将默认的大端堆换成小端堆 就改符号就行了
    }
};
priority_queue<A,vector<A> > pq;
int main()
{
    int n,t,m,x;
    scanf("%d",&t);
    for(int i=1;i<=t;i++){
        int flag = 0;
        scanf("%d%d",&m,&n);
        for(int k=1;k<=m;k++){
            for(int w=1;w<=n;w++){
                scanf("%d",&x);
                if(flag == 0)//其实不用flag 用w就可以了
                    u[w] = x;
                else
                    v[w] = x;
            }
            flag++;
            if(flag == 1)//第一要排序u数组,后面每次的z数组都是有序的 复制给u,所以u也是有序的
                sort(u+1,u+n+1);
            else
                sort(v+1,v+n+1);

            if(flag > 1){
                while(!pq.empty()) pq.pop();//之前每次没有把堆清空
                A a;
                a.i = a.j = 1;
                a.k = 0;
                pq.push(a);
                for(int w=1;w<=n;w++){
                    A tp = pq.top(),tp0(tp);
                    pq.pop();
                    z[w] = u[tp.i] + v[tp.j];
                    if(tp.k == 0){
                        tp0.i++;
                        if(tp0.i<=n)//感觉不进行这个检查也是对的
                            pq.push(tp0);
                    }
                    tp.j++;
                    tp.k = 1;
                    if(tp.j<=n)
                        pq.push(tp);
                }
                for(int w=1;w<=n;w++)
                    u[w] = z[w];
            }
        }
        for(int i=1;i<=n;i++){
            printf("%d ",u[i]);//如果m为1 那么z就不存在,z不能选做答案
            u[i] = v[i] = z[i] = 0;
        }
        puts("");//之前没有加换行
    }
    return 0;
}

时间复杂度为 O ( m n l o g n ) O(mnlogn) O(mnlogn)
光盘里的堆 的结构是

priority_queue<pair<int, pair<int, int> > > w;

因为w默认是大端堆 光盘里面的处理是push进去负数,核心部分是这里,并没有像书上说的用了三元组
在这里插入图片描述
这里是怎么样保证不会插入重复的,而且排到x,y的时候 x+1,y和x,y+1能插入到堆中
( x , y ) (x,y) (x,y) y ≠ 1 y \ne 1 y=1时, ( x , y + 1 ) (x,y+1) (x,y+1)是肯定有的 对于 ( x + 1 , y ) (x+1,y) (x+1,y)来说只要堆里有 ( x + 1 , w ) (x+1,w) (x+1,w)其中 w < y w<y w<y那么 ( x + 1 , y ) (x+1,y) (x+1,y)就不会被使用 这个时候可以不用加到堆了 如果不存在这样的 w w w,那么 ( x + 1 , w ) (x+1,w) (x+1,w),那么(x+1,y-1)之前肯定被使用过了,由于增加的规则,堆里就会有(x+1,y)
至于不重复 很明显 y == 1的情况 所有添加 的x,y对 y都为1 当y不为1的时候,所有添加的x,y对 y都不为1。至于能遍历所有可能的x,y对,可以将x,y想象成x和y轴组成的格子,沿水平方向即(x方向)增加的时候增加一个向上的方向,对于y>1的格子 所有格子都会向上走, 想象一下!

BZOJ1150

在这里插入图片描述
在这里插入图片描述
电线肯定是架在两个相邻的楼之间,两个相邻楼不能是三个楼相邻因为这样会违反2K个相异办公楼的要求,所以题目可以抽象成一个有n-1个值的序列 从中选出两两不相邻的K个数,要让这K个数的和最小
感觉这个题可以用dp来做,转移方程是:
d p [ 1 , n , k ] = a r r [ x ] + d p [ 1 , x − 2 , a ] + d p [ x + 2 , n , b ] 其 中 k = = a + b dp[1,n,k] = arr[x] + dp[1,x-2,a] + dp[x+2,n,b] 其中k == a + b dp[1,n,k]=arr[x]+dp[1,x2,a]+dp[x+2,n,b]k==a+b
感觉这个方法很复杂,想想有没有贪心的方法,一开始选择的是序列中最小的数,这样会有问题吗?10 2 1 2 10 选2个 那么首先选1不是好的选择,康康书上的解析:
书上的思想就是由简向难进行演绎,先考虑 K = 1 K=1 K=1的情况 再考虑 K = 2 K = 2 K=2的情况,
根据书上的思想 我自己写一下:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
#include<utility>

using namespace std;

const int N = 100004;
int a[N],valid[N];
struct A{
    int val,prv,nxt;
}d[N];


int main()
{
    int n,k,ans = 0;
    scanf("%d%d",&n,&k);
    priority_queue<pair<int,int> > pq;
    for(int i=1;i<=n;i++)
        scanf("%d",a + i);
    for(int i=1;i<=n-1;i++){
        d[i].val = a[i+1] - a[i];
        d[i].prv = i - 1;
        d[i].nxt = i + 1;
        pq.push(make_pair(-d[i].val,i));
    }
    //找出最小值 插入新值
    for(int i=1;i<=k;i++){
        while(valid[pq.top().second] == 1) pq.pop();
        pair<int,int> tp = pq.top();
        ans -= tp.first;
        pq.pop();
        if(d[tp.second].prv == 0){
            d[d[d[tp.second].nxt].nxt].prv = 0;
            valid[tp.second] = 1;
            valid[d[tp.second].nxt] = 1;
        }
        else if(d[tp.second].nxt == n){
            d[d[d[tp.second].prv].prv].nxt = n;
            valid[tp.second] = 1;
            valid[d[tp.second].prv] = 1;
        }
        else{
            d[tp.second].val = d[d[tp.second].nxt].val + d[d[tp.second].prv].val - d[tp.second].val;
            valid[d[tp.second].nxt] = valid[d[tp.second].prv] = 1;
            d[tp.second].nxt = d[d[tp.second].nxt].nxt;
            d[tp.second].prv = d[d[tp.second].prv].prv;
            pq.push(make_pair(-d[tp.second].val,tp.second));
        }
    }
    printf("%d\n",ans);
    return 0;
}

这又是常见的示例通过了 但是是WA,我感觉我的代码没有毛病啊,后来发现 我的链表操作有问题,改了以后就AC了 4784 kb 568 ms
最后else的地方 还要加上

d[d[tp.second].nxt].prv = tp.second;
d[d[tp.second].prv].nxt = tp.second;

康康光盘里面的代码:

光盘里有两份代码 第一份没有署名XuHt的代码,这个代码是自己实现的堆,而不是用的priority_queue,他堆里面的代码为:

int f[100010],a[100010],pre[100010],next[100010],v[100010];
int n,m,p,i,x,ans;//全局的p为堆里的元素个数

void up(int p)//p是堆的索引
{
	while(p>1)//每个索引p 都有一个映射f,使得索引p对应数组a里面的f[p]元素,对于堆的调整就是调整f数组
		if(a[f[p]]<a[f[p>>1]])
		{
			swap(f[p],f[p>>1]);
			swap(v[f[p]],v[f[p>>1]]);//v数组是f数组的反映射  f[x] = y  v[y] = x
			p>>=1;
		}
		else break;
}

void down(int l,int r)//感觉这里的r不用加,因为p是个全局变量,而且在这个函数里面并没有修改r
{
	int t=2*l;
	while(t<=r)
	{
		if(t<r&&a[f[t]]>a[f[t+1]]) t++;
		if(a[f[l]]>a[f[t]])
		{
			swap(f[l],f[t]);
			swap(v[f[l]],v[f[t]]);
			l=t,t=2*l;
		}
		else break;
	}
}

void insert(int x)
{
	f[++p]=x; v[x]=p;
	up(p);
}

void erase(int x)
{
	f[v[x]]=f[p]; v[f[p]]=v[x]; p--;//把x所在堆的位置换成 数组索引为p的元素(数组最后一个元素)
	up(v[x]),down(v[x],p);
}

学习了!!!!!

Huffman树

特点是只有叶子结点带有权值

NOIP2004/CH1701 合并果子

在这里插入图片描述
这题就是个二叉Huffman树的模板,太水了,AC代码:

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
#include<utility>
using namespace std;
priority_queue<int> pq;
int main()
{
    int n,x,ans = 0;
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&x);
        pq.push(-x);
    }
    while(pq.size() > 1){
        int x = pq.top(),y;
        pq.pop();
        y = pq.top();
        pq.pop();
        ans = ans - x - y;
        pq.push(x+y);
    }
    printf("%d\n",ans);
    return 0;
}
NOI2015/BZOJ4198 荷马史诗

在这里插入图片描述
在这里插入图片描述
这个题印像深刻啊 是THU考研群的入群题,要huffman树满足(n-1)%(k-1) == 0 要向树里面插入值为0的结点,树中每个结点不仅要有值还要有结点的高度,但存在多个相同值的结点,选高度最小的 这样形成的huffman树高度最小 AC代码:4368 kb 872 ms

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<stack>
#include<map>
#include<vector>
#include<sstream>
#include<climits>
#include<set>
#include<utility>

using namespace std;
typedef long long ll;
const int N = 100004;
priority_queue<pair<ll,ll> > pq;
int main()
{
    int n,k;
    ll x,ans = 0;
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++){
        scanf("%lld",&x);
        pq.push(make_pair(-x,0));//因为是大端堆 所以都取负号,前面是结点的值 后面是结点的高度
    }
    while((pq.size() - 1) % (k-1) != 0)//补充值为0的结点 直到树结点个数pq.size() - 1可以被k-1整除
        pq.push(make_pair(0,0));
    while(pq.size() != 1){
        pair<ll,ll> s = make_pair(0,0);
        for(int v=1;v<=k;v++){//k个结点合并
            s.first += pq.top().first;
            s.second = min(s.second,pq.top().second);//高度取最大高度
            pq.pop();
        }
        s.second--;//高度再加上1
        ans -= s.first;
        pq.push(s);
    }
    printf("%lld\n",ans);
    printf("%lld\n",-pq.top().second);
    return 0;
}

来康康几个月前 我自己写的代码:

#include <bits/stdc++.h>
using namespace std;
using ll = long long;
using P = pair<ll,ll>;

int main(){
    ll n,k,input,sum = 0,count = 0,value = 0,depth = 0;
    cin>>n>>k;
    priority_queue<P,vector<P>,greater<P> > pq;//用greater 构造出的小端堆
    ll remainder = n;
    while(remainder > 0)//n个结点每次合并 会少k-1个结点,最后剩下的remainder 应该是第一次合并就合并的
        remainder-=(k-1);
    remainder += k-1;
    int flag = 1;
    if(remainder == 1) flag = 0;

    for(ll i=0;i<n;i++){//input
        cin>>input;
        pq.emplace(make_pair(input,1));
    }
    if(n == 1)
        cout<< pq.top().first<<" "<<1<<endl;
    while(pq.size() != 1 || sum > 0){
        sum += pq.top().first;
        if(depth < pq.top().second)//取最大高度
            depth = pq.top().second;
        pq.pop();
        count++;
        if(flag && (count == remainder)){//可能存在有一个结点的子结点的个数是小于k的,flag为1的时候表示存在
            pq.emplace(make_pair(sum,depth+1));
            value += sum;
            depth = sum = flag = count = 0;//因为至多只有一个这样的结点 所以处理完flag为1的情况后,flag就永远为0了
            continue;
        }
        if(count == k){//count为k说明有k个结点参加了合并,然后将合并完的结点插入到堆中
            pq.emplace(make_pair(sum,depth+1));
            value += sum;
            depth = sum = count = 0;
        }
    }
    cout<<value<<" "<<pq.top().second-1<<endl;
    return 0;
}

感觉到了进步!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值