【问题标题】:C++ memoization not appending map objectC++ 记忆不附加地图对象
【发布时间】:2022-01-04 08:55:46
【问题描述】:

我正在尝试在 C++ 中练习基本的动态编程,但由于某种原因,它没有将新值插入到 std::map 对象中。结果,它只是像标准斐波那契函数一样运行,而不是使用动态编程制作的快速版本。谁能告诉我我做错了什么?

int fib(int n, std::map<int, int> memo = {}) {
    if (memo.count(n) >= 1) {
        return memo[n];
    }
    if (n <= 2) {
        return 1;
    }
    else {
        int result = fib(n - 1, memo) + fib(n - 2, memo);
        memo.insert(std::pair<int,int>(n,result));
        return result;
    }
}

【问题讨论】:

  • memo 应该通过引用传递。此外,您的第一个 if 不必要地搜索了两次地图,您应该使用 find
  • 通过引用传递备忘录,它会起作用。
  • 这一定是个骗子……已经问了很多次了……
  • 另外:必须注意,在斐波那契数列中使用std::map 是非常低效的......

标签: c++ dynamic-programming memoization


【解决方案1】:

您每次都在创建一个新的map,因为您通过值而不是引用传递memo

实现这一点的另一种方法是创建一次map,然后通过引用将其传递给实际实现。下面的例子就是这样做的,它的优点是它隐藏了你如何做记忆:

int fib(int n)
{
    std::map<int, int> memo;
    return fib_impl(n, memo)
}

int fib_impl(int n, std::map<int, int> &memo) {
    if (memo.count(n) >= 1) {
        return memo[n];
    }
    if (n <= 2) {
        return 1;
    }
    else {
        int result = fib(n - 1, memo) + fib(n - 2, memo);
        memo.insert(std::pair<int,int>(n,result));
        return result;
    }
}

