贪心算法笔记

贪心算法笔记

大概内容

贪心就是对于一个问题有很多个步骤,我们在每一个步骤中都选取最优的那一个,最后得出答案。就是在一些函数中可行,但是有些比如二次函数,因为它的转折点不一定最优,就是不可行的。那么如何判断贪心呢?有这么几种

  • 看时间复杂度,一般的就是 O ( n ) O(n) O(n) 或者是排序 O ( n   l o g n ) O(n \ log n) O(n logn)
  • 或者猜测,看着像就可以试试。
  • 自己用数学证明方法,比如归纳法,交换法,就是如果交换之后答案变得小或者大就可以了。

那贪心在比赛中怎么办呢?可以先尝试一下大小样例,再尝试对拍,有个保底,如果可以也可以证明一下。

然后还有一种贪心,叫做反悔贪心,就是可以撤销,也没别的。

最后因为贪心每次取局部最优,所以代码不长,而且时间复杂度不大。

例题讲解
凌乱的yyy / 线段覆盖

题目大意:有 n n n 个线段,问最多有多少个不重叠的线段。

思路:贪心,然后按照 r i r_{i} ri 排序,每次选取尽可能早的场次并且要合法,也就是不能前面重叠。

证明:如果不相交,那么图片如下。

贪心算法笔记_第1张图片

很明显,一场弄完,另一场。如果包含,图片如下:

贪心算法笔记_第2张图片

那样我们就可以选择第一场,因为它结束得早,可以取另外的场次。所以证明取更早的是永远比更晚的要更优。

时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)

#include
struct noip//有同学不会结构体的多开几个数组 
{
	int begin;//开始时间 
	int finish;//结束时间 
}a[2000005];
bool cmp(noip x,noip y)
{
	return x.finish<y.finish;//按结束时间排 
}
using namespace std;
int main()
{
	int n,ans=1;//初始化 
	cin>>n; 
	for(int i=1;i<=n;i++)
	{
		cin>>a[i].begin>>a[i].finish;
	}
	sort(a+1,a+n+1,cmp);//快速排序(不用我多说了吧) 
	int mini=a[1].finish;//定义mini统计最后结束时间 
	int j=1;
	while(j<=n)//我用了while,感兴趣的同学可以用for(提示:for不用定义j) 
	{
		j++;
		if(a[j].begin>=mini)//新的比赛开始时间要晚于mini 
		{
			ans++;//统计比赛数 
			mini=a[j].finish;//更新mini 
		}
	}
	cout<<ans<<endl;//输出 
	return 0;//功德圆满 
}
Teleporters (Easy Version)

思路:直接 a i + i a_{i}+i ai+i 排序,就好了。

证明:就是每次都是走过去,传回来,所以直接排序就可以了。

时间复杂度 O ( n log ⁡ n ) O(n \log n) O(nlogn)

#include
using namespace std;
struct node
{
	int x,id;
}c[200005];
int n,m,tmp;
bool cmp(node a,node b)
{
	return a.x+a.id<=b.x+b.id;//跟思路一样排序
}
int main()
{
	int t;
	cin>>t;
	while(t--)
	{
		memset(c,0,sizeof(c));
		cin>>n>>m;
		for(int i=1;i<=n;i++)
		{
			cin>>tmp;
			c[i].x=tmp;
			c[i].id=i;
		}
		sort(c+1,c+n+1,cmp);
		int i=0;
		while(m>=0&&i<=n)
		{
			i++;
			m=m-c[i].x-c[i].id;
		}
		cout<<i-1<<endl;	
	}
	return 0;
}
Teleporters (Hard Version)

思路:这一题就是先前缀和记录一下能用的传送门的价值之和,然后在使用二分答案,但是因为起点是 0 0 0 的缘故,所以还要另外枚举 0 0 0 点。

