贪心,思维总结

贪心,思维总结


套路:

1.在具有多个要素的题中,考虑通过排序,让某些要素呈现单调性,使对其他要素的处理变得简单

2.在对所有元素都要做处理时,可以从极端情况着手

一.微扰法

在考虑先后两个人的先后顺序时,可以考虑两种情况表示出来,作差比较

排队接水
n个人一起排队接水,第i个人需要b[i]的时间来接水。
1 <= n <= 1000
0 <= b[i] <= 1000
同时只能有一个人接水,正在接水的人和没有接水的人都需要等待。
完成接水的人会立刻消失,不会继续等待。
你可以决定所有人接水的顺序,并希望最小化所有人等待时间的总和。

在第一个人接水时,有n个人等待,在第二个人接水时,有n-1个人等待…就想到了将所有人从小到大排序
可以用微扰法证明:

设两个人接水的时间为a,b

第一人先接: 2 ∗ a + b 2*a+b 2a+b

第二人先接: 2 ∗ b + a 2*b+a 2b+a

做差比较=a-b 需<0,即当a<b时,a才能在b前接水。

接水问题二
n个人一起排队接水,第i个人的重要性是a[i],需要b[i]的时间来接水。
1 <= n <= 100000
0 <= b[i] <= 1000
0 <= a[i] <= 1000
同时只能有一个人接水,正在接水的人和没有接水的人都需要等待。
完成接水的人会立刻消失,不会继续等待。
你可以决定所有人接水的顺序,并希望最小化所有人等待时间乘以自己的重要性a[i]的总和。

首先我们发现交换第i人与第i+1人对第i+2及以后是没有影响的。

设两个人的接水时间为$ a_{1} , a_{2} , 重 要 性 为 ,重要性为 , b_{1} , b_{2}$.

第一个人先接: a 1 ∗ b 1 + ( a 1 + a 2 ) ∗ b 2 a_{1}*b_{1}+(a_{1}+a_{2})*b_{2} a1b1+(a1+a2)b2

第二个人先接: a 2 ∗ b 2 + ( a 1 + a 2 ) ∗ b 1 a_{2}*b_{2}+(a_{1}+a_{2})*b_{1} a2b2+(a1+a2)b1

做差比较 = a 1 ∗ b 2 − a 2 ∗ b 1 =a_{1}*b_{2}-a_{2}*b_{1} =a1b2a2b1 需<0才能让第一人先接,即 a 1 b 1 &lt; a 2 b 2 \frac{a_{1}}{b_{1}}&lt;\frac{a_{2}}{b_{2}} b1a1<b2a2

但在排序时应直接用乘法判断。用除法需要特判分母为0的情况。

还需要特判a,b均为0的情况

二.涉及区间的问题

涉及区间时,可以考虑区间dp,数据结构,如果是贪心,基本上就会涉及到某个端点的排序
1.

做任务一
B君要完成n个任务。
第i个任务有一个开始时间和结束时间[si,ei)
同一个任务只能完成一次,并且中间不能换人。问B君一个人最多可以完成多少个任务。
(所有 n 求和 <= 200000)(开始时间 <= 结束时间,如果等于的话,意味着这个任务可以瞬间被做完,但是不能没有人做)

从左向右扫描,找到当前右端点最小的任务来完成。
2.

做任务三
B君和m个人一起,要完成n个任务,在这个题中,B君和m个人,什么都不做。
第i个任务有一个开始时间和结束时间[si,ei)
同一个任务只能完成一次,并且中间不能换人。
B君和m个人,想知道要想完成这些任务,至少需要几个人?(所有 n 求和 <= 200000)(开始时间 <= 结束时间,如果等于的话,意味着这个任务可以瞬间被做完,但是不能没有人做)

将所有任务排序,找到当前没人做的任务就加上一个人。

个人感觉这样做有点麻烦,这本质上是找所有区间重合的最多次数,直接差分就好。

