4.贪心进阶与经典好题

贪心进阶

Huffman问题

Huffman树正确性证明:
核心:证明大的Huffman树是由小的Huffman树经过一步贪心选择得来的,即证明大的Huffman树是由少了两个最小的叶子节点的小的Huffman树加上最小的两个叶子节点得到的。只需要证明大的Huffman树的wpl等于小的Huffman树的wpl加上最小的两个叶子节点的值即可。细节上,通过一些假设利用Huffman树是所有数中最小的“<=”号来证上下界即可,可以假定最小的两个是兄弟,因为在最优解中他们一定在同一层,交换成兄弟即可。最后可以证明最优的树是由贪心策略从小问题构造出来的。

这种将大的问题规约的小的问题的规约证明的思想要学习

合并果子

题目描述:每次找两堆果子合并,花费为他们的和,最小化花费

正确性证明同Huffman树的正确性证明

单调队列做法:先将一开始的序列排序,然后压入队列中每次从两个队列中选队首最大的,压入第二个队列,可以证明第二个队列也是单调的,因此每次取的都是最值。

本质上是维护了两个有序的序列,用二路归并排序。

多路归并的本质是把多个不同种类的需要从前向后处理的序列使用多个指针维护,从而达到按某种需要的顺序处理的目的,支持有序处理,删除(取)最值和插入(最值)插入如果和前面的构不成需要的顺序那么就需要新开一个序列。

证明:第二个队列中压入了一个比之前队尾小的元素,这种情况是不可能的,因为先假定第二个队列到队尾为止是单调递增的,第一个队列由于排列序也是单调递增的,因此如果新压进拉的比较小一定在更早就出现了,所以这种情况不可能发生,证毕。

这种做法结合桶排序,基数排序或者给定序列就是有序的可以做到 O ( n ) O(n) O(n)

k进制Huffman问题:荷马史诗

#include
#include
#include
#define int long long
using namespace std;
const int maxn=1000010;
int a[maxn],n,m,k,ans,dp[maxn];
struct node{
	int id,val;
	node(int id_,int val_){
		id=id_,val=val_;
	}
	node(){}
}; 
bool operator<(node x,node y){
    if(x.val==y.val) return dp[x.id]>dp[y.id];//要使得最长s最短,
	//即树高要尽量小,因此这里必须比较高度 
	return x.val>y.val;
}
priority_queue<node>q;
signed main(){
	scanf("%lld%lld",&m,&k);
    n=m;
    while((n-1)%(k-1)!=0) n++;//最后要留一个,其余的一次减少k-1个,保证了最后一次合并刚好k个,这条语句保证了去除一个后刚好分成很多个k-1的组,最后一个k-1的组与一开始去除的1组成一个k的组 
    printf("n=%lld\n",n) ;
	for(int i=m+1;i<=n;i++) q.push(node(i,0));
	for(int i=1;i<=m;i++) scanf("%lld",&a[i]),q.push(node(i,a[i]));
	int cnt=n;
	while(q.size()!=1){
		int tmp=0;
		cnt++;
		for(int i=1;i<=k;i++) tmp+=q.top().val,dp[cnt]=max(dp[cnt],dp[q.top().id]+1),q.pop();
		q.push(node(cnt,tmp));
		ans+=tmp;
	}
	printf("%lld\n%lld",ans,dp[cnt]);
} 

关于k叉哈夫曼树

以下选自lyd大佬的话:

对于k叉哈夫曼树的求解,直观的想法是在贪心的基础上,改为每次从堆中去除最小的k个权值合并。然而,仔细思考可以发现,如果在执行最后一次循环时,堆的大小在(2~k-1)之间(不足以取出k个),那么整个哈夫曼树的根的子节点个数就小于k。这显然不是最优解————我们任意取哈夫曼树中一个深度最大的节点,改为树根的子节点,就会使∑w[i]*l[i]变小。

采用规约的方法解决问题一定要考虑问题减少不同的规模规约到最后是否可以得出确定小规模可以解决的问题,并且解决完全部问题是最优解,否则要考虑通过假如虚拟点等手段保证任意原问题规约到最后得到的最小子问题都一样。

采用规约的方法解决问题要保证子问题与原问题完全一致。加入的虚拟结点就做到了。

二分+贪心

trick:利用二分+贪心可以将具有单调性的最优性问题转为判定性问题,从而更容易的求解

分组

