4

我正在研究一个包含以下内容的问题:您正在驾驶一辆燃油消耗量为 m 的汽车(在我们的示例中,我们将采用 8l/100km)并且您正在驾驶长度为 x 的直道(例如:1000km)。汽车以一定量的燃料 f 启动(例如:22l)。您的汽车有一个大小为 g 的油箱(例如:55 升),并且在直道周围(例如 100 公里(1.45 美元/升)、400 公里(1.40 美元/升)和 900 公里)有加油站(每升价格) (1.22$/l). 我很难创建算法的目标是:用最少的停靠点(所以不是最便宜的路线,而是在加油站停靠最少的路线)找到最便宜的方式并告诉用户他必须在哪个加油站加油多少升以及需要多少钱。

目前使用递归和for循环(大概是O(n ^ 2))我已经创建了一种算法,可以解决某些复杂度达到一定程度的问题,当有大约100个加油站时它开始挣扎。

我的算法是如何工作的:

  • 转到从一开始就可用的油箱站(示例中的 22l)
  • 去每个油箱站并通过加满油箱来列出范围内的油箱站(或终点)(因为汽车可以在油箱站加油,你可以加满油箱)我确实将它保存在一个列表中,所以它不是计算了两次。
  • 然后 for 循环每个可以到达的油箱站并发生递归,然后我几乎保存了所需的最少停止次数并冲洗并重复,瞧我得到最小的答案,即(在我们的示例中)停止在 100 加油10.00 升,支付 14.5 美元,然后停在 400 加油 48 升,支付 67.20 美元

我仍然存在的问题:

  • 我如何(甚至可能)将复杂性降低到 O(N log N) 或线性,以便可以检查所有(即使是 100 多个加油站)。目前,递归方法下降到 10 次以上的递归,这使得该算法几乎无法解决超过 100 个加油站的任何问题。

  • 目前,我的算法只加油到达下一个加油站或终点所需的燃料:如果第一个加油站比第二个加油站便宜,并且你​​可以多加油“n升,那么解决问题的最佳方法是什么? “所以你在第二个加油站买的少了。因为在理想情况下,您在行程结束时还剩 0 升。

额外说明:

  • 到达加油站的燃油为 0 升算作已经到达。
  • 编辑:必须找到价格相同且站点数量最少的所有路径。

我当前的代码(片段)我认为方法名称是自我解释的,如果不是,请添加注释。,

    void findRoutes(List<GasStation> reachableStations, List<GasStation> previousStations) {
        int currentSteps = previousStations.size();
        if (currentSteps > leastSteps) {
            return;
        }
        // Reached the end (reachableStations will be null if it can reach the end)
        if (reachableStations == null) {
            // less steps
            if (currentSteps < leastSteps) {
                routes.clear();
                routes.add(previousStations);
                leastSteps = previousStations.size();
                return;
            } else {
                // same amount of steps
                routes.add(previousStations);
                return;
            }
        }
        // would be too many steps
        if (currentSteps + 1 > leastSteps) {
            return;
        }
        // Check those further away so we get a smaller step amount quicker
        Collections.reverse(reachableStations);
        for (GasStation reachableStation : reachableStations) {
            List<GasStation> newPrevious = new LinkedList<>(previousStations);
            newPrevious.add(reachableStation);
            findRoutes(reachableStation.getReachableGasStations(), newPrevious);
        }
    }
4

1 回答 1

3

tl;dr:按照评论中提到的论文,求解器的 C# 实现(如下)在老化的笔记本电脑上在大约 14 毫秒内处理了 500 个随机分布的站点的情况,因此特别是,这很容易处理 100 个站点的情况,并且比使用 MIP 求解器快几个数量级,正如评论中所建议的那样。

通常,加油站问题(我们应该真正开始称其为充电站问题,但那是另一回事)假设起始燃料量为 0:更一般的情况可能会通过添加新的起始站减少到 0 的情况在距离您的初始起点有一段距离的免费燃料,导致汽车到达您的初始起点,油箱装有您给定数量的燃料。这可以在不破坏下面解决方案的一般复杂性的情况下完成。

注意到这一点,问题归结为“加油或不加油:加油站问题”中描述的问题,正如@PySeeker在评论中所指出的那样。特别是,$O(N \log N)$ 似乎很乐观。在本文中,相关定理在 $O(\Delta N^2 \log N)$ 中处理您的情况,其中 $\Delta$ 是所需的最小停止次数(如果需要,您可以轻松地在线性时间内预先计算)。另一篇论文A fast algorithm for the gas station problem描述了如何解决 $O(\Delta N^2 + N^2 \log N)$ 中的问题,但我们只关注前一篇论文。

它的定理 2.2 解决了固定 $\Delta$ 的问题,您实际上只对可能的最低值感兴趣。由于它们的递归设置是为了解决增加 $\Delta$ 的问题,这相当于简单地停止算法,一旦 $A(s, \Delta, 0)$,在论文的符号中,变得有限。

另请注意,与处理边权重形成度量的一般图的一般问题相比(上述论文的第二篇中提到的要求,但由于某种原因不在第一篇中),您的情况对于顶点 $0 来说更简单, \dots, N - 1$ 和距离 $d_{uv} = d[v] - d[u]$。