int main(){
	scanf("%lld",&n);
	for(ll i=1;i<=n;i++) {
		scanf("%lld%lld",&a[i].s,&a[i].e);
		d[++cnt]=a[i].s;d[++cnt]=a[i].e;
	}
	sort(d+1,d+cnt+1);
	ll num=unique(d+1,d+cnt+1)-(d+1);
	for(ll i=1;i<=n;++i){
		ll pos=lower_bound(d+1,d+num+1,a[i].s)-d;
		b[pos]++;
		pos=lower_bound(d+1,d+num+1,a[i].e)-d;
		b[pos]--;
	}
	for(ll i=1;i<=num;++i){
		b[i]+=b[i-1];
		ans=max(b[i],ans);
	}
	printf("%lld\n",ans);
	return 0;
}

51nod 1460 连接小岛
有n个小岛,每一个小岛是直线型的,他们不相互相交,第i个小岛所占的区间是[li, ri],而且, r i &lt; l i + 1 ri&lt;li+1 ri<li+1对于所有的 1 ≤ i ≤ n − 1 1≤i≤n-1 1in1。现在要将相邻的小岛用桥连接起来。现在有一条桥的长度是a,第i个岛和第i+1个岛能够连接的条件是,存在x,y使得 l i ≤ x ≤ r i li≤x≤ri lixri, l i + 1 ≤ y ≤ r i + 1 li+1≤y≤ri+1 li+1yri+1 y − x = a y-x=a yx=a成立。
现在有m条桥,每条桥最多被使用一次,问能否把这些岛连接起来。

这道题的关键是把模型抽象出来:

对于每两个小岛,它们之间的桥的长度有一个范围[L,R],那么每座桥就是一个点X.问题被抽象成了有n-1条线段,每个线段要被一个点覆盖,求m个点能否覆盖完所有线段。

方法就显然了。将所有的点也看成线段,所有线段按右端点从小到大排序。对于真正的线段,找大于等于它左端点的最小的点。可以用multiset+lower_bound实现。

注意排序时右端点相同就按左端点从大到小排序。最后点优先于线段。

multiset <ll> st;
multiset <ll> ::iterator it;
struct node{
	ll l,r;
	int id;
	bool operator < (const node &b) const{
		if(r!=b.r) return r<b.r;
		if(l!=b.l) return l>b.l;
		return id>b.id; 
	}
}a[N<<1];
int main(){
//	freopen("a.txt","r",stdin);
	n=read();m=read();
	for(ll i=1;i<=n;++i){
		l[i]=read();r[i]=read();
		if(i!=1) 
			a[i-1]=(node){l[i]-r[i-1],r[i]-l[i-1],0};
	}
	for(ll i=1,b;i<=m;++i){
		b=read();
		a[i+n-1]=(node){b,b,1};
	}
	sort(a+1,a+n+m);
	for(ll i=1;i<n+m;++i){
		if(a[i].id)	st.insert(a[i].l);
		else{
			it=st.lower_bound(a[i].l);
			if(it==st.end()){
				puts("NO");
				return 0;
			}
			st.erase(it);
		}
	}
	puts("YES");
	return 0;
}

三.考虑极端情况

在所有元素都必须达到某种条件时,可以从极端情况开始考虑

独木舟问题
n个人,已知每个人体重,独木舟承重固定,每只独木舟最多坐两个人,可以坐一个人或者两个人。显然要求总重量不超过独木舟承重,假设每个人体重也不超过独木舟承重,问最少需要几只独木舟?

将所有人按体重排序,考虑将最重的人,要是他能和最轻的一起就一起,否则就只能自己坐。

四.顺序

在决策两件事谁先的时候,可能会用到排序。关键是找出以什么排序。

任务执行顺序
有N个任务需要执行,第i个任务计算时占R[i]个空间,而后会释放一部分,最后储存计算结果需要占据O[i]个空间(O[i] < R[i])。例如:执行需要5个空间,最后储存需要2个空间。给出N个任务执行和存储所需的空间,问执行所有任务最少需要多少空间。

按(ri-oi)排序。

NOIP 2012 国王游戏
题目描述
恰逢 H 国国庆,国王邀请 n 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 n 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。

按左手上与右手上值的乘积排序。

五.二维偏序问题

二维偏序问题:给定N个有序对(a,b),求对于每个(a,b),满足a2 < a且b2 < b的有序对(a2,b2)有多少个。
今天才知道这类题叫二维偏序。

