【问题标题】:How to optimize a recursive tree traversal?如何优化递归树遍历?
【发布时间】:2020-04-25 03:24:59
【问题描述】:

我最近与一位同事讨论了以下(相当基本的)问题,并想知道如何优化它以及优化后的版本将具有什么运行时。

问题:

你必须用一套可用的硬币退还一定数量的钱。你有无限数量的硬币可用。如果无法返回此金额,则应返回错误。

示例 1:

一组硬币(本例中为欧元,单位为 ct){200, 100, 50, 20, 10, 5, 2, 1},返还金额99

回答:50, 20, 20, 5, 2, 2

示例 2:

套币{50, 20, 2},返还金额99

答案:no solution found!(显然,因为没有办法返回奇数!)。

我的解决方案:

(本文末尾的代码)。我的解决方案是一个贪婪的递归函数,但如果贪婪方法不起作用,它会尝试使用较小的硬币。最后,我基本上是以深度优先的方式遍历解空间(一棵树)。

在最坏的情况下(例如{97, 98, 99}300)找不到解决方案,我必须遍历整个树,这导致O(l^(a\s)),其中l 是硬币集合的长度( 8 在欧元的情况下,3 在这种情况下),a 是金额(示例中为 300),s 是可用的最小硬币(本示例中为 97),\integer division如果我错了,请纠正我

我正在寻找的优化:

您可以看到“解决方案树”中有一些不需要探索的路径。这是因为硬币的顺序无关紧要,但仍然会遍历不同的顺序(例如,99, 98, 9798, 99, 97 都被遍历)。 如何以只遍历必要路径的方式修剪树?我正在考虑某种缓存,但无法想出一些聪明的方法。 这种解决方案的运行时间是多少?

C++17 代码:

#include <iostream>
#include <list>
#include <optional>
#include <string>

std::string print_list(std::optional<std::list<int>> lst)
{
    if (lst)
    {
        std::string res = "";
        for (int num : lst.value())
        {
            res += std::to_string(num) + ", ";
        }
        return res + "\n";
    }
    return "no solution found!\n";
}

std::optional<std::list<int>> get_coins(int remainder, std::list<int> &currency, std::list<int> coins)
{
    if (remainder == 0)
    {
        return std::optional{coins};
    }
    for (int coin : currency)
    {
        if (coin <= remainder)
        {
            coins.push_back(coin);
            auto result = get_coins(remainder - coin, currency, coins);
            if (result)
            {
                return result;
            }
        }
    }
    return std::nullopt;
}

int main()
{
    // Example 1
    std::list<int> eur{200, 100, 50, 20, 10, 5, 2, 1};
    std::cout << print_list(get_coins(99, eur, {})); // 50, 20, 20, 5, 2, 2,
    // Example 2
    std::list<int> fail{50, 20, 2};
    std::cout << print_list(get_coins(99, fail, {})); // no solution found!
}

【问题讨论】:

  • 为什么要搜索不同的订单?
  • 我不想。我只想保持一种贪婪的方法,如果有一个解决方案,它总是会给我一个解决方案。我搜索它们是因为我正在探索整个解决方案空间,而不是因为我想要
  • 您想更快地解决问题,而又不想避免最明显的时间浪费:明白了。
  • 您是否仅在寻找解决方案或其他解决方案,可能与此不同,但会比您最初提出的解决方案更有效地完成工作?提示:可以尝试分割方法(取整个部分,忽略提示)或使用double for(嵌套;也可以用一个完成;我立即想出了双循环的代码)。

标签: c++ recursion tree runtime


【解决方案1】:

这是一个经典的动态规划问题。可以在O(返回金额*币种数)内解决。

// named vector.  A vector of coin values.
struct currency {
  std::vector<int> values;
  int const* begin() const {return values.data();}
  int const* end() const {return values.data()+values.size();}
};
// named map.  A map of coin counts
struct coins {
  std::map<int, int> count;
  // helper to remove bounds-checking elsewhere
  void add_coin( int type ) {
      ++count[type];
  }
};
struct coins_required {
  std::vector<std::optional<std::optional<int>>> count = {{0}}; // number of coins required for [i] money
  // outer optional is "have I solved this", inner is "is this possible".

