[题解]考前一些贪心技巧题
比如今年 S-T1,去年 NOIp-T1,讲真我挺害怕这种贪心的,所以记录一些偏向思维/技巧的贪心题。
受 Codeforces 的启发,尝试这样一种新的题解风格(或许叫“苏格拉底式”题解?)。
这样做大概是为了让自己搞懂“为什么想到这样转化”,对考场思维起一点微弱的引导作用(?)。
真心希望不要再 T1 做不出来了(哭)
1. AGC048B Bracket Score
Hint 1
转化一下题意,考虑一个只有小括号的串串,你要将其中一部分小括号替换成中括号。替换第 \(i\) 个小括号对答案的贡献是多少?
Hint 2
中括号应该怎么放才能使整个串串合法?Hint 3
左括号还是右括号并不重要。Hint 4
一种替换方案合法的充要条件是什么?Sol
先令答案为 $\sum a_i$。我们发现,一种替换方案合法,当且仅当每对中括号的下标异奇偶。
为此,将奇数和偶数位的 \(b_i-a_i\) 分别存起来,从大到小排序。
每次我们取出两个序列的最大值 \(x,y\),若 \(x+y>0\) 则累入答案,相当于我们将两个小括号换成了一对中括号。
代码实现用的是优先队列。
总时间 \(O(n\log n)\)。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,a[N],b[N],ans;
priority_queue<int> q1,q2;
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;i++) cin>>a[i],ans+=a[i];
for(int i=1;i<=n;i++){
cin>>b[i];
if(i&1) q1.push(b[i]-a[i]);
else q2.push(b[i]-a[i]);
}
while(!q1.empty()&&!q2.empty()){
int x=q1.top(),y=q2.top();
if(x+y<0) break;
q1.pop(),q2.pop();
ans+=x+y;
}
cout<<ans<<"\n";
return 0;
}
2. AGC018C Coins
Hint 1
考虑沿用上题的转化。先令所有人币种一样,再进行调整。Hint 2
考虑一个人的 $2$ 种调整策略对答案的贡献分别是多少?Hint 3
尝试用交换法来搞出一个贪心策略?Sol
先令答案为 $\sum a_i$,即所有人都选金币。然后,我们使用 \(y\) 个银币、\(z\) 个铜币替换掉一部分金币。
改换银币、铜币对答案的贡献是 \((d,e)=(b-a,c-a)\)。
假设“\(i\) 选 \(d\),\(j\) 选 \(e\)”优于“\(i\) 选 \(e\),\(j\) 选 \(d\)”,也即 \(d_i+e_j\ge e_i+d_j\),移项得 \(d_i-e_i\ge d_j-e_j\)。
按 \(d-e\) 从大到小排序后,一定存在一个最优解,满足选 \(d\) 的全在选 \(e\) 的左边。
考虑枚举这个分段点,并预处理出它左边选 \(y\) 个 \(d\) 的最大贡献,以及它右边选 \(z\) 个 \(e\) 的最大贡献。
预处理可以用堆。计算每个前缀选 \(d\) 的最大贡献,我们开一个小根堆,大小超过 \(y\) 后弹出堆顶即可。\(e\) 同理。
总时间 \(O(n\log n)\)。
Code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int n,x,y,z,ts,lmx[N],rmx[N],ans=-1e18;
struct Nd{int d,e;}s[N];
priority_queue<int,vector<int>,greater<int>> q;
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>x>>y>>z,n=x+y+z;
for(int i=1,a,b,c;i<=n;i++){
cin>>a>>b>>c;
ts+=a,s[i]={b-a,c-a};
}
sort(s+1,s+1+n,[](Nd i,Nd j){return i.d-i.e>j.d-j.e;});
for(int i=1;i<=n;i++){
lmx[i]=lmx[i-1]+s[i].d;
q.push(s[i].d);
if(q.size()>y) lmx[i]-=q.top(),q.pop();
}
while(!q.empty()) q.pop();
for(int i=n;i;i--){
rmx[i]=rmx[i+1]+s[i].e;
q.push(s[i].e);
if(q.size()>z) rmx[i]-=q.top(),q.pop();
}
for(int i=y;i<=n-z;i++) ans=max(ans,lmx[i]+rmx[i+1]);
cout<<ts+ans<<"\n";
return 0;
}
3. CF2157C Meximum Array 2
Hint 1
对于每个位置,考虑讨论它的限制条件。Hint 2
如果这一位既有 $\min$ 也有 $\text{MEX}$,此时填什么都不会对两个限制条件有用。Hint 3
如果这一位只有 $\min$,我们不妨全为 $k$。Hint 4
如果这一位只有 $\text{MEX}$,应该怎么填呢(注意题目保证有解)?Sol
我们对于每一位,讨论它被那些限制条件覆盖。如果这一位什么限制都没有,随便填就可以。
如果这一位既有 \(\min\) 也有 \(\text{MEX}\),说明我们填的数必须 \(\ge k\) 且 \(\ne k\)。
我们发现,此时当前填什么都不会对两个限制条件有用,所以在 \([k+1,n]\) 中随便取填即可。
如果这一位只有 \(\min\),我们不妨全填为 \(k\)。
因为只有这些位置可能对 \(\min\) 的限制有用。
如果这一位只有 \(\text{MEX}\),我们可以将这些位置取出来,循环填 \(0,1,2,\dots,k-1\)。
因为只有这些位置可能对 \(\text{MEX}\) 的限制有用。
由于题目保证有解,所以构造一定是正确的。
总时间 \(O(t(n+q))\)。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=105;
int t,n,k,q,s1[N],s2[N],cnt;
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>t;
while(t--){
cin>>n>>k>>q;
memset(s1,0,sizeof s1),memset(s2,0,sizeof s2);
for(int i=1,c,l,r;i<=q;i++){
cin>>c>>l>>r;
if(c==1) s1[l]++,s1[r+1]--;
else s2[l]++,s2[r+1]--;
}
for(int i=1;i<=n;i++){
s1[i]+=s1[i-1],s2[i]+=s2[i-1];
if(s1[i]&&s2[i]) cout<<k+1<<" ";
else if(s1[i]) cout<<k<<" ";
else if(s2[i]) cout<<cnt%k<<" ",cnt++;
else cout<<"1 ";
}
cout<<"\n";
}
return 0;
}
Bonus
CF 官方题解最后提到了一点,将代码中的 cnt%k 改成 i%k 同样可以通过。下面我们证明这一点。
首先,\(\min\) 区间不可能被包含在 \(\text{MEX}\) 区间里,否则无解。
这说明一个 \(\text{MEX}\) 区间中,不被 \(\min\) 覆盖的位置是连续的。
那么我们用 \(i\) 还是 \(cnt\) 去模 \(k\) 都一样了。
4. CF2157D Billion Players Game
Hint 1
最坏情况一定在 $x=l$ 或 $x=r$ 时取到。Hint 2
存在一种最优解,满足将 $a$ 从小到大排序后,第三种策略是一个前缀,第二种策略是一个后缀。Hint 3
第一种策略最多使用一次。Sol
首先绝对值没啥用,题意转化一下就是每个元素选择 $\{x-a_i,a_i-x,0\}$ 中的一种,加起来。要让整个式子在 $[l,r]$ 上的最小值最大化。记选择 \(x-a_i\) 的集合为 \(A\),选择 \(a_i-x\) 的集合为 \(B\),那么答案可以描述为:
整理一下:
由此发现两个结论:
-
存在一种最优解,满足将 \(a\) 从小到大排序后,\(A\) 是一个前缀,\(B\) 是一个后缀。
考虑反证,若 \(A\) 不是前缀,则可以将其中一个 \(a_i\) 替换为更小的值,且 \(|A|\) 不变。\(B\) 同理。
-
\(A,B\) 以外的元素最多只有 \(1\) 个。
考虑反证,若不止 \(1\) 个,则可以将较前的加入 \(A\),较后的加入 \(B\),且 \(|A|-|B|\) 不变。
有了这两个结论,我们就可以直接枚举 \(A,B\) 的选择方案。要么是 \([1\sim i-1],[i+1\sim n]\),要么是 \([1\sim i],[i+1\sim n]\)。
时间复杂度 \(O(n\log n)\)。
另外 jokersen 给出了一个线性做法,但我还没弄懂,先扔个链接:Here。
Code
#include<bits/stdc++.h>
#define int long long
#define calc(x) (((i-1)-(n-i))*(x)-s[i-1]+(s[n]-s[i]))
#define calc2(x) (((i)-(n-i))*(x)-s[i]+(s[n]-s[i]))
using namespace std;
const int N=2e5+5;
int t,n,l,r,a[N],s[N];
signed main(){
ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
cin>>t;
while(t--){
cin>>n>>l>>r;
for(int i=1;i<=n;i++) cin>>a[i];
sort(a+1,a+1+n);
for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i];
int ans=-1e18;
for(int i=1;i<=n;i++) ans=max(ans,min(calc(l),calc(r)));//1~i-1:A i+1~n:B
for(int i=0;i<=n;i++) ans=max(ans,min(calc2(l),calc2(r)));//1~i:A i+1~n:B
cout<<ans<<"\n";
}
return 0;
}
正在找题……如果有任何推荐请在评论踢我。
upd on 2025/11/29
喜报:做出 T1 力
悲报:黄紫黑黑
浙公网安备 33010602011771号