什么是合理的方法来改善解决递归问题?

时间:2011-01-03 04:10:12

标签: c++ algorithm recursion

我喜欢在TopCoder网站上解决算法问题。我可以实现大多数基本的递归问题,例如回溯,dfs ......但是,每当我遇到复杂的递归时,通常需要花费数小时和数小时。当我检查其他程序员的解决方案时,我对自己感到非常羞耻。我已经编程了将近5年。我可以看到其他编程技术的重大改进,例如操纵字符串,图形,GUI ......但不是递归?任何人都可以分享一些如何处理递归问题的经验吗?谢谢!

更新

我熟悉单元测试方法。甚至在我知道单元测试之前,我经常会编写一些小的测试函数来查看结果是否符合我的要求。面对递归问题时,我自然会失去这种能力。我可以插入几个“cout”语句来查看当前结果,但是当调用深度嵌套时,我不再能够跟踪它。所以大部分时间,我要先用铅笔和纸来解决它,要么我已经完成了(不能使用常规方法,比如将它分成小块)。我觉得递归必须作为一个整体工作。

最好的问候,

9 个答案:

答案 0 :(得分:8)

我发现铅笔和纸张非常方便。将问题分成更小的块也是一个好主意,例如使用非常小的数据集。您应该做的第一件事是确定您的基本条件,即标记递归调用结束的条件。从那里你可以处理递归问题的主体,并使用更大的数据集来测试/验证它。

我还想补充一点,速度并不是成为优秀工程师的唯一资格。工程师可以拥有许多其他技能,包括能够在框外观察和思考,说服其他人了解特定行动方案,打破问题并向外行人员(利益相关者和客户)解释这些技能等等。更多。

答案 1 :(得分:6)

这是一个非常好的问题。

我得到的最佳答案是分解:分而治之。这在C ++中有点棘手,因为它不能很好地支持更高阶的函数,但你可以做到。最常见的例程是地图和折叠等。 [C ++已经有一个名为std :: accumulate的cofold]。

您必须仔细考虑的另一件事是如何构造代码以尽可能提供尾递归。很快就会认识到尾部调用并将它们视为循环,这可以减少大脑过载在各处递归。

另一项优秀技术称为 trust 。这意味着,你写了一个你可能还没有定义的函数的调用,并且你信任它会做你想做的事情而不用多说。例如,您相信它将正确访问树的节点,即使它必须调用您当前正在编写的函数。写下评论说明前后条件是什么。

另一种方法(我很抱歉)首先使用像Ocaml或Haskell这样的真正的编程语言,然后尝试将漂亮的干净代码翻译成C ++。通过这种方式,您可以更轻松地查看结构,而不会陷入家务细节,难以理解的语法,缺乏本地化以及其他内容。一旦你做对了,你可以机械地将它翻译成C ++。 (或者您可以使用Felix为您翻译)

我说对不起的原因是..如果你这样做,你就不会再想写C ++了,这将使得很难找到一份令人满意的工作。例如,在Ocaml中,只需添加列表元素(不使用折叠):

let rec addup (ls: int list) : int = match ls with 
| [] -> 0                (* empty list *)
| h::t -> h + addup t    (* add head to addup of tail: TRUST addup to work *)

这不是尾递归,但这是:

let addup (ls: int list) : int = 
  let rec helper ls sum = match ls with
  | [] -> sum
  | h :: t -> helper t (h+ sum)
  in
helper ls 0

上述转变众所周知。当你理解它正在做什么时,第二个例程实际上更简单。我太懒了把它翻译成C ++,也许你可以对它进行转码..(算法的结构本身应该足以弄清楚语法)

答案 2 :(得分:2)

问题的哪些部分会花费数小时和数小时?

其他编码员的解决方案你自己没有想到了什么?

作为一般建议,请记住考虑基本情况,然后记住您认为必须在递归的每个级别保持的不变量。错误经常出现,因为在递归调用中没有正确保留不变量。

答案 3 :(得分:2)

我曾经去过夏令营,为喜欢编程的疯狂青少年做准备。他们教我们解决问题的“法语方法”(内部参考)(递归和其他)。

1)在您的所有者词中定义您的问题,并做一些有用的例子。

2)进行观察,考虑边缘情况,约束(例如:“算法必须处于最差O(n log n)”)

3)决定如何解决问题:图论,动态规划(复述),组合学。

从此处开始特定的递归:

4)确定“子问题”,通常可以帮助猜测约束中可能存在多少子问题,并使用它来猜测。最终,一个子问题会在你脑中“点击”。

5)选择自下而上或自上而下的算法。

6)代码!

