一贪到底——贪心算法从局部最优到全局最优------根据目的直接构造cmp排序函数解决贪心算法问题 //工序排序问题//约翰逊法则//信息学奥赛一本通1422

目录

引言

原题

题目描述

我的思路以及代码

思路

代码

排序方式时间复杂度分析

方法一:分组排序法

方法二:逐个放置法

两种方案时间复杂度分析

方法一:分组排序法

方法二:逐个放置法

结论

核心原理------计算任意产品加工完的时间

详细解释

示例

原理总结

大佬的博客代码(添上了点注释)

证明过程(反证法验证)

分析步骤

情况 1: a ≥ d 且 b ≥ c

情况 2: a ≥ d 且 b < c

情况 3: a < d 且 b ≥ c

情况 4: a < d 且 b < c

总结与结论

正常逻辑推理过程:

交换论证法

思考与改进------直接根据目的构造cmp函数来排序

推导 cmp 排序逻辑

比较两个任务的加工时间

比较公式

为什么这种排序逻辑能够确保整体最优?

与简化后的公式的时间复杂度对比

直接比较函数

大佬简化后的比较函数

运行效率分析

运行效率对比

实际运行效率

对比总结

最终代码

一点点心得与体会


引言

才写完信息学奥赛一本通1422:【例题1】活动安排,详情见https://blog.youkuaiyun.com/2301_80881806/article/details/145433913?spm=1001.2014.3001.5501

感觉自己的方法比较笨拙,比较常规,或者说是比较暴力,所以说上网搜寻了一下一些优秀做法,果不其然,找到了非常简短的解题思路,让我大为赞叹。https://www.cnblogs.com/fashpoint/p/11309412.html

原题

题目描述

某工厂收到了 n 个产品的订单,这些产品需要分别在 A 和 B 两个车间进行加工,并且必须先在 A 车间加工后才可以到 B 车间加工。每个产品在 A 车间和 B 车间的加工时间分别为 AiBi。如何安排这 n 个产品的加工顺序,才能使总的加工时间最短?

这里所说的加工时间是指:从开始加工第一个产品到最后所有的产品都已在 A、B 两车间加工完毕的时间。

输入格式

  • 第一行仅一个数据 n,表示产品的数量;
  • 接下来的 n 个数据是表示这 n 个产品在 A 车间加工各自所需的时间;
  • 最后的 n 个数据是表示这 n 个产品在 B 车间加工各自所需的时间。

输出格式

  • 第一行一个数据,表示最少的加工时间;
  • 第二行是一种最小加工时间的加工顺序。

输入样例

5
3 5 8 7 10
6 2 1 4 9

输出样例

34
1 5 4 2 3

我的思路以及代码

思路

参照常规的思路,约翰逊法则,引用一下基本思路,图片取自以下文章https://blog.youkuaiyun.com/weixin_68261415/article/details/129013266

代码

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

struct Product {
    int id;
    int timeA;
    int timeB;
};

// 比较函数,用于对组 1 按在 A 车间加工时间升序排序
bool cmpA(const Product& p1, const Product& p2) {
    return p1.timeA < p2.timeA;
}

// 比较函数,用于对组 2 按在 B 车间加工时间降序排序
bool cmpB(const Product& p1, const Product& p2) {
    return p1.timeB > p2.timeB;
}

