What are reasonable ways to improve solving recursive problems?

余生长醉 提交于 2020-01-01 01:56:07

问题


I like solving algorithm problems on TopCoder site. I can implement most of the basic recursive problems such as backtracking, dfs... However, whenever I encounter a complex recursion, it often takes me hours and hours. And when I check the solution of other coders, I feel so shame on myself. I've been programming for almost 5 years. I can see the significant improvement on other programming technique such as manipulating string, graphics, GUI ... but not recursion? Can anyone share some experiences how to approach a recursive problems? Thanks!

Update

I'm familiar with Unit-Test methodology. Even before I knew Unit Test, I often write some small test functions to see if the result is what I want. When facing recursive problems, I lost this ability naturally. I can insert several "cout" statements to see the current result, but when the call is nested deeply, I no longer can keep track of it. So most of the time, either I solved it using pencil and paper first or I'm done ( can't use regular method like breaking it into smaller pieces ). I feel like recursion has to work as a whole.

Best regards,
Chan


回答1:


This is a very good question.

The best answer I have is factoring: divide and conquer. This is a bit tricky in C++ because it doesn't support higher order functions well, but you can do it. The most common routines are things like maps and folds. [C++ already has a cofold called std::accumulate].

The other thing you have to consider carefully is how to structure your code to provide tail recursion where possible. One soon gets to recognize tail calls and think of them as loops, and this reduces the brain overload from recursing everywhere quite a bit.

Another good technique is called trust. What this means is, you write a call to a function you may not even have defined yet, and you trust that it will do what you want without further ado. For example you trust it will visit the nodes of a tree bottom up correctly, even if it has to call the function you're currently writing. Write comments stating what the pre- and post-conditions are.

The other way to do this (and I'm sorry about this) is to use a real programming language like Ocaml or Haskell first, then try to translate the nice clean code into C++. This way you can see the structure more easily without getting bogged down with housekeeping details, ugly syntax, lack of localisation, and other stuff. Once you have it right you can translate it to C++ mechanically. (Or you can use Felix to translate it for you)

The reason I said I'm sorry is .. if you do this you won't want to write C++ much anymore, which will make it hard to find a satisfying job. Example, in Ocaml, just add elements of a list (without using a fold):

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 *)

This isn't tail recursive, but this is:

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

The transformation above is well known. The second routine is actually simpler when you understand what it is doing. I'm too lazy to translate this into C++, perhaps you can transcode it.. (the structure of the algorithms alone should be enough to figure out the syntax)




回答2:


I find that a pencil and paper comes in really handy. It's also a good idea to break the problem apart into smaller chunks, such as using a really small data set. The first thing you should do is identify your base condition, the condition that marks the end of the recursive calls. From there you can work on the body of the recursion problem and test/validate it using larger data sets.

I also want to add that speed isn't the only qualifier for being a good engineer. There are many other skills an engineer can possess, including the ability to see and think outside of the box, persuade others as to a particular course of action, break problems down and explain them to the layperson (stakeholders and customers) and much, much more.




回答3:


What parts of the problem take you hours and hours?

What about the solution of other coders did you not figure out on your own?

As a general piece of advice, remember to think about the base case and then remember the invariants that you believe must hold at each level of the recursion. Bugs often arise because the invariants are not properly being preserved across recursive calls.




回答4:


I once went to a summer camp for mad teenagers who liked to program. They taught us the "French Method" (internal refernce) for solving problems (recursive & others).

1) Define your problem in your owner word, and do a few worked examples.

2) Make observations, consider edge cases, contraints (eg: "The algorithm must be at worst O(n log n)")

3) Decide how to tackle the probelem: graph theory, dynamic programming (recusion), combanitromics.

From here onwards recursion specific:

4) Identify the "sub-problem", it can often be helpful to guess how many sub-problems there could be from the constraints, and use that to guess. Eventually, a sub-problem will "click" in your head.

5) Choose a bottom-up or top-down algorithm.

6) Code!

Throughout these steps, everything should be on paper with a nice pen untill step 6. In programming competitions, those who start tapping right away often have below-par performance.

Walking always helps me get an algorithm out, maybe it will help you too!




回答5:


Get a copy of The Little Schemer and work through the exercises.

Don't be put off by the book using Scheme instead of C++ or C# or whatever your favorite language is. Douglas Crockford says (of an earlier edition, called The Little LISPer):

In 1974, Daniel P. Friedman published a little book called The Little LISPer. It was only 68 pages, but it did a remarkable thing: It could teach you to think recursively. It used some pretend dialect of LISP (which was written in all caps in those days). The dialect didn't fully conform to any real LISP. But that was ok because it wasn't really about LISP, it was about recursive functions.




回答6:


try automatic memoization in c++0x :). The original post: http://slackito.com/2011/03/17/automatic-memoization-in-cplusplus0x/

and my mod for recursive functions:

#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;
}



回答7:


  • Identify Base case : that is this identifies case when to stop recursive.

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

  • Identify the sub-problem by splitting problem into smallest possible case.

then we can approach in two ways to solve it by coding

  • head recursion
  • tail recursion

In head recursive approach, recursive call and then processing occurs. we process the “rest of” the list before we process the first node. This allows us to avoid passing extra data in the recursive call.

In tail recursive approach, the processing occurs before the recursive call




回答8:


I think it is best idea to avoid recursion. Loops are more elegant and easier to understand on most cases. Loops are also more efficient and long loops will not crash the program with stack overflow error.

I have found very few problems for what recursion is the most elegant solution. Usually such problems are about navigation in graph or on surface. Fortunately that field is studied to death so you can find plenty of algorithms on the net.

When navigating in some simpler graph (like tree) that contains nodes of different types visitor pattern is usually simpler than recursion.




回答9:


Dynamic programming helps. Memoization is also helpful.



来源:https://stackoverflow.com/questions/4582005/what-are-reasonable-ways-to-improve-solving-recursive-problems

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!