[学习笔记] 反悔贪心

总结

这东西直接刷题吧。根据我做过的题有下列几个方法:

  • 先乱贪心,然后设计反悔机制来修正答案。
  • 先建出网络流模型,然后研究性质(凸凹性)
  • 先建出费用流模型,然后模拟费用流(网络流的本质也是反悔贪心)

这东西和网络流关系密切,很多时候要结合着用。

UPD20217/7/17:今天 ( t cf) 的一道反悔贪心把我干傻了,我补充一下如何判断设计的反悔贪心是否正确。

种树

题目描述

点此看题

解法

据花姐姐说可以直接 (wqs) 二分,这种时候直接感性理解就好(这不是重点)

我们首先考虑一种乱贪心,也就是直接拿当前权值最大的,然后删去相邻的就可以了。

考虑这种贪心为什么会错,就是因为我们不一定选最大的,而可能同时选他相邻的两个

反悔贪心的思想就是,我们先按照乱贪心的方法做,然后通过反悔来修正答案,这就需要我们根据乱贪心出现的问题来设计反悔机制。比如这道题,我们可以设计反悔机制为:再选一次当前这个数,代表反悔之前的选择,而选择旁边的两个数。

现在维护这个反悔机制就行了,我们可以维护一个双向链表提供每个位置两边的数,然后每次就直接优先队列找权值最大的位置,找到之后删除旁边的两个数,再更新这个点 (t) 的权值为 (a[l[t]]+a[r[t]]-a[t]),更新一下双端队列后把这个数放回优先队列就行了。

但是还有一点细节的问题,戳这里

#include <cstdio>
#include <vector>
#include <algorithm>
#include <queue>
using namespace std;
const int M = 500005;
#define db double
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,k,ans,a[M],l[M],r[M],vis[M];
struct node
{
	int x,c;
	bool operator < (const node &b) const
	{
		return c<b.c;
	}
};priority_queue<node> q;
int main()
{
	n=read();k=read();
	if(n<k*2)
	{
		puts("Error!");
		return 0;
	}
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		q.push(node{i,a[i]});
		l[i]=i-1;r[i]=i+1;
	}
	l[1]=n;r[n]=1;
	while(!q.empty())
	{
		int t=q.top().x;q.pop();
		if(vis[t]) continue;
		vis[l[t]]=vis[r[t]]=1;
		ans+=a[t];
		a[t]=a[l[t]]+a[r[t]]-a[t];
		k--;
		if(k==0) break;
		//更新双向链表
		l[t]=l[l[t]];
		r[t]=r[r[t]];
		r[l[t]]=t;
		l[r[t]]=t;
		q.push(node{t,a[t]});
	}
	printf("%d
",ans);
}

建筑抢修

题目描述

点此看题

解法

典型的反悔贪心。考虑乱贪心就是按 (T_2) 排序,每次就按着结束时间修建筑,能修就修。

会出的问题就是可以结束时间早的会占用很多时间,把后面的时间挤压了。那么我们考虑每次替换成时间消耗更小的,也就是维护一个优先队列考虑替换队首就行了。

