[NOIP 2014复习]第四章:高效算法与高效数据结构

24 篇文章 1 订阅
15 篇文章 1 订阅

一、区间选点问题

问题描述:给定若干个区间,要求用最少的点,使得每个区间内都包含至少一个点

根据贪心思想,我们只需要在每个区间的右端点安置点即可,这样如果两个区间相交的话,它们可以共用一个点。

首先根据线段右端点为第一关键字升序、左端点为第二关键字降序排序,如上图所示,由上到下线段编号逐渐增大,维护一个边界End,每次加入一条新的线段,检查这条线段的左端点是否比End大,如果比End大,则需要增加一个点,并更新End值(没有相交,不能共用一个点),否则跳过(两区间相交,可以共用一个点)


1、POJ 1328 Radar Installation

http://poj.org/problem?id=1328

覆盖每个点的圆的圆心的区间如上图中绿色线段所示,对于每个象征岛屿的点,我们只需要找出覆盖它的圆心区间即可,然后便能套用区间选点的贪心思想来做,但是这个题很容易错,需要小心

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <cmath>
#include <algorithm>

#define MAXN 1010

using namespace std;

struct Line
{
    double l,r;
}line[MAXN];

bool cmp(Line a,Line b)
{
    return a.l<b.l;
}

int main()
{
    int n,d,Case=0;
    while(++Case)
    {
        int ans=1;
        scanf("%d%d",&n,&d);
        if(!n&&!d) break;
        bool flag=true;
        for(int i=1;i<=n;i++)
        {
            int x,y;
            scanf("%d%d",&x,&y);
            if(d>=y&&flag==true)
            {
                line[i].l=x-sqrt((double)d*d-(double)y*y);
                line[i].r=x+sqrt((double)d*d-(double)y*y);
            }
            else flag=false;
        }
        if(!flag) //无论如何雷达也覆盖不住所有点
        {
            printf("Case %d: -1\n",Case);
            continue;
        }
        sort(line+1,line+n+1,cmp); //按线段右端点升序排序,左端点为第二关键字降序排序
        double End=line[1].r;
        for(int i=2;i<=n;i++)
        {
            if(End<line[i].l)
            {
                End=line[i].r;
                ans++;
            }
            else if(line[i].r<End) End=line[i].r;
        }
        printf("Case %d: %d\n",Case,ans);
    }
    return 0;
}

二、其他类型的贪心

1、POJ 2287 Tian Ji -- The Horse Racing

http://poj.org/problem?id=2287

题目说白了就是田忌赛马的问题,输入田忌和国王每匹马的性能,田忌每次比赛胜一场得200元,输一次扣200元,求田忌最后最多能拿到多少钱

我们可以分4类情况讨论

1、田忌最快马比国王最快马快,不用说,当然就是让田忌最快马和国王最快马比一次,田忌得200

2、田忌最快马比国王最快马慢,那么田忌所有的马都比不上国王的最快马,注定这局要输,干脆让田忌的最慢马去和国王最快马比得了,让他的最慢马消耗掉国王的最快马,田忌扣200

如果田忌最快马和国王最快马速度一样,则比较他们的最慢马

3、如果田忌最慢马比国王最快马快,那么注定这局田忌赢,用太快的马和国王的最慢马比就浪费了,所以让田忌最慢马和国王最慢马比,田忌得200

4、如果田忌最慢马比国王最慢马慢,那么田忌的这匹最慢马无论如何都不可能赢,但田忌其他的马和国王最慢马比有可能赢,田忌的这匹马归根结底是要输的,就让它去和国王最快马比,消耗掉国王的最快马

5、否则,这局平局。

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>

#define MAXN 1010

using namespace std;

int TianJiHorse[MAXN],KingHorse[MAXN]; //田忌的马速度、国王的马速度

