这周学的主要是贪心算法的知识。相对于第一周来说,难度增加了不少。关于贪心算法的基础知识,详见我上一篇博客。
索引
贪心算法
工欲善其事,必先利其器。贪心算法亦是如此。作为解决问题的一种工具,在合适的条件下能达到事半功倍的效果。关于贪心算法的选择条件在此不再累赘。接下来,通过引入小题,逐步构建贪心体系。
一、贪心结构
整体结构
下面这两个mermaid类型TB图表可能在部分手机上字体显示过小,在电脑上是正常的
编程结构
二、典型题型载入
1.简单贪心
最优装载量
问题模型:存在一定容积容器,承重为M
,有n件待装物品
,体积为 nm ,容器承受体积不限制,尽可能多
的装载物品。
思路
步骤一:确定贪心原则——按照最轻到最重装载;
步骤二:确定排序方式:按重量升序;
步骤三:判断终止条件——累加物品,达到承重极限,输出。
背包问题
问题模型:载重量为M的背包,有n个物品,第n个物品重量为mn,价值为wn,要求尽可能装满背包,且总价值最大
。
步骤一:确定贪心原则——性价比;
步骤二:确定排序方式——按性价比降序;
步骤三:判断终止条件——累加至承重极限,输出。
2.区间贪心
区间调度
问题模型:有n项工作,每项工作从Si开始,Ti结束。其中S = {1, 2, 4, 6, 8}, T = {3, 5, 7, 8, 10}。对每项工作,都可以选择参加与否,若参加,则必须全程参与,且时间不能重叠,求最多能参加多少工作。
下面是其甘特图
:
贪心策略:每次选取结束时间最早,第二权重为每次选取开始时间最早;
做作业
问题模型:要做作业,但每次只能做一个,每个作业都有截止时间,超时会扣分,咋让扣分最小呢?
贪心策略:先截止的先安排,扣分多的先安排,如果一定扣分了,替换前面一个已安排但扣分少的。
贪心实现:
先对时间排序,如果截止时间相同,哪个扣分大那个放在前;
后对时间数据遍历,如果到第i个时已经超过了截止时间,记下这个i和其下标。在这之前找一个扣分最少的来作为要扣的分数
。
三、代码技能与实现
巧用乘法代替除法 / double之比较
看这样一段代码:
...
struct node{
double value;
double weight;
}pool[10010];
...
bool cmp(node a, node b){
double cp_1, cp_2;
cp_1 = 1.0 * a.value / a.weight;
cp_2 = 1.0 * b.value / b.weight;
return cp_1 > cp_2;
//第一处
}
...
if(pool[i].value == pool[i + 1].value){
//第二处
...
}
以上代码可能会出现在上面的背包问题(按性价比
)里。我们知道,double
类型精度不可控,进行比较时,尤其是代码注释第二处的相等比较,很可能会出错。
我们分析一下注释第一处结构:
c
p
1
=
a
.
v
a
l
u
e
a
.
w
e
i
g
h
t
c
p
2
=
b
.
v
a
l
u
e
b
.
w
e
i
g
h
t
cp_1 = \frac{a.value}{a.weight}\\ cp_2 = \frac{b.value}{b.weight}\\
cp1=a.weighta.valuecp2=b.weightb.value
那么
c
p
1
>
c
p
2
cp_1 > cp_2
cp1>cp2
即
a
.
v
a
l
u
e
a
.
w
e
i
g
h
t
∗
b
.
v
a
l
u
e
b
.
w
e
i
g
h
t
\frac{a.value}{a.weight} * \frac{b.value}{b.weight}
a.weighta.value∗b.weightb.value
其实可以表示为
a
.
v
a
l
u
e
∗
b
.
w
e
i
g
h
t
>
b
.
v
a
l
u
e
∗
a
.
w
e
i
g
h
t
a.value*b.weight>b.value*a.weight
a.value∗b.weight>b.value∗a.weight
而且精度判断可以在10-6内。
为此,将以上代码按照如下方式写的话:
...
bool cmp(node a, node b){
return a.value * b.weight > b.value * a.weight;
}
...
if(pool[i] - pool[i + 1] <= 1e-6){
...
}
这样,准确度会大大提高。
struct中的bool
有时需要判断一个元素是不是被用过,我们可以在struct
中如下定义:
...
struct node{
int a;
int b;
bool k;
}pool[10010];
...
for(statement){
...
pool[i].k = 1;
...
}
...
if(pool[i].k == 1){
continue;
}
...
pair数组合并
在上面的区间调度问题中过,我们对Si和Ti进行“双排序”
。这是使用pair数组
即可轻松解决这个问题:
...
pair<int, int> itv[n];
...
pair数组包含两个元素,一个是first
,另一个是second
。这里我们对S优先,所以把Si放入first
,把Ti放入second
。
...
for(int i = 0; i < n; i++){
itv[i].first = S[i];
itv[i].second = T[i];
}
sort(itv, itv + n);
...
四、心得
这周主要讲的贪心算法。贪心算法要求很强的发散思维和数学功底,因此,对待这种问题有时候时候会“不走寻常路”
。不能拘泥于正向思维,可以试试反向思维。
这周是开始做vjudge提的第一周。说实话,vjudge题库(大部分来源于POJ)的难度比OpenJudge提高了一个等级。这主要存在于四个方面:
1.vjudge是全英文题库;
2.vjudge上的题对发散性思维要求十分高;
3.vjudge不给出单题得分,即WA就是0分,导致自己很难分析出到底错在哪里;
4.我经常看错条件。
其实第四条最重要。
要知道为什么我做的这么慢。。。我其实努力做了,但是每次都会因为一点小细节(让输入double我整了个int,用0隔开我没看见之类的)查好几遍甚至好几十遍代码,这很耗时间。所以我代码尽量加上注释,尽量用有意义的变量。其实我在算法上是没有太大问题的。。。
所以对症下药:每次做题一定要注意小细节。
昨天晚上还因为代码的低级错误发了场脾气,班助和同学劝了劝我,我才冷静了下来。
今天又静下心来思考了思考。计算机本来就是出于多年的爱好和兴趣去选的,ACM课也是出于极高的兴趣去选的,那自己还有什么理由在自己所爱好的领域里得到痛苦呢?这就和写算法实现一样,选择了一种算法,就认真地把这种算法写好。既然自己已经迈出了第一步,那就要把剩下的路走完
。
这周还意外收获了一个人生道理:
寻找最优解。
贪心算法本身就是个寻找最优解的过程,从局部最优扩展至全局最优。生活亦是如此,把握住当下的局部最优,也许在最后会得到整个人生中的全局最优解。
因此,我们在学习算法时,不仅要学习算法的理论知识,还要把算法的核心价值扩展至方方面面,这样一个算法的价值就大大增加了。