#include
#define maxn 2900001
#define int long long
using namespace std;
int T,n,m,ans,a[maxn],s[maxn];
int check(int c,int x,int id)
{//二分最长的合法前缀区间
	int l=1,r=n,sum=-1;
	while(l<=r)
	{
		int mid=l+r>>1;
		if(s[mid]-(mid>=id?x:0)<=c)
        	 sum=mid+(mid>=id?-1:0),l=mid+1;//特判x的影响
		else 
			r=mid-1;
	}
	return sum==-1?0:sum;//可能无解
}
signed main()
{
	scanf("%lld",&T);
	while(T--)
	{
		ans=0;
		map<int,int>mp;//记录位置
		scanf("%lld%lld",&n,&m);
		for(int i=1;i<=n;i++) 
		{
			scanf("%lld",&a[i]);
			s[i]=min(a[i]+(n-i+1),a[i]+i);//取最小花费
		}
		sort(s+1,s+n+1);
		for(int i=1;i<=n;i++) 
			mp[s[i]]=i,s[i]+=s[i-1];//前缀和
		for(int i=1;i<=n;i++)
		{
			if(m-a[i]-i<0) 
				continue;//可能不合法
			ans=max(ans,check(m-a[i]-i,min(a[i]+(n-i+1),a[i]+i),mp[min(a[i]+(n-i+1),a[i]+i)])+1);//注意要加上当前点i
		}
		printf("%lld\n",ans);
	}
	return 0;
}

国王游戏

思路:就是按照 a i b i a_ib_i aibi​​的大小从小到大排序

证明:我们不妨设两个人分别写了 a 1 , b 1 , a 2 , b 2 a_1,b_1,a_2,b_2 a1,b1,a2,b2 且满足 a 1 b 1 > a 2 b 2 a_1b_1>a_2b_2 a1b1>a2b2 则有 a 2 b 1 = a 1 b 2 \tfrac{a_2}{b_1}=\tfrac{a_1}{b_2} b1a2=b2a1

所以有一下两种情况

  1. 1在2的前面
  2. 1在2的后面

设在设在 1 , 2 1,2 1,2 之前所有人左手乘积为 k k k,那么对于第一种情况,我们的答案就是

a n s 1 = max ⁡ ( k b 1 , k a 1 b 2 )   = k ⋅ max ⁡ ( 1 b 1 , a 1 b 2 ) \begin{matrix}ans_1&=&\max(\frac{k}{b_1},\frac{ka_1}{b_2})\ &=& k\cdot \max(\frac{1}{b_1},\frac{a_1}{b_2})\end{matrix} ans1=max(b1k,b2ka1) =kmax(b11,b2a1)

∵ a 2 b 1 < a 1 b 2 , a 2 > 1 \because \frac{a_2}{b_1} < \frac{a_1}{b_2},a_2>1 b1a2<b2a1,a2>1
∴ 1 b 1 ≤ a 2 b 1 < a 1 b 2 \therefore \frac{1}{b_1} \leq \frac{a_2}{b_1} < \frac{a_1}{b_2} b11b1a2<b2a1

∴ a n s 1 = k × a 1 b 2 \therefore ans_1=\frac{k \times a_1}{b_2} ans1=b2k×a1

对于第二种,答案就是

a n s 2 = m a x ( k b 2 , k × a 2 b 1 ) ans_2=max(\frac{k}{b_2},\frac{k \times a_2}{b_1}) ans2=max(b2k,b1k×a2)