int main() {
    int n;
    cin >> n; // 读取产品数量
    vector<Product> products(n);
    for (int i = 0; i < n; ++i) {
        products[i].id = i + 1;
        cin >> products[i].timeA; // 读取每个产品在 A 车间的加工时间
    }
    for (int i = 0; i < n; ++i) {
        cin >> products[i].timeB; // 读取每个产品在 B 车间的加工时间
    }

    vector<Product> group1, group2;
    for (const auto& product : products) {
        if (product.timeA <= product.timeB) {
            group1.push_back(product);
        } else {
            group2.push_back(product);
        }
    }

    // 对组 1 按在 A 车间加工时间升序排序
    sort(group1.begin(), group1.end(), cmpA);
    // 对组 2 按在 B 车间加工时间降序排序
    sort(group2.begin(), group2.end(), cmpB);

    // 合并组 1 和组 2
    vector<Product> schedule;
    schedule.insert(schedule.end(), group1.begin(), group1.end());
    schedule.insert(schedule.end(), group2.begin(), group2.end());

    // 计算总的加工时间
    int timeA = 0, timeB = 0;
    for (const auto& product : schedule) {
        timeA += product.timeA;
        timeB = max(timeB, timeA) + product.timeB;
    }

    cout << timeB << endl; // 输出最少的加工时间
    for (const auto& product : schedule) {
        cout << product.id << " "; // 输出加工顺序
    }
    cout << endl;

    return 0;
}

就是基本的约翰逊法则思路,只不过先分成两组分别排序最后合并,而不是一个个放到最前或是最后。这样实际操作起来更简单,而且时间复杂度更低。

排序方式时间复杂度分析

方法一:分组排序法

这种方法的步骤如下:

  1. 分组

    • 将所有任务分成两组:组 1 和组 2。
    • 组 1:在 A 车间加工时间小于等于在 B 车间加工时间的任务。
    • 组 2:在 A 车间加工时间大于在 B 车间加工时间的任务。
  2. 排序

    • 对组 1 按在 A 车间加工时间升序排序。
    • 对组 2 按在 B 车间加工时间降序排序。
  3. 合并

    • 将组 1 和组 2 合并,组 1 在前,组 2 在后。

方法二:逐个放置法

这种方法的步骤如下:

  1. 选择最小工时

    • 找到所有任务中最小的加工时间 t
    • 如果 t 是在第一台机器上的加工时间,则将对应的任务排在最前面。
    • 如果 t 是在第二台机器上的加工时间,则将对应的任务排在最后面。
  2. 划去已排序的任务

    • 将已排序的任务从任务列表中移除。
  3. 重复上述步骤

    • 对剩余的任务重复上述步骤,直到所有任务都排序完毕。
两种方案时间复杂度分析
方法一:分组排序法
  1. 分组

    • 遍历所有任务,将它们分成两组。时间复杂度为 O(n)
  2. 排序

    • 对组 1 进行排序,时间复杂度为 O(n log n)
    • 对组 2 进行排序,时间复杂度为 O(n log n)
  3. 合并

    • 将两个已排序的组合并,时间复杂度为 O(n)

总时间复杂度为 O(n) + O(n log n) + O(n log n) + O(n) = O(n log n)

方法二:逐个放置法
  1. 选择最小工时

    • 每次选择最小的加工时间,时间复杂度为 O(n)
  2. 划去已排序的任务

    • 将已排序的任务从任务列表中移除,时间复杂度为 O(n)
  3. 重复上述步骤

    • 对剩余的任务重复上述步骤,直到所有任务都排序完毕。总共需要进行 n 次选择和移除操作。

总时间复杂度为 O(n^2)

结论

  • 分组排序法:时间复杂度为 O(n log n)
  • 逐个放置法:时间复杂度为 O(n^2)

因此,分组排序法的时间复杂度比逐个放置法低,效率更高。当然,这里没什么好讲的,一般人都会这么做,我相信很少有人纯模拟约翰逊法则一个个取最小来进行排序。

核心原理------计算任意产品加工完的时间

上篇文章我已经讲了,这里简单复制一下

# 关键代码解析

// 计算总的加工时间
int timeA = 0, timeB = 0;
for (const auto& product : schedule) {
    timeA += product.timeA;
    timeB = max(timeB, timeA) + product.timeB;
}

