簡介

先來描述一下問題,這裡以下樓梯為例:

有一個人下樓梯,一次可以走一階或兩階,假設總共有N階樓梯,共有幾總走法?例如 N = 3,他可以走 1 + 1 + 1 、1 + 2 或 2 + 1 三種走法。

實做一個程式能夠輸入 N,能回傳共有幾總走法。

演算法

這個問題直接使用 Bottom-Up 可能不容易思考,所以我們先利用遞迴的 Top-Down 思考邏輯來想這個問題,結果我們很容易就可以這樣想:

第N階的走法相當於在 N - 1 階的時候走一階過來,或者在 N - 2 階的時候走兩階過來;換句話說,就是第N階的走法等於N - 1階的走法加上N - 2階的走法。

所以我們就可以寫出遞迴式:

1
2
3
F(N) = F(N - 1) + F(N - 2)
F(1) = 1
F(2) = 2

列出來的式子和費波那西數列 (Fibonacci) 還蠻像的,我們可以用不同的方式去實做解這問題,這問題較適合用動態規劃 (Dynamic Programming) 去解,直接用 Divide and Conquer 會重覆計算子問題。由於此問題和費波那西數列相當相似,這邊只簡單實做迭代法和動態規劃的方式。

迭代法

由於每次新的問題的答案來自於前兩個問題,所以每次運算都保留前兩次的資料即可。如果新問題的答案來自前三個問題,則保留三個,以此類推。

動態規劃

使用遞迴的動態規劃則可以記錄過計算過的部分;另外也可以使用迴圈解的動態規劃,在思路上並不複雜,而且也減少Function call的消耗,這邊就不實做此方式,可參考費波那西數列這篇文章。

語法

C#

迭代法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DownStairs
{
class Iterative
{
public static long DownStairs(int n)
{
long v1 = 1;
long v2 = 2;
long result = n;
for (int i = 3; i <= n; ++i)
{
result = v2 + v1;
v1 = v2;
v2 = result;
}
return result;
}
}
}

動態規劃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace DownStairs
{
class DynamicProgramming
{
static Dictionary<int, long> map = new Dictionary<int, long>() { { 1, 1 }, { 2, 2 } };
public static long DownStairs(int n)
{
if (map.ContainsKey(n))
return map[n];
return map[n] = DownStairs(n - 1) + DownStairs(n - 2);
}
}
}

Java

迭代法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Iterative {
public static long DownStairs(int n)
{
long v1 = 1;
long v2 = 2;
long result = n;
for (int i = 2; i <= n; ++i)
{
result = v2 + v1;
v1 = v2;
v2 = result;
}
return result;
}
}

動態規劃

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.util.HashMap;
import java.util.Map;


public class DynamicProgramming {
static Map map = new HashMap() {{ put(1, 1L); put(2, 2L); }};
public static long DownStairs(int n)
{
if (map.containsKey(n))
return map.get(n);
map.put(n, DownStairs(n - 1) + DownStairs(n - 2));
return map.get(n);
}
}

C++

迭代法

Iterative.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#ifndef ITERATIVE_H
#define ITERATIVE_H


class Iterative
{
public:
static long DownStairs(int n);
protected:
private:
};

#endif // ITERATIVE_H

Iterative.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include "Iterative.h"

long Iterative::DownStairs(int n)
{
long v1 = 1;
long v2 = 2;
long result = n;
for (int i = 3; i <= n; ++i)
{
result = v2 + v1;
v1 = v2;
v2 = result;
}
return result;
}

動態規劃

DynamicProgramming.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifndef DYNAMICPROGRAMMING_H
#define DYNAMICPROGRAMMING_H

#include <map>

using namespace std;

class DynamicProgramming
{
public:
static long DownStairs(int n);
protected:
private:
static pair init[];
static map m;
};

#endif // DYNAMICPROGRAMMING_H

DynamicProgramming.cpp

1
2
3
4
5
6
7
8
9
10
#include "DynamicProgramming.h"

pair DynamicProgramming::init[] = { make_pair(1, 1), make_pair(2, 2) };
map DynamicProgramming::m(DynamicProgramming::init, DynamicProgramming::init + 2);
long DynamicProgramming::DownStairs(int n)
{
if (DynamicProgramming::m.count(n) == 1)
return DynamicProgramming::m[n];
return DynamicProgramming::m[n] = DynamicProgramming::DownStairs(n - 1) + DynamicProgramming::DownStairs(n - 2);
}

效能實測部分同樣可參考費波那西數列一文。

延伸閱讀

本文的標題叫做上下樓梯問題 & (籃球) 得分問題,到底跟分數有甚麼關係?其實得分問題本質上和上下樓梯是一樣的,只是換不同描述方式,通常會以籃球比賽為例:

籃球比賽可能的得分方式有一分球、兩分球和三分球,得 N 分的方式有幾種?

看完題目描述之後就可以看出來是差不多的,只是組合的方式變更多種。

動態規劃 (Dynamic Programming)