{ ∵ a 1 > 1 ∴ k × a 1 b 2 ≥ k b 2 ∵ a 2 b 1 < a 1 b 2 ∴ k × a 1 b 2 > k × a 2 b 1 \left\{\begin{matrix} \because a_1>1 \\ \therefore \frac{k \times a_1}{b_2} \ge \frac{k}{b_2}\\ \because \frac{a_2}{b_1}<\frac{a_1}{b_2} \\ \therefore \frac{k \times a_1}{b_2}>\frac{k \times a_2}{b_1}\end{matrix}\right. a1>1b2k×a1b2kb1a2<b2a1b2k×a1>b1k×a2

∴ a n s 1 ≥ a n s 2 \therefore ans_1 \ge ans_2 ans1ans2

所以按照 按照 a i b i a_ib_i aibi的大小从小到大排序的方法没有问题。

细节:注意高精度

#include
#include
#include
using namespace std;
int N;
int a[1005],b[1005],ka,kb;
int ans[20000],t[20000],lena,lent,tt[20000],t2[20000],len;
void _qst_ab(int l,int r)
{
	int i=l,j=r,ma=a[(i+j)>>1],mb=b[(i+j)>>1];
	while(i<=j)
	{
		while(a[i]*b[i]<ma*mb) i++;
		while(a[j]*b[j]>ma*mb) j--;
		if(i<=j)
		{
			swap(a[i],a[j]);
			swap(b[i],b[j]);
			i++;j--;
		}
	}
	if(l<j) _qst_ab(l,j);
	if(i<r) _qst_ab(i,r);
}
void _init()
{
	scanf("%d%d%d",&N,&a[0],&b[0]);
	for(int i=1;i<=N;i++)
	    scanf("%d%d",&a[i],&b[i]);
}
void _get_t(int Left,int Right)
{
	for(int i=1;i<=lent;i++)
	{
		tt[i]+=t[i]*Left;
		tt[i+1]+=tt[i]/10;
		tt[i]%=10;
	}
	lent++;
	while(tt[lent]>=10)
	{
		tt[lent+1]=tt[lent]/10;
		tt[lent]%=10;
		lent++;
	}
	while(lent>1&&tt[lent]==0) lent--;
	len=lent;
	memcpy(t,tt,sizeof(tt));
	memset(tt,0,sizeof(tt));
	for(int i=1,j=len;i<=len;i++,j--)
	    t2[i]=t[j];
	int x=0,y=0;
	for(int i=1;i<=len;i++)
	{
		y=x*10+t2[i];
		tt[i]=y/Right;
		x=y%Right;
	}
	x=1;
	while(x<len&&tt[x]==0) x++;
	memset(t2,0,sizeof(t2));
	for(int i=1,j=x;j<=len;i++,j++)
	    t2[i]=tt[j];
	memcpy(tt,t2,sizeof(t2));
	len=len+1-x;
}
bool _cmp()
{
	if(len>lena) return true;
	if(len<lena) return false;
	for(int i=1;i<=len;i++)
	{
	    if(ans[i]<tt[i]) return true;
	    if(ans[i]>tt[i]) return false;
	}
	return false;
} 
void _solve()
{
	_qst_ab(1,N);
	t[1]=1;lent=1;
	for(int i=1;i<=N;i++)
	{
		memset(tt,0,sizeof(tt));
		len=0;
		_get_t(a[i-1],b[i]);
		if(_cmp())
		{
		    memcpy(ans,tt,sizeof(tt));
		    lena=len;
		}
	}
	for(int i=1;i<=lena;i++)
	    printf("%d",ans[i]);
	printf("\n");
}
int main()
{
	_init();
	_solve();
	return 0;
}
[USACO07MAR] Face The Right Way G

思路:就是利用贪心,碰到 B B B 的话就全部翻转。然后枚举长度。但是时间复杂度如果没有任何别的优化就是 O ( n 3 ) O(n^3) O(n3) 这样呢是过不了的,

#include
using namespace std;
const int maxn = 10009;
int n;
bool A[maxn],B[maxn];
int main()
{
	scanf("%d",&n);
	char ch;
	for(int i=1;i<=n;++i)
	{
		cin>>ch;
		A[i]=ch=='B'?0:1;
	}
	int mincnt=0x7fffffff,anslen;
	for(int len=1;len<=n;++len)
	{
		memset(B,0,sizeof B);
		bool b=0,flag=1;int cnt=0;
		for(int i=1;i<=n;i++)
		{
			b^=B[i];
			if(!(A[i]^b))
			{
				if(i+len-1>n)
				{
					flag=0;
					break;
				}
				b^=1;
				B[i+len]^=1;
				++cnt;
			}
		}
		if(flag)
		{
			if(cnt<mincnt)
			{
				mincnt=cnt;0
				anslen=len;
			}
		}
	}
	printf("%d %d\n",anslen, mincnt);
} 
异或与乘积题解

n n n 个数你可以将两个数字异或,最终将所有数字相乘,问总乘积最大是多少

由于答案可能很大,输出对 1000000007 1 000 000 007 1000000007 取模后的结果。

思路概述

首先,我们需要了解一个问题:异或是什么东西?异或简单来说0就是而进制中的加法,也就是不同就是 1 1 1 ,如果相同就是 0 0 0 。好的,那我们就可以了解一个事情,异或肯定小于等于加法,加法小于乘法,但是,有两种情况会出现反例,什么呢?就是有 0 0 0 的情况,但是不难发现, a i ≥ 1 a_{i} \ge 1 ai1 所以不用管。还有一种就是 a i = 1 a_{i}=1 ai=1 那么如果他异或偶数,那么就 + 1 +1 +1 ,不然就 − 1 -1 1 。所以我们就先分离出 1 1 1 的位置,然后利用和一定,差小积大的特点,把小的偶数先拿过去异或就可以了。

#include
using namespace std;
const int Mod=1e9+7;
int a[100010];
int b[100010];
int main()
{
	int n;
	cin>>n;
	long long ans=1;//一定要注意开long long 不然死得很惨。
	int cnt=0;
	int len=1;
	int len1=1;
	for(int i=1;i<=n;i++)//分离奇数和偶数,记录1的个数
	{
		int x;
		cin>>x;
		if(x==1)
		{
			cnt++;
		}
		else
		{
			if(x&1)
			{
				a[len++]=x;
			}
			else
			{
				b[len1++]=x;
			}
		}
	}
	sort(b+1,b+len1);
	for(int i=1;i<len1;i++)//开始异或
	{
		if(cnt-1>=0)
		{
			cnt--;
			b[i]+=1;
		}
	}
	for(int i=1;i<len1;i++)
	{
		ans=1LL*(ans*b[i])%Mod;
	}
	for(int i=1;i<len;i++)
	{
		ans=1LL*(ans*a[i])%Mod;
	}
	cout<<ans<<endl;
	return 0;
}
替换字母2题解

题目大意

有长度为 n n n 的字符串,仅包含小写字母。小信想把字符串变成只包含一种字母。他每次可以选择一种字符 c c c,然后把长度最多为 m m m 的子串中的字符都替换成 c c c 。小信想知道最少需要操作几次能让字符串只包含一种字母。

思路分析

我一开始想怎么去贪心,结果怎么都不会,然后转头想去暴力,结果稍微加一点点贪心就过了。我们首先枚举26个小写字母,代表我们要把这一个字符串全部变成这个小写字母,然后我们枚举下标 j j j 如果 s j s_{j} sj 和哪个 i i i 是一样的,那么就 c o n t i n u e continue continue ,如果不一样的话我们就将 j + = m − 1 j+=m-1 j+=m1 这里必须要注意,因为下次 j + + j++ j++ 导致必须是 m − 1 m-1 m1 ,还要 a n s + + ans++ ans++

#include
using namespace std;
string s;
string s1;
int main()
{
	int n,m;
	cin>>n>>m;
	cin>>s;
	int ans1=INT_MAX;
	for(int i=0;i<26;i++)
	{
		int ans=0;
		for(int j=0;j<n;j++)
		{
			if(s[j]==i+'a')
			{
				continue;
			}
			else
			{
				j+=m-1;
				ans++;
			}
		}
		ans1 = min(ans1,ans);
	}
	cout<<ans1;
	return 0;
}
平衡后缀

这一题思路的话还是贪心,然后中间填一个表然后最后后缀取一下最大值和最小值

#include
using namespace std;
int t,n,k,p,f,m[1001],vis[1001];
int main()
{
	cin>>t;
	while(t--)
	{
		f=1;
		p=0;
		memset(vis,0,sizeof(vis));
		memset(m,0,sizeof(m));
		string s;
		cin>>n>>k>>s;
		for(int i=0;i<s.size();++i) 
		{
			vis[s[i]-97]=1;
			m[s[i]-97]++;
			p=max(p,m[s[i]-97]);
		}
		for(int i=0;i<26;++i)
		if(vis[i]&&p-m[i]>k)
		{
			cout<<-1;
			f=0;
			break;
		}
		if(f)
		{
			for(int i=0;i<s.size();++i)
			{
				for(int j=0;j<26;++j)
				{
					if(m[j])
					{
						int mx=0,mn=INT_MAX;
						m[j]--;
						for(int k=0;k<26;++k)
						{
							if(vis[k]) 
							{
								mx=max(mx,m[k]);
								mn=min(mn,m[k]);
							}
							if(mx-mn>k) 
								m[j]++;
							else
							{
								cout<<char(j+97);
								break;
							}
						}
					}				
				}	
			}
		}
		cout<<endl;
	}
	return 0;
}
奶牛玩杂技

题目大意:每头牛都有自己的体重以及力量,编号为 i i i 的奶牛的体重为 W i W_i Wi,力量为 S i S_i Si。当某头牛身上站着另一些牛时它就会在一定程度上被压扁,我们不妨把它被压扁的程度叫做它的压扁指数。对于任意的牛,她的压扁指数等于摞在她上面的所有奶牛的总重(当然不包括她自己)减去它的力量。奶牛们按照一定的顺序摞在一起后, 她们的总压扁指数就是被压得最扁的那头奶牛的压扁指数。

思路概述:贪心,但是正常我们看到的贪心是只有一个变量的,但是这道题目有两个:分别是奶牛的重量和奶牛的力气。那让我们来思考一下:重量大的是不是应该放的更靠下?放的越靠下他的重量影响的奶牛就越少。所以 W i W_i Wi 越大越要越往下放。那么,奶牛力气越大的就越往下放也是类似的道理,力气越大能够支持的重量越多,所以受到重量影响就越小,为了让最大值最小就应该让力气大的去承受更多的重量因为一个力气小的和 S 1 S1 S1 一个 力气大的 S 2 S2 S2 去承受同样的重量, W − S 1 < W − S 2 W−S_1 < W−S_2 WS1<WS2 所以力气越大的就应该越往下放这个时候就有两个需要贪心的了,但是,这是感性思考,需要理性,所以,开始证明。

假设只有两只奶牛,一只奶牛的重量为 W 1 W1 W1 ,力气为 S 1 S1 S1
另一只奶牛的重量为 S 1 S_1 S1 ,力气为 S 2 S_2 S2
如果前者在下压扁指数即为 W 2 − S 1 W_2−S_1 W2S1
如果后者在下压扁指数即为 W 1 − S 2 W_1−S_2 W1S2
假设前者在下更优即为前者在下压扁指数更小
那么 W 2 − S 1 < W 1 − S 2 W_2−S_1W2S1<W1S2 移项得 W 2 + S 2 < W 1 + S 1 W_2+S_2W2+S2<W1+S1 这个时候是处于前者在下更优的情况下所以得出结论 W i + S i W_i+S_i Wi+Si 越大就应该越往下放

#include
using namespace std;
const int N=50005;
struct node
{
	int w,s;
}a[N];
int n,m;
bool cmp(node a,node b)
{
	return a.w+a.s<b.w+b.s;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d",&a[i].w,&a[i].s);
	}
	sort(a+1,a+n+1,cmp);
	int ans=-1e9,tmp=0;
	for(int i=1;i<=n;i++)
	{
		ans=max(ans,tmp-a[i].s);
		tmp+=a[i].w;
	}
	printf("%d",ans);
	return 0;
}
分金币

题目描述:圆桌上坐着 n n n 个人,每人有一定数量的金币,金币总数能被 n n n 整除。每个人可以给他左右相邻的人一些金币,最终使得每个人的金币数目相等。你的任务是求出被转手的金币数量的最小值。

思路概述:这题首先还是贪心,但是因为左边和右边都可以给你金币,而且你也可以给别人金币,所以一左一右可以完美抵消掉,那么思路也就非常简单了,我们不妨假设一个人原有 a i a_i ai 个金币,给了别人 b i b_i bi 个金币,那么 a i − b i + b i − 1 = k a_i-b_i+b_{i-1}=k aibi+bi1=k k k k 的话就是金币总数的平均值。但是我们是要让 b i b_i bi 的绝对值最小。那么我们就把这些数值放到数值上, ∣ b n − k i ∣ |b_n-k_i| bnki 的几何意义,然后再取一个中位数即可。

#include
using namespace std;
long long n, g, sum, a[100005], b[100005];
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		sum+=a[i];//求和,求平均数
	}
	g=sum/n;
	for(int i=n-1;i;i--)
	{
		b[i]=b[i+1]-1LL*(a[i+1]-g);//前缀和记录
	}
	sort(b+1,b+n+1);
	long long mid=(1+n)>>1;
	long long ans=0;
	for(int i=1;i<=n;i++)
	{
		ans+=1LL*abs(b[mid]-b[i]);//取中位数
	}
	cout<<ans<<endl;
	return 0;
}
细胞分裂