详细解释

  • int timeA = 0, timeB = 0;:初始化两个变量 timeA 和 timeB,分别表示当前在 A 车间和 B 车间的总加工时间。
  • for (const auto& product : schedule):遍历所有产品,按照之前确定的加工顺序 schedule
  • timeA += product.timeA;:将当前产品在 A 车间的加工时间 product.timeA 加到 timeA 上,表示当前产品在 A 车间加工完毕后的总时间。
  • timeB = max(timeB, timeA) + product.timeB;
    • max(timeB, timeA):确保 B 车间的加工时间不会早于 A 车间的加工时间,因为产品必须先在 A 车间加工完毕才能进入 B 车间。
    • + product.timeB:将当前产品在 B 车间的加工时间 product.timeB 加到 timeB 上,表示当前产品在 B 车间加工完毕后的总时间。

示例

假设有三个产品,按照 schedule 的顺序如下:

产品A 车间时间B 车间时间
136
252
381

计算过程如下:

  • timeA = 0 + 3 = 3
  • timeB = max(0, 3) + 6 = 9
  • timeA = 3 + 5 = 8
  • timeB = max(9, 8) + 2 = 11
  • timeA = 8 + 8 = 16
  • timeB = max(11, 16) + 1 = 17

最终,总的加工时间 timeB 为 17。

原理总结

简而言之,对于任意一个产品,先把它在A工厂加工的时间直接加上,算出他在A工厂出来的时间节点。但是,从A工厂出来并不意味着能直接进入B工厂加工,根据上一个产品最终加工完成的时间,也就是B工厂空闲下来的时间,取一个max,从而得到最终进入B工厂加工的时间。再加上在B工厂加工的时间,得到最后的总时间。

大佬的博客代码(添上了点注释)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
using namespace std;

int n, ans; // n 表示任务数量,ans 表示最终的最小加工时间
struct Node {
    int a, b, c; // a 表示在 A 机器上的加工时间,b 表示在 B 机器上的加工时间,c 表示任务的原始顺序
} x[1500];

// 比较函数,用于对任务进行排序
bool cmp(Node x, Node y) {
    return min(y.b, x.a) < min(x.b, y.a);
}

int main() {
    scanf("%d", &n); // 读取任务数量
    for (int i = 1; i <= n; i++) scanf("%d", &x[i].a); // 读取每个任务在 A 机器的加工时间
    for (int i = 1; i <= n; i++) {
        scanf("%d", &x[i].b); // 读取每个任务在 B 机器的加工时间
        x[i].c = i; // 记录任务的原始顺序
    }
    // 使用 sort 函数对任务进行排序,排序依据是 cmp 比较函数
    sort(x + 1, x + n + 1, cmp);

    int ta = 0, tb = 0; // ta 表示当前在 A 机器的总加工时间,tb 表示当前在 B 机器的总加工时间
    for (int i = 1; i <= n; i++) {
        ta += x[i].a; // 更新 A 机器的总加工时间
        tb = max(ta, tb) + x[i].b; // 更新 B 机器的总加工时间,确保 B 机器的加工时间不会早于 A 机器的加工时间
    }
    printf("%d\n", tb); // 输出最少的加工时间
    for (int i = 1; i <= n; i++) printf("%d ", x[i].c); // 输出加工顺序
    return 0;
}
bool cmp(Node x, Node y) {
    return min(y.b, x.a) < min(x.b, y.a);
}

其中,这段比较规则是整个代码的关键,它通过一个简洁而高效的表达式,成功地制定了任务的排序规则,从而确保了任务的最优排列。可以说这是整篇代码的精髓所在。之后的内容就是把排完序的任务一个一个取出来进行时间计算,与我代码的后半段基本一致。

证明过程(反证法验证)

详细正向证明见高手的博客,我这里不详细赘述。总而言之,要想正儿八经推出这个简短而精确的式子,可谓是不简单,我看证明都看了老半天,更别提自己证了。所以,我这里提供一下我的反证法,来反向验证这个式子的正确性。

针对甲乙两个产品,我们对其在AB工厂的加工时间分别设为甲:a,b  乙:c,d

针对这行代码

