【算法打卡day34(2026-03-30 周一)】DFS专项训练(今日算法:DFS & 记忆化搜索 & 回溯)

- 第 197 篇 -
Date: 2026 - 03- 30 | 周一
Author: 郑龙浩(仟墨)
今日算法:DFS & 记忆化搜索 & 回溯

2026-03-30-算法打卡day34-DFS专项训练

1-洛谷P1464-Function-DFS.cpp

【题目】

题目描述

对于一个递归函数 w(a,b,c)

  • 如果 a≤0 或 b≤0 或 c≤0 就返回值 1。
  • 如果 a>20 或 b>20 或 c>20 就返回 w(20,20,20)
  • 如果 a<b 并且 b<c 就返回 w(a,b,c−1)+w(a,b−1,c−1)−w(a,b−1,c)。
  • 其它的情况就返回 w(a−1,b,c)+w(a−1,b−1,c)+w(a−1,b,c−1)−w(a−1,b−1,c−1)

这是个简单的递归函数,但实现起来可能会有些问题。当 a,b,c 均为 15 时,调用的次数将非常的多。你要想个办法才行。

注意:例如 w(30,−1,0) 又满足条件 1 又满足条件 2,请按照最上面的条件来算,答案为 1。

输入格式

会有若干行。

并以 −1,−1,−1 结束。

输出格式

输出若干行,每一行格式:

w(a, b, c) = ans

注意空格。

输入输出样例

输入 #1复制

1 1 1

2 2 2

-1 -1 -1

输出 #1复制

w(1, 1, 1) = 2

w(2, 2, 2) = 4

说明/提示

数据规模与约定

保证输入的数在 [−9223372036854775808,9223372036854775807] 之间,并且是整数。

保证不包括 −1,−1,−1 的输入行数 T 满足 1≤T≤105。

【思路】

按照题目中的要求写出递归即可,但是会超时

所以需要使用「记忆化搜索」,所以需要将中间的结果存储下来

【代码1】记忆化搜索

cpp 复制代码
/* 2026-03-30-算法打卡day34-DFS专项训练
 * 1-洛谷P1464-Function-记忆化搜索.cpp
 * Author:郑龙浩
 * Date:2026-03-30
 * 算法:递归 & 记忆化搜索
 */

#include "bits/stdc++.h"
#include "numeric"
#include "algorithm"
using namespace std;
typedef long long ll;
int memo[21][21][21];
int 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 (memo[a][b][c] != -1) return memo[a][b][c];
    
    int result; // 目的是存储返回结果,用于记忆化搜索
    if (a < b && b < c) result = w(a, b, c - 1) + w(a, b - 1, c - 1) - w(a, b - 1, c);
    else result = w(a - 1, b, c) + w(a - 1, b - 1, c) + w(a - 1, b, c - 1) - w(a - 1, b - 1, c - 1);
    memo[a][b][c] = result;
    return result;
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    memset(memo, -1, sizeof(memo)); // memo全部初始化为-1
    int a, b, c;
    while (1) {
        cin >> a >> b >> c;
        if (a == -1 && b == -1 && c == -1) break;
        printf("w(%d, %d, %d) = %d\n", a, b, c, w(a, b, c));
    }
    return 0;
}

【代码2】记忆化搜索 & 预处理

因为洛谷中有两个特殊案例,需要再次优化

假如输入的n行当中,有很多个重复的 a b c,比如输入了了多个 w(1, 2, 3), 那么每次都要重新计算一遍,大大浪费了时间,与其如此,不如提前预处理

因为数据量很小,只需要将所有的情况提前计算出来即可,最后输出那个情况就好了

cpp 复制代码
/* 2026-03-30-算法打卡day34-DFS专项训练
 * 1-洛谷P1464-Function-记忆化搜索-优化版.cpp
 * Author:郑龙浩
 * Date:2026-03-30
 * 算法:递归 & 记忆化搜索 & 预处理
 */

