[APIO2012]守卫

题目描述:

输入n,k,m3个数,n个灌木丛排成一列,一共有k个忍者躲在这n个灌木丛里(每个灌木丛最多只能躲一个忍者),有m个3元组输入,li,ri,typei,l和r是区间的起始点和结束点,type只有两种取值,0代表这个区间里没忍者,1代表这个区间里至少有1个忍者,输出必然有忍者的灌木丛编号,若没有任何一个灌木丛的后面必然藏着忍者就输出“-1”.

灌木的数量 1 ≤ N ≤ 100,000 ;

忍者数 1 ≤ K ≤ N ;

守卫数 0 ≤ M < 100,000 。

对于 10%的数据,N ≤ 20, M ≤ 100;

对于 50%的数据,N ≤ 1000, M ≤ 1000。

10%做法:可以暴力搜索出k个位置,一一判断是否对于m个区间合法,然后开个数组,每搜索出一种合法方案,就把合法的方案的有忍者的点的下标对应数组里的位置的值加1,然后统计出合法方案的总数,输出数组里下标所存值等于合法方案总数的下标即可。

时间复杂度:

50%做法:

首先我们可以明显想到,将所有和0有关的区间的所有数排除在考虑范围之外。做个特判,如果剩下的有可能有忍者的灌木丛的数量等于忍者总数,就输出这些灌木丛,return 0。将type为1的区间的坐标转换成有可能有忍者的灌木丛区间序列,然后显然剩下type为1的区间中,如果两个区间存在包含关系显然较大的区间是没有用的,便可以删去,这样我们就剩下一个区间起始点和结束点一起单调递增的区间序列。先特殊判断将只包含1个点的区间的灌木丛标记为必然有忍者,然后对剩下长度>=2的区间进行处理。枚举考虑的灌木丛,运用贪心的方法,求出不在这个灌木丛放忍者的方案中,满足所有区间都有忍者的最少的忍者数,若所需忍者数大于k则此灌木丛必选。大于k则此灌木丛必选很好理解,那么为什么小于等于k就必然不选呢?首先若等于k则贪心出的方案即是一种合法方案,若小于k则可以将剩下的忍者随便分配到剩下的灌木从中而不和任何一个区间相矛盾。具体贪心过程如下:将所有序列按左端点排序,自然也就按右端点排好了序,然后枚举区间,若这个区间内没有忍者,就在这个区间的右端点放忍者。整个过程如下:

读入:  16   3   7

              7   9    1

             14   16  0

              1   4   1

              5   5   0

              4   7   1

             11  13  1

              9   10   1

这个样例


(红色代表1的区间,绿色代表0的区间)

先预处理出有可能成为必选灌木丛的标号:

然后贪心(以下是考虑编号为9的灌木丛是否一定有忍者):

找到第一个区间1-4,发现里面没有忍者就在末端点4放一个忍者              

接着找到区间4-7,发现里面已经有忍者了,就不理它,再找到区间7-9,发现里面没有忍者,本来应该在末端点,也就是9的位置上放一个忍者,但由于是考虑编号为9的灌木丛是否一定有忍者,所以编号为9的位置不能放忍者,于是向前一位,放在8号灌木丛里。


然后发现9-10没有忍者,于是又放一个忍者在10的位置上:


最后发现11-13的区间也没有忍者,就放一个忍者在13的位置上:


接着,我们发现如果9不能选的话就至少要4个忍者,而题目告诉我们一共才3个忍者,所以9是必选的。

现在来证明一下贪心为什么每次放在右端点一定是最优的:因为我们是按左右端点单调递增排序的,所以在考虑到放忍者的时候应让接下来放的忍者尽可能多的被其他区间覆盖,而考虑到第i个区间的时候,剩下的区间的右端点一定比第i个区间的大,也就是说在第i个区间放下的忍者不可能到剩下的区间的右边,只可能在剩下的区间的左边,为了保证放的忍者在最少的区间的左边,就一定要寻找一个最右的点放,也就是这个区间的右端点。

时间复杂度:

100%做法:

考虑在50%的做法上进行优化,我们真的要每枚举一个点就贪心一次吗?答案是否定的。我们从前往后贪心一次,所有点都可以选,这样,我们可以发现有可能是必选点的点只会是我们贪心时选了的点,也就必然是一个区间的右端点。再考虑,如果是从后往前同样的方法贪心,可能是必选点的点一定是一个区间的左端点。这样,我们就得出了结论:一个必选点必然即是一个区间的左端点又是一个区间的右端点。然后怎么判断呢,我们发现,有一个点不能选的贪心情况可以由从前往后贪心到某一位,所有点都可以取的情况和从后往前贪心到某一位,所有点都可以取的情况合并而来,例如一个考虑点是编号为i的区间的末尾,那么可以让前i-1的区间都满足的最少方案就是所有点都能取,前i-1个区间满足的方案(相当于将最后一个忍者放前了1位,对数量没有影响)。那么最后一个忍者的贡献给不了第i个区间,也就是剩下第i个区间到第n个区间还没有放,那么放低i个区间到第n个区间的最小值就是从后往前做,所有点都可以取,做到第i个区间时的值。想必讲到这里,大家都已经想到了应该怎么做了,就是求一边从前往后做的贪心前缀答案,和从后往前做的贪心后缀答案,每次判断只需要将两个答案合并判断即可。判断的时间复杂度是线性的,但由于要排序,总的时间复杂度:

                                          

                                                                            推荐番:《甲铁城的卡巴内瑞》

最后上代码:

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
struct data
{int l,r;};
data a[100005];
int n,k,m,ll,rr,type,p,ss,pp,fqian[100005],fhou[100005],line[100005],cha[100005],dao[100005];
int limit,cc,ma,ans,pan[100005],lef[100005],righ[100005],you[100005],biao[100005],bb[100005];
bool cmp(data a,data b)
{
   if (a.l!=b.l)return a.l>b.l;
   else return a.r<b.r;
}
int main()
{
	cin>>n>>k>>m;
	for (int i=1;i<=m;i++)
	{
	   scanf("%d %d %d",&ll,&rr,&type);
	   if(type==0)
	   {
	   	   cha[ll]++;cha[rr+1]--;
	   } 
	   else
	   {
	   	p++;a[p].l=ll;a[p].r=rr;
	   }
	}
	for (int i=1;i<=n;i++)
	{
		ss+=cha[i];if (ss==0){pp++;line[pp]=i;}
		dao[i]=pp;
	}
	if (pp==k)
	{
		for (int i=1;i<=pp;i++)printf("%d\n",line[i]);
		return 0;
	}
	for (int i=1;i<=p;i++)
	{
		a[i].l=dao[a[i].l-1]+1;a[i].r=dao[a[i].r];
	}
	sort(a+1,a+1+p,cmp);
	limit=n+1;
	for (int i=1;i<=p;i++)
	{
		if (a[i].r<limit)
		{
			if (a[i].l==a[i].r){pan[line[a[i].l]]=1;k--;limit=a[i].r;}
			else
			{
			  cc++;lef[cc]=a[i].l;righ[cc]=a[i].r;limit=a[i].r;
		    }
		}
	}
	ma=0;
	for (int i=1;i<=cc/2;i++)
	{
		swap(lef[i],lef[cc-i+1]);
		swap(righ[i],righ[cc-i+1]);
	}
	for (int i=1;i<=cc;i++)
	{
		 if (lef[i]<=ma)fqian[i]=fqian[i-1];
		 else
		 {
		 	ma=righ[i];fqian[i]=fqian[i-1]+1;you[righ[i]]=1;
		 }
		 biao[lef[i]]=i;bb[righ[i]]=i;
	}
	ma=n+1;
	for (int i=cc;i>=1;i--)
	{
		 if (righ[i]>=ma)fhou[i]=fhou[i+1];
		 else
		 {
		 	ma=lef[i];fhou[i]=fhou[i+1]+1;
		 }
	}
	  for (int i=1;i<=pp;i++)
	  {
	  	if (you[i]==1 && biao[i]!=0 && fqian[bb[i]]+fhou[biao[i]]>k)
	  	pan[line[i]]=1;
	  }
   ans=0;
   for (int i=1;i<=n;i++)
   {
   	if (pan[i]==1){printf("%d\n",i);ans++;}
   }
   if (ans==0)printf("-1\n");
   return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值