简化题意:将数组中任何一个元素替换为任意两个和为该元素的数字。将数组变成元素按非递减顺序排列的数组,所需的最少操作次数。

思路概述:这道题我认为就是一个贪心加数学的题目,因为题目要求所有操作过后,除最后一个数外,每个数都不大于后一个数。因此一个数越大,留给前面的数的空间才越多。所以得到贪心结论:最后一个数不拆。

那样根据我们得出的第一个结论,从后往前推,那么不妨设当前这个数字是 x x x ,它的后一位是 y y y 。那么久就只有这三种情况。

  • x ≤ y x \leq y xy ,根据贪心,我们不应该拆分了,已经满足了。
  • x > y x>y x>y ,我们需要拆分 x x x 使得拆分后的最大值不超过 y y y ,而且根据贪心,拆分后的最小值要尽可能的大,那我们设 ( k − 1 ) × y < x ≤ k × y (k-1) \times y(k1)×y<xk×y (也就是说 k = ⌈ x y ⌉ k=\left \lceil \frac{x}{y} \right \rceil k=yx),但是我们根据抽屉原理不难得出 x x x 至少要被拆分成 k k k 份,不然最大值一定会超过 y y y 。但是因为拆的次数越少,而且拆的平均才可能让这个值尽可能地大。因此我们对 x x x 进行 ( k − 1 ) (k-1) (k1) 次拆分把它变成 k k k 个数。其中的最小值呢,就是 k = ⌊ x k ⌋ k=\left \lfloor \frac{x}{k} \right \rfloor k=kx
  • x = y x=y x=y ,什么也不用动,最好的情况。
#include
using namespace std;
int n;
int a[100005];
int main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
	}
	long long ans=0;
	int last=a[n];//维护反向枚举中遇到的最小值(包括拆分之后的)
	for(int i=n-1;i>0;i--)
	{
		if(a[i]>last)
		{
			int cnt=ceil((double)a[i]/last);//一共要分成几个数
			ans+=cnt-1;//统计拆分次数
			last=a[i]/cnt;//切分后的最小值为a[i]/cnt(无论是否能整除)
		}
		else if(a[i]<last)//不需要拆分维护最小值
		{
			last=a[i];
		}
		//剩下的哪一类不用管。
	}
	cout<<ans<<endl;
	return 0;
}

你可能感兴趣的:(算法笔记,算法,笔记)