int main()
{
    while(1)
    {
        memset(TianJiHorse,0,sizeof(TianJiHorse));
        memset(KingHorse,0,sizeof(KingHorse));
        int n,ans=0; //ans=最终田忌得到的最多钱数
        scanf("%d",&n);
        if(!n) break;
        for(int i=1;i<=n;i++)
            scanf("%d",&TianJiHorse[i]);
        for(int i=1;i<=n;i++)
            scanf("%d",&KingHorse[i]);
        sort(TianJiHorse+1,TianJiHorse+n+1);
        sort(KingHorse+1,KingHorse+n+1);
        int minTian=1,minKing=1,maxTian=n,maxKing=n,cnt=0; //cnt=赛马次数
        while((++cnt)<=n)
        {
            if(TianJiHorse[maxTian]>KingHorse[maxKing]) //田忌最快马快于国王最快马,就让两匹马比赛
            {
                ans+=200;
                maxTian--;
                maxKing--;
            }
            else if(TianJiHorse[maxTian]<KingHorse[maxKing]) //如果田忌最快马比国王最快马慢,让田忌最慢马和国王最快马比赛,因为无论任何一匹马都比不过国王最快马
            {
                ans-=200;
                minTian++;
                maxKing--;
            }
            else //若两匹最快马相等,则比较最慢马
            {
                if(TianJiHorse[minTian]>KingHorse[minKing]) //如果田忌最慢马比国王最慢马快,则让两匹马比赛
                {
                    ans+=200;
                    minTian++;
                    minKing++;
                }
                else //若田忌最慢马比国王最慢马慢,则让它和国王最快马比赛,因为这匹马必输无疑
                {
                    if(TianJiHorse[minTian]<KingHorse[maxKing]) ans-=200;
                    minTian++; //如果没扣钱,就是平局了
                    maxKing--;
                }
            }
        }
        printf("%d\n",ans);
    }
    return 0;
}

2、POJ 3040 Allowance

http://poj.org/problem?id=3040

题意:农民每周要给一头奶牛发工资,每周工资不低于C,给出农民手头上的每种硬币的面值和个数,求农民最多能给奶牛发多少周的工资

思路:将硬币按面值升序排序,首先将面值大于C的硬币全部发光(只用这些硬币就够发好多周的工资了,把它们和小面值的硬币一起发的话浪费了),然后从大到小遍历剩下的硬币面值,凑出一个面额x(每种硬币可用多个),使得x<C且尽可能大,然后从小到大遍历硬币面值,在x的基础上加硬币(每种硬币同样可用多个),使得x加硬币后大于C且尽可能小,计算以这样的一种方案最多能发多少周工资,累加答案。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXN 22
#define INF 0x3f3f3f3f

using namespace std;

struct Coin
{
    int v,num; //硬币的面值、个数
}coins[MAXN];

int need[MAXN]; //need[i]=第i种硬币用的个数

int numOfCoins=0,valueOfCoins=0; //总硬币数、硬币总价值

bool cmp(Coin a,Coin b)
{
    return a.v<b.v;
}

int min(int a,int b)
{
    if(a<b) return a;
    return b;
}

int main()
{
    int n,c,ans=0;
    scanf("%d%d",&n,&c);
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d",&coins[i].v,&coins[i].num);
    }
    sort(coins+1,coins+n+1,cmp);
    for(int i=1;i<=n;i++)
    {
        if(coins[i].v>=c) //一种硬币价值比c大,则节约不了这个硬币,在一个星期把它发掉
        {
            ans+=coins[i].num;
            coins[i].num=0;
        }
    }
    while(1)
    {
        int sum=c; //等待凑足sum元
        memset(need,0,sizeof(need));
        for(int i=n;i>=1;i--) //从大到小凑硬币
        {
            if(sum>0&&coins[i].num>0) //这一周工资还没凑足,且当前种类硬币有货
            {
                int NumOfNeed=min(sum/coins[i].v,coins[i].num); //NumOfNeed=需要从这一种硬币拿掉的硬币个数
                if(NumOfNeed>0) //需要拿,就扣除这部分硬币
                {
                    sum-=NumOfNeed*coins[i].v;
                    need[i]=NumOfNeed;
                }
            }
        }
        for(int i=1;i<=n;i++) //从小到大凑硬币,把大硬币没有凑齐的部分补上
        {
            if(sum>0&&coins[i].num>0)
            {
                int NumOfNeed=min((sum+coins[i].v-1)/coins[i].v,coins[i].num-need[i]);
                if(NumOfNeed>0) //这种硬币需要拿,就扣除这部分硬币
                {
                    sum-=NumOfNeed*coins[i].v;
                    need[i]+=NumOfNeed;
                }
            }
        }
        if(sum>0) //凑不齐,就不再尝试了
            break;
        int AddNum=INF; //凑起来的发工资次数,这个次数取决于每种要拿的硬币,可以满足发工资的请求次数的最小值
        for(int i=1;i<=n;i++)
        {
            if(!need[i]) continue;
            AddNum=min(AddNum,coins[i].num/need[i]);
        }
        ans+=AddNum;
        //开始扣除每种硬币的个数
        for(int i=1;i<=n;i++)
        {
            if(!need[i]) continue;
            coins[i].num-=AddNum*need[i];
        }
    }
    cout<<ans<<endl;
    return 0;
}


 

 


 