挑剔的美食家
所有奶牛都对FJ提出了她对牧草的要求:第i头奶牛要求她的食物每份的价钱不低于A_i,并且鲜嫩程度不能低于B_i。商店里供应M种不同的牧草,第 i 种牧草的定价为C_i,鲜嫩程度为D_i 。 没有哪两头奶牛会选择同一种食物。 求FJ最少花多少钱。
$(1 <= N ,M <= 100,000) (1 \le A_{i} ,B_{i} ,C_{i} ,D_{i} \le 1000000000) $

二维偏序。首先排序解决一维,另一维贪心。

对于牛和草按照新鲜度从大到小排序,枚举每头牛,把满足条件新鲜度条件的草的价格插入(由于从大到小,若牧草i满足牛1的条件,就满足牛2的条件),找后继(大于等于)即可。

我原本想用线段树找后继,后来发现可以用set(此处是multiset),lower_bound(>=)找后继即可

*注意set,multiset删除时可以删除该地址的元素.set只删一个的时候可以删除元素的值(it)但multiset不可以

struct node{
	int co,val;
	bool operator < (const node &c) const{
		return val>c.val;
	}
}a[N],b[N];

multiset<int> ms;
multiset<int>::iterator it;

int main(){
//	freopen("a.txt","r",stdin);
	n=read();m=read();
	for(int i=1;i<=n;++i){
		a[i].co=read();
		a[i].val=read();
	}
	for(int i=1;i<=m;++i){
		b[i].co=read();
		b[i].val=read();
	}
	sort(a+1,a+n+1);
	sort(b+1,b+m+1);
	
	int nw=1;
	for(int i=1;i<=n;++i){
		while(nw<=m&&b[nw].val>=a[i].val)	ms.insert(b[nw++].co);//!!
		it=ms.lower_bound(a[i].co);
		if(it==ms.end()){
			printf("-1\n");
			break;
		}
		else{
			ans+=*it;
			ms.erase(it);//!!
		}
	}
	printf("%lld\n",ans);
	return 0;
}

六.反悔题

夹克老爷的逢三抽一
又到了诺德县的百姓孝敬夹克大老爷的日子,带着数量不等的铜板的村民准时聚集到了村口。
夹克老爷是一位很"善良"的老爷,为了体现他的仁慈,有一套特别的收钱的技巧。
1、让所有的村民排成一队,然后首尾相接排成一个圈。
2、选择一位村民收下他的铜钱,然后放过他左右两边的村民。
3、让上述三位村民离开队伍,并让左右两边的其他村民合拢起来继续围成一个圈。
4、重复执行2、3直到村民全部离开。
夹克老爷的家丁早早的组织村民排成一队并清点了村民人数和他们手里的铜钱数量。
作为夹克老爷的首席师爷,你要负责按照夹克老爷的收钱技巧完成纳贡的任务。
聪明的你当然知道夹克老爷并不像他表现出来的那样仁慈,能否收到最多的钱财决定了你是否能够继续坐稳首席师爷的位置。
今年村民的人数是N,恰巧是3的倍数。
提示:第2步选择村民时不需要按照任何顺序,你可以选择任何一位仍然在队伍里的村民收取他手中的钱财并放走他两侧的村民(这就意味着你无法同时收取到这两位的铜钱了)
(3 <= N <= 10^5 - 1, N % 3 == 0)

只想到了O(n^2)暴力。然后就看了题解。

问题等价于N长的数组中抽取N/3个不相邻的值使得和最大(首尾也不能同时取)

用贪心,类似最大M子段和的方式

初始全部数字可选状态,进优先队列,弹出最大值,结果中加上选中的最大值,删除最大值左右两边的值,将最大值位置的值修改为左右两边的和减去中间的数重新进入优先队列(给自己留下反悔的余地),循环处理直到拿到要求的数据个数。选的次数是不变的。

找前驱和后继用的是set+双向链表。

注意:有取模就定义为0->n-1!!!

st.begin() 返回一个迭代器,它指向容器c的第一个元素

