算法学习笔记:贪心
贪心
前言
贪心:即在程序的每一步骤中都使用当前的最优策略, 最终得到整个问题的最优答案。感觉思路非常简单,但笔者通过做题经验发现这个简单思路可以考到的题难度还是比较深的,这里主要总结一些常见 \(trick\) 以及一些比较牛逼的灵感题。
邻项交换法
在某种特定问题中比较巧妙求贪心策略的方法。这种问题一般是一个序列按某种顺序操作得到某种最优解,邻项交换法就是通过判断相邻元素的决策,推广到整个序列。
这种方法实际应用比较简单,看一道例题就可以了:
有 \(n\) 只怪兽, 初始你有 \(k\) 点血量, 打第 \(𝑖\) 个怪兽至少需要 \(𝑎_𝑖\) 的血量, 打完第 \(𝑖\) 个怪兽之后会掉 \(𝑏_𝑖\) 的血量,你可以按照任何顺序依次打所有怪兽, 问能否打完所有怪兽,并给出一种方案。
\(sol\) :首先感性理解 \(a_i\) 大的应该尽量先打, \(b_i\) 小的应该尽量先打,所以按 \(b_i-a_i\) 从小到大排序极有可能就是最优的。这就是笔者之前做这种题的思路,但是这样显然是不严谨的,所以用邻项交换法。假设现在有两个怪分别为 \(i\) 和 \(j\) 。这里我们钦定血量大于\(\max(a_i,a_j)\) 如果先打 \(i\) 的话我们至少需要 \(b_i+a_j\) 的血量,如果先打 \(j\) 的话需要 \(b_j+a_i\) 的血量,所有先打 \(i\) 更优当且仅当\(b_i+a_j<b_j+a_i\) 移一下项就是 \(b_i-a_i<b_j-a_j\) 所以按 \(b_i-a_i\) 从小到大排序攻击就好了就是最优的。
可撤销贪心
笔者第一次学这个东西的时候感觉特别牛逼,贪心处理了还能反悔。之后做了点题就感觉比较套路了。看几道比较典的和比较复杂的题。
题面:
给定一个长为 \(n\) 的序列,从中选不超过 \(m\) 个连续段使得和最大。
\(sol\) :
首先比较显然的是,连续的正数或连续的负数一起选一定比只选一部分更优。所以可以把这样的连续段看为一个数。处理完之后我们贪心地把所有正数段全选,但是这样有可能会超过 \(m\) 段,怎么办呢。我们可以反悔,把可以反悔的操作的权值加入优先队列中,没听懂没关系后面会解释。考虑我们可以怎么反悔使得段数小于等于 \(m\), 第一种方式是在已选的段中放弃一个值为 \(v_i\) 的段,可以将段数减少一,贡献为 \(-v_i\) 。操作完这个段会和前后两个不选的段连成一个新段,优先队列中加入一个元素,表示重新选择这个新段的操作。第二种方式是选择一个未选择的值为 \(v_i\) 段,也可以将段数减一,贡献为 \(v_i\) ,操作完这个段会和前后两个已选的段连成一个新段,优先队列中加入一个元素,表示放弃这个新段的操作。这样操作直到段数小于等于 \(m\) 就好了,同时用链表维护每个段前后的相邻段,实际代码稍微有一点细节但码量不大。
code
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll N=4e5+5,inf=1e18;
ll n,m,k[N],a[N]={-1},cnt,pre[N],to[N],p,res,vis[N],f[N];
priority_queue<pair<ll,int> > q;
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
scanf("%lld",a+i);
if(a[i]==0){i--;n--;continue;}
if(a[i]/abs(a[i])==a[i-1]/abs(a[i-1]))
k[cnt]+=a[i];
else k[++cnt]=a[i];
}
if(k[cnt]<0) k[cnt--]=0;
for(int i=1;i<=cnt;i++)
{
pre[i]=i-1,to[i]=i+1;
q.push(make_pair(-abs(k[i]),i));
if(k[i]>0) p++,res+=k[i];
}
to[cnt+1]=cnt+1;
k[0]=k[cnt+1]=inf;
while(p>m)
{
while(vis[q.top().second]==1) q.pop();
int w=q.top().first,j=q.top().second;
q.pop();
if(k[j]<0&&(pre[j]==0||to[j]==cnt+1)) continue;
p--;
res+=w;
k[j]=k[pre[j]]+k[j]+k[to[j]];
vis[pre[j]]=vis[to[j]]=1;
q.push(make_pair(-abs(k[j]),j));
pre[j]=pre[pre[j]];
to[j]=to[to[j]];
if(pre[j]) to[pre[j]]=j;
if(to[j]!=cnt+1)pre[to[j]]=j;
}
cout<<res;
return 0;
}
题面:
一家馅饼店买馅饼。规则是每全价购买一个馅饼,都可以免费得到一个价格严格更低的馅饼。求出为 \(n\) 个馅饼支付的最小花费。 \(n \leq 5\times 10^5\),\(1\leq a_i \leq 10^9\)。这个题比较牛逼讲详细一点, 其实是因为写了题解可以直接复制.
\(sol\):
考虑可撤销贪心+小根堆维护。首先价格相同的馅饼可以放到一起考虑,从大到小排序后考虑每种不同价格的馅饼。则第 \(i\) 种最多白嫖的个数为 \(p=\min(c_i,num-2*sum)\) ,其中 \(c_i\) 为馅饼个数,\(num\) 为已经考虑的更贵的馅饼的总数,\(sum\) 为前面决定白嫖的馅饼数量。这样不一定最优,但不慌,我们还可以反悔。我们把要白嫖的馅饼的贡献(省的钱)放入一个小根堆里。现在对于第 \(i\) 种我们要处理剩下 \(\min(c_i,num)-p\) 的白嫖机会。
每次我们取出堆顶元素 \(k\)
-
如果 \(k\ge val_i\) 说明仍然白嫖 \(k\) 更优。但是如果这时第 \(i\) 种馅饼还剩至少 \(2\) 个,并且\(2\times val_i-k>=0\) 说明我们还可以选择不买 \(k\) 这个馅饼,而用他白嫖一个当前馅饼,而原来用来白嫖 \(k\) 的馅饼也能提供一次白嫖的机会,而要撤销白嫖 \(k\) 的操作可以向堆中加入一个权值为 \(2val_i-k\) 的元素。
-
如果 \(k<val_i\) ,则白嫖当前馅饼更优,如果如果这时第 \(i\) 种馅饼还剩至少 \(2\) 个,则我们还可以再白嫖一个,这里需要注意的是堆中 \(k\) 表示的贡献不一定是原本价格,所以虽然 \(k<val_i\) 但因为我们是从大到小处理,所以 \(k\) 所对应的馅饼价格还是一定比 \(val_i\) 贵,也就是说它依然可以提供一次白嫖当前馅饼的机会。
一个细节,每次新加入的贡献不能直接加入堆中,原理很简单,处理当前馅饼时,堆中只能有之前馅饼的贡献,所以需要先放入一个栈中转存一下,处理结束了再加入堆。最后堆里元素的和就是所有白嫖的馅饼。
code
代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=500005;
char buf[1<<21],*p1,*p2;
inline char gc(){return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;}
inline ll rd()
{
char c;ll f=1;
while((c=gc())<'0'||c>'9')if(c=='-')f=-1;
ll x=c^48;
while('0'<=(c=gc())&&c<='9')x=(x<<1)+(x<<3)+(c^48);
return x*f;
}
ll n,ans,a[N];
ll val[N],c[N],cnt;
ll st[N],top;
priority_queue<ll,vector<ll>,greater<ll> >q;
int cmp(ll a,ll b){return a>b;}
int main()
{
n=rd();
for(int i=1;i<=n;i++)
ans+=(a[i]=rd());
sort(a+1,a+n+1,cmp);a[0]=1e18;
for(int i=1;i<=n;i++)
{
if(a[i]!=a[i-1]) val[++cnt]=a[i];
c[cnt]++;
}
ll p,tot,num=0;
for(int i=1;i<=cnt;i++)
{
p=min(num-2*(ll)q.size(),c[i]);
tot=min(c[i],num)-p;
for(int j=1;j<=p;j++)
st[++top]=val[i];
for(int j=1;j<=tot;j+=2)
{
int k=q.top();
q.pop();
if(k<val[i])
{
st[++top]=val[i];
if(j<tot) st[++top]=val[i];
}
else
{
st[++top]=k;
if(j<tot&&2*val[i]-k>=0)
st[++top]=2*val[i]-k;
}
}
while(top) q.push(st[top--]);
num+=c[i];
}
while(!q.empty())
ans-=q.top(),q.pop();
cout<<ans;
return 0;
}
其他
还有一些感觉还不错得到杂题,但感觉每道题做法没什么共同点,就浅谈两句或者直接挂我的题解链接了。
1.题面:
你有一个长为 n 的序列,每个位置是 * 或者 +,* 表示让变量 \(\times 2\),+ 表示让变量 \(+1\)。现在你要选出它的一个子序列,使得一个初始为 0 的变量在对子序列中的字符依次执行对应操作后对 \(2^k\) 取模所得结果尽可能大。求出最大可能的结果。
\(sol\):
分开考虑每个加号的贡献再找贪心策略。具体:Manci的序列
2.题面:
给定一个长为 \(n\) 的数列 \(a\) , 初始全部涂成白色. 每次操作, 你可以选择一个长为 \(k\) 的连续段全部涂成黑色或白色. 你可以做任意多次操作, 要求最大化最后涂成黑色的格子中的整数之和. 求这个和。
\(sol\) :
显然有一段长为 \(k\) 的一定是同色的,其他可以任意染色,所以直接贪心做就完了。
3.题面:
在一个数轴上有 \(n\) 个点,从里面选出 \(2k\) 个点组成 \(k\) 组,每组点之间的距离和最小。
\(sol\) :
把相邻两点的距离看成一个数,原题变成 \(n-1\) 个数,相邻两数不能都选,选 \(k\) 个使得和最大就成了可撤销贪心的板子。

浙公网安备 33010602011771号