题目描述:给定一些整数,如果两两相差为1可以在同一组,使得最短的组最大

可以直接贪心:每次能加之前组的尽量加之前最短的组,否则开新租,用堆维护。

证明:

1.如果新进来的能加之前组的开新租,交换一下不会变差

2.如果新进来的不加最短的组,交换一下不会变差

也可以二分+贪心;

原问题变成:给定一个x判断是否能使得最小区间的长度为x

我们可以思考,怎样的情况才可能出现更小的区间呢?显然是接不上之前的组要开新组的时候才可能产线更小的区间,我们考虑怎么样才会开新组

定义: f ( i ) = a [ i ] f(i)=a[i] f(i)=a[i]出现的频次(去重之后)

不难发现当 f ( i ) − f ( i − 1 ) > 0 f(i)-f(i-1)>0 f(i)f(i1)>0会以i开头新开 f ( i ) − f ( i − 1 ) f(i)-f(i-1) f(i)f(i1)个组

在[i,i+x-1]的频次都-1如果出现<0的情况就说明现有的元素不够开新组了return false

这里要做区间减法,可以用差分数组优化保证时间复杂度为 O ( n ) O(n) O(n)

trick:离线区间减法可以采用差分数组优化到 O ( n ) O(n) O(n)

```cpp
#include
#include
using namespace std;
const int maxn=2e5+10;
int ans,f[maxn],n,cnt,a[maxn],b[maxn],t[maxn],d[maxn];
bool check(int x){
	b[0]=b[1]-1;
	for(int i=1;i<=cnt;i++) t[i]=f[i],d[i]=0;
	for(int i=1;i<=cnt;i++){
		t[i]=f[i];
		int s=max(f[i]-f[i-1],b[i]-b[i-1]==1?0:f[i]);
		d[i]-=s,d[i+x]+=s;
		if(s&&b[i+x-1]-b[i]!=x-1) return 0;//这里由于去重之后两个元素之间至少相差1,所以可以这样判断连不连续
		d[i]+=d[i-1];
	}
	for(int i=1;i<=cnt;i++){
		t[i]+=d[i];
		if(t[i]<0) return 0;
	}
	return 1;
}
inline int read(){
	int flag=1,x=0;
	char ch=getchar();
	while(ch>'9'||ch<'0'){if(ch=='-') flag=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') x=x*10+ch-'0',ch=getchar();
	return x;
}
int main(){
	n=read();
	for(register int i=1;i<=n;i++) a[i]=read();
	sort(a+1,a+1+n);
	for(register int i=1;i<=n;i++)
		if(i==1||a[i]!=a[i-1])
			cnt++,f[cnt]=1,b[cnt]=a[i];
		else f[cnt]++;
	register int l=1,r=n;
	while(l<=r){
		int mid=(l+r)>>1;
		if(check(mid)) l=mid+1,ans=mid;
		else r=mid-1;
	}
	printf("%d",ans); 
	return 0;
}

另一道二分+贪心的简单题:跳石头

难题:Monitor

题目描述:给定一些区间,覆盖[0,10000],每个区间可以左右移动不超过x,最小化x,保证有解

本题属于覆盖区间类问题可以往贪心基础2(优先满足最左边的,保留对后面帮助大的)类似的思路方向想贪心策略

trick:比赛中,许多问题可以用二分答案+贪心解决。

先二分x,考虑check怎么写。

贪心策略:每次选可以覆盖当前左端点的右端点最小的进行覆盖,能覆盖到左端点的情况下尽量靠右,更新左端点,直到覆盖完成

直观的想:如果让右端点靠右的往左移动来覆盖这个端点,则牺牲了这个区间的潜力,所以我们要让能满足覆盖当前左端点的区间中右端点最小的(最不具有潜力)进行覆盖,从而对后面帮助更大

这题与之前的区间覆盖问题不同,最大的特点在于可以移动

trick:覆盖/满足类问题也可以往这个方面想:在能满足最左的端点中选一个最不具有潜力的,将潜力大的留到后面,尽量对后面帮助大(有点类似田忌赛马)

这种问题通常都是如果让潜力大的来覆盖当前左端点往往会牺牲它的潜力,或者造成一些花费,否则若没有产生额外花费通常直接选最优的即可

这种问题一般都用调整法(好用)证明有一个解可以用贪心策略满足,这也是证明的核心

证明:假设有一个解s第一步用了D2来盖A,则证明的关键的是存在一个解ss是用D1来覆盖A,其中D1是所有能盖到A右端点最小的

为了不是一般性,我们一般将一个不满足要求的解(s)设为尽量接近原来的解的情况,即D2的右端点要尽量靠右

设现在的左端点为A

1.D1移动后到不了D2与A对齐后的右端点,这时D2包含D1直接不调整D2位置将D1覆盖A点仍然是一个可行的解

2.D1移动后到得了D2与A对齐后的右端点设为B,可以假设第二个用了D1来覆盖B,因为假设s中利用了D3覆盖B,分情况讨论:(1)D3的右端点比D1小,与D1定义矛盾舍去,(2)D3的右端点比D1大,那么用D1覆盖B将D3移动到更右边不会变差,仍然是一个解。

设D1覆盖B时的右端点为C,我们要调整S中的D1,D2使得D1覆盖A,D2覆盖C且[A,C]这段区间仍然被覆盖。

这是肯定的。

显然D1可以覆盖到A,假设中可以看出

D2也可以覆盖到C因为D1右=[A,C],故存在一个解用D1覆盖A,证毕。

实现需要用到分类讨论的技巧。

对于未使用的区间分三类:

1.Born:左端点>A+delta。
2.Active:与[A-delta,A+delta]相交。也就是移动之后能覆盖到A点。
3.Dead:右端点

由于A不断变大,Dead的区间永远Dead。

算法要再Acive中找右端点最小的。

用堆可以解决。

trick:分类讨论需要安排的元素,并且采用数学,区间集合等方式可以得出高效易实现的实现方法。

Sgu171 Sarov zones

题目描述有k个赛区每个赛区要n个人,有一个难度值,有N个选手,每个选手有一个权值一个实力值,若实力>难度则累加其权值到ans中,最大化ans,求一个分配方案,保证选手人数与所有赛区一一对应

简化问题:将k个赛区每个赛区n个人拆成n个赛区每个赛区一个人,使得每个人都匹配一个赛区,这样从多个人进一个赛区的复杂问题转换为了一个人一个赛区的简单问题,要学习这种简化问题的思维

题解:这题与Monitor有些类似,都属于满足、覆盖类题,可以采用怎么样的分配方式可以使得当前获得的收益大,且对后面要尽量好的策略(田忌赛马),能用这种策略的原因分配了当前的选手会占用某个赛区,对后面产生影响,所以要保证对现在好的同时还要对未来好。

trick:遇到权值可以往现将权值排序的方面想,这其实就是要让现在尽量好。并且用权值排序出的贪心策略很容易用交换法证明正确性。

贪心策略:按权值从大到小排序,当前能晋级让他晋级(对现在好),但是要再能晋级的前提下选最难的赛区,若不能晋级则直接取最难的赛区(对未来好)

用交换法容易证明正确性

code中的坑点:一定要记录赛区和选手的编号,答案要求输出编号但是排了序之后编号就乱了所以遇到排序和编号一般要记录好原来的编号,记住这个细节可以在以后减少错误

矩形

给出平面上的n个点,请找出一个边与坐标轴平行的矩形,使得它的边界上有尽量多的点

暴力枚举s四条边TLE,考虑优化

贪心可以在某些枚举题中起到优化暴力的作用

这题可以用贪心省掉一个for变成三次方可以通过,以省略最下一条边为例,考虑到只有新的可以做最下一条边的价值比之前的边+两侧与之前最下面横边和现在枚举到的横边的两段之和<枚举到横边的点数才能选这条,用数学式表示出来可以证明正确性

交换相邻元素(无数据)

题目描述:给定一个排列,每次可以交换相邻的两个元素问不超过k步使得排列字典序最小的方案。

贪心策略:

从字典序的定义不难想出

第一步交换1-min(n,k)中最小的到第一位k-=j

第二步交换2-min(n,k)中最小的到第二位k-=j

引理:每次只会交换逆序对(有通用性,在交换相邻元素为约束的题目中都可以用)

证明引理:

1.若交换一次顺序对后后面某一步交换回来则删掉这两步,放到最后面交换一个相邻逆序对,不会变差

2.若交换一个逆序对后面某一步不交换回来则删掉这一步,放到最后面交换一个相邻逆序对,不会变差

证明正确性:若每次只交换逆序对,设逆序对个数为T

1.k>=T直接交换成最小字典序排序即可

2.k

归纳法证明的思路和只在交换相邻元素的题中交换逆序对的trick可以积累。

强偏序的问题一般都是贪心解的,因为可以逐位满足,并且问题可以规约到更小的问题。

划船
给定n个人名由姓+名组成,同名或者同姓的人可以做同一搜船,求最少需要多少船。

解法:
性质:最终答案的安排方案有多种。(同名或者同姓的要求并不强,并且本质相同。)

方法1;带权图0,1,表示同名、同姓。现在考虑如何满足图中关系的n个点的乘船方案。

首先不在同一连通分量中的点一定不可能乘同一搜船。现在考虑一个连通分量如何安排。

贪心策略:
找点数最小的环,把和其他环无关的相邻的两个点删掉。最后这个小环如果只剩一个点那么把他和更大的环上相邻的点一起删掉,如果更大的环原来是奇数个点,现在变成偶数链,如果更大的环是偶数个点那么总点数是奇数个最终一定最多剩一个删不掉的点。如果是偶数个则刚好删完。

结论:同一连通分量中的点一定能完全配对。

上面的策略不严谨,图有很多种情况,不好得出结论。

方法2:

由于最终方案有多种,并且同名或者同姓本质上结果都是可以两两相连的删掉,对答案的贡献本质相同,并且同姓的同姓还是同姓,满足父亲的父亲还是父亲这种同类传递性的关系,可以用二叉树来表示全部有用的关系。

树是能保证点的连通信息的最简洁的图,越简洁就越容易得出充分必要条件。

以任意一个点为根,左同名右同姓建二叉树。

一个点只有两类关系和自己有关并且关系具有传递性可以建立二叉树。

贪心策略:找最下面一个有两个儿子的父亲,这个父亲结点在根的左子树,就把他和右儿子删掉,左儿子接到自己的父亲的左儿子。(类似儿子兄弟表示法的两边版。)问题转换成和原问题完全一致的子问题(规约)。

右边是对称的。

trick:答案有多种构造方案(图表示的信息是连通性可以解决问题),边权对答案的贡献本质相同,相同类型边权具有传递性(父亲的父亲是父辈),可以考虑构造一个树来表示这个图,省略了很多边,很简洁(由于同类传递性。)

Z.zigzag

给定一个序列求最长上升下降子序列长度。

将点图形化在坐标系上表示出来不难发现每次都选拐点是最优的

序列问题中我们往往可以dp求解,但是对于某些问题,将序列图形化之后可能可以通过某些性质利用贪心求解,图形化的思维要掌握

证明:第一个点和最后一个点(如果不在只有两个元素且相等)一定选,否则可以分情况讨论,以第一个点为例,如果选了后面的某个点作为第一个点,调整为选第一个点不会变差,由于两拐点直接的函数具有单调性,如果一个点在拐点之前选去掉他改成拐点不会变差,如果拐点没选,改成选拐点不会变差

连接多位数

排序类的题,这类题的答案通常都需要一个顺序,属于贪心中比较常见的一类,通常可以往交换两个相邻元素会对答案产生什么影响方面想。

类似的题还有国王游戏

贪心方案:按s+t

证明:

传递性可以将字符串看成多位数利用自然数具有传递性证明

答案序列一定没有逆序对,如果有交换一下会更优,即最优解是按新定义排序后的序列,最小的一定在第一个根据结合传递性和归纳法,证毕

Pairs

题目描述:一个pair能满足两数之和是2的幂则ans++,每个数只能用一次,最大化ans

可以建一个图把两两有二的幂的连边

转化为图的最大匹配问题,可以用带花树解决,但是复杂且时间复杂度超了

用贪心优化问题

观察图,发现了选度为1的点进行匹配,即一个叶子唯一匹配某个点(这里只两个数字的唯一匹配关系,如果有相同大小的可以看成点权)之间选这两个匹配,用交换法可以证明:如果不选叶子一个非叶子与另外一个匹配,改成和叶子匹配不会变差

现在问题变为找度为1的点,首先度唯一可以想到和最大、最小值有关,最小值不可能,因为能匹配到成为最小值的两个数不一定只有一种可能,但是最大值只能唯一匹配,其次要证明一定有度为1的点,容易证明,出现了唯一匹配在图上就对应着度为1的点。

证明:设 i + j = 2 k i+j=2^k i+j=2k,k是能组成的最大的2的幂(证唯一匹配)

假设 i > = j i>=j i>=j那么 i > = 2 的 k − 1 次方 i>=2的k-1次方 i>=2k1次方

假设有另外一个数k可以与i组成2的幂

那么 i + k = 2 k 或 p o w ( 2 , ( k − 1 ) ) < = i + k < 2 k i+k=2^k 或 pow(2,(k-1))<=i+k<2^k i+k=2kpow(2,(k1))<=i+k<2k

即要么不存在k和i组成 2 k 2^k 2k要么 k = j k=j k=j

所以具有一一对应的关系

代码实现:从大到小枚举k

代码优化,

给定一个数在一个序列中找两个数使得这两个的和为这个数

暴力枚举 O ( n 2 ) O(n^2) O(n2),考虑先排序运用单调性优化

双指针优化:

i+j=k可以化为i=k-j

令i为自变量,不难发现当i增大时j单调递减

因此i从小到大,j从大到小,当两数之和>k时j–,

证明:j单调性容易证明,因为比较小的i加上某个大的j都>k那么更大的i加上之前的大j仍然>k因此j有单调性

引用某神犇博客中的一句话:尺取法就是在对于枚举每一个ll的时候,另一个坐标rr维护的答案也是单调的时候可以使用,能够均摊枚举的时间从而把时间复杂度降到 O ( n ) O(n) O(n).

当你发现所求的问题存在类似的单调性的时候,不妨思考一下尺取法.只是有些时候尺取法推动 l , r l,r l,r两个指针的复杂度达到了 O ( n ) O(n) O(n),这种时候便要另当别论了.

trick:优先考虑边界情况,度为1,最大最小,约束最强最弱。解决完一步特殊的问题后原问题往往能简化或者转化,进而迎刃而解。优先考虑边界情况就是一种贪心。

重构

题目描述:给定n个点的权值,连n-1条边,答案为sigma,d[i]*a[i]

d[i]为结点i的度。

直观的想,权值越大的要使得度越少越好

结论:给定任意的d序列只要满足d[i]>=1&&sum(d[i])=2(n-1),就一定能构造出一颗树*

看到点的度我们可以想到这个公式:各顶点度之和=2*边数,这个公式时常用在和度相关的题的证明上,建立了度与边的关系

有了这两个约束构造树可以将度为1的点和当前度最大的点连边,归纳的可以构造一颗树

现在的问题转换为了一个数学问题,一开始花费肯定是所有点度为1的情况,接下来用优先队列左一个多路归并的问题:每次增加的花费是 d e l t a = ( ( j + 1 ) 2 − ( j ) 2 ) ∗ a [ i ] delta=((j+1)^2-(j)^2)*a[i] delta=((j+1)2(j)2)a[i]由于j递增这个数肯定在每一路都是单调递增的,因此多路归并可以解决

sgu207

题目描述:给定n,n个x[i],m,y,求一个序列k使得S最小,令 S = s u m ( a b s ( x [ i ] / y − x [ i ] / m ) ) S=sum(abs(x[i]/y-x[i]/m)) S=sum(abs(x[i]/yx[i]/m)),所有k的和为m

这一题由于带绝对值,不具有单调性,所以这种安排一个序列的题用比较常用的多路归并行不通。

多路归并排序的每一个序列必须是有序的。

trick:贡献相同的题目采用均摊的方式是最好的,因为不容易出现极限情况处理不了。

这个绝对值也是对我们的提示,列式不难发现S的大小在所有都大于零的减法中每做一次,对S的贡献都是 1 / m 1/m 1/m,所以可以分成>=0和<0的情况,>=0的情况仅需将 k [ i ] = ( x [ i ] ∗ m ) / y k[i]=(x[i]*m)/y k[i]=(x[i]m)/y,即可(由于要下取整,直接考虑等于0的情况列式交叉相乘可以得出),现在考虑<0的情况,这一部分才需要用到贪心,我们要使得总花费最小,只需要按题目要求的式子排序,将最小的一些的k++即可

因为我们由Ki=m∗Xi/Y得KiKi只要再增加1,Ki就要考虑绝对值了,

所以Ki的最终值肯定为m∗Xi/Y或m∗Xi/Y+1。

#include
#include
#include
using namespace std;
const int maxn=1010;
struct node{
	int id,k;
	double x;
}a[maxn];
int n,m,y,ans[maxn];
bool cmp(node A,node B){
	return fabs(1.0*A.x/y-1.0*(A.k+1)/m)<fabs(1.0*B.x/y-1.0*(B.k+1)/m); 
}
int main(){
	scanf("%d%d%d",&n,&m,&y);
	int tot=m;
	for(int i=1;i<=n;i++) scanf("%lf",&a[i].x),a[i].id=i,a[i].k=0;
	for(int i=1;i<=n;i++) a[i].k=a[i].x*m/y,tot-=a[i].k;//下取整距离真实值最多差1,因此tot最大为一个小于n的数 
	sort(a+1,a+1+n,cmp);
	for(int i=1;i<=tot;i++) a[i].k++;
	for(int i=1;i<=n;i++) ans[a[i].id]=a[i].k;
	for(int i=1;i<=n;i++) printf("%d ",ans[i]);
	return 0;
}

启示:观察性质,能确定的先确定,确定不了再贪心。

sgu179

给定一个合法的括号序列,找到比这个括号序列字典序大的合法括号序列中字典序最小的。

解法:

首先,我们第一步不会进行右括号改左括号的操作,因为这样只会让字典序变小。

现在考虑第一步左括号改右括号怎么操作。

贪心,我们希望改动的最高位尽肯能低,并且新序列字典序要比现在大。

那么从低位开始,一步步向高位解决,尽肯能不向更高位尝试就是容易想到的贪心策略。

把括号序列看成二进制数,‘(’=0;‘)’=1。

如果找二进制数下一个显然+1即可,要考虑进位。

括号序列最右边一定是右括号,不能改掉。

那么贪从右起第一个左括号,改成右括号,然后再向左边改直到这个括号也匹配到一个左括号。

可以证明不会存在左边的任意一个0可以使得改动后碰到的高位0->1比最右边的0还低,这个定理若成立,正确性就成立。

定理的正确性显然,因为更左边的0改成1(可以证明不会出现更高位1->0的情况因为这样只会让字典序更小)之后一定会匹配到更高位的0,那么0->1只会在更高位的块(一块合法的括号序列,左右刚好匹配)中,显然不是字典序最小的。

那么这个贪心策略正确性成立。

改掉最右边的0->1后一定会有一个左边的1->0,那么问题规约到在左边再找第一个0->1。这样匹配下去直到每一个0->1都匹配好了就停下,保证不会动到更高位。

这里用到了贪心中的规约思想,并且利用了字典序高位影响大的性质。

一个值得学习的分析问题的思路:很多无从下手的问题我们一般先考虑第一步怎么做,分类讨论,分析性质,(trick:用数字代替括号、字符来找性质)。有趣的是,很多问题根据性质简化一下,转化一下,或者想清楚第一步之后,就很容易解决了。尤其是对于贪心中可以规约解决的问题,这种方法可以解决很多问题。

想不到贪心策略还可以打表(写暴力)找规律解决这题。

最轻的语言

题目描述:你需要在字符集大小为 kk 的所有字符串中选择 nn 个字符串出来,满足没有任何一个字符串是另一个字符串的前缀,且使得权值之和最小。一个字符串的权值为其中所有字符的权值之和(重复计算)。每个字母的权值事先给定。 输出这个最小值。

解法:问题有点像huffman树问题,但是huffman的编码个数是确定为n的,因此只需要n个叶子合并即可,但是这题不同,他给定了可以扩展的叉数k和需要的编码个数n,Huffman的问题只需要对给定的权值编码即可,但是这题需要更多的编码,因此需要迭代出n个,可以看成在一颗高为n的满tire中选一些结点为叶子,构成一颗Huffman树。

构造答案的操作是选一个字符串往后加k个字母迭代出新的k个字符串,贪心策略就选局部最优的,即当前权值最小的字符串迭代出来的是局部最优的。

trick:根据得到答案的操作贪当前得到局部最优的,是通过迭代(或者其他的某种操作)解决问题构造答案,的常见贪心策略思考方向。

注意:第一次迭代出的解不一定是最优解,因为还可能用比较小的再迭代把大的淘汰掉,但是给定的k个字母每个最多迭代一次,因为他迭代之后因为一个字母迭代后新的一定比原来对应的大,如果再迭代的还是用上个字母迭代的,那么用其他字母一定不优,迭代结束,最坏的情况是每次用新的没用过的字母迭代都比原来优最多k次

因此时间复杂度 O ( k ∗ n l o g n ) O(k*nlogn) O(knlogn)

这里也得到了一个优化策略,这次迭代不比上次优直接退出,因为再迭代下去只会越来越差

正确性证明:选最小的迭代,如果不是交换一下更好,

容器中只用保留最小的n个,因为其他不可能作为答案,否则交换一下会更好

要插入删除取最小、大,用平衡树维护

#include
#include
#include
#define int long long
using namespace std;
const int maxn=10010;
multiset<int>s;
int n,k,a[maxn],ans=0x3f3f3f3f,now;
signed main(){
	scanf("%lld%lld",&n,&k);
	for(int i=1;i<=k;i++) scanf("%lld",&a[i]),s.insert(a[i]),now+=a[i];
	while(now<ans){
		if(s.size()==n) ans=min(ans,now);
		auto it=s.begin();
		int tmp=*it; // 把要用的值存下来,尽量不要用迭代器解引用,避免迭代器指向的地址的值发生变化
		s.erase(it),now-=*it;
		for(int i=1;i<=k;i++) s.insert(a[i]+tmp),now+=a[i]+tmp;
		while(s.size()>n) now-=*(--s.end()),s.erase(--s.end());
	}
	printf("%lld",ans);
	return 0;
}

细节:把要用的值存下来,尽量不要用迭代器解引用,避免迭代器指向的地址的值发生变化

trick:想贪心策略时可以先想一个比较容易想到的,考虑在这基础上能不能再优化,可能优化之后的就是正确的贪心策略(贪了更贪)。

[ABC314G] Amulets

N < = 3 e 5 N<=3e5 N<=3e5 个怪物,编号为 1 1 1 N N N.编号为 i i i 的怪物有一个正整数攻击力 A i A_i Ai 和一个 1 1 1 M M M 之间的类型 B i B_i Bi

你的初始血量为 H H H,并且有 M M M 个不同的护符,编号为 1 1 1 M M M

每次冒险中,你都要从 M M M 个护符中选择若干个携带,然后按照编号从 1 1 1 N N N 依次对战每一只怪物.在对战怪物 i i i 时,如果你没有携带护符 B i B_i Bi,你的血量会扣 A i A_i Ai 点.若此时血量小于等于 0 0 0 你会死亡,否则视为你击败了怪物.

你需要对于每个整数 K = 0 , 1 , … , M K=0,1,\dots,M K=0,1,,M,求出携带恰好 K K K 个护符时,在不死亡的前提下能击败的最多怪物数量.每次冒险相互独立.

保证对于每个 i = 1 , 2 , … , M i=1,2,\dots,M i=1,2,,M,至少有一个类型为 i i i 的怪物.

解法:

直接维护k个护符最远可以到达哪个点不好维护,是一个整体的信息。

并且k-1的答案,与k的没有关联,无法转移,单个单个求容易TLE,很难得到AC的算法。

转化问题:(常用思想)考虑要维护信息的反面,将整体要维护的信息转化成单点要维护的信息,从而在单点与单点信息之间考虑转移,再由单点信息构造出整体答案。整体->单点。

trick:设计状态之后每一步根据贪心策略转移最后得出答案,这种方法一般用于状态之间选择的元素改变只有少数个的可以状态转移的题。

难点在于设计合理的可以转移(状态与状态之间改变的量很少)的状态,而不是每一步根据贪心策略转移。这个状态往往不是题目直接让你求的,而是要通过分析性质简化问题、转化问题得出的。

考虑维护 l i l_i li 冲过前i个点最少带的护符数,很容易发现 l l l 有单调递增的性质。

如果可以求出每个点的l,那么原问题解决。

考虑动态维护当前最小值。

要记录m的每个类到当前i的伤害前缀和。

然后使得当前选的类的个数最小,并且未选的类的每一个数的累加和小于h。

满足这个条件可以贪心。

首先可以得到性质:未选类和已选类互为补集。求出一个集合就可以得到另一个集合。

当前类只有个数最小的约束,而当前未选类有选的个数最多且累加和小于h的两个约束。

trick:优先考虑约束最强或者最弱的,从特殊到一般的思想。这是解决所有问题通用的解决方式,类似于拓扑排序中先找度为1的点,先把边界(叶子)解决,可以达到意想不到的转化问题的效果。在规约解决的贪心问题中也是一个常见的确定解决问题顺序的方法。

未选的显然从当前的每个种类的累加和中最小的选,并且选尽量多的是最优的。

然后每次i+1,改变的仅为集合中的某一类的前缀和的值。

而其他之前在未选集里的值都不变,
trick:动态维护最小的一些数,并且单点修改或插入,可以用对顶堆、set。

这里用对顶set维护即可,满足这些操作:查最大最小值,查集合个数,单点修改。

维护一些最大,最小的一些值(和偏序有关),往往采用将维护的和其他的排序,利用偏序的性质更好用数据结构维护。

用set维护贪心的另一题最轻的语言

这题搜索的解法很容易就想到,在树上搜索,找一些点,一条链上只能找一个点。

但是指数级TLE。考虑贪心。如果考虑每次将所有点都拓展一个字母作为前缀,那么题目要求的互不为前缀的约束难以实现。转换角度,考虑贪一个点扩展所有字母。容易想到贪心策略是当前权值最小的点拓展所有字母。

用归纳法很容易证明这个贪心策略的正确性。

考虑最多拓展多少次?

拓展到不能更新答案即可。因为当前最小的已经无法替换掉原来n个的值,那么下一个最小的一定比当前的还大,出来的结果更不能替换了。

在有n个单词的前提下。最多拓展n次每次至少多出来一个,拓展n次足以用两个单词把所有其他单词替换掉。即这个次数最多为n。

引理:两颗点个数一样的树,叶子点权和权值更小的和更大的用相同策略拓展,小的总会更优。(权值为正,正确性显然。)

于是,贪心正确性证明的核心是在任意树的状态下,不会有不用贪心策略创造出的新树的对应结点比用贪心策略的小。

这题核心是想出贪心策略需要转换到最小点的k个替换,而不是陷入每个点都拓展,由k个字母从权值小到大试。

trick:一个角度行不通考虑另外一个角度,整体不行考虑局部,或者换一个主体。就像dp的两种转移形式,考虑自己怎么拓展,和某种拓展方式怎么作用于所有状态。

区间图的着色问题

给定一些区间,每个区间都要一个颜色,相交的区间的颜色不相同。求最小需要的颜色个数和任意一个可行的染色方案。

解法:这种区间问题很常见的解法是贪心。

两种贪心方案:

方案一:左端点排序,选择没有图的颜色中最小的,如果没法选就新开。

实现可以用set,把n个颜色给压进set,使用一个区间就erase掉set的第一个,然后再r端点标记一个使用过这个颜色,开2*n个vector,然后把端点都离散化,扫到做端点就在set中取,扫到右端点就将vector中的全部insert进set。

证明:根据举例子观察可以发现,答案都不超过最多重叠在一起的区间个数。

即记一个点有多少个区间为这个点的深度。最大深度记为D。

那么ans=D可以证明。

按左端点排序,假设有一个区间已经用到了D个那么不可能再有一个区间用到D+1,否则那个区间的左端点一定是深度超过D的点,与题意矛盾,假设不成立,原结论成立。

左端点排序有一个用于证明的重要性质:就是超过最优解的那个区间的左端点,一定是包括在前面那些区间内的点。用这个条件很容易得到反证法。右端点排序则没用这个性质。左端点容易作为第一个不合法的点,利用的是多个区间信息最容易重合的地方。

方案二:右端点排序,每有一个新区间就把他染色成可以接到的区间的右端点最右的区间,并且染成同一个颜色。

直观的想,右端点排序扫的特征就是前面的区间都没了,出来完了,不会再有右端点伸过来影响了。所以这个区间接在右端点最靠右的后面,并且涂成同一个颜色,因为这样可以把右端点靠左的区间留给后面可能出现的左端点更左的区间用。(类似田忌赛马)

证明:若在最优解中,将这个区间没有放到可以放的最右的,那么交换不一样的那个区间的后面的整个整体和这个区间包括后面的整个整体。仍然是一个可行解。一直交换下去,按贪心策略给出的也是最优解。

右端点排序的性质是前面的区间不会有右端点伸过来,这个区间可以采用类似田忌赛马的策略,在能满足自己的前提下把更好的位置留给后面,由于更好的位置自己本身就可以占,所以很容易用交换法证明。多举几个例子就想出来了。

你可能感兴趣的:(OI/ACM核心算法详解,含大量优质题目及题解!,算法)