st.end() 返回一个迭代器,它指向容器c的最后一个元素的下一个位置

st.rbegin() 返回一个逆序迭代器,它指向容器c的最后一个元素

st.rend() 返回一个逆序迭代器,它指向容器c的第一个元素前面的位置

typedef pair<ll,ll> pli;
set<pli> s;

void del(ll pos){
	s.erase(make_pair(a[pos],pos));
	l[r[pos]]=l[pos];
	r[l[pos]]=r[pos];
}

int main(){
//	freopen("a.txt","r",stdin);
	n=read();
	for(ll i=0;i<n;++i){//!!
		a[i]=read();
		l[i]=(i-1+n)%n;
		r[i]=(i+1)%n;
		s.insert(make_pair(a[i],i));
	}
	for(ll i=3;i<=n;i+=3){
		ll it=s.rbegin()->second;
		s.erase(make_pair(a[it],it));
		ans+=a[it];
		ll nl=l[it],nr=r[it];
		del(nl);del(nr);
		a[it]=a[nl]+a[nr]-a[it];
		s.insert(make_pair(a[it],it));
	}
	printf("%lld\n",ans);
	return 0;
}

低买高卖
考虑股票市场,一共有n天。
对于第i天,B君知道股票的价格是每单位a[i]元
在每一天,B君可以选择买入一个单位的股票,卖出一个单位的股票,或者什么都不做。
刚开始B君有无穷多的钱,但是没有任何股票。
问n天之后B君最多可以赚多少钱。
(1 <= n <= 200000)
(1 <= a[i] <= 10000)

从后往前枚举每一天,在前面寻找价格最小股票,如果小于今天的股票,就买卖。

在前面找最小可以用线段树维护,或者优先队列。然后发现这是显然错误的可能后面的4买了前面的1,于是前面的5就买不到前面的1了。如:1 5 4

考虑用优先队列(小顶堆)维护这一过程,我们每次得到一个新的价格,将其和堆顶的价格比较,如果比堆顶的价格低,就直接放入堆中,如果比堆顶的价格高,就意味着我们可以提前以堆顶的价格买入一个物品,然后以当前价格卖出,因此我们可以算出本次收益加到总收益中,这样我们就要将堆顶pop掉,然后将本次价格push两次入堆,push两次是因为我们若以堆顶的价格买入,不一定最终是以当前价格卖出的,当前价格有可能只是我们贪心的一个跳板,例如价格1,2,3,10,如果我们以1买入,2卖出,3买入,10卖出我们只能获得8,然而如果我们以1买入,10卖出,2买入,3卖出就可以获得10,我们贪心的过程中肯定会1买入2卖出,而这次2卖出只是我们10卖出的跳板,并不一定是非要在2卖出,因此将某价格加入两次的作用分别是:

1.做中间价

2.做所有可能买入价中的一个(就和比堆顶低的价格直接扔入堆中一样的作用)

七.按意义贪心

缓存交换 HYSBZ - 1826
在计算机中,CPU只能和高速缓存Cache直接交换数据。当所需的内存单元不在Cache中时,则需要从主存里把数据调入Cache。此时,如果Cache容量已满,则必须先从中删除一个。 例如,当前Cache容量为3,且已经有编号为10和20的主存单元。 此时,CPU访问编号为10的主存单元,Cache命中。 接着,CPU访问编号为21的主存单元,那么只需将该主存单元移入Cache中,造成一次缺失(Cache Miss)。 接着,CPU访问编号为31的主存单元,则必须从Cache中换出一块,才能将编号为31的主存单元移入Cache,假设我们移出了编号为10的主存单元。 接着,CPU再次访问编号为10的主存单元,则又引起了一次缺失。我们看到,如果在上一次删除时,删除其他的单元,则可以避免本次访问的缺失。 在现代计算机中,往往采用LRU(最近最少使用)的算法来进行Cache调度——可是,从上一个例子就能看出,这并不是最优的算法。 对于一个固定容量的空Cache和连续的若干主存访问请求,聪聪想知道如何在每次Cache缺失时换出正确的主存单元,以达到最少的Cache缺失次数。

每次删除下一次访问尽量靠后的就好啦。因为下一次访问时一定伴有插入操作。