#include "bits/stdc++.h"
#include "numeric"
#include "algorithm"
using namespace std;
typedef long long ll;

// 记忆化数组,存储所有0-20范围内的计算结果
// 因为题目规定:如果a>20或b>20或c>20,就返回w(20,20,20)
int memo[21][21][21];

// 递归计算w(a,b,c)函数
int w(int a, int b, int c) {
    // 条件1:如果a、b、c中有任何一个小于等于0,直接返回1
    if (a <= 0 || b <= 0 || c <= 0) return 1;
    // 条件2:如果a、b、c中有任何一个大于20,转为计算w(20,20,20)
    if (a > 20 || b > 20 || c > 20) return w(20, 20, 20);
    // 记忆化检查:如果这个状态已经计算过,直接返回结果
    if (memo[a][b][c] != -1) return memo[a][b][c];
    int result; // 存储计算结果
    // 条件3:如果a<b且b<c
    if (a < b && b < c) result = w(a, b, c - 1) + w(a, b - 1, c - 1) - w(a, b - 1, c);
    // 其他情况
    else result = w(a - 1, b, c) + w(a - 1, b - 1, c) + w(a - 1, b, c - 1) - w(a - 1, b - 1, c - 1);
    
    // 将计算结果存入记忆化数组
    memo[a][b][c] = result;
    return result;
}

/* 老版:假如输入的n行当中,有很多个重复的 a b c,比如输入了多个 w(1, 2, 3),
 * 那么每次都要重新计算一遍,大大浪费了时间,与其如此,不如提前预处理
 * 因为数据量很小,只需要将所有的情况提前计算出来即可,最后输出那个情况就好了
 */
/*
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    memset(memo, -1, sizeof(memo)); // memo全部初始化为-1
    int a, b, c;
    while (1) {
        cin >> a >> b >> c;
        if (a == -1 && b == -1 && c == -1) break;
        printf("w(%d, %d, %d) = %d\n", a, b, c, w(a, b, c));
    }
    return 0;
}
*/

// 优化版本:预处理所有的情况
int main() {
    ios::sync_with_stdio(0);
    // 初始化记忆化数组为-1,表示该状态未计算
    memset(memo, -1, sizeof(memo));
    // 预处理阶段:计算所有0-20范围内的组合
    // 时间复杂度:21×21×21 = 9261,完全可以接受
    for (int i = 0; i <= 20; i++) 
        for (int j = 0; j <= 20; j++) 
            for (int k = 0; k <= 20; k++)
                w(i, j, k);
    ll a, b, c;  // 使用long long类型,因为输入可能很大(题目范围是64位整数)
    // 循环读取输入,直到遇到-1 -1 -1
    while (1) {
        // 使用%lld格式读取long long类型
        scanf("%lld %lld %lld", &a, &b, &c);
        // 检查是否结束输入
        if (a == -1 && b == -1 && c == -1) return 0;
        int ans;  // 存储最终答案
        // 条件1:如果a、b、c中有任何一个小于等于0,答案为1
        if (a <= 0 || b <= 0 || c <= 0) ans = 1;
        // 条件2:如果a、b、c中有任何一个大于20,答案为w(20,20,20)
        else if (a > 20 || b > 20 || c > 20) ans = memo[20][20][20];
        // 其他情况:直接查表获取结果
        else ans = memo[a][b][c];
        // 按照格式输出结果
        // 注意:a,b,c是long long类型,用%lld格式输出
        printf("w(%lld, %lld, %lld) = %d\n", a, b, c, ans);
    }
    return 0;
}

2-洛谷B3621-枚举元组

算法:DFS & 回溯

【题目】

题目描述

nnn 元组是指由 nnn 个元素组成的序列。例如 (1,1,2)(1,1,2)(1,1,2) 是一个三元组、(233,254,277,123)(233,254,277,123)(233,254,277,123) 是一个四元组。