【讨论】:

    【解决方案2】:

    将 memo 作为值类型传递会使事情变慢。您可以将备忘录作为参考类型传递,如下所示:

    int fib(int n, std::map<int, int> &memo) {
        if (memo.count(n) >= 1) {
            return memo[n];
        }
        if (n <= 2) {
            return 1;
        }
        else {
            int result = fib(n - 1, memo) + fib(n - 2, memo);
            memo.insert(std::pair<int,int>(n,result));
            return result;
        }
    }
    int main(){
        map<int,int>memo;
        cout<<fib(5,memo)<<"\n";
    }
    

    或者,你可以全局声明备忘录。

    map<int,int>memo; 
    int fib(int n) {
        if(memo.count(n)>=1)return memo[n];
        if (n <= 2) {
            return 1;
        }
        else {
            int result = fib(n - 1) + fib(n - 2);
            memo.insert(std::pair<int,int>(n,result));
            return result;
        }
    }
    
    int main(){
         
         cout<<fib(10)<<"\n";
    }
    

    【讨论】:

    • 您好,感谢您的回答。但是,尽管您的最终解决方案是正确的,但它与降低无关...按值传递 memo 永远不会起作用。查看其他答案。
    【解决方案3】:

    正如其他答案所说,您正在按值传递备忘录,这样它就不会保留值并重用值。因为按值传递可以防止备忘录被修改,因为只有副本被修改然后丢失。

    您必须通过引用传递它。但是通过引用传递有点棘手,因为您必须在函数之外创建备忘录并手动将其传递给函数。其他答案给出了如何通过引用从外部传递它的示例。

    我想向您展示其他方法。

    第一个变体是创建并传递std::shared_ptr。此方法允许您的函数的用户不必关心自己创建备忘录,他们不需要传递任何东西。不仅如此,这个备忘录函数参数现在可能成为一个可选参数,并且可以通过函数调用自动创建。让我们看看它的样子:

    Try it online!

    #include <map>
    #include <memory>
    #include <iostream>
    
    int fib(int n, std::shared_ptr<std::map<int, int>> memo =
            std::make_shared<std::map<int, int>>()) {
        if (memo->count(n))
            return memo->at(n);
        if (n <= 2)
            return 1;
        else {
            int result = fib(n - 1, memo) + fib(n - 2, memo);
            memo->insert(std::pair<int, int>(n, result));
            return result;
        }
    }
    
    int main() {
        std::cout << fib(45) << std::endl;
    }
    

    输出:

    1134903170
    

    这是 45 斐波那契的正确输出,正如我们在 Wolfram Alpha page 中看到的那样。

    可以看到,在上面的方法中,我不必向斐波那契函数传递任何东西,带有共享指针的可选参数是自动创建的。

    人们可能很想将可选参数作为可变引用传递,如下所示:

    int fib(int n, std::map<int, int> & memo = std::map<int, int>()) {
        ..........
        fib(n - 1, memo) + ...
    }
    

    上面的这种语法会非常方便并且可以解决问题,但不幸的是 C++ 不提供将临时对象分配给可变引用的能力。所以上面的代码不起作用,也不能编译。

    只能将临时对象分配给const &amp;,如下所示:

    std::map&lt;int, int&gt; const &amp; memo = std::map&lt;int, int&gt;()

    但 const 引用不允许我们修改底层备忘录并更改其值。

    所以在这种情况下,上面使用的共享指针通过允许创建临时对象并允许对其进行修改并递归传递来为我们节省了时间。

    另一种解决任务的有趣方法是在函数体内使用thread_local 变量:

    Try it online!

    #include <map>
    #include <memory>
    #include <iostream>
    
    int fib(int n) {
        thread_local std::map<int, int> memo;
        if (memo.count(n))
            return memo.at(n);
        if (n <= 2)
            return 1;
        else {
            int result = fib(n - 1) + fib(n - 2);
            memo.insert(std::pair<int, int>(n, result));
            return result;
        }
    }
    
    int main() {
        std::cout << fib(45) << std::endl;
    }
    

    上面的解决方案允许我们根本不向函数传递任何东西,而是将 memo 作为本地函数的变量。

    线程局部修饰符允许我们在每个线程的备忘录中重用相同的计算值。如果您打算使用来自多个内核和/或线程的函数,这个 thread_local 修饰符很方便。此修饰符允许不使用任何互斥锁来同步对其的写入。但是备忘录值只能在每个线程中单独共享。下一个解决方案通过使用 static 修饰符在所有线程之间共享值来帮助我们。

    Try it online!

    #include <map>
    #include <memory>
    #include <iostream>
    
    int fib(int n) {
        static std::map<int, int> memo;
        if (memo.count(n))
            return memo.at(n);
        if (n <= 2)
            return 1;
        else {
            int result = fib(n - 1) + fib(n - 2);
            memo.insert(std::pair<int, int>(n, result));
            return result;
        }
    }
    
    int main() {
        std::cout << fib(45) << std::endl;
    }
    

    static 上述方法与线程本地方法相同,允许不向函数传递任何内容。它还允许在不同的调用之间以及不同的线程/内核之间共享所有的备忘录值。但是与线程局部解决方案不同,静态方法不是线程安全的,这意味着没有互斥同步就不能使用它,因此使用静态变量的正确方法如下:

    Try it online!

    #include <map>
    #include <memory>
    #include <iostream>
    #include <mutex>
    
    int fib(int n) {
        static std::recursive_mutex mux;
        static std::map<int, int> memo;
        std::lock_guard<std::recursive_mutex> lock(mux);
        
        if (memo.count(n))
            return memo.at(n);
        if (n <= 2)
            return 1;
        else {
            int result = fib(n - 1) + fib(n - 2);
            memo.insert(std::pair<int, int>(n, result));
            return result;
        }
    }
    
    int main() {
        std::cout << fib(45) << std::endl;
    }
    

    上面可以看到我使用了递归互斥锁,这是同步递归函数的正确方法。现在有了这个互斥体,我们的函数是线程安全的,这意味着我们可以在多核线程上使用它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2019-05-24
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-12-05
      • 1970-01-01
      相关资源
      最近更新 更多