[学习笔记] 反悔贪心

总结

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

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

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

UPD2021/7/17:今天 \(\tt 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\n",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\n",q.size());
}

CF802O April Fools' Problem

题目描述

点此看题

解法

这个题真的是个妙妙题。

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

看到 恰好选k个 类似的字眼就想一想 \(\tt wqs\) 二分吧,这道题打印 \(k\) 道题就相当于恰好选 \(k\) 个嘛。但是 \(\tt wqs\) 还需要有凸单调性,从网络流的角度考虑,每次的最短路都在增加,也就是增量是递增的,那么 \(\tt 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\n",ans);
}

CF436E Cardboard Box

题目描述

点此看题

解法

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

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

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

考虑将物品分类,第一类 \(a_i<b_i\),第二列 \(a_i\geq 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(n\log n)\)

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

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

\(\tt 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\n",ans);
	for(int i=1;i<=n;i++)
		printf("%lld",vis[i]);
}

[NOI2019] 序列

题目描述

点此看题

解法

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

不难发现对上图跑最大费用最大流就能得到答案,但是 \(n\leq 2\cdot 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\n",ans);
	}
}

楼房搭建

题目描述

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

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

\(1\leq n,h_i\leq 10^6\)

解法

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

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

问题也是显然的,如果出现下图的情况就会 \(\tt 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\n",ans/3);
	return 0;
}

CF335F Buy One, Get One Free

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

题目描述

点此看题

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

#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\n",ans);
}
posted @ 2021-03-31 22:19  C202044zxy  阅读(715)  评论(1编辑  收藏  举报