下面,我将用三道题目说明贪心算法的思维方式、使用范围、使用方法、排序不等式证明和过程的推导。
一、多操作的贪心算法实现:如何趋向最优解
1. 形式化题目描述
现有两个数 XXX 和 YYY ,要求我们用以下两种操作在最少的次数内将 XXX 变成 YYY 。
- X←2XX \leftarrow 2XX←2X ;
- X←X−1X \leftarrow X-1X←X−1 。
求最少操作次数。
数据范围:1≤X,Y≤1×10101 \le X , Y \le 1 \times 10 ^{10}1≤X,Y≤1×1010

2. 实现方式
算法的选择:我们首先考虑使用 DFS\texttt{DFS}DFS 算法。对于每个节点,分两种情况:乘以 222 ,减去 111 。如果我们发现目前的数已经大于 2×10102 \times 10 ^ {10}2×1010 ,则到达递归边界,退出该轮递归。否则,继续分支,直到 Y=XY=XY=X 为止,并记录当前的操作次数。如果当前的操作次数是最优操作次数,更新答案,重新寻找是否有更优的操作次数。但是,这种算法的时间复杂度为 O(2Y−X)O(2^{Y-X})O(2Y−X) ,所以严重超时。
如果我们使用 DP\texttt{DP}DP ,则我们需要定义一个规模为 2×10102 \times 10^{10}2×1010 的数组,并且需要循环两重,所以时间复杂度最高可以达到 4×10204 \times 10^{20}4×1020 ,依旧严重超时。因此,我们考虑使用复杂度更低的贪心算法。
结论:我们可以使用逆向的推理方法,当 YYY 能除以 222 时(定义:Y>XY>XY>X 且 Ymod 2=0Y \text{mod } 2 = 0Ymod 2=0)将 Y←Y2Y \leftarrow \large{\frac{Y}{2}}Y←2Y ,否则将 Y←Y+1Y \leftarrow Y+1Y←Y+1 ,直到 X=YX=YX=Y 为止。
证明:使用反证法求证。
如果我们在 YYY 能除以 222 时不除以 222 而选择加上 111 且 Y>XY>XY>X,则
∵\because∵ 1≤Y≤1×10101 \le Y \le 1 \times 10^{10}1≤Y≤1×1010 ,即 Y∈N*Y \in \mathbb{N}\text{*}Y∈N* ,
∴\therefore∴ Y+1>Y2Y+1>\large{\frac{Y}{2}}Y+1>2Y 。
∵\because∵ Y>XY>XY>X ,
∴\therefore∴ Y+1−X>Y2−XY+1-X>\large{\frac{Y}{2}}-XY+1−X>2Y−X 。
∴\therefore∴ 无论我们如何变换 YYY 和 XXX ,Y2\large{\frac{Y}{2}}2Y 始终更加接近正确答案。
∴\therefore∴ 让 Y←Y2Y \leftarrow \large{\frac{Y}{2}}Y←2Y 能花费更少的次数。
同理,如果我们在 Y≤XY \le XY≤X 时选择将 Y←Y2Y \leftarrow \large{\frac{Y}{2}}Y←2Y ,则
∵\because∵ X−(Y+1)>X−Y2X-(Y+1)>X-\large{\frac{Y}{2}}X−(Y+1)>X−2Y (部分与上面类似的条件省略),
∴\therefore∴ 让 Y←Y+1Y \leftarrow Y+1Y←Y+1 能花费更少的次数。
证毕。
误区:误认为使用顺向思维,将 XXX 不断乘以 222 ,最后可以使次数最少。
反例如下:让 X=1,Y=1×1010X=1,Y=1 \times 10^{10}X=1,Y=1×1010 ,则使用上述的贪心算法只需要花费 393939 次,但使用上面的错误算法将要花费
⌈log2Y⌉+2⌈log2Y⌉−Y=⌈log21×1010⌉+2⌈log2Y⌉−Y=34+234−1010=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(log2Ylog2X)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. 形式化题目描述
我们现在有两个长度均为 NNN 的序列 aaa 和 bbb ,试通过恰当的位置调换使 ∑i=1N(ai∑j=1ibj)\sum^{N}_{i=1} (a_i\sum^{i}_{j=1}b_j)∑i=1N(ai∑j=1ibj) 最小。

2. 实现方式
算法的选择:此题可以看作带权值的“排队接水”问题,故应该使用贪心算法分析排序的不等式。
结论:排序不等式为 ajbi<aibja_jb_i<a_ib_jajbi<aibj 。
证明:假设我们当前看到的是第 iii 个和第 jjj 个。
∵\because∵ 当前两项的和有两种可能:aibi+aj(bi+bj)a_ib_i+a_j(b_i+b_j)aibi+aj(bi+bj) , ajbj+ai(bi+bj)a_jb_j+a_i(b_i+b_j)ajbj+ai(bi+bj) 。
∴\therefore∴ 排序不等式为 aibi+aj(bi+bj)<ajbj+ai(bi+bj)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∴ 经过推导可得
aibi+ajbi+ajbj<ajbj+aibi+aibjaibi+ajbi+ajbj<ajbj+aibi+aibjajbi<aibj
\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+ajbjajbi<ajbj+aibi+aibj<ajbj+aibi+aibj<aibj(1)
不等式 (1)(1)(1) 即为所求。
证毕。
时间复杂度:瓶颈为快排的复杂度,O(Nlog2N)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. 题意简述
我们现在要在一台电脑上运行 NNN 个程序,每个程序在运行时需要花费 rir_iri 的内存空间,最小化进入后台后需要花费 oio_ioi 的内存空间。
已知我们要运行全部的程序,请问最少需要占用多少的内存空间?
2. 算法框架
本题的关键在于排序不等式的推理。
我们首先关注题意,题目让我们求的是最少需要占用的内存空间数。那么,我们可以从以下几个方面来思考解决方式:
首先,我们发现,每次运行占用空间最多的项就是当我们存储上面的第 iii 个程序并开始运行下面的第 jjj 个程序时或只是上面的第 iii 个程序运行时占用最大的内存空间。那么,我们可以得到排序不等式如下:
max{ri,rj+oi}<max{rj,ri+oj}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;
}
四、总结
在使用贪心算法的时候,不要尝试去证明一个贪心算法,这样会花费大量的时间。我们可以尝试举出反例,反例一般都是在特殊或极端数据下出现。在不确定是否应该使用一个贪心算法的时候,我们可以使用对拍等方法来验证。若这种题考查的是排序不等式,则我们可以使用数学方法来推导这个不等式。否则,我们应该用画图理解的方式尝试去说明一个贪心策略的合理性。

被折叠的 条评论
为什么被折叠?



