- 第 193 篇 -
Date: 2026 - 03- 18 | 周三
Author: 郑龙浩(仟墨)
今日算法or技巧:回溯算法 & 蓝桥杯真题(简单题型)
文章目录
- [2026-03-18-算法刷题day26 - 回溯算法](#2026-03-18-算法刷题day26 - 回溯算法)
- [2026-03-18-算法刷题day26 - 蓝桥真题](#2026-03-18-算法刷题day26 - 蓝桥真题)
2026-03-18-算法刷题day26 - 回溯算法
本来昨晚想好了不刷回溯的题了,把重点放到基础题和其他算法上面,今天没忍住还是刷了
今天是第三天练习「回溯算法」,前两天对于回溯的题还是模模糊糊,今天可以做出简单的回溯了,但是依然需要模板 和 画图才能理解
但是有一点我依然掌握的不好,就是 一旦遇到「去重」的回溯,我就不会了将,就是可以写出不去重的版本,但是去重总是理解错误
后来我发现,我太注重于「递归」的所有过程了,我在写的时候,应该只关注「当前层的递归」,而不需要关注「递归的所有过程」,
否则,在大脑里面模拟如何递归嵌套,再如何从最底层一层层的return上去然后从某个节点再往下递归下去
这种模拟,如果简单还可以,那种复杂的,我的大脑很难模拟出来,然后就非常的难受
其实之前做普通递归和dfs的时候已经知道了这个抽象的理解方法,但是做回溯的时候我太想搞明白回溯的具体过程了,这对于目前的我来说,还是非常困难的,所以还是先不要理解细节了,抽象的理解就可以了
之前其实思考过这个问题,不要太关注与这个递归的所有过程,应该抽象的相信该递归函数已经实现了,也就是在函数中嵌套的使用递归的时候,应该赋予递归意义,比如
当前的这个函数已经可以实现某个功能了,相信该函数可以处理好后面的所有事情,这样抽象的去理解,可以节省很多大脑的使用空间,反而理解明白了
文章目录
- [2026-03-18-算法刷题day26 - 回溯算法](#2026-03-18-算法刷题day26 - 回溯算法)
- [2026-03-18-算法刷题day26 - 蓝桥真题](#2026-03-18-算法刷题day26 - 蓝桥真题)
1-力扣131-分割回文串
难度:中等
算法:回溯
【题目】
给你一个字符串 s,请你将 s 分割成一些 子串,使每个子串都是 回文串 。返回 s 所有可能的分割方案。
示例 1:
**输入:**s = "aab"
输出: [["a","a","b"],["aa","b"]]
示例 2:
输入: s = "a"
输出: [["a"]]
提示:
1 <= s.length <= 16s仅由小写英文字母组成
【思路】
注意:是 分割 出m个回文字符串,而不是 取出 m 个回文字符串
我刚开始理解错了,我以为取出所有的回文字符串
求的是所有的回文子串
所以,就要不断的去分割字符串
递归就是不断的拆分字符串
- 递归的startIndex表示的是拆分的字符串的 头下标(left)
- 递归中的for遍历的是拆分的字符串的 尾下标(right)
- 然后每次拆分都进行一次判断,如果拆分的
[left, right]是回文串,就插入结果集 - 题目中有个地方我差点忽视了,就是「分割」,而非「取出」
【代码】
cpp
/* 2026-03-18-算法打卡day26
* 1-力扣131-分割回文串
* Author:郑龙浩
* Date:2026-03-18
* 算法/技巧:回溯算法 & 递归
* 用时:30min
*/
#include "bits/stdc++.h"
using namespace std;
class Solution {
public:
vector <vector <string>> results;
vector <string> path;
vector<vector<string>> partition(string s) {
backtracking(s, 0);
return results;
}
// left是分割的右侧字符串的起始位置
void backtracking(string s, int left) {
if (left == s.size()) {
results.push_back(path);
return;
}
for (int right = left; right < s.size(); right++) {
string s2 = s.substr(left, right - left + 1);
if (IS(s2)) {
path.push_back(s2);
backtracking(s, right + 1); // 向下寻找下一个字符串
path.pop_back();
}
}
}
// 判断回文
bool IS(string s) {
int len = s.size();
for (int i = 0, j = len - 1; i < j; i++, j--) {
if (s[i] != s[j]) return false;
}
return true;
}
};
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
return 0;
}
2-力扣93-复原IP地址
【题目】
有效 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。
- 例如:
"0.1.2.201"和"192.168.1.1"是 有效 IP 地址,但是"0.011.255.245"、"192.168.1.312"和"192.168@1.1"是 无效 IP 地址。
给定一个只包含数字的字符串 s ,用以表示一个 IP 地址,返回所有可能的有效 IP 地址 ,这些地址可以通过在 s 中插入 '.' 来形成。你 不能 重新排序或删除 s 中的任何数字。你可以按 任何 顺序返回答案。
示例 1:
**输入:**s = "25525511135"
输出: ["255.255.11.135","255.255.111.35"]
示例 2:
输入: s = "0000"
输出: ["0.0.0.0"]
示例 3:
输入: s = "101023"
输出: ["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"]
提示:
1 <= s.length <= 20s仅由数字组成
【思路】
核心思想:回溯枚举所有分割方式,验证每段字符的合法性,如果合法,就加入
剪枝思路:如果剩余字符串长度 > 还需段数 * 3 或 剩余字符串长度 < 还需段数,就不需要再找后面合法字符串了
说人话就是:因为每一段要求是,长度>=1且<=3,所以每段最长为3,最短为1,剩余字符如果不够段数也不行,如果太多了,也不行
然后写一个IS函数用于判断截取的字符串是否合法
想象一个「树」结构
递归用于:定位每段截取的字符串的 头
for用于: 定位每段截取的字符串的 尾
这样就好理解一些
这道题算是模模糊糊做出来的吧,不能保证完全明白,只能希望比赛的时候可以根据些许分析 + 模板 + 题感 来套出来这种回溯吧,能对部分分就行
我第一版的代码是有问题的,后来让AI给我改了改才对,只能说模板上面没用错,大部分代码没错,错的是那种边界或者某些关键的点,思维还是不够活跃
【代码】
cpp
/* 2026-03-18-算法打卡day26
* 2-力扣93-复原IP地址-回溯算法
* Author:郑龙浩
* Date:2026-03-18
* 算法/技巧:回溯算法 & 递归
* 用时:
*/
#include "bits/stdc++.h"
using namespace std;
class Solution {
public:
vector <string> results;
string path;
vector<string> restoreIpAddresses(string s) {
backtracking(s, 0, 0);
return results;
}
// left 表示截取的字符串的头
// pointCnt 表示点的数量
void backtracking(string s, int left, int pointCnt) {
// 终止条件:到达字符串末尾 && 已经有了四个点(相当于有了四个字符串)
// 说人话就是:已经将字符串分为了四个部分了
if (left == s.size() && pointCnt == 4) {
// 先去掉path最后的'.'
path.pop_back();
results.push_back(path);
// 要恢复最后的点,否则返回上一层时的数据就是出错的
path.push_back('.');
return;
}
// 剪枝:如果剩余字符不能满足长度,直接return
int Cnt = 4 - pointCnt; // 还需要几段
int Cnt2 = s.substr(left).size(); // s剩余的字符数
//(因为每段最多3个字符,最少1个字符)
// 如果剩余字符数,不够那几段,直接return || 字符数量 > 还需段数 * 3
if (Cnt2 < Cnt || Cnt2 > Cnt * 3) return;
for (int right = left; right < s.size(); right++) {
string s2 = s.substr(left, right - left + 1);
if (IS(s2)) {
int originalSize = path.size(); // 还没加入下一个字符串时的path长度
path.append(s2 + '.');
backtracking(s, right + 1, pointCnt + 1);
path.erase(originalSize); // 删除刚才加入的字符-回溯
} else break; // 如果当前段无效,后面也不用看了,也绝对无效
}
}
// 判断截取的字符串是否是个有效整数
bool IS(string s) {
int len = s.size();
if (len == 1 && s[0] == '0') return true; // 如果长度1,字符为0,则是符合条件的
if (len > 1 && s[0] == '0') return false;// 如果长度>1,且s[0]为0,有前导0,则必须是false
int sum = 0;
// 判断是否全是数字
for (char ch : s) {
if (ch < '0' || ch > '9') return false;
sum = sum* 10 + (ch - '0');
}
// 判断加起来是否>255
if (sum > 255) return false;
return true;
}
};
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
return 0;
}
3-力扣78-子集
【题目】
给你一个整数数组 nums ,数组中的元素 互不相同 。返回该数组所有可能的子集(幂集)。
解集 不能 包含重复的子集。你可以按 任意顺序 返回解集。
示例 1:
输入: nums = [1,2,3]
输出: [[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
示例 2:
输入: nums = [0]
输出: [[],[0]]
提示:
1 <= nums.length <= 10-10 <= nums[i] <= 10nums中的所有元素 互不相同
【思路】
就是套个模板,唯一不同的就是,这次存储 「子集」的时候,要从if条件外存储
也就是,存储的不只是最终走到size的子集,中间的变换过程的子集也要算在内
插入子集在条件上面,这样能保证收集到所有组合,包括:
-
空子集(第一次进入递归时path为空)
-
中间状态的子集(每个递归层次的path)就是还没选完的组合
- 比如选了
[1]还想继续选,但先记录下来 - 比如选了
[1,2]还想继续选,但先记录下来 - 这些都是"半成品",但也是合法的子集
- 比如选了
-
完整的子集(递归到底层时自动包含)
- 比如
[1,2,3]三个都选了 - 比如
[1,3]跳过了2 - 这些都"成品"
- 比如
【代码】
cpp
/* 2026-03-18-算法打卡day26
* 3-力扣78子集-回溯算法
* Author:郑龙浩
* Date:2026-03-18
* 算法/技巧:回溯算法 & 递归
* 用时:
*/
#include "bits/stdc++.h"
using namespace std;
class Solution {
public:
vector <vector <int>> results;
vector <int> path;
vector<vector<int>> subsets(vector<int>& nums) {
backtracking(nums, 0);
return results;
}
void backtracking(vector <int>& nums, int startIndex) {
/* 正确写法:插入组合时,要写在条件上面
// 这样能保证收集到所有组合,包括:
// 1. 空子集(第一次进入递归时path为空)
// 2. 中间状态的子集(每个递归层次的path)就是还没选完的组合
- 比如选了 [1]还想继续选,但先记录下来
- 比如选了 [1,2]还想继续选,但先记录下来
- 这些都是"半成品",但也是合法的子集
// 3. 完整的子集(递归到底层时自动包含)
- 比如 [1,2,3]三个都选了
- 比如 [1,3]跳过了2
- 这些都是"成品"
*/
results.push_back(path); // 这样就不会漏掉 2 中间状态的子集
if (startIndex == nums.size()) {
return;
}
/* 错误写法,我第一次这样写的,导致我无法取出比如,所有的以单个数字为一个组合的这种结果出来
if (startIndex == nums.size()) {
results.push_back(path);
return;
}
*/
for (int cur = startIndex; cur < nums.size(); cur++) {
path.push_back(nums[cur]);
backtracking(nums, cur + 1);
path.pop_back();
}
}
};
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
return 0;
}
4-力扣92-子集II
【题目】
给你一个整数数组 nums ,其中可能包含重复元素,请你返回该数组所有可能的 子集(幂集)。
解集 不能 包含重复的子集。返回的解集中,子集可以按 任意顺序 排列。
示例 1:
输入: nums = [1,2,2]
输出: [[],[1],[1,2],[1,2,2],[2],[2,2]]
示例 2:
输入: nums = [0]
输出: [[],[0]]
提示:
1 <= nums.length <= 10-10 <= nums[i] <= 10
【思路】
正确理解for
- 第一句话 (操作层面):
for循环遍历当前可选的元素,每个元素都会被加入到当前路径中 - 第二句话 (结果层面):
每次加入新元素后形成的新路径 ,都会成为一个新的递归分支的起点
【AI生成】
这两句话说的是递归过程中同一个操作的两个侧面,不矛盾:
-
从操作执行的角度看 :
for循环中的每个i都是在当前组合的基础上添加一个新元素 -
从结果生成的角度看 :每次添加新元素后形成的新组合 ,就是接下来要探索的新分支的起点
用具体的递归层级来说明:
假设 nums = [1,2,3,4],我们正在生成所有子集。
第1层递归(初始状态):
-
path = [],startIndex = 0 -
for循环:i = 0,1,2,3
操作角度(第一句话):
-
i=0:将1加入当前空组合[]→ 得到[1] -
i=1:将2加入当前空组合[]→ 得到[2] -
...
结果角度(第二句话):
-
选择
1→ 开启以[1]为"头"的新分支 -
选择
2→ 开启以[2]为"头"的新分支 -
...
进入[1]分支(第2层递归):
-
path = [1],startIndex = 1 -
for循环:i = 1,2,3
操作角度:
-
i=1:将2加入当前组合[1]→ 得到[1,2] -
i=2:将3加入当前组合[1]→ 得到[1,3] -
...
结果角度:
-
选择
2→ 开启以[1,2]为"头"的新分支 -
选择
3→ 开启以[1,3]为"头"的新分支 -
...
【代码】
cpp
/* 2026-03-18-算法打卡day26
* 4-力扣90-子集II
* Author:郑龙浩
* Date:2026-03-18
* 算法/技巧:回溯算法 & 递归
* 这道题和力扣78子集类似,但是多了个内容,就是nums中是存在重复的元素的,所以必须要去重的操作
* 用时:
*/
#include "bits/stdc++.h"
using namespace std;
class Solution {
public:
vector <vector <int>> results;
vector <int> path;
vector<vector<int>> subsetsWithDup(vector<int>& nums) {
sort(nums.begin(), nums.end());
vector <bool> used(nums.size(), false); // 记录使用过的元素
backtracking(nums, 0);
return results;
}
void backtracking(vector <int>& nums, int startIndex) {
results.push_back(path);
if (startIndex == nums.size()) return; // 如果子集的开头到了末尾后面,也要return
// 关键理解:for循环的作用不是找到第二个可以加入path的元素,而是找到第二个组合的头元素
// 之前错误的将for的作用理解为了找到第二个可以加入path的元素
// 所以想要跳过重复的组合的话,直接不将重复的元素作为头就可以了
for (int cur = startIndex; cur < nums.size(); cur++) {
// 如果 如果 当前元素不是头元素 && 当前元素之前使用过,就continue
// 说人话就是,不将第二个相同的元素最为后面组合的头,因为第一次遇到这个元素的时候,就已经计算出来了所有的组合
if (cur > startIndex && nums[cur] == nums[cur - 1]) {
continue;
}
path.push_back(nums[cur]);
backtracking(nums, cur + 1);
path.pop_back();
}
}
};
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
Solution sol;
vector <int> nums = {1, 2, 2};
auto ans = sol.subsetsWithDup(nums);
for (auto i : ans) {
for (auto j : i) cout << j << ' ';
cout << '\n';
}
return 0;
}
2026-03-18-算法刷题day26 - 蓝桥真题
做回溯做的我头大,换个脑子,明天继续做算法题吧,等会要回宿舍了,剩下的时间做一些蓝桥杯的真题吧,从简单的做起
标签选择:蓝桥杯 省赛
排序:按照难度排序
5-蓝桥云课19701-穿越时空之门
问题描述
随着 20242024 年的钟声回荡,传说中的时空之门再次敞开。这扇门是一条神秘的通道,它连接着二进制和四进制两个不同的数码领域,等待着勇者们的探索。
在二进制的领域里,勇者的力量被转换成了力量数值的二进制表示中各数位之和。
在四进制的领域里,力量的转换规则相似,变成了力量数值的四进制表示中各数位之和。
穿越这扇时空之门的条件是严苛的:当且仅当勇者在二进制领域的力量等同于四进制领域的力量时,他才能够成功地穿越。
国王选定了小蓝作为领路人,带领着力量值从 11 到 20242024 的勇者们踏上了这段探索未知的旅程。作为小蓝的助手,你的任务是帮助小蓝计算出,在这 20242024 位勇者中,有多少人符合穿越时空之门的条件。
答案提交
这是一道结果填空题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。
【思路】
没什么技巧,纯模拟过程即可
答案:63
【代码】
cpp
/* 2026-03-18-算法打卡day26
* 5-蓝桥云课19701-穿越时空之门
* Author:郑龙浩
* Date:2026-03-18
* 题型:填空(只有输出没有输入)
* 用时:4min
*/
#include "bits/stdc++.h"
using namespace std;
typedef long long ll;
ll twoSum(ll num) {
ll result = 0;
while (num) {
result += num % 2;
num /= 2;
}
return result;
}
ll fourSum(ll num) {
ll result = 0;
while (num) {
result += num % 4;
num /= 4;
}
return result;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
ll cnt = 0;
for (int i = 1; i <= 2024; i++) {
if (twoSum(i) == fourSum(i)) cnt++;
}
cout << cnt;
return 0;
}
// 63
6-蓝桥云课19695-握手问题
问题描述
小蓝组织了一场算法交流会议,总共有 5050 人参加了本次会议。在会议上,大家进行了握手交流。按照惯例他们每个人都要与除自己以外的其他所有人进行一次握手 (且仅有一次)。但有 77 个人,这 77 人彼此之间没有进行握手 (但这 77 人与除这 77 人以外的所有人进行了握手)。请问这些人之间一共进行了多少次握手?
注意 AA 和 BB 握手的同时也意味着 BB 和 AA 握手了,所以算作是一次握手。
答案提交
这是一道结果填空的题,你只需要算出结果后提交即可。本题的结果为一个整数,在提交答案时只填写这个整数,填写多余的内容将无法得分。
【思路】
这就是个组合的题,纯数学题罢了
这道题的内容可以抽象:
- 总人数:50 人。
- 有 7 个人,他们两两之间不组成一组(即彼此不握手)
- 但这 7 人与其他 43 人 中的每个人都恰好组成一组一次(即互相握手一次)
- 其余 43 人 则两两之间正常组成一组一次。
总握手数 = C(43, 2) + C(7, 1) × C(43, 1)
然后模拟计算就可以了
答案 1204
【代码】
cpp
/* 2026-03-18-算法打卡day26
* 6-蓝桥云课19695-握手问题
* Author:郑龙浩
* Date:2026-03-18
* 题型:填空(只有输出没有输入)
* 用时:4min
*/
#include "bits/stdc++.h"
using namespace std;
typedef long long ll;
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
cout << 43 * 42 / 2 + 7 * 43;
return 0;
}
// 1204
7-蓝桥云课3533-棋盘
问题描述
小蓝拥有 n×nn×n 大小的棋盘,一开始棋盘上全都是白子。小蓝进行了 mm 次操作,每次操作会将棋盘上某个范围内的所有棋子的颜色取反(也就是白色棋子变为黑色,黑色棋子变为白色)。请输出所有操作做完后棋盘上每个棋子的颜色。
输入格式
输入的第一行包含两个整数 nn,mm,用一个空格分隔,表示棋盘大小与操作数。
接下来 mm 行每行包含四个整数 x1x1,y1y1,x2x2,y2y2,相邻整数之间使用一个空格分隔,表示将在 x1x1 至 x2x2 行和 y1y1 至 y2y2 列中的棋子颜色取反。
输出格式
输出 nn 行,每行 nn 个 00 或 11 表示该位置棋子的颜色。如果是白色则输出 00,否则输出 11。
样例输入
3 3
1 1 2 2
2 2 3 3
1 1 3 3
样例输出
001
010
100
评测用例规模与约定
对于 3030% 的评测用例,n,m≤500n,m≤500 ;
对于所有评测用例,1≤n,m≤20001≤n,m≤2000,1≤x1≤x2≤n1≤x1≤x2≤n,1≤y1≤y2≤m1≤y1≤y2≤m。
【思路】
题目的歧义:
题目中的 「接下来 mm 行每行包含四个整数 x1x1,y1y1,x2x2,y2y2,相邻整数之间使用一个空格分隔,表示将在 x1x1 至 x2x2 行和 y1y1 至 y2y2 列中的棋子颜色取反。」
我感觉描述的有歧义,让我产生了两种理解:
- 1 将
x1 ~ x2 || y1 ~ y2的矩阵颜色取反 --> 并集 - 2 将
x1 ~ x2 && y1 ~ y2的矩阵颜色取反 --> 交集
看了示例后,才知道是交集,刚开始没看示例就去写代码了,导致出错了
解题思路
这就是个普通的「二维差分」的题,直接用基础算法
【代码】
cpp
/*
* 7-蓝桥云课3533-棋盘 (修正版)
* 算法:二维差分 + 二维前缀和
* Author:郑龙浩
* Date:2026-03-18
*/
#include "bits/stdc++.h"
using namespace std;
typedef long long ll;
int main() {
ios::sync_with_stdio(0);
cin.tie(0); cout.tie(0);
int n, m;
cin >> n >> m;
// 创建差分数组,大小为 (n+2)*(n+2),多一圈边界,方便从1开始索引
vector<vector<int>> diff(n + 2, vector<int>(n + 2, 0));
// 读取m个操作并更新差分数组
for (int k = 0; k < m; ++k) {
int x1, y1, x2, y2;
cin >> x1 >> y1 >> x2 >> y2;
// 标准的二维差分区间更新操作
// 注意:此处是直接+1和-1,不进行取模运算
diff[x1][y1] += 1;
diff[x1][y2 + 1] -= 1;
diff[x2 + 1][y1] -= 1;
diff[x2 + 1][y2 + 1] += 1;
}
// 通过计算二维前缀和,得到每个位置 (i, j) 被操作的次数
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
diff[i][j] += diff[i-1][j] + diff[i][j-1] - diff[i-1][j-1];
}
}
// 输出结果,每个位置的操作次数对2取模即为最终状态
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
// 输出对2取模的结果,并确保是0或1
cout << diff[i][j] % 2;
}
cout << '\n';
}
return 0;
}