  // save on bounds checking code elsewhere.
  // note, is only trustworthy if someone called solve on i already.
  std::optional<std::optional<int>> operator[](int i) {
    if (count.size() <= i) count.resize(i+1);
    return count[i];
  }
  // finds the number of coins required to make up "i" money
  std::optional<int> solve( int i, currency const& c ) {
    //std::cerr << "Solving for " << i << "\n";
    if (i == 0)
    {
      return 0;
    }
    if ( (*this)[i] )
    {
      //std::cerr << "Answer is: ";
      if (*(*this)[i]) {
        //std::cerr << **(*this)[i] << "\n";
      } else {
        //std::cerr << "no solution\n";
      }
      return *(*this)[i];
    }
    std::optional<int> solution = {};
    for (auto coin:c) {
      if (i < coin) continue;
      std::optional<int> subsolution = solve( i-coin, c );
      if (!subsolution)
        continue;
      if (solution) {
        *solution = (std::min)( *subsolution + 1, *solution );
      } else {
        solution = *subsolution + 1;
      }
    }
    // count[] is safe, as we used *this[] above
    count[i] = solution;
    if (solution)
    {
      //std::cerr << i << " needs " << *solution << " coins\n";
    }
    return solution;
  }
  // returns a vector of coin counts for money value i, given currency c.
  std::optional<coins> get_coins( int i, currency const& c ) {
    if (i==0) return coins{};
    auto count = solve(i, c);
    if (!count) return {}; // no solution
    for (auto coin:c) {
      // can we remove this coin?
      if (coin > i)
        continue;
      // Does removing this coin result in an optimal solution?
      auto subsolution = solve(i-coin, c);
      if (!subsolution || *subsolution +1 > *count)
        continue;
      // recurse!  If we hit this, we should be guaranteed a solution.
      auto retval = get_coins( i-coin, c );
      assert(retval); // logically impossible
      if (!retval) continue;
      retval->add_coin(coin);
      return retval;
    }
    assert(false);// logically impossible to reach
    return {};
  }
};

双选项很有趣。

Live example.

【讨论】:

    【解决方案2】:

    我在考虑某种缓存

    您描述的问题是一个经典的dynamic programming 问题。您只需将全球地图添加到您的解决方案中,然后将您的血统设为breadth first。做到这一点的一种方法是有一个“边界列表”,一个由一堆硬币组成的列表(或向量)。您将从 1 堆硬币开始(值为 0 的空列表)。您还可以将此添加到您的地图中。现在你定义一个有两个循环的函数。第一个循环遍历边界列表的每个成员,并创建一个新列表。在循环结束时,将新的边界列表复制到旧的边界列表上并重复。
    内循环将每种类型的硬币添加到硬币堆中。然后它计算硬币的价值,并检查地图以查看该价值是否已经产生。如果有,那么现有的地图条目必须使用更少的硬币,所以跳过这个新的堆。
    如果没有,您已经创建了一个新值,因此将新桩添加到您的地图以及新的边界列表中。 所以在伪代码中。

    frontier_list.push_back(Pile{});
    while(!map.contains[target_value]) {
        for (const auto& pile: frontier_list)
            for (const auto& coin: coin_list) {
                const auto new_pile = pile + coin;
                const auto value = add_coins(new_pile);
                if (!map.contains(value)) {
                    new_frontier_list.push_back(new_pile);
                    map[value] = new_pile;
                }
           }
       }
       frontier_list = new_frontier_list;
    }
    

    这个伪代码包含很多优化机会,特别是它包含很多复制。但是,从概念上讲,这是您使用缓存来避免回溯相同路径的方式。

    【讨论】:

      猜你喜欢
      • 2018-07-12
      • 2014-03-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-05-11
      相关资源
      最近更新 更多