【反悔贪心】P2949WorkSchedulingG+P4053[JSOI2007]建筑抢修题解

这两天遇到了几个很神奇的题目——能反悔的贪心。赶紧记录一下。

例1(用时一定模型)

用时一定:每个任务完成的耗时都是一样的。

题面:Luogu - P2949 Work Scheduling G

大体思路是:先把所有任务按照截止时间从小到大排序,然后枚举,遇到一个能做任务的就把他做了,把他的贡献加入一个小根堆中(贪心);如果遇到一个任务不能做了,但它比之前做过的任务的收益更高(即大于小根堆的堆顶),就放弃堆顶的那个任务,做这个任务。
这样实现能保证答案最优。代码实现应该就很简洁明了了。

注:因为每完成一个工作需要的时间都是1,所以小根堆的size就是已经花费的时间。若任务i能完成,则Q.size()>a[i]的截止时间

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5 + 7;

struct Node{
    int d, p; // 截止时间、获得收益
    friend bool operator < (Node p1, Node p2){
        return p1.d < p2.d; // 排序规则:按照截止日期从小到大排序
    }
}a[maxn];

void solve()
{
    int n; cin >> n;
    for(int i = 1; i <= n; i ++) cin >> a[i].d >> a[i].p;
    sort(a + 1, a + n + 1);

    priority_queue <int, vector<int>, greater<int> > Q; // 小根堆,存储已完成任务的所有收益
    ll ans = 0; //! 开long long!!!
    for(int i = 1; i <= n; i ++){
        if(a[i].d > Q.size()){ // 不超时,正常加入
            Q.push(a[i].p);
            ans += a[i].p;
        }else{ // 超时的话,如果当前任务的报酬 大于 做完的任务报酬中最小的,就替换之前的任务
            if(a[i].p > Q.top()){
                ans -= Q.top(); Q.pop();
                ans += a[i].p;
                Q.push(a[i].p);
            }
        }
    }

    cout << ans << '\n';
}

signed main()
{
    ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);
    solve();
    return 0;
}

例2(价值一定模型)

价值一定:每完成一个任务,获得的积分(价值)是一样的。

再来一题:Luogu - P4053 [JSOI2007] 建筑抢修

看完第一题之后,这道题应该会灵光乍现(即使这是一道蓝题?)。可以先按照任务截止时间 T 2 T_2 T2 升序排序。然后从小到大枚举,如果能修理就先修上,同时把修理它要花费的时间 T 1 T_1 T1 存到大根堆 Q Q Q 中。一旦遇到不能修上的,就把修复耗时最大的那个(即 Q.top())报废掉,给后面的建筑留下机会。

在代码的主循环中,有人可能认为直接把当前的 a[i] 统计上答案会不妥,因为可能 a[i] 就是时间最长的那个。但如果是那种情况的话,a[i] 一定会成为 Q Q Q 的堆顶,会在后面被弹出,并不会影响答案。

Code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

struct Building{
	int t1, t2;
	friend bool operator < (Building p1, Building p2){
		return p1.t2 < p2.t2; // 按照任务的截止时间排序
	}
}a[150005];

void solve()
{
	int n; cin >> n;
	for(int i = 1; i <= n; i ++) cin >> a[i].t1 >> a[i].t2;
	sort(a + 1, a + n + 1);
	
	ll cnt = 0, tim = 0; // cnt表示修好的个数,tim表示当前时间
	priority_queue <int> Q; // 默认大根堆。存储所有修过的花费的时间
	for(int i = 1; i <= n; i ++){
		tim += a[i].t1;
		cnt ++;
		Q.push(a[i].t1);
		if(tim > a[i].t2){ // 超时了,就把之前最浪费时间的那个报废掉(也可能这一轮的a[i]刚放进去就被报废了)
			cnt --;
			tim -= Q.top(); Q.pop();
		} 
	}
	cout << cnt << '\n';
}

signed main()
{
	ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
	solve();
	return 0;
}

注意:因为 1 ≤ T 1 < T 2 < 2 31 1 \le T_1 < T_2 < 2^{31} 1T1<T2<231,稍微加一加就爆 int 了,要开long long!!!

补充:上面两个题都是比较简单的**“反悔堆”模型,还有更高级(也更难)的“反悔自动机”**模型。本人功力不足,建议大家上网查阅学习。

End

拓展延申:反悔贪心是OI竞赛中的一个重要知识点,会和不会的区分非常明显。一个很好的例子是,本文中两道题都是蓝题,但个人觉得其思维难度比某些绿题甚至黄题还低。

蒟蒻本人也在仔细研究。下面两篇文章写得特别详细特别好,建议大家阅读:

  1. 详细总结反悔贪心各种模型及做法
  2. 反悔贪心刷题指南+题解

洛谷大佬整理的反悔贪心题单:『进阶方法篇』反悔贪心系列

让我们一起学习进步吧!拜拜ヾ(•ω•`)o

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值