给定 nnn 和 kkk,请按字典序输出全体 nnn 元组,其中元组内的元素是在 [1,k][1, k][1,k] 之间的整数。

「字典序」是指:优先按照第一个元素从小到大的顺序,若第一个元素相同,则按第二个元素从小到大......依此类推。详情参考样例数据。

####输入格式

仅一行,两个正整数 n,kn, kn,k。

####输出格式

若干行,每行表示一个元组。元组内的元素用空格隔开。

####输入输出样例 #1

输入 #1
复制代码
2 3
输出 #1
复制代码
1 1
1 2
1 3
2 1
2 2
2 3
3 1
3 2
3 3
输入输出样例 #2

输入 #2

复制代码
3 3

输出 #2

复制代码
1 1 1
1 1 2
1 1 3
1 2 1
1 2 2
1 2 3
1 3 1
1 3 2
1 3 3
2 1 1
2 1 2
2 1 3
2 2 1
2 2 2
2 2 3
2 3 1
2 3 2
2 3 3
3 1 1
3 1 2
3 1 3
3 2 1
3 2 2
3 2 3
3 3 1
3 3 2
3 3 3
说明/提示

对于 100%100\%100% 的数据,有 n≤5,k≤4n\leq 5, k\leq 4n≤5,k≤4。

【思路】

这就是个模板题,「DFS回溯」模板题

【代码】

cpp 复制代码
/* 2026-03-30-算法打卡day34-DFS专项训练
 * 2-洛谷B3621-枚举元组
 * Author:郑龙浩
 * Date:2026-03-30
 * 算法:dfs & 回溯
 */

#include "bits/stdc++.h"
#include "numeric"
#include "algorithm"
using namespace std;
typedef long long ll;
vector <int> cur;
// depth: 递归深度
void dfs(int depth, int n, int k) {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    if (depth == n) { // 如果cur中存储了n个,就表示找到了一个结果,直接输出结果即可(depth即表示深度,也表示cur中存储的数量,或者理解为如果将递归改为for循环后的for循环的层数,可能会多一层,就是最后一层判断)
        for (int j = 0; j < n - 1; j++) cout << cur[j] << ' ';
        cout << cur[n - 1] << '\n'; // 最后一个后面不是空格,而是换行
        return;
    }
    for (int j = 1; j <= k; j++) {
        cur.push_back(j);         // 选择数字j
        dfs(depth + 1, n, k);     // 递归处理下一个位置
        cur.pop_back();           // 回溯,撤销选择
    }
}
int main() {
    ios::sync_with_stdio(0);
    cin.tie(0); cout.tie(0);
    int n, k; cin >> n >> k;
    dfs (0, n, k);
    return 0;
}

3-洛谷B3622-枚举子集

【题目】

题目描述

今有 nnn 位同学,可以从中选出任意名同学参加合唱。

请输出所有可能的选择方案。

输入格式

仅一行,一个正整数 nnn。

输出格式

若干行,每行表示一个选择方案。

每一种选择方案用一个字符串表示,其中第 iii 位为 Y 则表示第 iii 名同学参加合唱;为 N 则表示不参加。

需要以字典序输出答案。

输入输出样例 #1

输入 #1

复制代码
3

NNN
NNY
NYN
NYY
YNN
YNY
YYN
YYY
说明/提示

对于 100%100\%100% 的数据,保证 1≤n≤101\leq n\leq 101≤n≤10。

【思路】

翻译题目:有生成长度为 n 的, 有字符 N 和 Y 组成的所有可能的字符串

【代码】

cpp 复制代码
/* 2026-03-30-算法打卡day34-DFS专项训练
 * 3-洛谷B3622-枚举子集
 * Author:郑龙浩
 * Date:2026-03-30
 * 算法:DFS & 指数型枚举
 * 翻译题目:有生成长度为 n 的, 有字符 N 和 Y 组成的所有可能的字符串
 */

