下面,我将用三道题目说明贪心算法的思维方式、使用范围、使用方法、排序不等式证明和过程的推导。
一、多操作的贪心算法实现:如何趋向最优解
1. 形式化题目描述
现有两个数 X X X 和 Y Y Y ,要求我们用以下两种操作在最少的次数内将 X X X 变成 Y Y Y 。
- X ← 2 X X \leftarrow 2X X←2X ;
-
X
←
X
−
1
X \leftarrow X-1
X←X−1 。
求最少操作次数。
数据范围: 1 ≤ X , Y ≤ 1 × 1 0 10 1 \le X , Y \le 1 \times 10 ^{10} 1≤X,Y≤1×1010
2. 实现方式
算法的选择:我们首先考虑使用
DFS
\texttt{DFS}
DFS 算法。对于每个节点,分两种情况:乘以
2
2
2 ,减去
1
1
1 。如果我们发现目前的数已经大于
2
×
1
0
10
2 \times 10 ^ {10}
2×1010 ,则到达递归边界,退出该轮递归。否则,继续分支,直到
Y
=
X
Y=X
Y=X 为止,并记录当前的操作次数。如果当前的操作次数是最优操作次数,更新答案,重新寻找是否有更优的操作次数。但是,这种算法的时间复杂度为
O
(
2
Y
−
X
)
O(2^{Y-X})
O(2Y−X) ,所以严重超时。
如果我们使用
DP
\texttt{DP}
DP ,则我们需要定义一个规模为
2
×
1
0
10
2 \times 10^{10}
2×1010 的数组,并且需要循环两重,所以时间复杂度最高可以达到
4
×
1
0
20
4 \times 10^{20}
4×1020 ,依旧严重超时。因此,我们考虑使用复杂度更低的贪心算法。
结论:我们可以使用逆向的推理方法,当
Y
Y
Y 能除以
2
2
2 时(定义:
Y
>
X
Y>X
Y>X 且
Y
mod
2
=
0
Y \text{mod } 2 = 0
Ymod 2=0)将
Y
←
Y
2
Y \leftarrow \large{\frac{Y}{2}}
Y←2Y ,否则将
Y
←
Y
+
1
Y \leftarrow Y+1
Y←Y+1 ,直到
X
=
Y
X=Y
X=Y 为止。
证明:使用反证法求证。
如果我们在
Y
Y
Y 能除以
2
2
2 时不除以
2
2
2 而选择加上
1
1
1 且
Y
>
X
Y>X
Y>X,则
∵
\because
∵
1
≤
Y
≤
1
×
1
0
10
1 \le Y \le 1 \times 10^{10}
1≤Y≤1×1010 ,即
Y
∈
N
*
Y \in \mathbb{N}\text{*}
Y∈N* ,
∴
\therefore
∴
Y
+
1
>
Y
2
Y+1>\large{\frac{Y}{2}}
Y+1>2Y 。
∵
\because
∵
Y
>
X
Y>X
Y>X ,
∴
\therefore
∴
Y
+
1
−
X
>
Y
2
−
X
Y+1-X>\large{\frac{Y}{2}}-X
Y+1−X>2Y−X 。
∴
\therefore
∴ 无论我们如何变换
Y
Y
Y 和
X
X
X ,
Y
2
\large{\frac{Y}{2}}
2Y 始终更加接近正确答案。
∴
\therefore
∴ 让
Y
←
Y
2
Y \leftarrow \large{\frac{Y}{2}}
Y←2Y 能花费更少的次数。
同理,如果我们在
Y
≤
X
Y \le X
Y≤X 时选择将
Y
←
Y
2
Y \leftarrow \large{\frac{Y}{2}}
Y←2Y ,则
∵
\because
∵
X
−
(
Y
+
1
)
>
X
−
Y
2
X-(Y+1)>X-\large{\frac{Y}{2}}
X−(Y+1)>X−2Y (部分与上面类似的条件省略),
∴
\therefore
∴ 让
Y
←
Y
+
1
Y \leftarrow Y+1
Y←Y+1 能花费更少的次数。
证毕。
误区:误认为使用顺向思维,将
X
X
X 不断乘以
2
2
2 ,最后可以使次数最少。
反例如下:让
X
=
1
,
Y
=
1
×
1
0
10
X=1,Y=1 \times 10^{10}
X=1,Y=1×1010 ,则使用上述的贪心算法只需要花费
39
39
39 次,但使用上面的错误算法将要花费
⌈
log
2
Y
⌉
+
2
⌈
log
2
Y
⌉
−
Y
=
⌈
log
2
1
×
1
0
10
⌉
+
2
⌈
log
2
Y
⌉
−
Y
=
34
+
2
34
−
1
0
10
=
34
+
7179869184
=
7179869218
>
39
\begin{equation} \begin{split} \tag*{}&\text{ \text{ }\text{ }\text{ }\text{ }\text{ }}\lceil \log_2{Y} \rceil +2^{\lceil \log_2{Y}\rceil }-Y \\ &=\lceil\log_2{1 \times 10^{10}}\rceil + 2^{\lceil\log_2{Y}\rceil}-Y \\ &=34+2^{34}-10^{10} \\ &=34+7179869184 \\ &=7179869218\\ &> 39 \end{split} \end{equation}
⌈log2Y⌉+2⌈log2Y⌉−Y=⌈log21×1010⌉+2⌈log2Y⌉−Y=34+234−1010=34+7179869184=7179869218>39
∴
\therefore
∴ 不能使用以上方法求解。
时间复杂度: O ( log 2 Y log 2 X ) O(\large{\frac{\log_2{Y}}{\log_2{X}}}) O(log2Xlog2Y) 。
3. 代码实现
#include <bits/stdc++.h>
using namespace std;
#define ONLINE_JUDGE 1
typedef long long ll;
ll x, y;
int ans;
int main()
{
#if ONLINE_JUDGE
freopen("change.in", "r", stdin);
freopen("change.out", "w", stdout);
#endif
cin >> x >> y;
while (y > x)
{
if (y % 2 == 0) y /= 2;
else y++;
ans++;
}
ans += x - y;
cout << ans << endl;
return 0;
}
二、“排队接水”变形题目的贪心算法实例:分析排序不等式
1. 形式化题目描述
我们现在有两个长度均为
N
N
N 的序列
a
a
a 和
b
b
b ,试通过恰当的位置调换使
∑
i
=
1
N
(
a
i
∑
j
=
1
i
b
j
)
\sum^{N}_{i=1} (a_i\sum^{i}_{j=1}b_j)
∑i=1N(ai∑j=1ibj) 最小。
2. 实现方式
算法的选择:此题可以看作带权值的“排队接水”问题,故应该使用贪心算法分析排序的不等式。
结论:排序不等式为 a j b i < a i b j a_jb_i<a_ib_j ajbi<aibj 。
证明:假设我们当前看到的是第
i
i
i 个和第
j
j
j 个。
∵
\because
∵ 当前两项的和有两种可能:
a
i
b
i
+
a
j
(
b
i
+
b
j
)
a_ib_i+a_j(b_i+b_j)
aibi+aj(bi+bj) ,
a
j
b
j
+
a
i
(
b
i
+
b
j
)
a_jb_j+a_i(b_i+b_j)
ajbj+ai(bi+bj) 。
∴
\therefore
∴ 排序不等式为
a
i
b
i
+
a
j
(
b
i
+
b
j
)
<
a
j
b
j
+
a
i
(
b
i
+
b
j
)
a_ib_i+a_j(b_i+b_j)<a_jb_j+a_i(b_i+b_j)
aibi+aj(bi+bj)<ajbj+ai(bi+bj) 。
∴
\therefore
∴ 经过推导可得
a
i
b
i
+
a
j
b
i
+
a
j
b
j
<
a
j
b
j
+
a
i
b
i
+
a
i
b
j
a
i
b
i
+
a
j
b
i
+
a
j
b
j
<
a
j
b
j
+
a
i
b
i
+
a
i
b
j
a
j
b
i
<
a
i
b
j
\begin{align} \tag*{}a_ib_i+a_jb_i+a_jb_j&<a_jb_j+a_ib_i+a_ib_j \\ \tag*{}\bcancel{a_ib_i}+a_jb_i+\bcancel{a_jb_j}&<\bcancel{a_jb_j}+\bcancel{a_ib_i}+a_ib_j \\ \tag{1}a_jb_i&<a_ib_j \end{align}
aibi+ajbi+ajbjaibi
+ajbi+ajbj
ajbi<ajbj+aibi+aibj<ajbj
+aibi
+aibj<aibj(1)
不等式
(
1
)
(1)
(1) 即为所求。
证毕。
时间复杂度:瓶颈为快排的复杂度, O ( N log 2 N ) O(N\log_2{N}) O(Nlog2N) 。
3. 代码实现
#include <bits/stdc++.h>
using namespace std;
#define ONLINE_JUDGE 1
const int N = 1e5 + 10;
struct Node
{
long long x, y;
} a[N];
long long n, ans;
int main()
{
#if ONLINE_JUDGE
freopen("eat.in", "r", stdin);
freopen("eat.out", "w", stdout);
#endif
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> a[i].x >> a[i].y;
//x重要度 y等待时间
}
sort(a + 1, a + n + 1, [&](const Node& a, const Node& b)
{
return a.y * b.x < a.x * b.y;
});
int t = 0;
for (int i = 1; i <= n; ++i)
{
t += a[i].y;
ans += t * a[i].x;
}
cout << ans << endl;
return 0;
}
三、多任务执行测试的排序不等式推理方法
1. 题意简述
我们现在要在一台电脑上运行
N
N
N 个程序,每个程序在运行时需要花费
r
i
r_i
ri 的内存空间,最小化进入后台后需要花费
o
i
o_i
oi 的内存空间。
已知我们要运行全部的程序,请问最少需要占用多少的内存空间?
2. 算法框架
本题的关键在于排序不等式的推理。
我们首先关注题意,题目让我们求的是最少需要占用的内存空间数。那么,我们可以从以下几个方面来思考解决方式:
首先,我们发现,每次运行占用空间最多的项就是当我们存储上面的第
i
i
i 个程序并开始运行下面的第
j
j
j 个程序时或只是上面的第
i
i
i 个程序运行时占用最大的内存空间。那么,我们可以得到排序不等式如下:
m
a
x
{
r
i
,
r
j
+
o
i
}
<
m
a
x
{
r
j
,
r
i
+
o
j
}
max\{r_i,r_j+o_i\} < max\{r_j,r_i+o_j\}
max{ri,rj+oi}<max{rj,ri+oj}
总之,该题的整体思路与上面的题类似。
3. 代码实现
#include <bits/stdc++.h>
using namespace std;
#define ONLINE_JUDGE 1
const int N = 1e5 + 10;
struct Node
{
int o, r;
} a[N];
int n;
int main()
{
#if ONLINE_JUDGE
freopen("task.in", "r", stdin);
freopen("task.out", "w", stdout);
#endif
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 1; i <= n; ++i)
{
cin >> a[i].r >> a[i].o;
}
sort(a + 1, a + n + 1, [&](const Node& a, const Node& b)
{
return max(a.r, b.r + a.o) < max(b.r, a.r + b.o);
});
int ans = 0, t = 0;
for (int i = 1; i <= n; ++i)
{
ans = max(ans, t + a[i].r);
t += a[i].o;
}
cout << ans << endl;
return 0;
}
四、总结
在使用贪心算法的时候,不要尝试去证明一个贪心算法,这样会花费大量的时间。我们可以尝试举出反例,反例一般都是在特殊或极端数据下出现。在不确定是否应该使用一个贪心算法的时候,我们可以使用对拍等方法来验证。若这种题考查的是排序不等式,则我们可以使用数学方法来推导这个不等式。否则,我们应该用画图理解的方式尝试去说明一个贪心策略的合理性。