可后悔贪心 -- 解题报告

反悔贪心_Elicsyd的博客-CSDN博客

感觉普通贪心是每一个维度都是平等的,没有优先级。而可后悔贪心是存在某个维度是不可变的,不能直接用排序或者堆进行维护,常常需要经过某种处理,通过挖掘出题目中关于不可变维度的特殊性质,使其可以用排序或者堆等数据结构进行贪心。

可后悔贪心常用堆(priority_queue)进行维护。

[E. Buy Low Sell High](Problem - E - Codeforces)

问题描述:

您可以完美预测某只股票未来 N 天的价格。您想利用这一知识获利,但每天只想交易一股股票。也就是说,每天你要么买入一股,要么卖出一股,要么什么也不做。起初你拥有的股票为零,当你没有股票时,你不能卖出股票。在N天结束时,你希望再次拥有零股,但希望拥有尽可能多的资金。 — 插件 cf better 翻译

思路:这一题跟股票买卖差不多,但是这个是每次都是可以买/卖/不进行操作,而且对于买卖股票没有次数限制。直接贪心:i天买入,j天卖出,要求j天对于i来说是最大的,但是没有限制次数,这样贪心需要维护区间内的,可能需要用dp。

这题为什么会后悔:价格 a b c,a < b对于贪心来说要卖出,但是卖出后,到c发现a < b < c,即|c - a| 大于 |b - a|,这时会发现我们的贪心就错误了,应该在c再卖出。发现b - a + c - b == c - a,利用差值可以将局部最优转为全局最优。

具体代码思路:用一个小顶堆维护之前最小的价值,当当前价值大于之前最小的价值时可以卖出,pop后再将当前价值push到小顶堆中。之后在将当前价值push到小顶堆中,表示以当前价值为买入点的股票低点。

1 4 10 20为例,答案为25,是1 --> 10 4 --> 20 或者 1 --> 20 4 --> 10这样。

代码:

void solve() {
    
    
    int n; cin>>n;
    vector<int> a(n);
    for(auto &t: a) cin>>t;
    priority_queue<int, vector<int>, greater<int>> q;
    LL ans = 0;
    for(auto t: a) {
    
    
        if(q.size() && q.top() < t) {
    
    
            ans += t - q.top();
            q.pop();
            q.push(t);
        }
        q.push(t);
    }
    cout<<ans<<endl;
}

[P8769 [蓝桥杯 2021 国 C] 巧克力]([P8769 蓝桥杯 2021 国 C] 巧克力 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

问题描述:

小蓝很喜欢吃巧克力,他每天都要吃一块巧克力。

一天小蓝到超市想买一些巧克力。超市的货架上有很多种巧克力,每种巧克力有自己的价格、数量和剩余的保质期天数,小蓝只吃没过保质期的巧克力,请问小蓝最少花多少钱能买到让自己吃 x 天的巧克力。

思路:这题跟上一个类似,对abc按b进行排序,按b从大到小(从大到小如果后悔简单且一定是可以的)开始进行后悔贪心。与上题不同在于这题还有个数量c,上题没有。可以发现,满足条件只需要x个面包,且对于在i天而言,只要面包的保质期大于等于i,无脑选价钱最小的即可。因此可以按天数从结束天到开始天进行模拟,每次找没过保质期的最小价格的面包。

具体代码思路:用array<int,3>依次存入a b c,按b从大到小进行排序。用一个自定义小顶堆来维护对于遍历到i满足面包不过期的剩余面包的最小价格。每次找一个最小价格加入答案即可。

代码:

void solve() {
    
    
	int n,x; cin>>x>>n;
	vector<array<int,3>> va(n);
	for(auto &t:va) cin>>t[0]>>t[1]>>t[2]; // a b c
	sort(all(va), [](array<int,3> pre, array<int,3> suf) {
    
    
		return pre[1] > suf[1];
	});
	auto cmp = [](PII pre, PII suf) {
    
    
		return pre.vf > suf.vf;
	};
	priority_queue<PII, vector<PII>, decltype(cmp)> maq(cmp);
	LL ans = 0;
	int pos = 0;
	for(int i = x; i >= 1; --i) {
    
    
		while(pos < n && va[pos][1] >= i) {
    
    
			maq.push({
    
    va[pos][0], va[pos][2]});
			pos++;
		}
		if(maq.size() == 0) {
    
    
			cout<<-1;
			return ;
		}
		auto tmp = maq.top(); maq.pop();
		ans += tmp.vf; tmp.vs--;
		if(tmp.vs != 0) {
    
    
			maq.push({
    
    tmp});
		}
	}
	cout<<ans;
}

[P4053 [JSOI2007] 建筑抢修]([P4053 JSOI2007] 建筑抢修 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn))