#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
const int M = 150005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,sum;priority_queue<int> q;
struct node
{
	int x,y;
	bool operator < (const node &b) const
	{
		return y<b.y;
	}
}a[M];
int main()
{
	n=read();
	for(int i=1;i<=n;i++)
	{
		int x=read(),y=read();
		a[i]=node{x,y};
	}
	sort(a+1,a+1+n);
	for(int i=1;i<=n;i++)
	{
		int x=a[i].x;
		if(sum+x<=a[i].y)//可以直接修
		{
			q.push(x);
			sum+=x;
		}
		else if(x<q.top())
		{
			sum-=q.top();
			q.pop();
			q.push(x);
			sum+=x;
		}
	}
	printf("%d
",q.size());
}

CF802O April Fools' Problem

题目描述

点此看题

解法

这个题真的是个妙妙题。

首先不难建出网络流模型,源点向 (a_i) 连边,(a_i)(b_i) 连边,(b_i) 向汇点连边,相邻两个 (a_i,a_{i+1}) 连边。但是因为数据规模太大所以说无脑费用流是跑不动的。

看到 恰好选k个 类似的字眼就想一想 ( t wqs) 二分吧,这道题打印 (k) 道题就相当于恰好选 (k) 个嘛。但是 ( t wqs) 还需要有凸单调性,从网络流的角度考虑,每次的最短路都在增加,也就是增量是递增的,那么 ( t wqs) 二分就没问题了。

我们考虑在 (b_i) 上面加权,现在问题变成了无限制 (a,b) 匹配的最小代价,这个可以直接用反悔贪心实现,也就是我们在 (b_i) 和前面最小的 (a) 匹配之后也丢进优先队列,在后面的选择中可以反悔。

#include <cstdio>
#include <queue>
using namespace std;
#define int long long
const int M = 500005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,res,num,a[M],b[M];
struct node
{
	int c,x;
	bool operator < (const node &b) const
	{
		return c>b.c;
	}
};
void work(int mid)
{
	priority_queue<node> q;
	res=num=0;
	for(int i=1;i<=n;i++)
	{
		int x=b[i]-mid;
		q.push(node{a[i],1});
		node t=q.top();
		if(x+t.c<=0)
		{
			q.pop();//取了再弹 
			res+=x+t.c;
			num+=t.x;
			q.push(node{-x,0});//供反悔 
		}
	}
}
signed main()
{
	n=read();m=read();
	for(int i=1;i<=n;i++)
		a[i]=read();
	for(int i=1;i<=n;i++)
		b[i]=read();
	int l=0,r=2e9;
	while(l<=r)
	{
		int mid=(l+r)>>1;
		work(mid);
		if(num>=m)
		{
			ans=res+mid*m;
			r=mid-1;
		}
		else l=mid+1;
	}
	printf("%lld
",ans);
}

CF436E Cardboard Box

题目描述

点此看题

解法

虽然这道题没有用反悔贪心我还是把他加上来了

先模一下 Ark 巨佬,这个方法是真的顶。

先把所有 (b_i) 减去 (a_i) 得到新的 (b_i),也就是拿第二颗星需要多付出的代价(下文 (b_i) 都是这意思)

考虑将物品分类,第一类 (a_i<b_i),第二列 (a_igeq b_i),可以将两类物品分别求解再合并答案。

对于第一类物品,因为一定会先取 (a_i) 再取 (b_i),所以直接把所有东西混在一起排序即可,记 (g(x)) 表示选了 (x) 颗星星的最大价值,那么一定是选取排序后数组的一个前缀。

对于第二类物品,因为此时选取 (a_i) 的目的是选取对应的 (b_i),那么不难导出一个结论:可以一组一组地选物品,至多只有一组只选了一个 (a_i),那么做法就呼之欲出了,我们把每组按 (a_i+b_i) 排序,记 (f(x)) 表示选了 (x) 星星的最大价值。那么 (f(2x)) 就直接拿前 (i) 组即可,(f(2x+1)) 就拿前 (i) 组再加上后面的 (a_i),或者是拿前 (i+1) 组再除去一个前面的 (b_i)

因为只用算一个位置的值,所以直接枚举第一类物品选的星星个数即可,时间复杂度 (O(nlog n))

还有一个加强版,如果要算 (forall win[1,2n]) 的最优解的话,因为 (g(x)) 是个凸函数,所以可以用决策单调性优化 (max) 卷积,那么可以做到 (O(nlog n)) 啦!

还有这道题的结论是真的强,我已经是第二次见到了,是优化背包的经典结论吧!

( t luogu) 上说要算的是选取星数大于等于 (w) 的最优解,那你再把 (w+1) 算一次不就行了么?

下面写了一个暴力反悔贪心的代码....

#include <cstdio>
#include <cassert>
#include <iostream>
#include <queue>
using namespace std;
const int M = 300005;
#define int long long
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,a[M],b[M],vis[M];
struct n1
{
	int x;
	n1(int X=0) : x(X) {}
	bool operator < (const n1 &r) const
	{
		return a[x]>a[r.x];
	}
};
struct n2
{
	int x;
	n2(int X=0) : x(X) {}
	bool operator < (const n2 &r) const
	{
		return b[x]>b[r.x];
	}
};
struct n3
{
	int x;
	n3(int X=0) : x(X) {}
	bool operator < (const n3 &r) const
	{
		return a[x]+b[x]>a[r.x]+b[r.x];
	}
};
struct n4
{
	int x;
	n4(int X=0) : x(X) {}
	bool operator < (const n4 &r) const
	{
		return a[x]<a[r.x];
	}
};
struct n5
{
	int x;
	n5(int X=0) : x(X) {}
	bool operator < (const n5 &r) const
	{
		return b[x]<b[r.x];
	}
};
signed main()
{
	n=read();m=read();
	priority_queue<n1> q1;
	priority_queue<n2> q2;
	priority_queue<n3> q3;
	priority_queue<n4> q4;
	priority_queue<n5> q5;
	a[0]=b[0]=1e12;
	for(int i=1;i<=n;i++)
	{
		a[i]=read();b[i]=read()-a[i];
		q1.push(n1(i));
		q3.push(n3(i));
	}
	while(m>0)
	{
		while(!q1.empty() && vis[q1.top().x]!=0) q1.pop();
		while(!q2.empty() && vis[q2.top().x]!=1) q2.pop();
		while(!q3.empty() && vis[q3.top().x]!=0) q3.pop();
		while(!q4.empty() && vis[q4.top().x]!=1) q4.pop();
		while(!q5.empty() && vis[q5.top().x]!=2) q5.pop();
		//
		int x=q1.empty()?0:q1.top().x;
		int y=q2.empty()?0:q2.top().x;
		int z=q3.empty()?0:q3.top().x;
		int xx=q4.empty()?0:q4.top().x;
		int yy=q5.empty()?0:q5.top().x;
		int c1=a[x],c2=b[y],c3=a[z]+b[z],f=0;
		//
		if(xx) c3-=a[xx],f=1;
		if(yy && a[z]+b[z]-b[yy]<=c3)
			c3=a[z]+b[z]-b[yy],f=2;
		int mi=min(c1,min(c2,c3));
		//
		m--;ans+=mi;
		if(mi==c1)//a
		{
			assert(mi==a[x]);
			vis[x]=1;
			q2.push(n2(x));
			q4.push(n4(x));
		}
		else if(mi==c2)//b
		{
			assert(mi==b[y]);
			vis[y]=2;
			q5.push(n5(y));
		}
		else//a+b
		{
			vis[z]=2;
			if(f==1) assert(mi==a[z]+b[z]-a[xx]);
			if(f==2) assert(mi==a[z]+b[z]-b[yy]);
			assert(f>0);
			if(f==1)
				vis[xx]=0,q1.push(n1(xx)),q3.push(n3(xx));
			if(f==2)
				vis[yy]=1,q2.push(n2(yy)),q4.push(n4(yy));
		}
	}
	//for(int i=1;i<=n;i++)
	//	if(vis[i]==1) ans+=a[i];
	//	else if(vis[i]==2) ans+=a[i]+b[i];
	printf("%lld
",ans);
	for(int i=1;i<=n;i++)
		printf("%lld",vis[i]);
}

[NOI2019] 序列

题目描述

点此看题

解法

首先建出费用流模型(说实话有点难建),至少 (L) 个下标相同等价于至多 (k-L) 个下标不同,我们把选数看成一对一对地选,那么就等价于有 (k-L) 次机会不选下标相同的数,所以可以得到下图:

不难发现对上图跑最大费用最大流就能得到答案,但是 (nleq 2cdot 10^5) 显然是跑不动网络流的。

可以考虑模拟费用流,就是把费用流手玩出来嘛。那就要仔细研究研究这个图是怎么跑费用流的,首先如果红边有空余的流量,那么可以直接给红边流一点流量,因为经由他来流一定是最优的,这就相当于选一对未匹配 (A_i+B_j) 的最大。

我们还可以直接流一点黑边,这就相当于选一对未匹配的 (A_i+B_i) 最大。

最后一种方法就是使用费用流的返回边了,可以回撤某个 (A_i)(设其原来匹配的是 (B_k))到红边的流量,让他和 (B_i) 匹配,但是为了红边的流量平衡我们还需要补一个未匹配 (A_j) 的最大值上去,相当于我们匹配 (A_j)(B_k)

还可以回撤某个 (B_i),这个和回撤 (A_i) 的方法是一样的。

思路大概就是这样,最后讲一下实现的细节(这部分一定要认真看,不然会卡很久):

  • 我们要维护 (5) 个堆,分别是:未匹配 (A) 的最大值 (h1)(B) 匹配但 (A) 未匹配的 (A) 的最大值 (f1);未匹配 (B) 的最大值 (h2)(A) 匹配但是 (B) 未匹配的 (B) 的最大值 (f2)(A,B) 都未匹配的 (A+B) 最大值 (h3)

  • 模拟的时候就一点一点的加入流量,(4) 种流法混在一起写就行了。写的时候会涉及到很繁琐的出堆入堆,方便的写法是维护一个标记数组 (s),我们不急着弹堆,而是在取出堆顶的时候看他合不合法即可,不合法就弹出去。

  • 注意如果有下标相同的情况,就算是任意选择的,也不需要用红边了,一定要注意。

虽然没有注释,但是代码还是很好看的

#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
#define ll long long
const int M = 200005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int T,n,k,l,now,a[M],b[M],s[M];ll ans;
struct n1
{
	int x;
	n1(int X=0) : x(X) {}
	bool operator < (const n1 &r) const
	{
		return a[x]<a[r.x];
	}
};priority_queue<n1> f1,h1;
struct n2
{
	int x;
	n2(int X=0) : x(X) {}
	bool operator < (const n2 &r) const
	{
		return b[x]<b[r.x];
	}
};priority_queue<n2> f2,h2;
struct n3
{
	int x;
	n3(int X=0) : x(X) {}
	bool operator < (const n3 &r) const
	{
		return a[x]+b[x]<a[r.x]+b[r.x];
	}
};priority_queue<n3> h3;
void fuck()
{
	while(!h1.empty()) h1.pop();
	while(!f1.empty()) f1.pop();
	while(!h2.empty()) h2.pop();
	while(!f2.empty()) f2.pop();
	while(!h3.empty()) h3.pop();	
	for(int i=1;i<=n;i++)
	{
		s[i]=0;
		h1.push(n1(i));
		h2.push(n2(i));
		h3.push(n3(i));
	}
	while(k--)
	{
		while(!h1.empty() && (s[h1.top().x]&1)) h1.pop();
		while(!f1.empty() && (s[f1.top().x]^2)) f1.pop();
		while(!h2.empty() && (s[h2.top().x]&2)) h2.pop();
		while(!f2.empty() && (s[f2.top().x]^1)) f2.pop();
		while(!h3.empty() && s[h3.top().x]) h3.pop();
		if(now)
		{
			now--;
			int x=h1.top().x,y=h2.top().x;
			ans+=a[x]+b[y];
			s[x]|=1;s[y]|=2;
			if(s[x]^3) f2.push(n2(x));
			if(s[y]^3) f1.push(n1(y));
			if(x==y) now++;
			else
			{
				if(s[x]==3) now++;
				if(s[y]==3) now++;
			}
			continue;
		}
		int v1=0,v2=0,v3=0,c1=0,c2=0;
		if(!f2.empty())
		{
			v1=a[h1.top().x]+b[f2.top().x];
			c1=s[h1.top().x]==2?1:0;
		}
		if(!f1.empty())
		{
			v2=a[f1.top().x]+b[h2.top().x];
			c2=s[h2.top().x]==1?1:0;
		}
		if(!h3.empty())
			v3=a[h3.top().x]+b[h3.top().x];
		int mx=max(v1,max(v2,v3));ans+=mx;
		if(v1==mx && (v1>v2 || (v1==v2 && c1>=c2)))
		{
			int x=h1.top().x,y=f2.top().x;
			s[x]|=1;s[y]|=2;
			if(s[x]^3) f2.push(n2(x));
			else now++;
		}
		else if(v2==mx)
		{
			int x=f1.top().x,y=h2.top().x;
			s[x]|=1;s[y]|=2;
			if(s[y]^3) f1.push(n1(y));
			else now++;
		}
		else s[h3.top().x]=3;
	}
}
signed main()
{
	T=read();
	while(T--)
	{
		n=read();k=read();l=read();
		for(int i=1;i<=n;i++)
			a[i]=read();
		for(int i=1;i<=n;i++)
			b[i]=read();
		now=k-l;ans=0;
		fuck();
		printf("%lld
",ans);
	}
}

楼房搭建

题目描述

这道题是校内模拟赛的题,没有 ( t source)

(n) 个的楼房,初始时每个位置高度都是 (0),每次操作可以让相邻的两个楼房高度 (+1)(+2)(可以是左边加 (1),也可是是右边加 (1)),问最少需要操作多少次,使得操作后第 (i) 个楼房的高度不小于 (h_i)

(1leq n,h_ileq 10^6)

解法

本题可以用单调队列优化 (dp) 做到 (O(ncdot h_i)),但是无法优化,这里就不展开讲了。

先想一想我们是怎么乱贪心的,目的显然是让多加的高度最小,一种看起来比较合理的贪心是:当处理到建筑 (i) 的时候我们疯狂放 (2),然后建筑 (i+1) 就对应的放 (1)

问题也是显然的,如果出现下图的情况就会 ( t Wa) 掉:

最右边那个楼房就会浪费很多,我们本应该对第一个楼房实行 <1,2> 操作来削减第二个楼房留给后面的高度,但是我们无脑做 <2,1> 导致了错误,解决方法是在保证第一个楼房高度不变的情况下,我们尽可能升高第二个楼房

可以用反悔贪心来实现它,具体地,我们用两个 <1,2> 操作来反悔一个 <2,1> 操作,这样第二个楼房就会升高 (3) 的高度。

但是这样还是会出问题,你怎么知道什么时候反悔?如果我们无脑反悔的话可能出现第三个楼房极高,我们就不需要第二个楼房反悔得这么高的情况,那么我们现在从第三楼房的视角去看我们的反悔操作,现在保证第二个楼房的高度不变,那么可以用 <1,2>and<2,1> 或者是三个 <1,2> 把第三个楼房升高 (3) 或者 (6)也就是说反悔上一个建筑的反悔操作可以让这个楼房升高 (3) 或者 (6)

有了这两个理论之后我们就可以无脑做了,相当于是每次让当前建筑尽量高,如果这样不合适也没关系,因为相信后面能够反悔过来,那么 (O(n)) 扫一遍就可以解决问题。

实现的时候,"反悔反悔操作"和反悔操作放在一个变量存着即可,(+6) 可以看做两个 (+3)

下面我嫖了 oneindark 巨佬的代码:

#include <cstdio>
#include <iostream>
#include <cstring>
using namespace std;
inline int readint(){
	int a = 0; char c = getchar(), f = 1;
	for(; c<'0'||c>'9'; c=getchar())
		if(c == '-') f = -f;
	for(; '0'<=c&&c<='9'; c=getchar())
		a = (a<<3)+(a<<1)+(c^48);
	return a*f;
}
int main(){
	int n = readint();
	long long ans = 0;
	int v = 0; // already built
	int chance = 0; // how many repent is allowed
	for(int i=1; i<=n; ++i){
		int h = readint(); ans += h;
		h -= v, v = 0; // what's to do
		if(h <= 0){ // finished
			chance = 0; ans += (-h);
			continue; // waste -h
		}
		int x = min(h/3,chance); // how many +3 is applied
		int y = (h-3*x)>>1; // how many <2,1> is applied
		v = (((h-3*x)&1)<<1)+y; // if <1,2> is applied
		chance = (x<<1)+y; // +3 here can be twice +3 there
	}
	ans += v; // build on virtual n+1
	printf("%lld
",ans/3);
	return 0;
}

CF335F Buy One, Get One Free

这题我也没搞懂,别看我写的东西!

题目描述

点此看题

还是难啊,我本来以为我能直接切,但 (3000) 分的题怎么会那么容易!

错解1

首先考虑如果免费得到的是一个价格不严格小于的馅饼,那么就是一道水题,直接从大到小贪心即可。

这道题直接贪心不行,就反悔啊。我们考虑把所有免费的馅饼放进单调栈里面,对于每个馅饼带走一个严格比他小的,如果带不走那么就考虑反悔,买栈顶那个免费的馅饼,就能多带走当前的两个馅饼,判断条件是 (free<2cdot a_i) 时反悔。

样例随便过,但是一交上去就 ( t Wa) 穿了。

错解2

认真分析了一波,到处去翻题解。我发现不能带走严格比他小的馅饼啊,因为原本的贪心就不是这样设计的,原本的贪心是带走相邻的一个,不能这么乱做的。

我写不动,脑子炸了,留坑待填。

#include <cstdio>
#include <algorithm>
#include <queue>
using namespace std;
#define int long long
const int M = 500005;
int read()
{
	int x=0,f=1;char c;
	while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
	while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
	return x*f;
}
int n,m,ans,a[M],b[M],c[M],tmp[M];
priority_queue<int,vector<int>,greater<int> > q;
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)
		a[i]=read(),ans+=a[i];
	sort(a+1,a+1+n);
	for(int i=1,j;i<=n;i=j)
	{
		j=i;b[++m]=a[i];
		for(;a[i]==a[j];j++);
		c[m]=j-i;
	}
	for(int i=m,sc=0;i>=1;i--)
	{
		int num=q.size(),tp=0;
		int t=min(sc-2*num,c[i]),p=min(c[i],sc)-t;
		for(int j=1;j<=t;j++)
			tmp[++tp]=b[i];//dirctly greedy
		for(int j=1;j<=p;j+=2)
		{
			int k=q.top();q.pop();
			if(k<b[i])//regret previous REGRET
			{
				tmp[++tp]=b[i];
				if(j<p) tmp[++tp]=b[i];
			}
			else//regret
			{
				tmp[++tp]=k;
				if(j<p && 2*b[i]>k)
					tmp[++tp]=2*b[i]-k; 
			}
		}
		for(int j=1;j<=tp;j++)
			q.push(tmp[j]);
		sc+=c[i];
	}
	while(!q.empty()) ans-=q.top(),q.pop();
	printf("%lld
",ans);
}
原文地址:https://www.cnblogs.com/C202044zxy/p/14604078.html