实现算法时要小心的一件事是,虽然论文中的描述很好,但伪代码相当有缺陷/缺乏(参见例如这个问题)。下面我们实现了启动和运行算法所需的各种修复,以及一些索引以帮助提高性能。

您在编辑中提到,除了最佳解决方案的价值外,您还希望获得实际路径。下面的算法只输出值,即 $A(0, \Delta, 0)$,但是通过在值表更新时在单独的表中跟踪 argmin,您也将立即获得所需的路径. 这完全类似于您将如何实现例如 Dijkstra 算法。

您没有在问题中指定语言,所以我冒昧地用 C# 编写了这个;该代码非常C'y,因此如果需要(s/List/ArrayList/g),将其移植到Java应该很简单。符号跟随论文,所以让我简单地参考评论/文档(但我也很抱歉,如果没有手头的论文,实现可能无法阅读)。

解决方案并非一帆风顺:如上所述,存在一种复杂度更高的不同算法,尝试该算法也是很自然的;它不是特别复杂。此外,手头的实现有一个自然的性能优化,但没有包括在内:没有必要为所有 $q$ 增长表;例如,源顶点 $u = 0$ 将仅依赖于通过 的前一行R[0],因此通过预先计算 $\Delta$ 的最小值,我们可以避免一些冗余计算。

private static double Solve(double[] c, double[] d, double U)
{
    int n = d.Length;
    int t = n - 1;
    var R = new int[n][];
    var indep = new double[n][];
    var GV = new List<List<double>>();
    var GVar = new List<Dictionary<int, int>>();
    for (int u = 0; u < n; u++)
    {
        var l = new List<int>();
        for (int v = u + 1; v < n; v++)
        {
            if (d[v] - d[u] <= U)
                l.Add(v);
            else break;
        }

        R[u] = l.ToArray();
        indep[u] = new double[l.Count];
    }

    for (int u = 0; u < n; u++)
    {
        var l = new List<double> { 0 };
        var gvar = new Dictionary<int, int>();
        int i = 1;
        for (int w = 0; w < u; w++)
        {
            if (c[w] < c[u] && d[u] - d[w] <= U)
            {
                l.Add(U - (d[u] - d[w]));
                gvar[w] = i++;
            }
        }

        GV.Add(l);
        GVar.Add(gvar);
    }

    int q = 0;
    var Cq1 = new double[n][];
    var Cq2 = new double[n][];
    for (int i = 0; i < n; i++)
    {
        Cq1[i] = new double[GV[i].Count];
        Cq2[i] = new double[GV[i].Count];
        for (int j = 0; j < GV[i].Count; j++)
        {
            Cq1[i][j] = double.PositiveInfinity;
            Cq2[i][j] = double.PositiveInfinity;
        }
    }

    var toggle = true;
    while (true)
    {
        var Cq = toggle ? Cq1 : Cq2;
        var prev = !toggle ? Cq1 : Cq2;
        toggle = !toggle;
        for (int i = 0; i < n; i++)
        {
            for (int j = 0; j < GV[i].Count; j++)
                Cq[i][j] = double.PositiveInfinity;
        }
        for (int u = 0; u < n; u++)
        {
            Grow(u, q, t, c, d, U, R[u], GV[u], q == 0 ? null : prev, Cq, indep[u], GVar);
            if (u == 0 && !double.IsPositiveInfinity(Cq[u][0]))
                return Cq[u][0];
        }
        q++;
    }
}

private static void Grow(int u, int q, int t, double[] c, double[] d, double U,
        int[] r, List<double> gv, double[][] prev, double[][] ret, double[] indep,
        List<Dictionary<int, int>> GVar)
{
    double cost = c[u];
    if (q == 0 || u == t)
    {
        for (int i = 0; i < gv.Count; i++)
        {
            var g = gv[i];
            if (q == 0 && g <= d[t] - d[u] && d[t] - d[u] <= U)
                ret[u][i] = (d[t] - d[u] - g) * cost;
        }
        return;
    }

    for (var i = 0; i < r.Length; i++)
    {
        var v = r[i];
        indep[i] = c[v] <= cost ? prev[v][0] + (d[v] - d[u]) * cost : prev[v][GVar[v][u]] + U * cost;
    }

    Array.Sort(indep, r);
    var j = 0;
    var w = r[j];
    for (int gi = 0; gi < gv.Count; gi++)
    {
        var g = gv[gi];
        while (g > d[w] - d[u] && c[w] <= cost)
        {
            j++;
            if (j == r.Length) return;
            w = r[j];
        }

        ret[u][gi] = indep[j] - g * cost;
    }
}

示例用法和 500 站案例的基准测试:

static void Main(string[] args)
{
    var rng = new Random();
    var sw = new Stopwatch();
    for (int k = 0; k < 100; k++)
    {
        int n = 500;
        var prices = Enumerable.Range(1, n).Select(_ => rng.NextDouble()).ToArray();
        var distances = Enumerable.Range(1, n).Select(_ => rng.NextDouble() * n).OrderBy(x => x).ToArray();
        var capacity = 15;
        sw.Start();
        var result = Solve(prices, distances, capacity);
        sw.Stop();
        var time = sw.Elapsed;
        Console.WriteLine($"{time} {result}");
        sw.Reset();
    }
}
于 2019-10-19T11:27:14.910 回答