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\) 从小到大排序,从左往右顺着推。走到某个人发现人数不够时,发现自己根本不知道该回头去贿赂前面的哪个人才最省钱,直接卡死。破局锚点:时光倒流(倒序遍历) + 反悔贪心。

极简模型推导:

  1. 逆转时间线: 既然顺着走不知道该贿赂谁,那就从 \(m_i\) 最大(要求最高)的人开始倒着推。

  2. 建立“候选人池”: 倒序遍历时,把遇到的人的代价 \(p_i\) 统统扔进一个小根堆里(这些人都是可以随时花钱买来充数的人质)。

  3. 白嫖与花钱的界限: 如果当前不花钱的“白嫖人数”足够满足当前这个人的要求,就继续往前走;如果不满足,就从小根堆里拿出一个代价最小的 \(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;
}
posted @ 2026-03-13 19:56  LYET  阅读(1)  评论(0)    收藏  举报