三、单调栈、单调队列与滑动窗口

1、POJ 3250 Bad Hair Day(单调栈)

http://poj.org/problem?id=3250

题目大意:给定一个牛的高度序列,每头牛只能看见右边比它矮的牛,求所有牛能看到其他牛的个数之和。

比如上图,黄色牛可以被左边的这些蓝色牛看到。

由于题意特殊,每头牛可以看到的其他牛的高度不一定非要单调递减,所以不能用DP的LIS来求,但是可以维护一个单调栈,保证栈中上面的元素比下面的元素要小,每次放入一头牛,就更新这个单调栈,将栈顶所有比这头牛高度小的都弹出去,然后将栈的元素加入答案。

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>

#define MAXN 81000

using namespace std;

int priorityStack[MAXN],top=1;

int main()
{
    int n,h;
    long long int ans=0;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&h);
        while(top>0)
        {
            if(priorityStack[top]<=h)
                top--;
            else break;
        }
        ans+=top;
        priorityStack[++top]=h;
    }
    printf("%lld\n",ans);
    return 0;
}

2、POJ 2823 Sliding Window(滑动窗口&单调队列模板题)

http://poj.org/problem?id=2823

题意:给定一个长度为n的序列,滑动窗口长度为k,求在不同位置时滑动窗口中元素的最大值和最小值

用单调队列来维护滑动窗口中的最大值,保证单调队列单调递减,滑动窗口每向右移动一次,就让队尾比新的元素小的元素出队,并将新的元素入队;将队首不属于新的滑动窗口的元素出队,经过上述的维护,单调队列的队首就是新的滑动窗口最大值。单调队列维护滑动窗口最小值同上。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>

#define MAXN 1000100

using namespace std;

int MonoQueue[MAXN],pointer=1,n,k;
int a[MAXN];
int index[MAXN];

void getMax()
{
    int head=0,tail=1;
    for(int i=1;i<k;i++)
    {
        while(head<tail&&MonoQueue[tail-1]<=a[i]) //Monoq is not empty
            tail--;
        MonoQueue[tail++]=a[i]; //push the new element into Mono queue
        index[tail-1]=i;
    }
    for(int i=k;i<=n;i++)
    {
        while(head<tail&&MonoQueue[tail-1]<=a[i])
            tail--;
        MonoQueue[tail++]=a[i];
        index[tail-1]=i;
        while(index[head]<=i-k) head++; //队首不属于新的窗口的元素出队
        printf("%d ",MonoQueue[head]);
    }
}

void getMin()
{
    int head=0,tail=1;
    for(int i=1;i<k;i++)
    {
        while(head<tail&&MonoQueue[tail-1]>=a[i]) //Monoq is not empty
            tail--;
        MonoQueue[tail++]=a[i]; //push the new element into Mono queue
        index[tail-1]=i;
    }
    for(int i=k;i<=n;i++)
    {
        while(head<tail&&MonoQueue[tail-1]>=a[i])
            tail--;
        MonoQueue[tail++]=a[i];
        index[tail-1]=i;
        while(index[head]<=i-k) head++; //队首不属于新的窗口的元素出队
        printf("%d ",MonoQueue[head]);
    }
}

int main()
{
    scanf("%d%d",&n,&k);
    for(int i=1;i<=n;i++) scanf("%d",&a[i]);
    getMin();
    printf("\n");
    getMax();
    printf("\n");
    return 0;
}


四、二分

1、POJ 3258 River Hopscotch

http://poj.org/problem?id=3258

题目大意:给定n个石头距离起点0的长度、终点长度,要删去其中m个石头,求最终相邻石头之间的最小距离的最大值。

可以通过二分来猜这个相邻石头之间的最小距离,然后根据这个猜出来的最小距离,把比最小距离小的石头都删去,如果删去石头太多,减小二分上界,否则增大二分上界。

注意:终点和起点不能删除,因此终点的情况要单独考虑,如果除终点外最右边的一个石头与终点距离太近,则需要再删除掉一个石头(最右边的那个石头)。

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>

#define MAXN 51000

using namespace std;

int rock[MAXN]; //石头距起点的距离
int l,n,m;

