【笔记】贪心
贪心是一种算法策略,每次行动时只考虑某种指标下的眼前最优,不考虑后果。正确的贪心最终能得到全局最优。
贪心可以是从静态的集合中按优劣依次选取(离线处理,如排序),也可以是从边处理边更新的集合中每次选取最优(在线处理,如优先队列)。
很多常用的概念 / 算法都有贪心的影子。比如字典序的定义就完美契合贪心,Dijkstra、Kruskal 等算法也是以贪心为核心思想。
解贪心题时,最重要的是观察到贪心和证明正确性,思维难度较大。当不确定一道题是否是贪心时,可以尝试找几个贪心策略,然后自己造数据、对拍;或是暴力推式子,然后用反证法或归纳法尝试证明。
贪心也可以后悔。一种特殊的贪心是反悔贪心,即在贪心选择后,通过一定量的反悔找到最优,用于解决一些条件特殊的问题。
基础贪心
此部分的贪心较易观察且通常只需感性理解即可说明正确性,难点主要在于理解题意。
区间问题
区间类问题是贪心的经典问题,一个区间仅由左右端点即可唯一确定,易于按照一定规则贪心。
Luogu P1803 凌乱的yyy / 线段覆盖
此题要求最多选出多少互不重叠的线段。首先我们确定一个选择线段的顺序,比如从左往右,目前最右侧线段的右端点是 \(res\),想想选哪条线段最优。
显然在不重叠的前提下选右端点最靠左的最优,因为这样会留下更多空间,更有可能选更多的线段。
这样我们在选到任何一个线段时都这样找。如何用代码实现?发现按右端点从小到大排序正好可以连着选最优。最后考虑边界,右端点最靠左的线段只需任选一个。
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e6+10;
struct Node{
int a,b;
}t[maxn];
int n,s,ans;
bool cmp(Node x,Node y){
return x.b<y.b;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>t[i].a>>t[i].b;
sort(t+1,t+1+n,cmp);
for(int i=1;i<=n;i++){
if(t[i].a>=s){
ans++;
s=t[i].b;
}
}
cout<<ans;
return 0;
}
Luogu P2887 [USACO07NOV] Sunscreen G
把奶牛需要的阳光范围视作区间,防晒霜视作数轴上的点,题目便转化为:用这若干个点最多可以覆盖多少个区间(每个点只能覆盖一个)。
首先依然是找一个顺序考虑,我们仍然尝试从左到右考虑。
对于当前区间,我们更倾向于选最靠左的合法点,这样能留出更多点供右侧区间使用。
同理,对于当前点,我们更倾向于选右端点最靠左的合法区间,因为右端点靠右的区间更可能被更靠右的点覆盖。
怎么用一个贪心满足这两个法则?我们可以按区间右端点从小到大排序,然后对于每个区间,去选最靠左的点。
#include<bits/stdc++.h>
using namespace std;
const int N=2510;
int c,l,ans;
struct Cow{
int mins,maxs;
}cows[N];
struct Ss{
int num,spf;
}suns[N];
int mins[N],maxs[N];
bool cmpc(Cow a,Cow b){
return a.maxs<b.maxs;
}
bool cmps(Ss a,Ss b){
return a.spf<b.spf;
}
int main(){
cin>>c>>l;
for(int i=1;i<=c;i++){
cin>>cows[i].mins>>cows[i].maxs;
}
for(int i=1;i<=l;i++){
cin>>suns[i].spf>>suns[i].num;
}
sort(cows+1,cows+1+c,cmpc);
sort(suns+1,suns+1+l,cmps);
for(int i=1;i<=c;i++){
for(int j=1;j<=l;j++){
if(suns[j].num&&suns[j].spf<=cows[i].maxs&&suns[j].spf>=cows[i].mins){
suns[j].num--;
ans++;
break;
}
}
}
cout<<ans;
return 0;
}
Luogu P1250 种树 / Luogu P1325 [ICPC 2002 Beijing R] 雷达安装
第一题,给定若干个区间,要求第 \(i\) 个区间包含 \(t_i\) 个点,问最少点数量。
类似的,从左往右考虑区间,在最靠近右端点处的若干个空位种树使其更有可能覆盖到后面的区间。按照右端点排序后,对于每个区间进行考虑,暴力统计区间内已有树数量,再暴力种树。
对于更大的数据范围,可以用线段树二分优化,快速找到满足空位数量的区间。
第二题与第一题本质相同,最关键的是第一步转化,看起来好像是雷达表示区间范围,小岛表示点。但是小岛是二维的,并不能直接映射到数轴上的一点。所以我们把雷达在海岸上能扫到小岛的位置集合视作区间,雷达视作点,就转化为了区间种至少一棵树。
#include<bits/stdc++.h>
using namespace std;
const int N=3e4+10;
struct Node{
int b,e,t;
}sug[N];
int n,h,ans;
bool tr[N];
bool cmp(Node x,Node y){
return x.e<y.e;
}
int main(){
cin>>n>>h;
for(int i=1;i<=h;i++) cin>>sug[i].b>>sug[i].e>>sug[i].t;
sort(sug+1,sug+1+h,cmp);
for(int i=1;i<=h;i++){
int cnt=0;
for(int j=sug[i].b;j<=sug[i].e;j++) cnt+=int(tr[j]);
if(cnt>=sug[i].t) continue;
else{
cnt=sug[i].t-cnt;
ans+=cnt;
for(int j=sug[i].e;j>=sug[i].b&&cnt;j--){
if(!tr[j]) tr[j]=1;cnt--;
}
}
}
cout<<ans;
return 0;
}
Luogu U248682 区间覆盖
用尽可能少的区间覆盖一个给定范围 \([L,R]\),允许区间重叠。
假设当前的覆盖右端点为 \(r\),我们要在左端点 \(\le r\) 的区间里选择。为了让区间数尽可能小,我们选择能覆盖最多的,即右端点最靠右的区间。
实现方法即按左端点排序,双指针线性枚举即可。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
const int INF=2e9;
int n,L,R,cnt;
struct Node{
int l,r;
}a[N];
bool cmp(Node x,Node y){
return x.l<y.l;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>L>>R>>n;
for(int i=1;i<=n;i++) cin>>a[i].l>>a[i].r;
sort(a+1,a+1+n,cmp);
int res=L,i=1;
while(res<R){
int maxr=-INF;
bool flag=0;
while(i<=n&&a[i].l<=res){
flag=1;
maxr=max(maxr,a[i].r);
i++;
}
if(!flag){
cout<<-1;
return 0;
}
else cnt++,res=maxr;
}
cout<<cnt;
return 0;
}
字典序贪心
Luogu P1694 [USACO19FEB] The Great Revegetation B
注意输出格式:输出所有解中最小的 \(N\) 位数,即字典序最小。提醒我们使用贪心,从前往后每次尽量种编号小的草。
从前往后暴力枚举,对于当前草坪,找此前被同一奶牛喜欢的其他草坪,标记禁止选择。然后选择未标记的最小编号草即可。
#include<bits/stdc++.h>
using namespace std;
int n,m;
int g[110][110],k[110];
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
g[u][v]=1;g[v][u]=1;
}
for(int i=1;i<=n;i++){
bool f[5]={0};
for(int j=1;j<=i-1;j++){
if(g[i][j]){
f[k[j]]=1;
}
}
for(int j=1;j<=4;j++){
if(!f[j]){
k[i]=j;break;
}
}
cout<<k[i];
}
return 0;
}
Luogu P13270【模板】最小表示法
首先一定是断环为链,然后考虑找到两倍串中字典序最小的长度为 \(n\) 串。
暴力枚举循环同构串起点,与当前最优解作比较。暴力比较做到 \(O(n^2)\),用哈希 + 二分 / 倍增快速找到两串第一个不相同位置然后比较可以优化到 \(O(n\log n)\)。
但是对于本题还不够。
假设上一个最优解起点是 \(i\),当前枚举的起点是 \(j\)。如果这两个串在第 \(k\) 位出现不同,如果当前枚举的这个位置字典序更小,则经过这个位置的串,即以 \([i,i+k]\) 开头的串全都没有 \([j,j+k]\) 对应位置优。
于是直接更新最优解为 \(j\),跳到 \(i+k+1\) 处继续枚举。
如果 \(i+k+1\) 还没有 \(j\) 大,则 \([i+k+1,j-1]\) 都已枚举过,还没有 \(i\) 优,直接跳到 \(j+1\)。
这是一个选取局部最优 + 更新的过程。也算作是贪心。
#include<bits/stdc++.h>
using namespace std;
int n;
string s;
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>s;
s=" "+s+s;
int i=1,j=2,k=0;
while(i<=n&&j<=n&&k<n){
if(s[i+k]==s[j+k]) k++;
else{
if(s[i+k]>s[j+k]){
int l=i;
i=j,j=max(j+1,l+k+1);
}else j=j+k+1;
k=0;
}
}
for(int d=i;d<=i+n-1;d++) cout<<s[d];
return 0;
}
补充:删字符问题
没有找到例题。
从一个字符串中删除 \(k\) 个字符,使剩余字符字典序最小。
字典序是贪心的,前面小就是一定小,不会受后面影响。
因此我们遇到后面比前面字符小的,如果还能删就把前面删掉。可以用一个单调栈维护。
最小代价问题
Luogu P1090 [NOIP 2004 提高组] 合并果子
想一下此次选哪两堆合并最优。容易发现的是,越早加入合并的堆,在此后的合并中对代价的贡献越大。所以可以猜测,每次选两堆最小的合并最优。
更严格的证明可以将果子数目视作一个二叉树,其中叶子节点存放 \(a_i\),非叶子存放合并后的堆。发现总代价为 \(\sum_{i=1}^{n}a_i\times dep_i\),那么对于深度最大的两个点,只有在他们重量最小时最优。然后我们考虑删除这两个点,只保留合并后的那一堆,让其变成叶子,此时问题不变,最深的依然要是重量最小的两个点。
事实上,这种带权路径最优的树就是哈夫曼树,本题也可视作构造二叉哈夫曼树。
算法实现要我们动态的去维护最小的两堆,暴力做法 \(O(n^2)\),优先队列 \(O(n\log n)\),桶排 + 队列可以做到 \(O(n)\)(值域较小的情况)。
#include<iostream>
#include<queue>
using namespace std;
priority_queue<int,vector<int>,greater<int> > q;
int main(){
int n,sum=0;
cin>>n;
for(int i=1;i<=n;i++){
int temp;
cin>>temp;
q.push(temp);
}
for(int i=1;i<n;i++){
int temp;
temp=q.top();
q.pop();
temp+=q.top();
q.pop();
sum+=temp;
q.push(temp);
}
cout<<sum;
return 0;
}
Luogu P2168 [NOI2015] 荷马史诗
本题即是构造 \(k\) 进制哈夫曼编码。哈夫曼编码是在已知词库的条件下,将原电文每一个词用新编码代替,使得翻译好的电文长度最短。问题用哈夫曼树解决,把每个词的出现次数视为点权,编码的每一个字符视为边,每个词的编码长度即为深度,题目也就规约为最小带权路径。
根据上个题的证明,我们类比过来可以知道,每次合并最小的 \(k\) 堆最优。但有一个问题:如果在最后一次合并时不足 \(k\) 堆,那么答案一定不优。因为树的每一层最多可以放 \(k\) 个,最后一次合并不足 \(k\) 堆会导致树根儿子那一层不足 \(k\) 个,我们随便找一个深度更大的点放在第二层就会让答案变小。因此先用权值为 \(0\) 的点补足。要补多少个?每次合并都会使节点减少 \(k-1\) 个,最后还剩一个,也就是总共 \(n-1\) 个节点,只要满足 \(n-1\equiv0\pmod {k-1}\) 即可。
最后输出总长度以及树的深度。
#include<bits/stdc++.h>
#define int long long
using namespace std;
struct Node{
int x;
int d;
bool operator <(const Node &a)const {
if(x!=a.x) return x>a.x;
else return d>a.d;
}
};
int n,k,s,ans;
priority_queue<Node> q;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>k;
s=(n-1)/(k-1);
if((n-1)%(k-1)!=0){
s++;
int t=k-1-((n-1)%(k-1));
for(int i=1;i<=t;i++){
q.push({0,0});
}
}
for(int i=1;i<=n;i++){
int w;
cin>>w;
q.push({w,0});
}
for(int i=1;i<=s;i++){
int sum=0,maxd=-1;
for(int j=1;j<=k;j++){
Node v=q.top();
sum+=v.x;
maxd=max(v.d,maxd);
q.pop();
}
q.push({sum,maxd+1});
ans+=sum;
}
Node f=q.top();
cout<<ans<<' '<<f.d;
return 0;
}
临项交换贪心
Luogu P1080 [NOIP 2012 提高组] 国王游戏
看题目比较难以入手,那我们不妨选两个大臣,看看他们怎么排列能让最大值最小。
推式子,设大臣 1 左手为 \(l_1\) 右手为 \(r_1\),大臣 2 左手为 \(l_2\) 右手 \(r_2\),排在两人前面的人左手乘积为 \(x\)。
则 1 排在 2 前面时,最大值为 \(\max(\frac{x}{r_1},\frac{x\times l_1}{r_2})\);2 排在 1 前面时,最大值为 \(\max(\frac{x}{r_2},\frac{x\times l_2}{r_1})\)。
怎么比较这两个东西?首先考虑约分,同乘一个 \(\frac{r_1\times r_2}{x}\) 约掉所有数的分母。
现在两式为 \(\max(r_2,l_1\times r_1),\max(r_1,l_2\times r_2)\)。
因为每一项都是正整数所以显然 \(r_1\le l_1\times r_1,r_2\le l_2\times r_2\)。所以如果 1 排在 2 前面更优,要么是 \(\max(r_2,r_1)\le l_1\times r_1\le l_2\times r_2\),要么是 \(r_1\le l_1\times r_1\le r_2 \le l_2\times r_2\),注意到都含有 \(l_1\times r_1\le l_2\times r_2\),所以只需比较这个,\(r_1\) 和 \(r_2\) 无需比较。反过来也是一样。
所以我们知道了每次选 \(l_i\times r_i\) 最小的排在下一个位置最优,于是按照这个进行排序选取即可。
这就是所谓临项交换法,把相邻两种情况推出来,然后比大小,用于证明贪心。
此题还需要高精度。
#include<bits/stdc++.h>
using namespace std;
int n,x,y,sum,ans;
struct Node{
int l,r;
}a[1010];
bool cmp(Node x,Node y){
if(x.l*x.r==y.l*y.r) return x.r<y.r;
else return x.l*x.r<y.l*y.r;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>x>>y;
for(int i=1;i<=n;i++) cin>>a[i].l>>a[i].r;
sort(a+1,a+1+n,cmp);
sum=x;
for(int i=1;i<=n;i++){
if(sum/a[i].r>ans) ans=sum/a[i].r;
sum*=a[i].l;
}
cout<<ans;
return 0;
}
Luogu P1012 [NOIP 1998 提高组] 拼数
先考虑两个数 \(s_i,s_j\),因为此题是拼接所以我们用字符串存储。
然后定义 \(+\) 为拼接,则 \(s_i+s_j\) 表示 \(s_i\) 拼在 \(s_j\) 前面,字典序大小关系即是数的相对大小,字符串的比较运算符比较字典序。
于是 \(s_i\) 拼在 \(s_j\) 前更优当且仅当 \(s_i+s_j>s_j+s_i\)。
接下来考虑三个及以上的情况。如果 \(s_i\) 拼在 \(s_j\) 前面更优,\(s_j\) 拼在 \(s_k\) 前面更优,是否意味着 \(s_i\) 拼在 \(s_k\) 前更优?是否意味着 \(s_i+s_j+s_k\) 是最优情况?
从数学角度思考为什么 \(s_i+s_j>s_j+s_i\)。设 \(\operatorname{len}(s_i)=n,\operatorname{len}(s_j)=m\),则有:
移项得:
引入 \(a_k\),设 \(\operatorname{len}(s_k)=r\),则有:
由不等式传递性可得:
因此 \(s_i\) 拼在 \(s_k\) 前更优。
最后要证明的是,如果 \(s_i\) 拼在 \(s_j\) 前更优,那么无论两数中间加了什么,\(s_i\) 在前总是更优。
假设中间加了数 \(s_k\),且满足 \(s_i\) 优于 \(s_k\) 优于 \(s_j\)。
我们已经知道 \(s_k+s_j>s_j+s_k\),所以 \(s_i+s_k+s_j>s_i+s_j+s_k\)。
又因为 \(s_i+s_j>s_j+s_i\) 所以 \(s_i+s_j+s_k>s_j+s_i+s_k\)。
且 \(s_i+s_k>s_k+s_i\),所以 \(s_j+s_i+s_k>s_j+s_k+s_i\)。
所以 \(s_i+s_k+s_j>s_j+s_k+s_i\)。
推广到任意多个数,都可以证明,按照 \(s_i+s_j>s_j+s_i\) 规则排序后,顺次首尾相接都是最优。
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
string s[25];
cin>>n;
for(int i=1;i<=n;i++) cin>>s[i];
for(int i=1;i<=n;i++){
for(int j=i;j<=n;j++){
if(s[i]+s[j]<s[j]+s[i]) swap(s[i],s[j]);
}
}
for(int i=1;i<=n;i++) cout<<s[i];
return 0;
}
反悔贪心
Luogu P14361 [CSP-S 2025] 社团招新
先考虑没有人数限制怎么做:贪心地将每个人分配到最满意的部门即可。
然后不难注意到只可能会有一个部门超过 \(\frac{n}{2}\) 的成员限制。考虑怎么使其合法。
想到将他们分配到第二满意的部门。然后我们需要使调整的损失最小,那么就需要最小化最大满意度 \(-\) 第二满意度的差。
因此我们先为每人分配到最大满意度的部门,找到超员的部门,贪心地将当前最大满意度 \(-\) 第二满意度最小的人分配到第二满意部门,直到人数合法。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int T,n,ans;
int cnt[4],clb[N];
struct Node{
int a,p;
};
struct Stu{
Node d[4];
int pos;
}s[N];
bool cmp1(Node x,Node y){
return x.a>y.a;
}
bool cmp2(Stu x,Stu y){
return x.d[1].a-x.d[2].a<y.d[1].a-y.d[2].a;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>T;
while(T--){
cin>>n;
ans=0;
memset(cnt,0,sizeof(cnt));
for(int i=1;i<=n;i++){
int a,b,c;
cin>>a>>b>>c;
s[i].d[1]=(Node){a,1};
s[i].d[2]=(Node){b,2};
s[i].d[3]=(Node){c,3};
s[i].pos=i;
sort(s[i].d+1,s[i].d+4,cmp1);
cnt[s[i].d[1].p]++;
clb[i]=1;
}
int flag=0;
for(int i=1;i<=3;i++){
if(cnt[i]>n/2) flag=i;
}
if(flag){
sort(s+1,s+1+n,cmp2);
int i=1,j=1;
while(i<=n&&j<=cnt[flag]-n/2){
if(s[i].d[1].p==flag){
clb[s[i].pos]=2;
j++;
}
i++;
}
}
for(int i=1;i<=n;i++) ans+=s[i].d[clb[s[i].pos]].a;
cout<<ans<<'\n';
}
return 0;
}
Luogu P14635 [NOIP2025] 糖果店
首先不难想到,如果要取一个糖果两次,为了使花费最小,我们一定要取 \(x_i+y_i\) 最小的糖果。
然后是取一次的糖果,显然要取 \(x_i\) 最小的前若干个,所以先对 \(x_i\) 排序。
接着就有两种处理思路:
第一种,可以观察到花费随糖果数量增加而增加,因此我们可以二分糖果数量 \(k\),然后枚举 \(k\) 个糖果中,有多少是取两次得到的,有多少是取一次得到的。根据上面的规则,我们很容易计算出总花费。时间复杂度 \(O(n\log m)\)。
第二种,注意到取一次的糖果数量只能是 \(1\sim n\),所以我们直接枚举取多少个一次。一种实现是先贪心,尽可能多地取两次,然后再从小到大去取一次的糖果,钱不够就反悔删掉一组两次(比一组还大的一次一定不优,不用管),最终答案是其中产生的最大值。也可以直接枚举取多少一次,计算出对应的取两次数目。处理时间复杂度 \(O(n)\),总 \(O(n\log n)\)。
两种做法都足以通过,但显然第二种更优。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
struct Node{
int x,y;
}a[N],b[N];
int n,pos;
int m,ans,mina=1e18;
bool cmp(Node p,Node q){
return p.x<q.x;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i].x>>a[i].y;
b[i]=a[i];
if(a[i].x+a[i].y<mina){
mina=a[i].x+a[i].y;
pos=i;
}
}
ans+=(m/mina)*2;
m%=mina;
int t=ans;
sort(b+1,b+1+n,cmp);
for(int i=1;i<=n;i++){
if(m<b[i].x){
if(b[i].x>mina) break;
t-=2;
m+=mina;
};
t++;
m-=b[i].x;
ans=max(ans,t);
}
cout<<ans;
return 0;
}
Luogu P2107 小 Z 的 AK 计划
走到一个机房,最优肯定是进入并 AK。
但是时间不够了我们就必须反悔,反悔此前走过的任何机房都是可以的,所以我们反悔若干个 AK 时间最长的机房,直到不超时。
这样就相当于枚举了所有走过前 \(i\) 个机房时间恰好到达上限且机房最多的情况,取所有答案的最大值即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
int n,m,ans;
struct Node{
int x,t;
}a[N];
bool cmp(Node a,Node b){
return a.x<b.x;
}
priority_queue<int> q;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i].x>>a[i].t;
sort(a+1,a+1+n,cmp);
int res=0,cnt=0;
for(int i=1;i<=n;i++){
res+=a[i].x-a[i-1].x+a[i].t;
cnt++;
q.push(a[i].t);
while(!q.empty()&&res>m){
res-=q.top();
q.pop();
cnt--;
}
if(res>m) break;
ans=max(ans,cnt);
}
cout<<ans;
return 0;
}
Luogu P2949 [USACO09OPEN] Work Scheduling G
先贪心按截止时间顺序完成工作。
如果此前完成的工作数量与当前工作截止时间相等,那么此工作不能再被完成。这时候就尝试反悔,看看此前有没有收益更低的工作,有就退掉换此工作。
用优先队列维护,最后留在队列里的就是要完成的工作,求和即可。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+10;
const int INF=1e18;
int n,ans;
struct Node{
int d,p;
}a[N];
struct Heapnode{
int w;
bool operator <(const Heapnode &x) const{
return w>x.w;
}
};
bool cmp(Node x,Node y){
return x.d<y.d;
}
priority_queue<Heapnode> q;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i].d>>a[i].p;
sort(a+1,a+1+n,cmp);
int cur=0;
for(int i=1;i<=n;i++){
if(cur==a[i].d){
Heapnode f=q.top();
if(f.w<a[i].p){
q.pop();
q.push((Heapnode){a[i].p});
}
}else q.push((Heapnode){a[i].p}),cur++;
}
while(!q.empty()) ans+=q.top().w,q.pop();
cout<<ans;
return 0;
}
Luogu P1792 [国家集训队] 种树
一个贪心的想法是,每次选取还能种植的美观度最大的位置,然后将其相邻位置标记为不可种植。
这样是不对的,因为两侧位置的美观度之和可能大于中间,种中间不如种两边优。
因此我们需要反悔。我们把两侧节点美观度之和 \(-\) 中间节点美观度作为一个新节点,加入到贪心可选的范围中。这样,如果在事后又选择了这个节点,相当于反悔去选了两边节点,所得结果不变。
因为涉及到节点的删除,需要动态维护当前节点两侧都是谁。如果此次选择了该点,就把两侧节点删掉,把此节点的值改为上述反悔值,此节点的两侧改为原节点两侧的两侧,这样以后再选两侧的两侧时可以正确地删除此节点。用双向链表维护。
涉及到节点的添加删除,动态维护,使用优先队列。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int n,m,ans;
int a[N];
bool vis[N];
struct Node{
int val,prv,nxt;
}lst[N];
struct Heapnode{
int w,p;
bool operator <(const Heapnode &x) const{
return w<x.w;
}
};
priority_queue<Heapnode> q;
int main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
if(n<2*m){
cout<<"Error!";
return 0;
}
for(int i=1;i<=n;i++){
lst[i].val=a[i];
lst[i].prv=i-1,lst[i].nxt=i+1;
q.push((Heapnode){a[i],i});
}
lst[1].prv=n,lst[n].nxt=1;
for(int i=1;i<=m;i++){
while(vis[q.top().p]) q.pop();
Heapnode f=q.top();
q.pop();
ans+=f.w;
vis[lst[f.p].prv]=vis[lst[f.p].nxt]=1;
lst[f.p].val=lst[lst[f.p].prv].val+lst[lst[f.p].nxt].val-lst[f.p].val;
q.push((Heapnode){lst[f.p].val,f.p});
lst[f.p].prv=lst[lst[f.p].prv].prv;
lst[f.p].nxt=lst[lst[f.p].nxt].nxt;
lst[lst[f.p].prv].nxt=f.p;
lst[lst[f.p].nxt].prv=f.p;
}
cout<<ans;
return 0;
}
Codeforces 865D Buy Low Sell High
问题等价于给定一个序列 \(p\),要求构造一个合法括号序列,选左括号收益为 \(-p_i\),右括号为 \(p_i\),问最大收益。
首先遇到后面比前面大的时候就去贪心地构造一对括号,但这样的括号序列不是最优的。
于是考虑反悔,如果说我这个位置去换前面的右括号,收益会不会比新增一对括号更大。这样就能保证局部的括号序列是最优的,而被换掉的右括号位置也是在上一次最优时被换的,没有更优的情况了,只可能在未来放左括号。
可以用两个优先队列处理,一个放括号对,一个放未配对的左括号位,从前往后处理位置,比较一下增添还是替换更优,然后更新括号。
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=3e5+10;
const int INF=1e18;
int n;
int a[N],ans;
struct Heapnode1{
int w1,w2;
bool operator <(const Heapnode1 &x) const{
return w2>x.w2;
}
};
struct Heapnode2{
int w;
bool operator <(const Heapnode2 &x) const{
return w>x.w;
}
};
priority_queue<Heapnode1> q1;
priority_queue<Heapnode2> q2;
signed main(){
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i];
for(int i=1;i<=n;i++){
int res1=-INF,res2=-INF;
if(!q2.empty()) res1=a[i]-q2.top().w;
if(!q1.empty()) res2=a[i]-q1.top().w2;
if(res1>=res2&&res1>0) q1.push((Heapnode1){q2.top().w,a[i]}),q2.pop();
else if(res2>res1&&res2>0){
Heapnode1 f=q1.top();
q1.pop();
q1.push((Heapnode1){f.w1,a[i]});
q2.push((Heapnode2){f.w2});
}else q2.push((Heapnode2){a[i]});
}
while(!q1.empty()) ans+=q1.top().w2-q1.top().w1,q1.pop();
cout<<ans;
return 0;
}
Luogu P2672 [NOIP 2015 普及组] 推销员
考虑没有距离限制,自然是选择疲劳值最大的 \(x\) 户。
但是可能会出现距离换疲劳的情况,于是需要反悔。
反悔要找总疲劳最大的,也就是找除了选的以外疲劳 \(+\) 距离最大的。反悔最多反悔一个,如果还能找到第二个,那么选第二个减少的疲劳要小于距离最大值的增加,还不如只选第二个优。
每次暴力找显然不可行,于是我们对于疲劳值从大到小排序,然后做疲劳值前缀和,以及在此顺序下的前缀最大距离和后缀最大反悔值。统计时取是否反悔两者最大值即可。
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct street{
int a,s;
}ls[N];
int n;
int sum[N];
int ans1[N],ans2[N];
bool cmp(street x,street y){
return x.a>y.a;
}
int main(){
cin>>n;
for(int i=1;i<=n;i++) cin>>ls[i].s;
for(int i=1;i<=n;i++) cin>>ls[i].a;
sort(ls+1,ls+1+n,cmp);
for(int i=1;i<=n;i++) sum[i]=sum[i-1]+ls[i].a;
for(int i=1;i<=n;i++) ans1[i]=max(ans1[i-1],2*ls[i].s);
for(int i=n;i>=1;i--) ans2[i]=max(ans2[i+1],2*ls[i].s+ls[i].a);
for(int i=1;i<=n;i++) cout<<max(sum[i]+ans1[i],sum[i-1]+ans2[i]);
return 0;
}

浙公网安备 33010602011771号