1592 字
8 分钟
递推与递归
2025-08-23
2025-10-04
统计加载中...

递推与递归#

0. 引入#

  • 递推:从小到大,自己“走过去”。实现多是循环

  • 递归:把大问题交给“更小的自己”先做;有递(下潜)归(返回)两阶段。

举两个简单的例子:

int sum(int n) {
int res = 0;
for (int i = 1; i <= n; i++) {
res += i;
}
return res;
}
int sum(int n) {
if (n == 1) return 1; // 递归出口
return sum(n - 1) + n; // 先递,再归
}

alt text

  • sum(1..n) 写成:求 sum(1..n-1):再加 n
  • 一个 5 层小“调用栈”:sum(5) 压栈到 sum(1) 再一层层返回。

add(a, b) 写成递归:

alt text

例如:

我们高中熟悉的通项公式:

f(n)=10+2×(n1)f(n) = 10 + 2\times(n-1)

递推公式(状态转移方程):

{f(1)=10f(n)=f(n1)+2n>1\left\{\begin{matrix} f(1) = 10 \\ f(n)=f(n-1)+2 & n>1 \end{matrix}\right.

1. 方法与模板#

1.1 三步方法#

  1. 定义清楚f[i]solve(x) 表示什么?
  2. 边界:最小规模的答案?如 n==0/1、空集、叶子结点。
  3. 关系:大的如何由小的组成?(先后/限制/组合)

1.2 三个模板#

(A) 递推

// 以 f[i] 为例
vector<int> f(n+1);
f[0] = 基值0; f[1] = 基值1;
for (int i = 2; i <= n; ++i) {
f[i] = 由 f[i-1], f[i-2] ... 推出;
}

(B) 递归(带记忆化)

unordered_map<int,int> memo;
int solve(int x){
if (x <= 边界) return 边界值;
if (auto it = memo.find(x); it != memo.end()) return it->second;
int ans = 通过 solve(更小规模) 得到;
return memo[x] = ans; // 仅为“去重复”
}

(C) 回溯(做选择 → 递归 → 撤销选择)

vector<int> path; // 当前选择
void dfs(int u /*深度或下标*/){
if (到达目标/长度) { 记录答案; return; }
for (auto choice : 当前可选集){
path.push_back(choice); // 做选择
dfs(u + 1); // 递
path.pop_back(); // 撤销
}
}

一个语法#

  • lambda表达式

Lambda表达式是一种在被调用的位置或作为参数传递给函数的位置定义匿名函数对象(闭包)的简便方法。Lambda表达式的基本语法如下:

[capture list] (parameter list) -> return type { function body }

下面一个例子就可以很好的理解:

  1. add(a, b)的函数形式
int add(int a, int b) {
return a + b;
}
  1. add(a, b)的lambda表达式
auto add = [&](auto a, auto b) {
return a + b;
};

2. 入门例子#

2.1 斐波那契数列#

定义:

f(0)=0,f(1)=1,f(n)=f(n1)+f(n2) (n2)f(0)=0,\quad f(1)=1,\quad f(n)=f(n-1)+f(n-2)\ (n\ge 2)
  • 递推 O(n)O(n)
long long fib_iter(int n) {
if (n < 2) return n;
long long a = 0, b = 1;
for (int i = 2; i <= n; ++i) {
long long c = a + b;
a = b, b = c;
}
return b;
}
  • 递归 + 记忆化 O(n)O(n)
long long fib_memo(int n, vector<long long>& memo) {
if (n < 2) return n;
if (memo[n] != -1) return memo[n];
return memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo);
}
  • 快速倍增 O(logn)O(logn)
pair<long long,long long> fib_fast(long long n) {
if (n == 0) return {0, 1};
auto [a, b] = fib_fast(n >> 1); // a=F(k), b=F(k+1)
long long c = a * (2*b - a); // F(2k)
long long d = a*a + b*b; // F(2k+1)
if ((n & 1) == 0) return {c, d};
else return {d, c + d};
}

递归树:

alt text

朴素递归会重复计算同一子问题(重叠子问题),导致指数级调用次数。 记忆化避免重复计算,每个 n 只算一次。

小结

  • 依赖前几个状态 → 优先迭代(快且省内存)。
  • 需要自顶向下表达式清晰 → 递归+记忆化。
  • 超大 n 或取模 → 快速倍增。

2.2 爬楼梯(基础 + 常见变体)#

基础版:每次上 1 或 2 阶,求到第 n 阶的方法数。

  • dp[i] = dp[i-1] + dp[i-2],dp[1]=1, dp[2]=2
long long climb2(int n) {
if (n <= 2) return n;
long long a = 1, b = 2;
for (int i = 3; i <= n; ++i) {
long long c = a + b;
a = b, b = c;
}
return b;
}

2.3 数字三角形(递推与记忆化二选一)#

  • 递推
int n;
cin >> n;
vector<vector<int>> a(n+1, vector<int>(n+1, 0));
for (int i = 1; i <= n; ++i)
for (int j = 1; j <= i; ++j)
cin >> a[i][j];
for (int i = n - 1; i >= 1; --i)
for (int j = 1; j <= i; ++j)
a[i][j] += max(a[i+1][j], a[i+1][j+1]);
cout << a[1][1] << '\n';
  • 记忆化
int n;
vector<vector<int>> a, memo;
int solve(int i, int j) {
if (i == n) return a[i][j];
int &res = memo[i][j];
if (res != INT_MIN) return res;
res = a[i][j] + max(solve(i+1,j), solve(i+1,j+1));
return res;
}

2.4 快速幂#

long long qpow(long long a, long long b, long long mod) {
long long res = 1 % mod;
a %= mod;
while (b > 0) {
if (b & 1) res = res * a % mod;
a = a * a % mod;
b >>= 1;
}
return res;
}

3. 重点例题:P1028《数的计算》#

题意:以 n 开头,后一项 ≤ 前一项的一半,问序列个数(“只取自己”也算一种)。

推导:

f(n)=1+1n2f(i)f(n) = 1 + \sum_{1}^{\frac{n}{2} } f(i)

方法 A:记忆化

方法 B:递推 + 前缀和 O(n)O(n)


4. 回溯入门:P1036《选数》#

题意:n 个数里选 k 个,使和是素数,求方案数(n ≤ 20)。

  • 组合回溯

  • 拓展:位枚举(非递归)

int count_prime_sum(const vector<int>& a, int k) {
int n = a.size(), ans = 0;
for (int mask = 0; mask < (1 << n); ++mask) {
if (__builtin_popcount((unsigned)mask) != k) continue;
int s = 0;
for (int i = 0; i < n; ++i)
if (mask >> i & 1) s += a[i];
ans += is_prime(s);
}
return ans;
}

5. 一个经典问题#

5.1 汉诺塔#

function<void(int,char,char,char)> hanoi = [&](int n, char A, char B, char C) {
if (n == 0) return;
hanoi(n - 1, A, C, B);
cout << A << " -> " << C << '\n';
hanoi(n - 1, B, A, C);
};

6. 拓展练习#

6.1 P1464《函数 w》#

long long memo[21][21][21];
bool vis[21][21][21];
long long w(int a, int b, int c) {
if (a <= 0 || b <= 0 || c <= 0) return 1;
if (a > 20 || b > 20 || c > 20) return w(20, 20, 20);
if (vis[a][b][c]) return memo[a][b][c];
vis[a][b][c] = 1;
long long &res = memo[a][b][c];
if (a < b && b < c)
res = w(a, b, c-1) + w(a, b-1, c-1) - w(a, b-1, c);
else
res = w(a-1, b, c) + w(a-1, b-1, c) + w(a-1, b, c-1) - w(a-1, b-1, c-1);
return res;
}

6.2 P1025《数的划分》#

把 n 划分成 k 个正整数的和(顺序不计,非降约束)

int n, k, ans;
void dfs(int pos, int last, int sum) {
if (pos == k) { ans += (sum == n); return; }
for (int x = last; x <= n; ++x) {
if (sum + x + (k - pos - 1) * x > n) break; // 剪枝:最小填充也超了
dfs(pos + 1, x, sum + x);
}
}

6.3 括号生成(回溯,约束计数)#

void gen_paren(int n) {
string path; vector<string> ans;
function<void(int,int)> bt = [&](int open, int close) {
if ((int)path.size() == 2*n) { ans.push_back(path); return; }
if (open < n) { path.push_back('('); bt(open+1, close); path.pop_back(); }
if (close < open) { path.push_back(')'); bt(open, close+1); path.pop_back(); }
};
bt(0, 0);
// 输出 ans
}

6.4 全排列(回溯就地交换)#

void perm(vector<int> a, int u, vector<vector<int>>& all) {
if (u == (int)a.size()) { all.push_back(a); return; }
for (int i = u; i < (int)a.size(); ++i) {
if (i != u && a[i] == a[u]) continue; // 若需要去重:先排序 + 这一行
swap(a[i], a[u]);
perm(a, u + 1, all);
}
}
递推与递归
https://lansganbs.cn/posts/算法竞赛/递推与递归/
作者
LANSGANBS
发布于
2025-08-23
许可协议
CC BY-NC-SA 4.0