二分、三分、01分数规划2

这篇博客介绍了如何运用二分查找策略解决三道不同的编程竞赛题目:一是关于扑克牌组合的问题,二是关于教室借用的调度问题,三是寻找数组中特定位置的最大值。通过对问题的分析和二分查找算法的灵活应用,展示了在处理区间加减和优先分配等问题时的有效解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

链接:登录—专业IT笔试面试备考平台_牛客网
来源:牛客网

[CQOI2010]扑克牌

你有n种牌,第i种牌的数目为ci。另外有一种特殊的牌:joker,它的数目是m。你可以用每种牌各一张来组成一套牌,也可以用一张joker和除了某一种牌以外的其他牌各一张组成1套牌。比如,当n=3时,一共有4种合法的套牌:{1,2,3}, {J,2,3}, {1,J,3}, {1,2,J}。 给出n, m和ci,你的任务是组成尽量多的套牌。每张牌最多只能用在一副套牌里(可以有牌不使用)。

思路:依然是二分寻找答案,关键是判断给出的答案是否满足条件。我们可以通过枚举最多可组成牌的套数,再计算joker是否够用;还有一点,joker不能在一套牌中出现两次,这就是说最终牌的套数需要大于或等于用上的joker数。

接下来就是讨论到底能组成几套牌的问题。不得不说,想出不办法时,手动模拟各种类型(变着花样举例)真是最好的办法。我们假设有6张joker,第一张牌a[1]有3张,第二张牌a[2]4张,其他略去。用j表示joker,x表示正常牌,则不妨看以下例子:

第一张牌:j        j        j        x        x        x

第二张牌:x      x       x        j         j         x

其他……

按照这种对齐的方式,我们可以将牌配对,要求每一列必须只有一个joker,所需joker总数即是枚举套牌数答案mid-a[i]。要是多加了怎么办?又由鸽笼原理,我们惊喜地发现只要最终答案套牌数小于等于joker数即可。当然,还要小于等于已给定joker数。

#include<iostream>
#include<cstdio>
#include<algorithm>
//scanf("%d",&);
using namespace std;
const int maxn=300005,inf=0x7fffffff;
const double esp=1e-8;
int n,m,j,l,r,c[maxn];
int jg(int x){
	long long sum=0;
	for(int i=1;i<=n;++i)
		if(c[i]<x) sum+=x-c[i];
	return sum<=x && sum<=j;
}
int main(){
	cin>>n>>j;
	for(int i=1;i<=n;++i){
		scanf("%d",&c[i]);
	}
	r=1e9;
	while(l<=r){
		int mid=(l+r)>>1;
		if(jg(mid)) l=mid+1;
		else r=mid-1;
	}
	cout<<l-1;
	return 0;
}

链接:登录—专业IT笔试面试备考平台_牛客网
来源:牛客网


[NOIP2012]借教室

 面对海量租借教室的信息,我们自然希望编程解决这个问题。

    我们需要处理接下来n天的借教室信息,其中第i天学校有ri个教室可供租借。共有m份订单,每份订单用三个正整数描述,分别为dj, sj, tj,表示某租借者需要从第sj天到第tj天租借教室(包括第sj天和第tj天),每天需要租借dj个教室。

    借教室的原则是先到先得,也就是说我们要按照订单的先后顺序依次为每份订单分配教室。如果在分配的过程中遇到一份订单无法完全满足,则需要停止教室的分配,通知当前申请人修改订单。这里的无法满足指从第sj天到第tj天中有至少一天剩余的教室数量不足dj个。

    现在我们需要知道,是否会有订单无法完全满足。如果有,需要通知哪一个申请人修改订单。

