一、什么是反悔贪心
反悔贪心就是在普通贪心的过程中"反悔",从而使得一些看似不太好贪心的题变成贪心可做题。
二、反悔贪心普遍流程
就是先使用一个好想的贪心策略,使用优先队列进行维护,然后如果在贪心时发现一个东西不能选,那你要考虑之前选的东西里面是否有比这个东西相对意义更差的东西,如果有就丢掉之前选的那个比较差的东西,把这个比较好的东西塞进优先队列里,这就是"反悔"。
三、反悔贪心框架
cpp
priority_queue<int>q;//这里默认大根堆,但有时候需要小根堆
开始贪心
{
if(这个东西可以选)
{
q.push(这个东西);
//增加计数,有些题目可能还需要增加其它东西
}
else if(q.size()&&队列里最不好的东西比这个东西差)
{
//增加计数,有些题目可能还需要增加其它东西
q.pop();//扔掉差的
q.push(这个东西);//扔进好的
}
}
注意:这只是板子,应用时请随机应变。
四、反悔贪心例题讲解
CF1974G Money Buys Less Happiness Now
反悔贪心模板题,首先如果你能增加幸福值就增加幸福值,如果你不能增加(没钱了)那就看看之前是在哪些月份获取了幸福值,找到价格最贵的那一个月,如果那一个月的价格比这个月的价格高,扔掉那一个月,加入这个月(因为这样会让钱变得更多,而且不会影响幸福值)。
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
signed main()
{
int _;
scanf("%d",&_);
while(_--)
{
priority_queue<int>q;
int n,m,sum = 0,num = 0;
scanf("%d %d",&n,&m);
for(int i = 1;i<=n;i++,sum+=m)
{
int x;
scanf("%d",&x);
if(x<=sum)
{
sum-=x;
num++;
q.push(x);
}
else if(q.size()&&q.top()>x)
{
sum+=q.top()-x;
q.pop();
q.push(x);
}
}
printf("%d\n",num);
}
return 0;
}
CF1526C2 Potions (Hard Version)
首先如果读入的 \(x \ge 0\),那肯定是选的,因为不会对答案造成任何负面影响,然后如果 \(sum+x \ge 0\),虽然会让 \(sum\)(目前前缀和)变得更小,但是依旧不影响,所以也是可以直接选的,只不过这里得加入优先队列(因为前面大于等于 \(0\) 的数,对后面无法选择不会造成影响,没必要加入优先队列,因为后面如果无法选择那肯定是负数,负数不可能大于大于等于 \(0\) 的数),然后如果 \(sum+x<0\),就是无法选择,那么就看一下前面选的最差的负数有没有比这个差,如果比这个差就"反悔"。
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
signed main()
{
int n;
long long sum = 0;
int num = 0;
scanf("%d",&n);
priority_queue<int,vector<int>,greater<int>>q;
for(int i = 1;i<=n;i++)
{
int x;
scanf("%d",&x);
if(x>=0)
{
sum+=x;
num++;
}
else if(sum+x>=0)
{
sum+=x;
num++;
q.push(x);
}
else
{
if(q.size()&&q.top()<x)
{
sum+=x-q.top();
q.pop();
q.push(x);
}
}
}
printf("%d",num);
return 0;
}
CF1185C2 Exam in BerSU (hard version)
还是同样的套路,如果能选就选,不能选的话我们就反悔。
本题的输出在不能选的情况下得再准备一个优先队列,因为如果不能选的话,我们每次肯定得从优先队列里面取出最大的(最拖后腿的),放到这个临时的优先队列里,然后输出完后再放回去,然后在不能选的情况下输出的时候还要减 \(1\),因为你那个时候还没有将这个东西放入优先队列(你也可以先放,这样就不用减 \(1\) 了)。
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
signed main()
{
priority_queue<int>q,tmp;
int n,m,sum = 0;
scanf("%d %d",&n,&m);
for(int i = 1;i<=n;i++)
{
int x;
scanf("%d",&x);
if(sum+x<=m)
{
sum+=x;
q.push(x);
printf("%d ",i-q.size());
}
else
{
while(sum+x>m)
{
sum-=q.top();
tmp.push(q.top());
q.pop();
}
printf("%d ",i-q.size()-1);
while(tmp.size())
{
sum+=tmp.top();
q.push(tmp.top());
tmp.pop();
}
if(q.top()>x)
{
sum-=q.top()-x;
q.pop();
q.push(x);
}
}
}
return 0;
}
P2949 [USACO09OPEN] Work Scheduling G
同样的套路。首先得给这些任务按照时间顺序排序,然后开始贪心,依旧是能选的就选,不能选的就从优先队列里找到最差的看下有没有比当前的差,如果有就放入。
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5+5;
struct node
{
int d;
int p;
}a[N];
int cmp(node x,node y)
{
return x.d<y.d;
}
signed main()
{
priority_queue<int,vector<int>,greater<int>>q;
int num = 0,sum = 0;
int n;
scanf("%lld",&n);
for(int i = 1;i<=n;i++)
{
scanf("%lld %lld",&a[i].d,&a[i].p);
}
sort(a+1,a+n+1,cmp);
for(int i = 1;i<=n;i++)
{
if(num<a[i].d)
{
num++;
q.push(a[i].p);
sum+=a[i].p;
}
else if(q.top()<a[i].p)
{
sum+=a[i].p-q.top();
q.pop();
q.push(a[i].p);
}
}
printf("%lld",sum);
return 0;
}
CF865D Buy Low Sell High
由于只有一个股票,我们不确定一个股票如果买了要到什么时候买最好,我们可以贪心地选择,如果当前的优先队列里最便宜的股票价钱比当前这个股票价钱便宜,那么就"卖掉"这个股票(这样一定是对的,因为就算到后面再卖了赚的差价更高,我们之前卖出的价钱也会放到优先队列里,我们理论上是卖了那个股票,但是其实不仅能当最终结果,也可以当中继器,所以还可以和后面的股票进行交易,答案就不会出现问题),然后不管怎样,都得往优先队列里面放当前这个股票,因为它还有机会当中继器。
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
signed main()
{
priority_queue<int,vector<int>,greater<int>>q;
int n,sum = 0;
scanf("%lld",&n);
for(int i = 1;i<=n;i++)
{
int x;
scanf("%lld",&x);
if(q.size()&&q.top()<x)
{
sum+=x-q.top();
q.pop();
q.push(x);
}
q.push(x);
}
printf("%lld",sum);
return 0;
}
后面还会更新更多例题,敬请期待!!
五、什么是局部调整法
局部调整法就是在做选择性贪心时挑两个相邻的位置进行数学中的不等式分析,从而得出排序法则,同时,它还有一个重要的用处------判断选择性题目是否可以贪心。
六、局部调整法普遍推导过程
就是先定义两个相邻数 \(x,y\),然后假设其它位置的数固定,而 \(x,y\) 可以互换,那么我们分别写出 \(1,2,\dots,x,y,\dots,n-1,n\) 的贡献和 \(1,2,\dots,y,x,\dots,n-1,n\) 的贡献,然后假设 \(1,2,\dots,x,y,\dots,n-1,n\) 的贡献比 \(1,2,\dots,y,x,\dots,n-1,n\) 的贡献更优,然后这就是排序法则,当然,有时候这个排序法则比较麻烦,你可以对它进行化简。那局部调整法如何用来快速判断选择性题目是否可以贪心呢?很简单,只需要判断我们化简后的不等式是否满足偏序关系,偏序关系就是你用小的连大的不会成环,当然偏序关系还有纯数学的定义。
七、如何快速判断一个不太好判断的不等式是否满足偏序关系
- 手动造数据,然后根据这个不等式写排序方式,如果能排出来就满足偏序关系,拍不出来就说明不满足偏序关系(注意,此方法较唐)。
- 直接用偏序关系的三个性质验证,如果都满足就没啥问题了,如果不满足就有问题(注意:此方法也比较唐)。
两种方法虽然都不一定保证一定正确,但是合在一起正确率肯定高于百分之九十九,而且这也是算法竞赛中很好用的方法,当然,你应用时完全不需要合在一起,随便用一种方法就行了。
七、局部调整法例题讲解
P1012 [NOIP 1998 提高组] 拼数
首先先把两者的贡献写下来,组成不等式(这里 \(A\) 表示 \(x\) 以前的数拼在一起的结果,\(B\) 表示 \(x,y\) 后面的数拼在一起的结果):
\[A \times 10^{|B|+|a_x|+|a_y|}+a_x \times 10^{|B|+|a_y|}+a_y \times 10^{|B|}+B>A \times 10^{|B|+|a_x|+|a_y|}+a_y \times 10^{|B|+|a_x|}+a_x \times 10^{|B|}+B \]
\[a_x \times 10^{|B|+|a_y|}+a_y \times 10^{|B|}>a_y \times 10^{|B|+|a_x|}+a_x \times 10^{|B|} \]
\[a_x \times 10^{|a_y|}+a_y>a_y \times 10^{|a_x|}+a_x \]
然后令 \(|DFG|\) 表示 \(D\) 和 \(F\) 和 \(G\) 拼起来的结果,那么:
\[|a_xa_y|>|a_ya_x| \]
然后这个个排序规则排序就好了。
至于 \(a_x \times 10^{|a_y|}+a_y>a_y \times 10^{|a_x|}+a_x\) 为啥是偏序是因为:
\[a_x \times 10^{|a_y|}+a_y>a_y \times 10^{|a_x|}+a_x \]
\[a_x \times 10^{|a_y|}-a_x>a_y \times 10^{|a_x|}-a_y \]
\[a_x \times (10^{|a_y|}-1)>a_y \times (10^{|a_x|}-1) \]
\[\frac{a_x}{10^{|a_x|}-1}>\frac{a_y}{10^{|a_y|}-1} \]
发现一边全是 \(x\),另一边全是 \(y\),所以一定是偏序。
代码:
cpp
#include <bits/stdc++.h>
using namespace std;
string a[25];
string ans;
bool cmp(string a,string b)
{
string c = a+b,d = b+a;
return c>d;
}
int main()
{
int n;
cin >> n;
for(int i = 1;i<=n;i++)
{
cin >> a[i];
}
sort(a+1,a+n+1,cmp);
for(int i = 1;i<=n;i++)
{
ans+=a[i];
}
cout << ans;
return 0;
}
P5963 [BalticOI ?] Card 卡牌游戏【来源请求】
首先把两者的贡献写下来,组成不等式(假设有两组卡片 \((x_i,y_i),(x_j,y_j)\)):
\[-x_i+y_j<-x_j+y_i \]
\[y_j-x_i<y_i-x_j \]
\[y_j+x_j<y_i+x_i \]
这不直接就搞定了吗,而且还是一边全是 \(j\),一边全是 \(i\) 的情况,所以一定是偏序。
注意:十年 OI 一场空,不开 long long 见祖宗。
代码:
cpp
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 6e5+5;
struct node
{
int x;
int y;
int sum;
}a[N];
int cmp(node x,node y)
{
return x.sum<y.sum;
}
signed main()
{
int n;
scanf("%lld",&n);
for(int i = 1;i<=n;i++)
{
scanf("%lld %lld",&a[i].x,&a[i].y);
a[i].sum = a[i].x+a[i].y;
}
sort(a+1,a+n+1,cmp);
int ans = 0;
for(int i = 1;i<=n/2;i++)
{
ans+=min(a[i].x,a[i].y);
}
for(int i = n/2+1;i<=n;i++)
{
ans-=max(a[i].x,a[i].y);
}
printf("%lld",ans);
return 0;
}