#include <bits/stdc++.h>
using namespace std;

int n;              // 同学总数
vector<char> path;  // 存储当前的选择方案

/**
 * 深度优先搜索函数
 * @param pos 当前处理到第几个同学
 */
void dfs(int pos) {
    // 递归终止条件:已经处理完所有同学
    if (pos == n) {
        // 输出当前方案
        for (char c : path) cout << c;
        cout << '\n';
        return;
    }
    
    // 分支1:不选第pos个同学(第pos个位置选择N)
    path.push_back('N');    // 记录选择
    dfs(pos + 1);          // 递归处理下一个位置
    path.pop_back();       // 回溯,撤销选择
    
    // 分支2:选第pos个同学(第pos个位置选择选择Y)
    path.push_back('Y');    // 记录选择
    dfs(pos + 1);          // 递归处理下一个位置
    path.pop_back();       // 回溯,撤销选择
}

int main() {
    cin >> n;     // 输入同学数量
    dfs(0);       // 从第0个同学开始搜索
    return 0;
}

4-洛谷B3623-枚举排列-DFS-排列型枚举

【题目】

题目描述

今有 nnn 名学生,要从中选出 kkk 人排成一列拍照。

请按字典序输出所有可能的排列方式。

输入格式

仅一行,两个正整数 n,kn, kn,k。

输出格式

若干行,每行 kkk 个正整数,表示一种可能的队伍顺序。

输入输出样例 #1

输入 #1

复制代码
3 2

输出 #1

复制代码
1 2
1 3
2 1
2 3
3 1
3 2
说明/提示

对于 100%100\%100% 的数据,1≤k≤n≤101\leq k\leq n \leq 101≤k≤n≤10。

【思路】

本道题使用「DFS」算法去做

翻译题目:总共n个同学,选出k个同学进行排列

将K层递归想象为

  • 有K层for循环
  • 每层for循环中选择一个[1, N]中没有选过的数字
    就容易理解多了
    当递归层到第K层的时候(从第0层开始的),就表示已经选择了K个答案了,此时就可以直接输出结果了,要return,回到上一个节点,回到上个节点后回溯,然后再继续

树结构:

pos总共有2层,也就是如果写成for循环的话,有两层for循环

pos为0的时候(层为0的时候),for遍历下一层有几种选择, 有三种

pos为1的时候(层为1的时候),for遍历下一层有几种选择

  • 当选择1时,有23两个选择
  • 当选择2...
  • ...
    pos为2的以后(层为2的时候),pos == N,直接返回结果

实际上pos层数应该是3层,但是对于我来说不容易理解,所以我理解为了

复制代码
层
0             [空]
          /     |     \
1       1       2       3
       / \     / \     / \
2     2   3   1   3   1   2

【代码】

cpp 复制代码
/* 2026-03-30-算法打卡day34-DFS专项训练
 * 4-洛谷B3623-枚举排列-DFS-排列型枚举
 * Author:郑龙浩
 * Date:2026-03-30
 * 算法:DFS & 排列型的枚举
 * 翻译:总共n个同学,选出k个同学进行排列
 * 
 */