问题描述:

小刚在玩 JSOI 提供的一个称之为“建筑抢修”的电脑游戏:经过了一场激烈的战斗,T 部落消灭了所有 Z 部落的入侵者。但是 T 部落的基地里已经有 �N 个建筑设施受到了严重的损伤,如果不尽快修复的话,这些建筑设施将会完全毁坏。现在的情况是:T 部落基地里只有一个修理工人,虽然他能瞬间到达任何一个建筑,但是修复每个建筑都需要一定的时间。同时,修理工人修理完一个建筑才能修理下一个建筑,不能同时修理多个建筑。如果某个建筑在一段时间之内没有完全修理完毕,这个建筑就报废了。你的任务是帮小刚合理的制订一个修理顺序,以抢修尽可能多的建筑。

思路:每一个建筑有一个最后截止时间和抢修耗时。对截止时间从小到大进行(原理同上)排序,用一个变量记录当前时间,如果当前时间加上维修时间在截止时间之内则可以,否则在前面可以的建筑中找一个最耗时的,如果那个最好时的大于当前耗时则进行替换,挤出点时间,为后面进行成功增加几率。

具体代码思路:用pair存入维护时间,截止时间。按截止时间进行升序排序。用一个变量lasttm记录当前进行到的时间,如果lasttm + vf <= vs表示这个建筑在截止前可以抢修成功则更新lasttm,并将维护时间vf放入到大顶堆中;如果不满足要求,表示这个建筑不可能被维修成功在截止时间之前,则在之前维修好的建筑中将一个维修时间较长的跟这个进行替代,lasttm更新,次数更新的lasttm一定比之前的未更新的lasttm要小。

代码:

void solve() {
    
    
    int n; cin>>n;
    vector<PII> vp(n);
    for(auto &t: vp) cin>>t.vf>>t.vs;
    sort(all(vp), [&](PII pre, PII suf) {
    
    
        return pre.vs < suf.vs;
    });
    int cnt = 0;
    int lasttm = 0;
    priority_queue<int> pq;
    for(int i = 0; i < n; ++i) {
    
    
        if(lasttm + vp[i].vf <= vp[i].vs) {
    
    
            cnt++;
            lasttm += vp[i].vf;
            pq.push(vp[i].vf);
        } else if(pq.top() > vp[i].vf) {
    
    
            lasttm -= pq.top(); pq.pop();
            lasttm += vp[i].vf; pq.push(vp[i].vf);
        }
    }
    cout<<cnt;
}

[tokitsukaze and Soldier](tokitsukaze and Soldier (nowcoder.com))

问题描述:

链接:https://ac.nowcoder.com/acm/problem/50439
来源:牛客网

在一个游戏中,tokitsukaze需要在n个士兵中选出一些士兵组成一个团去打副本。
第i个士兵的战力为v[i],团的战力是团内所有士兵的战力之和。
但是这些士兵有特殊的要求:如果选了第i个士兵,这个士兵希望团的人数不超过s[i]。(如果不选第i个士兵,就没有这个限制。)
tokitsukaze想知道,团的战力最大为多少。

思路:按特殊要求的不超过人数进行降序排,因为如果要求人数下降了,可以将之前进入的士兵无脑出队,而不用额外考虑其他信息。

具体代码思路:用pair依次存入战力和要求人数,按要求人数进行降序排序。用小顶堆维护满足要求人数限制条件的士兵的战力。对于第i个士兵,进入答案++,之后处理满足当前士兵要求的人数限制,如果有多余的则将战力最低的出队。每次遍历士兵时都取了max(因为可能中间要求得是最大战力之和,但是记录是遍历到的最后一个士兵时的最大战力之和。

代码:

void solve() {
    
    
    int n; cin>>n;
    vector<PII> vp(n);
    for(auto &t: vp) cin>>t.vf>>t.vs; // v s1
    sort(all(vp), [](PII pre, PII suf) {
    
    
        return pre.vs > suf.vs;
    });
    LL ans = 0;
    LL ma = -3;
    priority_queue<int,vector<int>,greater<int>> pq;
    for(auto t: vp) {
    
    
        ans += t.vf;
        pq.push(t.vf);
        while(pq.size() > t.vs) {
    
    
            ans -= pq.top(); pq.pop();
        }
        ma = max(ma, ans);
    }
    cout<<ma;
}

总结

可后悔贪心感觉用排序和堆完美的用上了题目中的性质,解决掉了普通贪心的不足。

发现是贪心 --> 常规贪心有点奇怪 --> 考虑可后悔贪心 / 换算法 / 再挣扎挣扎 --> 其他

猜你喜欢

转载自blog.csdn.net/qq_63432403/article/details/132651726