P1090合并果子

题目传送https://www.luogu.org/problem/show?pid=1090

这道水题有很多种做法是显然的,,,,在这里的就介绍一下我的几种做法吧

法一:

先排序,取两个最小的相加加入数组,再排序。。。

(这种做法谁都想得到吧,,,然后显然超时)

法二:

事先不是已经排过一遍序了吗?

那我把合并好了的那个数插入到数组中合适的位置不就ok了吗?

代码:

#include<bits/stdc++.h>
#define r(i,a,b) for(i=a;i<=b;i++)
using namespace std;
int a[10001],n,i,t,ans;
int main()
{
    scanf("%d",&n);
    r(i,1,n)    scanf("%d",&a[i]);
    sort(a+1,a+n+1);
    r(i,1,n-1)
    {
        a[i+1]+=a[i];
        ans+=a[i+1];
        t=i+1;
        while(t<n&&a[t]>a[t+1])
            swap(a[t],a[t+1]),t++;
    }
    printf("%d",ans);
return 0;
}
这个做法虽然不会超时(对于洛谷的数据,最大的300ms左右),但是效率还是比较低的。

法三:

想必优先队列都知道吧(不会的点这里http://blog.csdn.net/morewindows/article/details/6976468)

既然知道优先队列岂不是很简单了?

从优先队列里拿出最小的两个,然后合并再插入队列。

因为本人比较懒,所以就没写什么priority_queue<int,vector<int>,greater<int> >q;了,直接插入负值就可以(priority_queue<int>q;这样写是默认是降序的),然后最后再取ans的相反数就ok了

#include<queue>
#include<cstdio>
using namespace std;
priority_queue<int>q;
int n,i,a,b,x,ans;
int main()
{
    scanf("%d",&n);
    for(i=1;i<=n;i++)scanf("%d",&x),q.push(-x);
    while(q.size()>1){
        a=q.top();q.pop();
        b=q.top();q.pop();
        ans+=a+b;
        q.push(a+b); 
    }
return !printf("%d",-ans);
}
法四:

堆。其实和上一种做法差不多,弄个小根堆就可以了。

堆的话不想自己写就用 stl的heap吧(不会的点这里http://blog.csdn.net/morewindows/article/details/6967409)

法五(其实的法二是改进版):

贪心:
每次取出两个最小的合并 后再变为一个再插入递增序列里
由于一直sort会超时
所以直接尝试了一下插入排序

#include <stdio.h>
#include <algorithm>
#include <memory.h>
int n;
int Ft[10000];
int Fte;    //当前序列里数的个数
int Out;
void Input()  //输入及初始化
{
    scanf("%d",&n);
    int wi;
    for(wi=0;wi<n;++wi)
        scanf("%d",Ft+wi);
    std::sort(Ft,Ft+n);
    Fte=n;
}
void Stuck(int na)  //把一个数插入有序数组中,方便起见没用二分查找或者lower_bound
{
    Fte-=2;    
    if(Fte==0)
    {
        Ft[0]=na;
        ++Fte;
        return;
    }
    memmove(Ft,Ft+2,(Fte)*4);
    if(Ft[Fte-1]<=na)
    {
        Ft[Fte]=na;
        ++Fte;
        return;
    }
    int wi=0;
    while(Ft[wi]<na)++wi;
    memmove(Ft+wi+1,Ft+wi,(Fte-wi)*4);
    Ft[wi]=na;
    ++Fte;
    return;
}
int main()
{
    Input();
    int bf;
    while(1)
    {
        if(Fte==1)     //边界条件——序列里只有一个数(只有一堆某东西)退出
            break;
        bf=Ft[0];        //取出前两个最小的
        bf+=Ft[1];
        Out+=bf;
        Stuck(bf);
    }
    printf("%d",Out);
    return 0;
}
法六(借鉴的一位神犇的):

快排+单调队列
为了快速&简洁,所以快排直接调用std::sort。假设数据用数组a存储,再新建数组b。
分别用两个指针指向两个队列的队首和队尾(p,q指向a;r,s指向b)。由于a,b都是单调队列,所以最少的两堆果子有3种情况:1.a[p]和a[p+1];2.a[p]和b[r];3.b[r]和b[r+1]
合并它们,并插到b的队尾,循环做n-1次即可。
时间复杂度:快排O(nlogn),单调队列O(n),加起来还是[color=red]O(nlogn)[/color]
空间复杂度:需要O(n)的辅助空间
代码复杂度:较低,只有0.6K
实测0ms秒过,瞬间rank1

#include<cstdio>
#include<algorithm>
int a[10010],b[10010],n,t,p,q,r,s,k;
long long ans;
int main(){
    scanf("%d",&n);
    for (int i=1;i<=n;i++) scanf("%d",&a[i]);
    ans=0;
    std::sort(a+1,a+n+1);
    p=1;
    q=n;
    r=1;
    s=0;
    for (int i=1;i<n;i++){
        t=2100000000;
        if (q>p){
            t=a[p]+a[p+1];
            k=1;
        }
        if (q>=p && s>=r && a[p]+b[r]<t){
            t=a[p]+b[r];
            k=2;
        }
        if (s>r && b[r]+b[r+1]<t){
            t=b[r]+b[r+1];
            k=3;
        }
        ans=(long long)t+ans;
        if (k==1) p+=2;
        if (k==2){
            p++;
            r++;
        }
        if (k==3) r+=2;
        s++;
        b[s]=t;
    }    
    printf("%lld\n",ans);
}
法七(同样也是借鉴一位神犇的):

每次先集合懒散值最小的两个群,所耗费的体力最小,一般通过快排+二分排序即可解决,但此方法时间复杂度高,因此可以考虑使用计数排序法,其基本思想是设置若干个箱子,依次扫描待排序的数,将关键字=K的记录全装入第K个箱子(分配),然后按序号依次将各非空箱子首尾连接起来(收集)。


先定义一个下标从1~20000的整型数组number[],数组相应下标元素存放对应值的数的个数。


合并时,如合并的值X不超过20000,则number[X]++,即个数加1,如果超过20000,则顺序存入BigNumber[]中,位置由BigNumber[0]决定。然后采取贪心法,依次找最小的两个值,而且两数组已依次排好大小,所以直接顺序取值即可,无需排序。


#include <stdio.h>
#include <string.h>
#include <iostream>
#include <stdlib.h>
using namespace std;
int number[20000+1];//存放下标对应值的数的个数 
int BigNumber[15000+1];//顺序存放大于2万的群数据 
int n;//为人数 
int power;//消耗体力值 
void solve()
{
  int i,p,q;
  long total=0;
  p=1;//存放number数组没合并前的最小指针数 
  q=1;//存放BigNumber数组没合并前的最小指针数 
  i=0;//存放每次合并时已合并的群数 
  while(n>1)//如还没合并完,则继续合并 
  {
    if(p<=20000)//如有个数小于2万的群没有合并 
    {
      if(number[p]>0)//如果number下标对应群存在,则合并 
      {
        i++;
        total+=p;
        number[p]--;
      }
      else
        p++;//没有个数为p的值则往下找 
    }
    else//所有群的个数都大于2万,则顺序从number数据中取出群合并 
    {
      i++;
      total+=BigNumber[q];
      q++;
    }
    if((total<=20000)&&(i==2))//一次合并后(i=2),且合并后的个数不超过2万 
    {
      number[total]++;//将合并的值个数放入小数组中 
      n--;
      i=0;
      power+=total;
      total=0;
    }
    else
      if(i==2)//一次合并完,且合并后的个数大于2万 
      {
        BigNumber[0]++;//哨兵,表示下一次取值从大数组的何位置取 
        BigNumber[BigNumber[0]]=total;//存入大数组 
        n--;
        power+=total;
        i=0;
        total=0;
      }
  }
  printf("%d\n",power);
}
void init()
{
  int i,k;
  scanf("%d",&n);
  for(i=1;i<=n;i++)
  {
    scanf("%d",&k);
    number[k]++;
  }
}
int main()
{
  power=0;
  init();//处理输入数据 
  solve();
  return 0;
}


法八(所见最快的):

先排序a数组,然后在开一个数组b

于是乎就有3个指针了(a数组指针"i",b数组指针"j"和b数组大小的指针"m"

把合并好了的放到b数组的最后(显然b是单调的)

每次取MIN=min{a[i]+a[i+1],b[j]+b[j+1],a[i]+b[j]}

然后ans+=MIN,然后把这个MIN插入到b的末尾,然后指针往后移;

#include<cstdio>
#include<algorithm>
#define aa(x) a[x]+a[x+1]
#define ab(x,y) a[x]+b[y]
#define bb(x) b[x]+b[x+1]
const int N=12017,max=1<<29;
int n,m,i,j,ans,Min,a[N],b[N];
inline int min(int x,int y){return x<y?x:y;}
int main()
{
	scanf("%d",&n);
	for(i=1;i<=n;i++)scanf("%d",a+i),b[i]=max;
	if(n==1)return !puts("0");
	std::sort(a+1,a+n+1);a[n+1]=max;
    b[1]=a[1]+a[2];ans=b[1];
	i=3;j=1;m=1;
	while(i<=n||j<m)
	{
		Min=min(ab(i,j),min(aa(i),bb(j)));
		if(Min==ab(i,j))
		    b[++m]=a[i]+b[j],ans+=b[m],i++,j++;
		else if(Min==aa(i))
		    b[++m]=a[i]+a[i+1],ans+=b[m],i+=2;
		else
		    b[++m]=b[j]+b[j+1],ans+=b[m],j+=2;
	}
return !printf("%d",ans);
}
我觉得我这个想法还是挺不错的


总结

其实这个题目本身很水,但他却有这么多做法(耗时是递减的)。

其实对于一道题,我们不是会一种做法就ok了,而是应在时间允许的范围内,去钻研最优解,钻研有没有更好的做法,而不是知道一种就够了。

此余之所得矣。

  • 9
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值