字符串连接
输入n个字符串s[i],你要把他们按某个顺序连接起来,使得字典序最小。
(1 <= n <= 100)
(每个字符串长度 <= 100)
(字符串只包含小写字母)

用sort自定义cmp函数比较字符串字典序大小,排序后输出即可。

最容易想到的是按字典序排序。举个反例是ba b,答案是bab不是bba。空字符最小还是最大?(字典中是最小即a<aa)

如果认为是最大的话,反例是bc b,答案是bbc

对于任意2个字符串,如果交换后更优,就交换。类似冒泡排序,相当于按照(string) a + b < b + a排序。

CodeForces 605A Sorting Railway Cars
题目大意:一个1n的排列,每次可以把其中任一个数放到序列头或尾,问最少经过多少次操作可以把该序列变成1n

就是保证尽量多的数不变,找最长递增子序列就好啦~

The Pilots Brothers’ refrigerator POJ - 2965 题意:一个冰箱上有4*4共16个开关,改变任意一个开关的状态(即开变成关,关变成开)时,此开关的同一行、同一列所有的开关都会自动改变状态。要想打开冰箱,要所有开关全部打开才行。
输入:一个4×4的矩阵,+表示关闭,-表示打开;
输出:使冰箱打开所需要执行的最少操作次数,以及所操作的开关坐标。

法一:显然一个点不可能操作两次。直接O(2^16)暴力即可。

法二:在不改变已经打开的开关状态的情况下,把关闭的打开。策略:把开关本身以及其同一行同一列的开关(总共7个)都进行一次操作,(开关本身状态改变了7次,开关同一行、同一列的开关状态改变了4次,其他开关状态改变了2次。)新颖

51NOD 1335 子序列翻转
初始有一个字符串s,串的长度L不超过2500。你可以对串中一个子串进行一次翻转,确切的说,你可以选择一对整数{ x,y } 其中0<=x<=y< L,然后翻转字符串中索引在x到y区间上的子串,将该串从s[x]s[x+1]…s[y]变为s[y]…s[x+1]s[x]。你的目的是翻转一次后,使字符串的字典序尽可能的小,那么问最优的 { x,y }是多少?如果存在多组解能使s变化后字典序最小,那么输出其中x最小的那组解,如果还有多组解,那么输出y最小的解。

从前往后比较每个数,如果它后面有比它大的数,它就一定是x。所以可以把原串按字典序sort一遍,原串与排序后的串的第一个不匹配的地方就是x。然后暴力找y即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N=2500+2;
char s[N];
int T,n,x,len,cnt,a[N];
struct node{
	int pos;
	char ss[N];
}tmp[N];
int main(){
	freopen("a.txt","r",stdin);
	scanf("%d",&T);
	while(T--){
		n=0;cnt=0;x=0;
		scanf("%s",s+1);
		n=strlen(s+1);
		for(int i=1;i<=n;++i)	a[i]=s[i];
		sort(a+1,a+n+1);
		for(int i=1;i<=n;++i)
			if(a[i]!=s[i]){
				x=i;
				break;
			}
		if(!x){
			puts("0 0");
			continue;
		}
		for(int i=x;i<=n;++i){
			int idx=0;
			tmp[++cnt].pos=i;
			for(int j=i;j>=x;--j)
				tmp[cnt].ss[++idx]=s[j];
			for(int j=i+1;j<=n;++j)
				tmp[cnt].ss[++idx]=s[j];
		}
		len=n-x;
		for(int i=2;i<=cnt;++i){
			for(int j=1;j<=len;++j)
				if(tmp[i].ss[j]<tmp[1].ss[j]){
					tmp[1]=tmp[i];
					break;
				}
				else if(tmp[i].ss[j]>tmp[1].ss[j]) break;
		}
			
		printf("%d %d\n",x-1,tmp[1].pos-1);
	}
	return 0;
}

参考:

强烈感谢:浪小花酱 真的写的好。
就只粘一篇链接啦。https://blog.csdn.net/qian2213762498/article/details/81782641
https://blog.csdn.net/lby767087094/article/details/78215600

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值