#include <bits/stdc++.h>
using namespace std;
int N, K;
vector <int> path;
unordered_set <int> visited;
// pos 表示第pos个位置选择的同学是谁,或者理解为如果将递归改为for后的for的层数(会多一层最后用于结果判断的)
void dfs(int pos) {
    if (pos == K) { // 如果选择了K个人的话,就可以输出了
        for (int i = 0; i < K - 1; i++) cout << path[i] << ' ';
        cout << path[K - 1] << '\n';
    }
    for (int j = 1; j <= N; j++) {
        // 如果这个学生已经被选过了,跳过
        if (visited.find(j) != visited.end()) continue; // 如果之前插入过,就不能再插入了,因为这个是排列
        path.push_back(j); // 将学生j加入当前排列
        visited.insert(j); // 标记学生j已被使用
         // 递归:继续选择下一个位置的学生
        dfs(pos + 1); 
         // 回溯:撤销选择,尝试其他可能性
        path.pop_back();
        visited.erase(j);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    cin >> N >> K;
    dfs(0);
    return 0;
}

5-洛谷P10448-组合型枚举-DFS

【题目】

题目描述

从 1∼n1 \sim n1∼n 这 nnn 个整数中随机选出 mmm 个,输出所有可能的选择方案。

输入格式

两个整数 n,mn, mn,m ,在同一行用空格隔开。

输出格式

按照从小到大的顺序输出所有方案,每行 111 个。

首先,同一行内的数升序排列,相邻两个数用一个空格隔开。

其次,对于两个不同的行,对应下标的数一一比较,字典序较小的排在前面(例如 1 3 5 7 排在 1 3 6 8 前面)。

输入输出样例 #1

输入 #1

复制代码
5 3

输出 #1

复制代码
1 2 3 
1 2 4 
1 2 5 
1 3 4 
1 3 5 
1 4 5 
2 3 4 
2 3 5 
2 4 5 
3 4 5 
说明/提示

对于所有测试数据满足 0≤m≤n0 \le m \le n0≤m≤n , n+(n-m) \\le 25

【思路】

个人感觉「组合型枚举DFS」比「排列型枚举DFS」好写一些,因为组合型枚举的实现,只要是选了数字i的时候,只需要选择i之后的就好了,前面不选就好了,这个就是组合

排列的话,选择数字i个时候,可能需要选择i前面的为了凑组合

【代码】

cpp 复制代码
/* 2026-03-30-算法打卡day34-DFS专项训练
 * 5-洛谷P10448-组合型枚举-DFS
 * Author:郑龙浩
 * Date:2026-03-30
 * 算法:DFS & 组合型的枚举
 */

#include <bits/stdc++.h>
using namespace std;
int N, M;
vector <int> path;
// pos表示选择到了第pos个位置
// curNum 表示当前选择到了cur数字,然后下一个数字只能选择cur后面的才能构成组合
void dfs(int pos, int curNum) {
    if (pos == M) { // 如果到了第M个位置
        for (int i = 0; i < M; i++) 
            if (i == M - 1)cout << path[i] << '\n';
            else cout << path[i] << ' ';
        return; // 返回上一层,找其他的可能数字
    }
    // 从curNum当前数字的下一个数字开始尝试(保证了是组合,这样就不会有重复的数字重复出现在path的vector中了)
    for (int nextNum = curNum + 1; nextNum <= N; nextNum++) {
        path.push_back(nextNum); // 选择数字nextNum
        dfs(pos + 1, nextNum); // 递归选择下一个位置
        path.pop_back(); // 回溯:撤销选择,尝试其他数字-->如果想不明白,就在脑子里模拟「多叉树」的回溯过程
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    cin >> N >> M;
    dfs(0, 0); // 从第0个位置开始DFS搜索,为了让dfs在第1个位置选择的是1 2 ... N,传入的时候是从 第0个位置的数字0开始的,这样dfs第一次就会从1开始
    return 0;
}

6-烤鸡DFS-DFS回溯

【题目】

题目背景

猪猪 Hanke 得到了一只鸡。

题目描述

猪猪 Hanke 特别喜欢吃烤鸡(本是同畜牲,相煎何太急!)Hanke 吃鸡很特别,为什么特别呢?因为他有 101010 种配料(芥末、孜然等),每种配料可以放 111 到 333 克,任意烤鸡的美味程度为所有配料质量之和。

现在, Hanke 想要知道,如果给你一个美味程度 nnn ,请输出这 101010 种配料的所有搭配方案。

输入格式

一个正整数 nnn,表示美味程度。

输出格式

第一行,方案总数。

第二行至结束,101010 个数,表示每种配料所放的质量,按字典序排列。

如果没有符合要求的方法,就只要在第一行输出一个 000。

输入输出样例 #1
复制代码
11

10
1 1 1 1 1 1 1 1 1 2 
1 1 1 1 1 1 1 1 2 1 
1 1 1 1 1 1 1 2 1 1 
1 1 1 1 1 1 2 1 1 1 
1 1 1 1 1 2 1 1 1 1 
1 1 1 1 2 1 1 1 1 1 
1 1 1 2 1 1 1 1 1 1 
1 1 2 1 1 1 1 1 1 1 
1 2 1 1 1 1 1 1 1 1 
2 1 1 1 1 1 1 1 1 1 
说明/提示

对于 100%100\%100% 的数据,n≤10000n \leq 10000n≤10000。

【思路】

【代码】

cpp 复制代码
/* 2026-03-30-算法打卡day34-DFS专项训练
 * 6-烤鸡DFS-DFS回溯
 * Author:郑龙浩
 * Date:2026-03-30
 * 算法:DFS & 回溯
 */

#include <bits/stdc++.h>
using namespace std;
vector <vector <int>> results;
vector <int> path; // 存储美味方案
int N; // 需要的美味程度
int sum = 0; // 当前路径的美味程度总和
int cnt = 0; // 方案总数
// 搜索所有可能的配料方案
// 当前正在处理第pos个配料的位置(0-9)
void dfs(int pos) { // pos 是当前是第pos个配料的位置 & 正在处理当前位置选择配料
    if (sum > N) return; // 剪枝
    if (pos == 10 && sum == N) { // 如果走到了第10个位置,就表示已经有了,N个配料了 && 如果再有当前配料和为N,那么就表示总美味程度也够,直接输出配料
        results.push_back(path);
        cnt++;
        return;
    }
    // 这个条件刚开始没想起来,导致出错了,其他的写的都还行
    if (pos == 10 && sum < N) { // 如果到了第pos个配料,但是sum不为N的话,也要retun,否则后面的for会执行,就会出现一直dfs的情况
        return; 
    }
    for (int next = 1; next <= 3; next++) {
        path.push_back(next); // 选择next作为当前配料的美味值
        sum += next; // 前面路劲的美味程度 + 下一个
        dfs(pos + 1); // 递归处理下一个配料
        // 回溯:撤销当前选择,尝试其他可能性
        path.pop_back();
        sum -= next;
    }
}
int main() {
    ios::sync_with_stdio(false);
    cin.tie(0); cout.tie(0);
    cin >> N;
    // 简单剪枝:如果N<10或N>30,无解
    if (N < 10 || N > 30) {
        cout << 0 << endl;
        return 0;
    }
    dfs(0);
    cout << cnt << '\n';
    for (auto i : results) {
        for (int j : i) cout << j << ' ';
        cout << '\n';
    }
    return 0;
}
相关推荐
罗湖老棍子2 小时前
【 例 1】区间和(信息学奥赛一本通- P1547)(基础线段树和单点修改区间查询树状数组模版)
数据结构·算法·线段树·树状数组·单点修改 区间查询
旺仔.2912 小时前
常用算法 详解
数据结构·算法
今儿敲了吗2 小时前
算法复盘——差分
数据结构·c++·笔记·学习·算法
qq_398586542 小时前
平衡三进制超前进位加法器
算法
西西弟2 小时前
最短路径之Dijkstra算法(数据结构)
数据结构·算法
沉鱼.442 小时前
树形DP题目
算法·深度优先
VelinX3 小时前
【个人学习||算法】多维动态规划
学习·算法·动态规划
AlenTech3 小时前
139. 单词拆分 - 力扣(LeetCode)
算法·leetcode·职场和发展
墨韵流芳3 小时前
CCF-CSP第41次认证第一题——平衡数
c++·算法·ccf·平衡数