贪心算法"/>
[week4]DDL的恐惧——贪心算法
目录
- 题意
- Input
- Output
- 输入样例
- 输出样例
- 提示
- 分析
- 总结
- 代码
题意
ZJM 有 n 个作业,每个作业都有自己的 DDL,如果 ZJM 没有在 DDL 前做完这个作业,那么老师会扣掉这个作业的全部平时分。
所以 ZJM 想知道如何安排做作业的顺序,才能尽可能少扣一点分。
请你帮帮他吧!
Input
输入包含T个测试用例。输入的第一行是单个整数T,为测试用例的数量。
每个测试用例以一个正整数N开头(1<=N<=1000),表示作业的数量。
然后两行。第一行包含N个整数,表示DDL,下一行包含N个整数,表示扣的分。
Output
对于每个测试用例,您应该输出最小的总降低分数(总扣分),每个测试用例一行。
输入样例
3
3
3 3 3
10 5 1
3
1 3 1
6 2 3
7
1 4 6 4 2 4 3
3 2 1 7 6 5 4
输出样例
0
3
5
提示
上方有三组样例。
对于第一组样例,有三个作业它们的DDL均为第三天,ZJM每天做一个正好在DDL前全部做完,所以没有扣分,输出0。
对于第二组样例,有三个作业,它们的DDL分别为第一天,第三天、第一天。ZJM在第一天做了第一个作业,第二天做了第二个作业,共扣了3分,输出3。
分析
合理分配DDL问题也属于最优化问题,可以用贪心算法来求解。
💡关于贪心算法的小解读👉[week3]区间选点问题——贪心算法
- 题目分析
分配ddl使最终所扣分数最低,等同于,合理分配ddl使其所得分数最高。在这个问题中,剩下未安排的任务总扣分数之和最少,即达到目标。
一堆任务中一定存在一个最晚的ddl。题目没有规定起始时间,那么将最晚的ddl作为一个完整时间段的最后一天,那么我们就需要在长度=最晚ddl的这段时间内分配任务,使得按时完成的任务的总分最高。
- 贪心标准的选择
显然,做越多的任务得分会越高,做的任务分越高得分也会越高。那按照这两个标准来选择能得到最优解吗?
-
尽可能多的做任务
-
在一段固定时间里如何能保证做的任务最多呢?
那就是把所有的任务都尽可能地在它们的ddl之内往后延,使得前面更多的时间空闲出来安排其他的任务。
-
总分一定最高吗?
不一定。假设每个ddl都有不止一个任务拥有,而这些m个任务的得分一定至少存在两个不同。那么按这种标准来进行选择,无法保证最后选择出来的任务都是候选任务中分数最高的。
-
-
优先选择分数最高的任务
-
总分一定最高吗?
这个标准非常诱惑,因为这直接与分数联系。但是仅仅将这个作为标准也无法得到最优解。因为如果优先安排分数最高的那些任务,将会使得部分时间段浪费。而我们无法保证浪费时间段内可完成的任务总分是否一定低于优先选择的任务分数。
-
综上,这两个标准只取其一都无法成为一个完美的标准。因此我们要将这两者进行结合。从时间段的最后一天往前开始安排任务,选择那些ddl在当前安排时间及之后的任务中分数最高的。
仔细想想,这是不是就满足了两个标准?把任务尽可能往后拖,那么拖到ddl当天,或者离ddl最近的时候,自然就是最晚的时候;在那些保证时间合理且脱的最晚的任务中选择分数最高的,也就满足了我们总是希望优先完成分数高的任务的要求了。
【小tip:在本题中给出的是扣分分数,同理,总是优先选择扣分分数最高的,就会使得最后那些剩下没做的任务的扣分分数之和最少了☝️】
- 如何实现贪心算法及性能优化
-
未优化做法(适用于较小数据范围)
- 首先将所有任务根据分数降序排列
- 依次从前至后遍历所有任务,为其安排时间
- 对于当前扫描的任务,从它的ddl开始向前扫描时间段,将其安排的在最先遇到的空白时间里。
这种设计中,主要时间消耗在依次为每个任务安排时间的过程中,也就是寻找空白时间段,每一次都要遍历时间段。
- 优化做法(适用于较大数据范围)
那么为了进行优化,需要提高搜索的性能。优化的方式不止一种。
-
为每个时间选择合适的任务,并用最大堆存储任务
这样只需要在一次时间遍历中,为每个时间找到可以放在这里的分数最高的任务即可。而使用最大堆对所有当前可选任务进行存储能提高寻找分数最高任务的性能。
-
为任务选择时间,将所有已被选择的时间合并为一个集合(并查集)
使用并查集将已选择的时间合并之后,即能所有的空闲时间只会被访问一次,可以省去重复扫描已被访问时间段的时间。
- 实现过程中遇到的问题
- 如何将输入数据合理存储?
由输入样例可知,数据包括一行ddl以及一行分数,每个分数和对应的ddl是相关联的。
- pair类型+vector❌
首先很容易就会想到使用pair类型来关联两个数据一起存储。而我在最开始的时候使用的是vector容器来存储一组数据,因为动态存储更方便,不会出现数组下标越界的问题。
但是,基本类型为pair的vector数组必须一次输入pair的两个元素。也就是说不能首先输入所有pair的first元素,再第二次遍历输入所有second元素。可输入样例是将两个元素域分开进行输入,因此这样是行不通的。
【小tip:
1.此处提到的pair类型的vector数组的初始化关键在于pair类型的初始化必须是同步的。
例如
vector<pair<int,int>> a; pair<int,int> s(0,0);
a[0]=s;
✔️
cin>>a[0].first>>a[0].second;
✔️
cin>>a[0].first; s[0].second=0;
❌
a[0]=s; cin>>a[0].first;
❌
2.同样提醒,int类型的vector数组不能像普通数组一样直接通过输入初始化。只能通过其成员函数insert或是push_back。
例如
vector<int> b; int k = 0;
cin>>b[0];
❌
cin>>k; b.insert(0,b);
✔️
cin>>k; b.push_back(b);
✔️
】
不过这并不代表不能通过改变策略来满足pair类型vector存储的目的,但是个人认为我自己想到的策略比较愚蠢,不如换个方法。🤷♂️
- 用两个数组分别存储,通过map映射ddl到任务分数❌
显然这种想法可以解决输入样例的格式问题。但是问题也很明显,同一个ddl可能对应多个任务分数,而相同的任务分数也可能对应多个ddl。
于是这个方法立刻否决。
- 动态不规则的二维数组✔️
多亏了vector实现了我这个危险的想法。【233】
简单的来说,每个数组下标代表了ddl时间,每个ddl时间下可能存有多个任务,将这些任务的分数存到对应下标的数组空间中即可。
但是需提前声明数组容量,需要至少包含样例数据范围。【所以为什么危险,因为只要让我声明大数组容量我就害怕😕】
在第一行的输入中使用一个数组空间来记录所有ddl,在第二次输入中再依次将每个分数存到对应ddl为下标的数组中即可。
- 当遍历所有ddl时
遍历ddl存在两个小问题需要考虑。
- 如何知道从哪一个ddl开始向前遍历?
这个问题的意思也就是我们无法直接知道最晚的ddl,因此在输入ddl的过程中,我们就需要记录最晚的ddl。
- 依序遍历数组时是否存在并没有该ddl的下标
当然可能存在。题目并没有保证ddl一定连续,因此在遍历过程中只需要判断一下当前扫描下标存储的数组是否为空即可。如果为空,则不需要进行将任务放到大根堆中的循环操作。
- 如何选择任务
因为遍历一定从最后一天开始,因此到第i天时,第i+1到最后一天的所有未被安排的任务一定在大根堆中。当扫描到第i天时,将ddl在当天及之后(为了保险)的所有任务放到大根堆中。
大根堆堆顶即是分数最高的可安排在当天的任务。在将待选任务放入堆中完毕后,取出堆顶并弹出即可继续前进到下一次任务安排中。
- 如何计算剩余被扣分数
根据设计的安排方法,当一次遍历完全后,所有的任务都一定已经放入堆中。因此只需要在结尾计算堆中剩余分数的和即可。
总结
- vector虽好用,奇怪要求也多。👋
- pair也很奇怪!
- 样例输入的格式可能会影响存储数据的结构。
代码
//
// main.cpp
// lab-a
//
//#include <iostream>
#include <vector>
#include <queue>
#include <numeric>
using namespace std;vector<int> ddl[100000]; //ddl
vector<int> day;
priority_queue<int> choose; //大根堆,将扣分按降序排列bool cmp(const int& a,const int& b)
{return a > b;
}int main()
{int t = 0,n = 0,sum = 0,score = 0,d = 0;
// pair<int, int> day(0,0);cin>>t;while( t-- ){
// cout<<"--------"<<endl;sum = 0;cin>>n;int lastday = 0; //记录所有ddl中的最后一天for( int i = 0 ; i < n ; i++ ) //记录所有作业ddl的时间{cin>>d;day.push_back(d);if( d > lastday )lastday = d;}for( int i = 0 ; i < n ; i++ ) //将扣分加入到ddl对应下标的数组中{cin>>score;ddl[day[i]].push_back(score);}day.clear(); //清空数组int j = 0;for( int i = lastday ; i > 0 ; i-- ) //从最后一天向前依次遍历{for( j = i ; j >= i ; j-- ) //将当前扫描日期及之后ddl的扣分放入大根堆中{if( ddl[j].size() > 0 ) //若当前日期没有ddl,则略过{for( int k = 0 ; k < ddl[j].size() ; k++ ) //将当前ddl为当前日期的所有扣分压入大根堆中choose.push(ddl[j][k]);ddl[j].clear(); //将数组清空}}if( !choose.empty()) //防止大根堆中没有可选项choose.pop(); //选择大根堆中扣分最多的一天,并将其取出}//所有ddl都一定会被压入队列中//因此最后队列中剩下的扣分,即为最后所扣分while( !choose.empty() ) //将大根堆中剩下的扣分累加,顺便清空大根堆{sum+=choose.top();choose.pop();}cout<<sum<<endl;}return 0;
}
更多推荐
[week4]DDL的恐惧——贪心算法
发布评论