线段树的板子虽然忘了,但这道题用二分一样可以做。我们只需要不断二分得到最大天数,然后判断是否符合要求即可。具体判断方法:一眼望去就是区间加减数,这也是为啥可用线段树维护的原因,但不难想到可以通过用前缀和数组维护,只需要最后和每天可借的教室数量作比较即可。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<queue>
#include<algorithm>
#include<cmath>
using namespace std;
const int maxn=1000005, inf=0x7fffffff;
int n,m,ri[maxn],l,r;
long long dt[maxn];
struct node{
	int d,s,t;
}a[maxn];
int jg(int x){
	int sum=0,ed=0;
	memset(dt,0,sizeof(dt));
	for(int i=1;i<=x;++i){
		dt[a[i].s]-=a[i].d;
		dt[a[i].t+1]+=a[i].d;
		ed=max(ed,a[i].t);
	}
	for(int i=1;i<=ed+1;++i){
		sum+=dt[i];
		if(sum+ri[i]<0) return 1;
	}
	return 0;
}
int main(){
	scanf("%d%d",&n,&m);
	l=1; r=n;
	for(int i=1;i<=n;++i)
		scanf("%d",&ri[i]);
	for(int i=1;i<=m;++i)
		scanf("%d%d%d",&a[i].d,&a[i].s,&a[i].t);
	while(l<=r){
		int mid=(l+r)>>1;
		if(jg(mid)) r=mid-1;
		else l=mid+1;
	}
	if(l-1==n) {
		cout<<0;
		return 0;
	}
	cout<<-1<<endl<<r+1;
	
	return 0;
}

链接:登录—专业IT笔试面试备考平台_牛客网
来源:牛客网

第 K 个号码


Alice 得到一个包含 N 个数字的数组 A[1..N]。
现在 Alice 想通过一个参数 K 构建一个数组 B,规则如下:
最初,数组 B 是空的。考虑数组 A 中的每个区间。如果此区间的长度小于 K,则忽略此区间。否则,在这个区间中找到第 K 个最大的数,并将这个数添加到数组 B
中。实际上 Alice 并不关心数组 B 中的每个元素。她只想知道数组 B 中的第 M 个最大元素。请帮她找到这个号码。

考虑用二分进行优化。通过枚举第m个最大元素的值x,来寻找符合条件的区间,很明显应该有m-1个区间中的第k个最大的数要大于x。这里可以通过尺取来优化(刚查了查尺取,理解到其时间复杂度为O(n)的原因是头指针不再返回QAQ)。至于为什么:

假设k=3,M=2,数组为4 3 2 5,x为2,尺取的head指向2,tail指向4。首先看前三个数,第k小数为2,不符合条件,head向后移动,指向5.这时第k小数变为3,符合条件的区间数+1.而此时发现无论如何之后的第k小数都不会小于x=2了,从而保证了head不用像二重循环一样还要回来一趟。

至于为啥二分得到的值一定存在于队列之中,这种东西自己去举一些例子就好。我们来关注二分指针,如果答案是6,是否有可能指针在指向7时被判合理呢?答案是否定的,因为与之前假设的6矛盾(满足条件区间计数需要a[i]<=x才加1),也就是说7一定会使指针向左移;同理,若指向5也肯定会向右移。这样就保证了能够指到区间中确定的值。QAQ花了好久弄明白

另外,不要忘了第m个最大的值有蛊!因为有可能这个m贼大,超出了int的范围(我吐了QAQ)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<queue>
#include<map>
//scanf("%d",&);
using namespace std;
typedef long long ll;
const int maxn=100005,inf=0x3f3f3f3f;//inf设大了 
const double esp=1e-8;
ll t,m;
int a[maxn],n,k;
int jg(ll x) {
	//x为二分估计的答案值
	ll cnt = 0;//第K大的数大于等于x的区间个数
	ll num = 0;//当前尺取的区间中大于等于x的数的个数
	for (ll left = 1, right = 1; right <= n; right++) {
		if (a[right] >= x) num++;
		//遇到大于等于x的数便计数
		if (num == k) {
			cnt += n - right + 1;
			//加上该区域向右拓展的区域数量
			while (a[left] < x) {
				cnt += n - right + 1;
				//加上该区域向左拓展的区域数量
				left++;
			}
			//循环删除区域内左边比x小的节点
			left++;
			num--;
			//为了往后遍历,区间第一个节点删除
			//第一个节点一定大于等于x,所以num--
		}
	}
	if (cnt >= m) return 1;
	else return 0;
	//M第M大的数,cnt为理应比M大的区域数量
	//所以当cnt比M大时说明区域多了要缩小,所以返回1取更大的值
}
int main(){
	cin>>t;
	while(t--){
		scanf("%d%d%lld",&n,&k,&m);
		for(int i=1;i<=n;++i)
			scanf("%d",&a[i]);
		ll l=1,r=inf;//long long
		while(l<=r){
			ll mid=(l+r)>>1;
			if(jg(mid)) l=mid+1;
			else r=mid-1;
		}
		printf("%lld\n", l-1);
	}
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值