2025 ICPC 区域赛 沈阳 B题 (铜牌题,贪心) 及贪心引申题单 (一)
下午单人 VP 区域赛,找到了签到到铜牌的题写。写到最后 B 题时,发生了以下心理变化:
想了一个看似好像有点对但实际并不对的贪心,wa。于是考虑 DP,显然的 \((nm)^2\),且没有什么优化空间。于是彻底红温。
看题解,发现就是贪心,且很好想也好理解,为什么赛时想不到
补完之后找 gemini 要了一份类似的贪心题单
25 ICPC区域赛 沈阳 B Buggy Painting Software I
https://qoj.ac/contest/2641/problem/14941
有一些数是要单开一层的,一些数是不单开一层的。
不要去考虑,一个一个往里放的时候是怎么放的,因为放的过程中,前面会影响后面,后面会影响前面。
直接假设所有层都已经处理完了
按照数的出现次数从大到小枚举,显然,如果单开 \(i\) 层,一定是给前 \(i\) 个数单开。且从上往下就是枚举的顺序。
需要特殊处理一下 \(0\)
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
using ll=long long;
using pii=pair<int,int>;
const ll inf = 1e18;
const int mod = 1e9+7;
void solve(){
int n,m,a,b;
cin>>n>>m>>a>>b;
map<int,int> mp;
vector g(n+1,vector<int>(m+1));
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
cin>>g[i][j];
mp[g[i][j]]++;
}
}
vector<pii> t;
t.push_back({0,mp[0]});
for(auto [x,y]:mp){
if(x==0) continue;
t.push_back({x,y});
}
sort(t.begin()+1,t.end(),[&](pii p1,pii p2)-> bool {
return p1.second>p2.second;
});
int c0=mp[0];
int sum=n*m;
int ans=(sum-c0)*a;//一开始忽略了 0 层的情况
int now=c0;
int pre=0;
for(int i=1;i<t.size();i++){
auto [color,cnt]=t[i];
pre+=cnt*(i-1)*b;
now+=cnt;
ans=min(ans,pre+(sum-now)*a+c0*i*b);
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
int ct=1;
cin>>ct;
while(ct--) solve();
return 0;
}
1. CF 3D Least Cost Bracket Sequence
tag: 贪心,反悔贪心,括号序列
从前往后一个一个放,如果可以改为 ),就改为 ),当不得不修改为 ( 时,直接给前缀中最优的位置改为 (
从前往后,设 balance=0,遇到 ( 就给 balance +1,遇到 ) 就给 balance -1。
如果是 ?,则先无脑变成 )
如果 balance <0,则在之前所有 ? 变成 ) 的位置中,找一个最优的变回来。
如果过程中或最后 balance 变不成 0,则 -1
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
using ll=long long;
using pii=pair<int,int>;
const ll inf = 1e18;
const int mod = 1e9+7;
void solve(){
string s;
cin>>s;
int n=s.size();
if(n&1){
cout<<-1<<endl;
return;
}
s=" "+s;
vector<int> a(n+1),b(n+1);
int ans=0;
priority_queue<pii> q;
for(int i=1;i<=n;i++){
if(s[i]!='?') continue;
cin>>a[i]>>b[i];
}
int balance=0;
for(int i=1;i<=n;i++){
if(s[i]=='(') balance++;
else if(s[i]==')') balance--;
else{
ans+=b[i];
balance--;
q.push({b[i]-a[i],i});
s[i]=')';
}
if(balance<0){
if(q.size()){
auto [val,pos]=q.top();
q.pop();
ans-=val;
s[pos]='(';
balance+=2;
// cout<<"pos: "<<pos<<endl;
}
else{
cout<<-1<<endl;
return;
}
}
}
if(balance != 0){
cout<<-1<<endl;
return;
}
cout<<ans<<endl;
for(int i=1;i<=n;i++){
cout<<s[i];
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
int ct=1;
// cin>>ct;
while(ct--) solve();
return 0;
}
CF 1251E2 - Voting (Hard Version) (Rating: 2400)
死因:一直在顺着题意,考虑从小到大该怎么抉择,但是会被前后都影响,所以很难处理
可以考虑按照 \(m\) 从大到小排序,假设小于 \(m\) 的都有了,则此时的已有的数量和 \(m\) 差值就是当前 \(m\) 必须买的数量。注意,此时不仅可以买 \(m\) 的,也可以买比 \(m\) 大且之前没被买的
gemini:
题目表象: \(n\) 个人投票,每个人有要求 \(m_i\)(当前已投票人数达到 \(m_i\) 才会跟风)和贿赂代价 \(p_i\)。求让所有人投票的最小代价。
致命直觉(为什么会死): 按照 \(m_i\) 从小到大排序,从左往右顺着推。走到某个人发现人数不够时,发现自己根本不知道该回头去贿赂前面的哪个人才最省钱,直接卡死。破局锚点:时光倒流(倒序遍历) + 反悔贪心。
极简模型推导:
-
逆转时间线: 既然顺着走不知道该贿赂谁,那就从 \(m_i\) 最大(要求最高)的人开始倒着推。
-
建立“候选人池”: 倒序遍历时,把遇到的人的代价 \(p_i\) 统统扔进一个小根堆里(这些人都是可以随时花钱买来充数的人质)。
-
白嫖与花钱的界限: 如果当前不花钱的“白嫖人数”足够满足当前这个人的要求,就继续往前走;如果不满足,就从小根堆里拿出一个代价最小的 \(p_i\) 付钱。这样既满足了当前,也增加了全局的已投票人数。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
using ll=long long;
using pii=pair<int,int>;
const ll inf = 1e18;
const int mod = 1e9+7;
void solve(){
int n;
cin>>n;
vector<vector<int>> p(n+1);
for(int i=1;i<=n;i++){
int m,t;
cin>>m>>t;
p[m].push_back(t);
}
vector<int> prem(n+1);
for(int i=0;i<=n;i++){
prem[i]=p[i].size();
if(i) prem[i]+=prem[i-1];
}
//now: 当前已经付费拿了 now 个
int ans=0,now=0;
priority_queue<int,vector<int>,greater<int>> q;
priority_queue<int> q2;
for(int m=n;m>=0;m--){
if(p[m].size()==0) continue;
for(auto val:p[m]){
q.push(val);
}
while(q.size() && (m==0?0:prem[m-1])+now<m){
auto val=q.top();
q.pop();
ans+=val;
q2.push(val);
now++;
}
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
int ct=1;
cin>>ct;
while(ct--) solve();
return 0;
}
CF 1383A - String Transformation 1 (Rating: 1700)
看出来了可以给所有位置的变化建图,但没有想好该怎么建图,也完全没有观察出最优做法
实际上,对于每一对转变需求,可以在这两个字母之间建立一条边。边可以用一种类似并查集的形式,直接将这两个字母所在的联通块合并起来。这其实是一个拓扑图(因为字母只能严格变大)
完成之后,就会发现,每一个点的个数为 \(v\) 的联通块,都可以通过最多 \(v-1\) 次变换,满足这个联通块中的所有需求。
很巧妙,完全想不到,也不知道赛时该怎么想到
gemini:
赛场上的思维推演还原(如何想到 \(v-1\))
第一步:不要看宏观,去看最恶心的局部(极端构造)假设你现在的字符串 A 里面全都是 a。但是 B 里面要求这堆 a 分别变成 c, d, e。你想:我能一次把 a 全变成 c, d, e 吗?绝对不可能,因为一次操作会把所有的 a 变成同一个字母。
第二步:被迫的贪心选择(绝境中的唯一出路)既然只能变同一个字母,而且字母只能从小往大变,那你面对 a -> c, a -> d, a -> e 这三个需求,你第一步必须把 a 变成什么?你必须把所有的 a 先变成这里面最小的 c。如果你敢把 a 变成 d,那原来需要变成 c 的那些字符就永远变不回去了(因为不能变小)。
第三步:见证奇迹的“债务转移”好,现在你执行了操作 a -> c。原来需要 a -> c 的那批字符,已经圆满完成任务了。但是,原来需要 a -> d 和 a -> e 的字符,现在变成了什么?它们变成了 c -> d 和 c -> e!这就是最核心的破局点!你发现了吗?a 并没有消失,它只是把自己的“变形债务”转移给了 c。在逻辑上,a 和 c 已经被合并成了一个整体。
第四步:自然推导出并查集(连通块)一旦你发现了“债务转移”,图论的本质就彻底暴露了:如果有 \(v\) 个字母,它们之间通过各种需求纠缠在一起(形成一个连通块)。你每次操作,其实就是挑出里面最小的字母,让它向第二小的字母靠拢(合并)。每次合并,消除一个最小字母。要让这 \(v\) 个字母最终汇聚成同一个最大的目标字母,你需要合并多少次?正好就是 \(v - 1\) 次。
点击查看代码
#include<bits/stdc++.h>
#define int long long
#define endl '\n'
using namespace std;
using ll=long long;
using pii=pair<int,int>;
const ll inf = 1e18;
const int mod = 1e9+7;
class DSU{
public:
vector<int> fa,sz;
int setCount;
int n;
DSU(){}
DSU(int n){
init(n);
}
void init(int n){
fa.resize(0);
fa.resize(n+1);
sz.resize(0);
sz.resize(n+1,1);
this->n=n;
setCount=n;
iota(fa.begin(), fa.end(), 0);
}
int find(int x) {
if(fa[x] == x) return x;
return fa[x] = find(fa[x]);
}
void unite(int x, int y) {
x = find(x),y = find(y);
if(x == y) return;
if(sz[x] <= sz[y] ) swap(x, y);
fa[y] = fa[x];
sz[x] += sz[y];
--setCount;
}
};
void solve(){
int n;
cin>>n;
DSU dsu(20);
string a,b;
cin>>a>>b;
for(int i=0;i<n;i++){
if(a[i]>b[i]){
cout<<-1<<endl;
return;
}
dsu.unite(a[i]-'a'+1,b[i]-'a'+1);
}
int ans=0;
for(int i=1;i<=20;i++){
if(dsu.fa[i]==i){
ans+=(dsu.sz[i]!=1?dsu.sz[i]-1:0);
}
}
cout<<ans<<endl;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
int ct=1;
cin>>ct;
while(ct--) solve();
return 0;
}

浙公网安备 33010602011771号