Каковы разумные пути улучшения решения рекурсивных проблем?

Мне нравится решать проблемы с алгоритмом на сайте TopCoder. Я могу реализовать большинство основных рекурсивных задач, таких как backtracking, dfs... Однако всякий раз, когда я сталкиваюсь с сложной рекурсией, это часто требует меня часов и часов. И когда я проверяю решение других кодеров, мне так стыдно. Я программировал почти 5 лет. Я вижу значительное улучшение в других методах программирования, таких как манипулирование строкой, графикой, графическим интерфейсом... но не рекурсией? Может ли кто-нибудь поделиться опытом, как подойти к рекурсивным проблемам? Спасибо!

Обновление

Я знаком с методологией Unit-Test. Еще до того, как я знал Unit Test, я часто пишу некоторые небольшие тестовые функции, чтобы увидеть, является ли результат тем, что я хочу. Когда вы сталкиваетесь с рекурсивными проблемами, я естественным образом потерял эту способность. Я могу вставить несколько инструкций "cout", чтобы увидеть текущий результат, но когда вызов вложен глубоко, я больше не могу его отслеживать. Поэтому большую часть времени я либо решил это с помощью карандаша и бумаги, либо я сделал (не могу использовать обычный метод, например, разбивать его на более мелкие части). Я чувствую, что рекурсия должна работать в целом.

С уважением,
Chan

Ответ 1

Это очень хороший вопрос.

Лучший ответ, который у меня есть, - факторинг: разделить и победить. Это немного сложно в С++, потому что он не поддерживает функции более высокого порядка, но вы можете это сделать. Наиболее распространенными подпрограммами являются такие вещи, как карты и складки. [С++ уже имеет кодовое имя, называемое std:: accumulate].

Другая вещь, которую вы должны тщательно рассмотреть, - как структурировать свой код, чтобы обеспечить хвостовую рекурсию, где это возможно. Скоро можно распознать хвостовые звонки и думать о них как о петлях, и это значительно уменьшает перегрузку мозга от рекурсии повсюду.

Другая хорошая техника называется доверием. Это означает, что вы пишете вызов функции, которую вы еще не определили, и вы верите, что она будет делать то, что вы хотите, без дальнейших церемоний. Например, вы доверяете, что он правильно вернется к узлам дерева снизу вверх, даже если он должен вызвать функцию, которую вы сейчас пишете. Напишите комментарии, указав, что такое предварительные и пост-условия.

Другой способ сделать это (и мне очень жаль) - использовать настоящий язык программирования, например, Ocaml или Haskell, а затем попытаться перевести чистый чистый код на С++. Таким образом, вы можете легко увидеть структуру, не увязнув с деталями домашнего хозяйства, уродливым синтаксисом, отсутствием локализации и другими вещами. Как только вы это сделаете правильно, вы можете перевести его на С++ механически. (Или вы можете использовать Felix, чтобы перевести его для вас)

Причина, по которой я сказал, что я сожалею... если вы это сделаете, вы больше не захотите писать С++, что затруднит поиск удовлетворительной работы. Например, в 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

Преобразование выше хорошо известно. Вторая процедура на самом деле проще, когда вы понимаете, что она делает. Я слишком ленив, чтобы перевести это на С++, возможно, вы можете перекодировать его.. (для того, чтобы понять синтаксис, достаточно структуры алгоритмов)

Ответ 2

Я нахожу, что карандаш и бумага очень полезны. Также неплохо разбить проблему на мелкие куски, например, с использованием действительно небольшого набора данных. Первое, что вам нужно сделать, это определить ваше базовое условие, условие, которое отмечает конец рекурсивных вызовов. Оттуда вы можете работать над телом проблемы рекурсии и тестировать/проверять его с помощью больших наборов данных.

