[题解]考前一些贪心技巧题

比如今年 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\),那么答案可以描述为:

\[\sum_{i\in A} (x-a_i)+\sum_{i\in B} (a_i-x) \]

整理一下:

\[(|A|-|B|)x+\sum_{i\in B} a_i-\sum_{i\in A} a_i \]

由此发现两个结论:

  • 存在一种最优解,满足将 \(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 力
悲报:黄紫黑黑

posted @ 2025-11-27 18:44  Sinktank  阅读(446)  评论(0)    收藏  举报
★CLICK FOR MORE INFO★ TOP-BOTTOM-THEME
Enable/Disable Transition
Copyright © 2023 ~ 2025 Sinktank - 1328312655@qq.com
Illustration from 稲葉曇『リレイアウター/Relayouter/中继输出者』,by ぬくぬくにぎりめし.