在这些步骤中,所有内容都应该在纸上用漂亮的笔直到第6步。在编程比赛中,那些立即开始使用的人通常会有低于标准的表现。

走路总能帮我算出算法,也许它对你有帮助!

答案 4 :(得分:2)

获取 The Little Schemer 的副本并完成练习。

请不要使用Scheme而不是C ++或C#或任何您喜欢的语言来推迟本书。 Douglas Crockford说(早期版本称为 The Little LISPer ):

  

1974年,Daniel P. Friedman出版   一本名为 The Little的小书   利斯佩尔。它只有68页,但它   做了一件了不起的事:它可以教   你要递归地思考。它使用了一些   假装LISP的方言(这是   当时用全部大写写的)。   方言并不完全符合   任何真正的LISP。但那没关系,因为   事实并非如此,关于LISP   关于递归函数。

答案 5 :(得分:0)

尝试在c ++ 0x :)中自动记忆。原帖:http://slackito.com/2011/03/17/automatic-memoization-in-cplusplus0x/

和我的递归函数的mod:

#include <iostream>
#include <functional>
#include <map>

#include <time.h>

//maahcros
#define TIME(__x) init=clock(); __x; final=clock()-init; std::cout << "time:"<<(double)final / ((double)CLOCKS_PER_SEC)<<std::endl;
#define TIME_INIT  clock_t init, final;
void sleep(unsigned int mseconds) { clock_t goal = mseconds + clock(); while (goal > clock()); }

//the original memoize
template <typename ReturnType, typename... Args>
std::function<ReturnType (Args...)> memoize(std::function<ReturnType (Args...)> func)
{
    std::map<std::tuple<Args...>, ReturnType> cache;
    return ([=](Args... args) mutable  {
        std::tuple<Args...> t(args...);
        if (cache.find(t) == cache.end()) {
            cache[t] = func(args...);
        }
        return cache[t];
    });
}

/// wrapped factorial
struct factorial_class {

    /// the original factorial renamed into _factorial
    int _factorial(int n) {
        if (n==0) return 1;
        else {
         std::cout<<" calculating factorial("<<n<<"-1)*"<<n<<std::endl;
         sleep(100);
         return factorial(n-1)*n;
        }
    }

    /// the trick function :)
    int factorial(int n) {
        if (memo) return (*memo)(n);
        return _factorial(n);
    }

    /// we're not a function, but a function object
    int operator()(int n) {
        return _factorial(n);
    }

    /// the trick holder
    std::function<int(int)>* memo;

    factorial_class() { memo=0; }
};

int main()
{
 TIME_INIT
    auto fact=factorial_class(); //virgin wrapper
    auto factorial = memoize( (std::function<int(int)>(fact) ) ); //memoize with the virgin wrapper copy
    fact.memo=&factorial; //spoilt wrapper
    factorial = memoize( (std::function<int(int)>(fact) ) ); //a new memoize with the spoilt wrapper copy

    TIME ( std::cout<<"factorial(3)="<<factorial(3)<<std::endl; ) // 3 calculations
    TIME ( std::cout<<"factorial(4)="<<factorial(4)<<std::endl; ) // 1 calculation
    TIME ( std::cout<<"factorial(6)="<<factorial(6)<<std::endl; ) // 2 calculations
    TIME ( std::cout<<"factorial(5)="<<factorial(5)<<std::endl; ) // 0 calculations

    TIME ( std::cout<<"factorial(12)="<<factorial(12)<<std::endl; )
    TIME ( std::cout<<"factorial(8)="<<factorial(8)<<std::endl;  )
    return 0;
}

答案 6 :(得分:0)

  • 识别Base case:这是识别何时停止递归的情况。

    Ex: if (n == null) { return 0; }

  • 通过将问题分解为尽可能小的情况来识别sub-problem

然后我们可以通过编码

以两种方式解决它
  • 头部递归
  • 尾递归

head recursive方法中,发生递归调用和处理。在处理第一个节点之前,我们处理列表的“其余部分”。这允许我们避免在递归调用中传递额外的数据。

tail recursive方法中,处理发生在递归调用

之前

答案 7 :(得分:-3)

动态编程有帮助。记忆也很有帮助。

答案 8 :(得分:-3)

我认为最好避免递归。在大多数情况下,循环更优雅,更容易理解。循环也更有效,长循环不会崩溃程序堆栈溢出错误。

我发现递归是最优雅的解决方案的问题很少。通常这些问题涉及图形或表面导航。幸运的是,这个领域被研究死亡,所以你可以在网上找到大量的算法。

在包含不同类型节点的一些更简单的图形(如树)中导航时,访问者模式通常比递归更简单。