Я также хочу добавить, что скорость не является единственным квалификатором для хорошего инженера. Есть много других навыков, которыми может обладать инженер, в том числе способность видеть и мыслить вне коробки, убеждать других в том, что касается определенного курса действий, ломать проблемы и объяснять их непрофессионалу (заинтересованным сторонам и клиентам) и многое, многое больше.

Ответ 3

Какие части проблемы занимают часы и часы?

Как насчет решения других кодеров, вы сами не определились?

Как общий совет, не забудьте подумать о базовом случае, а затем вспомнить инварианты, которые, по вашему мнению, должны выполняться на каждом уровне рекурсии. Ошибки часто возникают из-за того, что инварианты не сохраняются должным образом по рекурсивным вызовам.

Ответ 4

Я однажды отправился в летний лагерь для сумасшедших подростков, которые любили программировать. Они научили нас "французскому методу" (внутреннему референдуму) для решения проблем (рекурсивных и других).

1) Определите свою проблему в своем слове владельца и сделайте несколько отработанных примеров.

2) Сделайте наблюдения, рассмотрите случаи краев, противопоказания (например: "Алгоритм должен быть в худшем случае O (n log n)" )

3) Решите, как решать теорию зонда: диаграмму, динамическое программирование (recusion), комбинатотромики.

Отсюда и далее рекурсия:

4) Определите "субарему", часто бывает полезно угадать, сколько подсумств там может быть связано с ограничениями, и использовать это для угадывания. В конце концов, суб-проблема будет "click" в вашей голове.

5) Выберите алгоритм "снизу вверх" или "сверху вниз".

6) Код!

На всех этих этапах все должно быть на бумаге с красивым пером до шага 6. В соревнованиях по программированию те, кто начинает сразу же нажимать, часто имеют более низкую производительность.

Прогулка всегда помогает мне получить алгоритм, возможно, это тоже поможет!

Ответ 5

Получите копию The Little Schemer и выполните упражнения.

Не откладывайте книгу, используя Scheme вместо С++ или С#, или как ваш любимый язык. Дуглас Крокфорд говорит (более раннее издание под названием The Little LISPer):

В 1974 году Даниэль П. Фридман опубликовал маленькая книга под названием The Little Lisper. Это было всего 68 страниц, но это сделала замечательную вещь: она могла научить вы должны думать рекурсивно. Он использовал некоторые притворный диалект LISP (который был написанных во всех шапках в те дни). Диалект не полностью соответствовал любой реальный LISP. Но это было нормально, потому что это было не совсем о LISP, это было о рекурсивных функциях.

Ответ 6

попробуйте автоматическую memoization в С++ 0x:). Оригинальное сообщение: http://slackito.com/2011/03/17/automatic-memoization-in-cplusplus0x/

и мой мод для рекурсивных функций:

#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

  • Идентифицируйте Base case: это означает, что когда останавливается рекурсивный.

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

  • Определите sub-problem, разделив проблему на наименьший возможный случай.

то мы можем подойти двумя способами, чтобы решить его путем кодирования

  • рекурсия головы
  • хвостовая рекурсия

В подходе head recursive происходит рекурсивный вызов, а затем обработка. мы обрабатываем "остальную часть" списка, прежде чем обрабатывать первый node. Это позволяет избежать передачи дополнительных данных в рекурсивный вызов.

В подходе tail recursive обработка происходит до рекурсивного вызова

Ответ 8

Динамическое программирование помогает. Воспоминание также полезно.

Ответ 9

Я думаю, что лучше избегать рекурсии. В большинстве случаев петли более элегантны и понятны. Циклы также более эффективны, и длинные петли не будут вызывать ошибку при ошибке.

Я нашел очень мало проблем для того, какая рекурсия является самым изящным решением. Обычно такие проблемы касаются навигации в графике или на поверхности. К счастью, это поле изучается до смерти, поэтому вы можете найти множество алгоритмов в сети.

При навигации по некоторому более простому графику (например, дереву), содержащему узлы разного типа, шаблон посетителя обычно проще, чем рекурсия.