bool cmp(Node x, Node y) {
    return min(y.b, x.a) < min(x.b, y.a);
}

翻译结果就是min(d,a)<min(b,c)

这是得出的结论,现在我们将其视作条件来反证

如果甲排在乙产品前面,那么依据核心原理(见上面),两个产品依次完成的时间为a+max(b,c)+d

如果乙排在甲前面,那么总时间为c+max(a,d)+b

要想证明a+max(b,c)+d<c+max(a,d)+b

即证明c+b-(a+d)+max(a,d)-max(b,c)>0

为了分析不等式

c + b - (a + d) + max(a, d) - max(b, c) > 0

对于所有 a, b, c, d 为正整数的情况是否恒成立,我们需要仔细考虑不同情况下 max(a, d) 和 max(b, c) 的取值,并进行详细的数学推导。

分析步骤

  1. 简化不等式: c + b - a - d + max(a, d) - max(b, c) > 0

  2. 考虑不同的最大值情况:

情况 1: a ≥ d 且 b ≥ c
  • max(a, d) = a
  • max(b, c) = b

代入不等式: c + b - a - d + a - b > 0 简化后: c - d > 0 即: c > d

代入条件 min(d, a) < min(b, c): 由于 a ≥ d,则 min(d, a) = d。 因此,条件变为 d < min(b, c)。

因为 c > d 已经满足了条件 d < min(b, c),所以该不等式在这种情况下成立。

情况 2: a ≥ d 且 b < c
  • max(a, d) = a
  • max(b, c) = c

代入不等式: c + b - a - d + a - c > 0 简化后: b - d > 0 即: b > d

代入条件 min(d, a) < min(b, c): 由于 a ≥ d,则 min(d, a) = d。 因此,条件变为 d < min(b, c)。

因为 b > d 已经满足了条件 d < min(b, c),所以该不等式在这种情况下也成立。

情况 3: a < d 且 b ≥ c
  • max(a, d) = d
  • max(b, c) = b

代入不等式: c + b - a - d + d - b > 0 简化后: c - a > 0 即: c > a

代入条件 min(d, a) < min(b, c): 由于 a < d,则 min(d, a) = a。 因此,条件变为 a < min(b, c)。

因为 c > a 已经满足了条件 a < min(b, c),所以该不等式在这种情况下成立。

情况 4: a < d 且 b < c
  • max(a, d) = d
  • max(b, c) = c

代入不等式: c + b - a - d + d - c > 0 简化后: b - a > 0 即: b > a

代入条件 min(d, a) < min(b, c): 由于 a < d,则 min(d, a) = a。 因此,条件变为 a < min(b, c)。

因为 b > a 已经满足了条件 a < min(b, c),所以该不等式在这种情况下也成立。

总结与结论

通过上述四种情况的分析,并代入条件 min(d, a) < min(b, c),我们可以得出以下结论:

  • 在情况 1 中,需要满足 c > d,并且该条件在 d < min(b, c) 下成立。
  • 在情况 2 中,需要满足 b > d,并且该条件在 d < min(b, c) 下成立。
  • 在情况 3 中,需要满足 c > a,并且该条件在 a < min(b, c) 下成立。
  • 在情况 4 中,需要满足 b > a,并且该条件在 a < min(b, c) 下成立。

因此,在给定条件 min(d, a) < min(b, c) 下,原不等式对任意正整数 a, b, c, d 都成立。

正常逻辑推理过程:

Johnson's Rule 的数学证明基于交换论证法。交换论证法的核心思想是通过交换两个任务的位置,证明交换后的顺序不会比原来的顺序更差,从而证明原来的顺序是最优的。

交换论证法

