【问题标题】:Finding the cost of the cheapest way through a 2d array on x or y axis通过 x 或 y 轴上的 2d 数组查找最便宜方式的成本
【发布时间】:2021-11-26 20:45:41
【问题描述】:

最近在求职面试中被问到类似的问题,我很难想出一个更有效的解决方案。

问题的规则,假设旅行是通过 x 轴完成的,为简单起见,从左到右:

  • 在每一步中,您必须向右移动一个索引。
  • 您可以从任意行开始。
  • 您只能使用一行一次,直到您切换到另一行(因此不能返回一行)。
  • 您可以切换到任何尚未使用的行。

目标是,鉴于这些限制,找到从左到右穿过阵列的最便宜的方式。

例子:

Array: {
  {920, 239, 191, 267, 166, 879, 791, 998, 447,  617, 31,  169},
  {361, 516, 506, 279, 406, 231, 828, 410, 408,  199, 507, 671},
  {143, 69,  675, 847, 871, 704, 471, 796, 1000, 711, 42,  380},
  {559, 407, 555, 390, 672, 415, 902, 570, 803,  29,  394, 937},
  {407, 336, 427, 801, 509, 803, 267, 617, 47,   710, 529, 423},
  {377, 26,  561, 950, 134, 343, 542, 342, 549,  65,  39,  900}
};

Result: 2634
Path: 143, 69, 506, 279, 134, 343, 542, 342, 47, 29, 31

我的示例代码:

#include <iostream>
#include <algorithm>
#include <list>
#include <sstream>
#include <set>

namespace stack_overflow_example {

    int const x_dim = 6;
    int const y_dim = 12;
    int expected = 2634;

    int arr[x_dim][y_dim] =
            {
                    {920, 239, 191, 267, 166, 879, 791, 998, 447,  617, 31,  169},
                    {361, 516, 506, 279, 406, 231, 828, 410, 408,  199, 507, 671},
                    {143, 69,  675, 847, 871, 704, 471, 796, 1000, 711, 42,  380},
                    {559, 407, 555, 390, 672, 415, 902, 570, 803,  29,  394, 937},
                    {407, 336, 427, 801, 509, 803, 267, 617, 47,   710, 529, 423},
                    {377, 26,  561, 950, 134, 343, 542, 342, 549,  65,  39,  900}
            };

    int minimumValue = INT32_MAX;

    void recursive_function(int x, int y, int value, std::set<int> unusedX) {
        int summed = value + arr[x][y];
        if (summed > minimumValue) {
            return;
        }
        if ((y + 1) < y_dim) {
            recursive_function(x, y + 1, summed, unusedX);
            unusedX.erase(x);
            for (auto const &i: unusedX) {
                recursive_function(i, y + 1, summed, unusedX);
            }
        } else {
            minimumValue = minimumValue < summed ? minimumValue : summed;
        }
    }

    void calculate() {
        std::set<int> unusedX;
        for (int i = 0; i < x_dim; i++) {
            unusedX.emplace(i);
        }
        for (int i = 0; i < x_dim; i++) {
            recursive_function(i, 0, 0, unusedX);
        }
        std::cout << "Expected was: " << expected << " received " << minimumValue;
    }

}

int main() {
    stack_overflow_example::calculate();
}

我提出的解决方案可以正常工作,但算法复杂性很差,大小为 [12][24] 的数组已经花费了不可接受的时间来传输。招聘人员暗示有一个更快的答案,但我无法想出一个解决方案,这就是我希望你帮助的地方。任何编程语言都可以,我只是在 cpp 中发布了问题,因为它是我用来解决算法问题的语言。 提前致谢!

【问题讨论】:

  • 阅读A* search algorithm,它可能不是您想要的——但它是一个很好的起点。
  • 这正是使用动态编程的“接缝雕刻”所做的。只需谷歌它,你就会找到经典的解决方案。
  • 我不明白,从哪里到哪里的最短路径?
  • 更新了示例,使其更易于理解
  • 我会采用蛮力方法枚举所有可能的路径,我不明白你的解决方案,但它看起来非常优雅,除了草率的后缀增量。

标签: c++ arrays algorithm path-finding


【解决方案1】:

这是一个动态规划问题。

DP 问题可以用“子任务”(及其相关答案)和“动态函数”(根据已解决的子任务计算任务答案的函数)来描述。

在这种情况下,“子任务”是从左侧到第 i 列的部分路径的最小成本。更具体地说,子任务的“关键”(或“问题”)可以定义为Pair&lt;Set&lt;Int&gt;, Int&gt;,其中Set&lt;Int&gt;是已访问行的集合,第二个Int是最后访问的行(行当前子路径结束的地方)。子任务的“值”(或“答案”)是单个 Int — 与“键”关联的子路径的最小成本。

动态函数需要根据已经计算的长度为i的最小子路径计算长度为(i+1)的子路径的最小成本。下面的伪代码可以很好地说明这一点:

for (set, last) in "valid subpath_next indices":
  subpath_next[set, last] = min(

    ## staying on the same (last) row 
    subpath_prev[set, last],    ​
​
​    ## changing row (from j to last)
    ​min(subpath_prev[set - last, j] for j in set)     

 ​) + arr[i+1, last]

由于我们还不知道“有效的 subpath_next 索引”,因此更好的方法是“自下而上”的方式:

for (set, last) in subpath_prev.indices:
  ## staying on the same (last) row
  subpath_next[set, last] = min(
    subpath_next[set, last],
    subpath_prev[set, last] + arr[i+1, last]
  )

  ​## changing row (from last to j)
  for j in rows:
    if j is not in set:
      subpath_next[set + j, j] = min(
        subpath_next[set + j, j],
        subpath_prev[set, last] + arr[i+1, j]
      )

最初,对于第一步(i=1,路径长度为 1),subpath_prev 被初始化为每行的单个值 (subpath_prev[{i}, i]=arr[i, 0])。

subpath_prev 中拥有ith 步骤的已解决子任务集,此循环计算i+1th 步骤subpath_next 的子任务的值。之后,subpath_next 变为 subpath_prev 进行下一步。 (注意,i 的外部循环没有显示在上面的伪代码中。

在为最后一列 (i == n) 计算 subpath_next 后,答案将是 subpath_next 的元素之间的最小路径。


现在,您可能会注意到您的recursive_function 与上面给出的动态函数的相似之处。有两个主要区别:

  1. 自下而上的方法只计算一次子任务,而递归函数(没有显式缓存)将多次执行相同的工作。使用带有显式缓存的递归函数是一种通过“自上而下”的方法解决 DP 问题的可能方法。

  2. 与将x 作为子任务“关键”的一部分不同,自下而上的方法允许我们仅存储相关的中间结果,仅存储上一步的结果。这允许减少空间使用。但是,这仅在问题不需要完整的重建路径作为答案时才有效,只需要成本。如果我们要重建整个路径,我们需要将前一行存储为子任务的“键”或“值”的一部分。


复杂性分析:

m 是行数,n 是列数。

空间:O(m * 2m):

这基本上就是subpath_prev 在每一步有多少键。

时间:O(n * m * space) 或 O(n * m² * 2m):

i1 移动到n,对于每个i 步骤,我们考虑subpath_prev (space) 中的每个键,并且对于每个键,我们还迭代每一行(m) .

【讨论】:

    猜你喜欢
    • 2017-05-27
    • 1970-01-01
    • 1970-01-01
    • 2023-04-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-02
    相关资源
    最近更新 更多