int binarySearch() //二分查找答案
{
    int upperBound=l,mid,lowerBound=0;
    while(upperBound>=lowerBound)
    {
        int cnt=0; //删去石头的个数
        int pre=1; //这一个石头之前的一个石头
        mid=(upperBound+lowerBound)/2; //mid=两石头之间最小距离
        for(int i=2;i<=n+1;i++) //删去距离终点小于mid的石头
        {
            if(rock[i]-rock[pre]<mid) //第i个石头和之前一个石头之间距离小于mid,要删掉第i个石头
                cnt++;
            else pre=i;
        }
        if(l-rock[pre]<mid) cnt++;
        if(cnt>m) //删多了
            upperBound=mid-1;
        else //删少了
            lowerBound=mid+1;
    }
    return upperBound;
}

int main()
{
    scanf("%d%d%d",&l,&n,&m);
    for(int i=2;i<=n+1;i++) scanf("%d",&rock[i]);
    rock[1]=0,rock[n+2]=l;
    sort(rock+1,rock+n+3);
    printf("%d\n",binarySearch());
    return 0;
}



洛谷P2088 果汁店的难题

题目描述 Description
炎热的夏天,来上一杯现榨的冰爽果汁想想都是一件惬意的事情!话说小王就看准了这一商机,在学校附近开了这么一家果汁店,但是最近他碰到了一个不大不小的难题:小王的果汁店里准备了K台榨汁机,当然每台榨汁机只能榨一种果汁,在某个时段内,一个客人点了某种果汁,如果恰好有某台果汁机榨过这种果汁,那么就直接给客人用这台果汁机接着榨就可以了,但是如果点的是一种新的果汁就需要找一台干净的果汁机来用,问题就出在这,如果这时候还有空的果汁机还好,如果没有的话小王就需要将某台刚才用过的拿去清洗,清洗的话呢就得浪费很多的时间和很多的水,小王是个很有经济头脑的人,他想知道在排队客人需求已知的情况下最少需要清洗多少次果汁机?假定开始时所有果汁机都是干净的,为了方便描述,我们将果汁编号为1(橙汁),2(苹果汁),3(葡萄汁)......
[友情提示:本店不售卖混合果汁]

输入格式:
每组测试数据第一行包括两个整数K,N(1<=K<=10,1<=N<=100), 其中,K表示小王准备了K台干净的榨汁机,N表示排队等待的有N个客人,接下来N行,每行一个整数表示一个客人点的果汁种类Xi(1<=xi<=100).
输出格式:
输出在当前的请求序列下,小王最少需要清洗果汁机的次数












题目不是很难,但是贪心策略没想好就会WA了。。。这个题就是贪心加模拟,首先完成前k个订单,然后从第k+1个订单开始,如果榨汁机中找不到和新的订单的种类相同的榨汁机,则表明需要洗一台,洗的这一台一定要保证这台榨汁机的果汁种类以后尽量晚点出现,所以要找到从k+1到n号订单中找下一次出现最远的、存在相同颜色榨汁机的一个订单,并将与这个订单颜色相同的榨汁机洗掉。
#include <iostream>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

#define MAXN 1000

using namespace std;

bool hasPeople[MAXN];
int tot=0; //tot=果汁机个数
int color[MAXN]; //每种榨汁机的果汁种类
int kind[10];
int juice[MAXN]; //果汁请求

int main()
{
    int k,n,ans=0;
    cin>>k>>n;
    for(int i=1;i<=n;i++)
    {
		scanf("%d",&juice[i]);
	}
	for(int i=1;i<=k;i++)
	{
		color[i]=juice[i];
		juice[i]=0;
	}
	ans=k;
	for(int i=k+1;i<=n;i++)
	{
		bool flag=true; //flag=true表示已经用过的机器中没有第k+1个请求的果汁种类的
		for(int j=1;j<=k;j++)
			if(color[j]==juice[i])
			{
				flag=false;
				break;
			}
		if(flag) //找不到这种果汁种类的榨汁机,则必须洗一台榨汁机
		{
			int NumOfJuice=0,NumOfMechine=0;
			for(int j=1;j<=k;j++) //机器编号为j
				for(int l=i;l<=n;l++) //果汁订单编号为l
				{
					if(juice[l]==color[j]) //j和l种类相同
					{
						if(l>NumOfJuice) //l的编号比之前找到的大
						{
							NumOfJuice=l;
							NumOfMechine=j;
						}
						break;
					}
					if(l==n)
					{
						NumOfJuice=n;
						NumOfMechine=j;
					}
				}
			ans++;
			color[NumOfMechine]=juice[i];
		}
		juice[i]=0;
	}
	cout<<ans-k<<endl;
    return 0;
}







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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值