假设有两个任务 i 和 j,它们在 A 机器和 B 机器上的加工时间分别为 a_i, b_i 和 a_j, b_j。我们需要证明,如果 x.a + max(x.b, y.a) + y.b < y.a + max(x.a, y.b) + x.b,那么任务 i 应该排在任务 j 的前面。

  1. 假设当前顺序是 i 在前,j 在后

    • 总加工时间为 T(i, j) = a_i + max(b_i, a_j) + b_j
  2. 交换顺序,假设 j 在前,i 在后

    • 总加工时间为 T(j, i) = a_j + max(b_j, a_i) + b_i
  3. 比较两种顺序的总加工时间

    • 如果 x.a + max(x.b, y.a) + y.b < y.a + max(x.a, y.b) + x.b,则 T(i, j) <= T(j, i)

通过交换论证法,可以证明 Johnson's Rule 的比较函数 x.a + max(x.b, y.a) + y.b < y.a + max(x.a, y.b) + x.b 能够确保局部最优,从而推广到全局最优。

思考与改进------直接根据目的构造cmp函数来排序

要想经过正常的证明,得到这样简单的排序规则实在是有些难

bool cmp(Node x, Node y) {
    return min(y.b, x.a) < min(x.b, y.a);
}

我们能不能不知道这个公式的情况下,也将这题做出来?

答案是可以的。

推导 cmp 排序逻辑

为了确保总的加工时间最短,我们需要确定任务的最佳顺序。我们通过比较两个任务 x 和 y 的加工时间,来确定它们的相对顺序。

比较两个任务的加工时间

假设有两个任务 x 和 y,它们在 A 机器和 B 机器上的加工时间分别为 x.a, x.b 和 y.a, y.b。我们需要比较以下两种情况:

  1. 任务 x 在前,任务 y 在后

    • 总加工时间为 T(x, y) = x.a + max(x.b, y.a) + y.b
  2. 任务 y 在前,任务 x 在后

    • 总加工时间为 T(y, x) = y.a + max(y.b, x.a) + x.b

比较公式

为了确定任务的最佳顺序,我们比较 T(x, y) 和 T(y, x)
 

bool cmp(Node x, Node y) {
    return x.a + max(x.b, y.a) + y.b < y.a + max(x.a, y.b) + x.b;
}
  • 如果 x.a + max(x.b, y.a) + y.b < y.a + max(x.a, y.b) + x.b,则任务 x 应该排在任务 y 的前面。
  • 否则,任务 y 应该排在任务 x 的前面。

为什么这种排序逻辑能够确保整体最优?

  1. 贪心选择性质

    • 每一步都选择当前最优的解,这个选择依赖于当前的状态,不考虑未来的后果。
    • 通过比较两个任务在不同机器上的加工时间,确定它们的相对顺序,从而确保每一步的选择都是局部最优的。
  2. 最优子结构性质

    • 一个问题的最优解包含其子问题的最优解。
    • 通过对任务进行两两比较,并按照比较结果进行排序,可以确保每个子问题的最优解,从而构建出全局最优解。

因而,我们直接根据我们要实现的目的,推导出我们的cmp排序逻辑,从而简化问题。

与简化后的公式的时间复杂度对比

直接比较函数

bool cmp(Node x, Node y) {
    return x.a + max(x.b, y.a) + y.b < y.a + max(x.a, y.b) + x.b;
}

大佬简化后的比较函数

bool cmp(Node x, Node y) {
    return min(y.b, x.a) < min(x.b, y.a);
}

运行效率分析

  1. 原始比较函数

    • x.a + max(x.b, y.a) + y.b 和 y.a + max(x.a, y.b) + x.b 各包含一次 max 操作和三次加法操作。
    • max 操作的时间复杂度是常数时间 O(1)
    • 加法操作的时间复杂度也是常数时间 O(1)
    • 因此,原始比较函数的时间复杂度是 O(1)
  2. 化简后的比较函数

    • min(y.b, x.a) 和 min(x.b, y.a) 各包含一次 min 操作。
    • min 操作的时间复杂度是常数时间 O(1)
    • 因此,化简后的比较函数的时间复杂度是 O(1)

运行效率对比

