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