从时间复杂度的角度来看,两个比较函数的时间复杂度都是常数时间 O(1)。因此,在理论上,它们的运行效率是相同的。

实际运行效率

虽然两个比较函数的时间复杂度相同,但在实际运行中,化简后的比较函数可能会稍微快一些,因为它涉及的操作更少。具体来说:

  • 原始比较函数包含两次 max 操作和六次加法操作。
  • 化简后的比较函数包含两次 min 操作。

由于 min 和 max 操作的时间复杂度都是常数时间 O(1),但加法操作的数量不同,因此化简后的比较函数可能会稍微快一些。

对比总结

尽管化简后的比较函数在实际运行中可能稍微快一些,但由于两者的时间复杂度均为 O(1),这种差异通常可以忽略不计。对于数学方面不是很擅长地coder而言,直接构造原始比较函数可能是更为合理的选择。特别是当性能提升微乎其微时,花费大量精力进行化简并不值得。我们完全可以直接根据问题本身要我们比较的内容,直接制定cmp函数,来实现整体贪心算法的排序逻辑。

最终代码

#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;

int n, ans;
struct Node {
    int a, b, c;
} x[1500];

// 比较函数,用于对任务进行排序
bool cmp(Node x, Node y) {
    return x.a + max(x.b, y.a) + y.b < y.a + max(x.a, y.b) + x.b;
}

int main() {
    scanf("%d", &n); // 读取任务数量
    for (int i = 1; i <= n; i++) {
        scanf("%d", &x[i].a); // 读取每个任务在 A 机器的加工时间
    }
    for (int i = 1; i <= n; i++) {
        scanf("%d", &x[i].b); // 读取每个任务在 B 机器的加工时间
        x[i].c = i; // 记录任务的原始顺序
    }
    // 使用 sort 函数对任务进行排序,排序依据是 cmp 比较函数
    sort(x + 1, x + n + 1, cmp);

    int ta = 0, tb = 0; // ta 表示当前在 A 机器的总加工时间,tb 表示当前在 B 机器的总加工时间
    for (int i = 1; i <= n; i++) {
        ta += x[i].a; // 更新 A 机器的总加工时间
        tb = max(ta, tb) + x[i].b; // 更新 B 机器的总加工时间,确保 B 机器的加工时间不会早于 A 机器的加工时间
    }
    printf("%d\n", tb); // 输出最少的加工时间
    for (int i = 1; i <= n; i++) {
        printf("%d ", x[i].c); // 输出加工顺序
    }
    return 0;
}

一点点心得与体会

在解决调度问题时,贪心算法是一种非常有效的策略。通过贪心算法,我们可以直接根据问题的要求来制定比较函数 cmp,从而简化问题的解决过程。以下是我在使用贪心算法解决问题时的一些心得与体会:

  1. 直接根据问题要求制定比较函数

    • 在解决调度问题时,我们可以直接根据问题的要求来制定比较函数 cmp。例如,在这个问题中,我们需要比较两个任务在不同机器上的加工时间,从而确定它们的相对顺序。
    • 通过比较两个任务的加工时间,我们可以确保每一步的选择都是局部最优的,从而构建出全局最优解。
  2. 简化问题的解决过程

    • 通过直接制定比较函数,我们可以简化问题的解决过程,而不需要进行复杂的数学化简。
    • 这种方法不仅提高了代码的可读性和可维护性,还减少了出错的可能性。
  3. 数学化简可以省略

    • 虽然数学化简可以提高代码的运行效率,但在实际应用中,这种提升通常是微乎其微的。
    • 因此,对于数学方面不是很擅长的开发者而言,直接根据问题要求制定比较函数可能是更为合理的选择。
  4. 贪心算法的核心思想

    • 贪心算法的核心思想是每一步都选择当前最优的解,这个选择依赖于当前的状态,不考虑未来的后果。
    • 通过这种局部最优的选择,我们可以确保整体的最优解。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值