DFS-深度优先搜索
回溯法简介
回溯法一般使用DFS(深度优先搜索)实现,DFS是一种遍历或搜索图,树或图像等数据结构的算法。上述数据结构不保存下来就是回溯法。
常见的是搜索树,排列型搜索树(节点数一般为n!)与子集型搜索树(节点数一般为2n)。
DFS从起始点开始,沿着一条路尽可能深入,直到无法继续回溯到上一节点为止,继续搜索,直到遍历完整个树或图。DFS使用栈与递归管理节点,一般使用递归。
排列树
子集树
即4种方案。
回溯法模板
//求1~n的全排列
int a[N];
bool vis[N];//表示数字i(或某个元素)是否使用过
void dfs(int dep) {
//当dep深度等于n+1时说明n层都已经算完了,直接输出结果
if (dep == n + 1) {
for (int i = 1; i <= n; i++)cout << a[i] << ' ';
cout << '\n';
return;
}
//以上为递归出口
//向下搜索,枚举范围
for (int i = 1; i <= n; i++) {
//排除不合法路径,如果i使用过了了就不能用了
if (vis[i])continue;
//修改状态,我们选上了,i现在已经被我们用了
vis[i] = true;
a[dep] = i;
//向下一层递归,形成一个搜索树
dfs(dep + 1);
//恢复现场,只有恢复了现场我们才能不受干扰地向下一个分支进行递归
vis[i] = false;
}
}
DFS剪枝
剪枝
就是将搜索过程中的一些不必要的部分剔除,因搜索的过程构成了一棵树,剃除不必要的部分好比是减去树上枝条,故名剪枝。
剪枝是回溯法的一种重要优化手段,往往先写一个暴力搜索,然后找到某些特殊的数学关系或逻辑关系,通过它们的约束让树尽可能小,从而达到降低时间复杂度的目的。
一般来说剪枝的复杂度难以计算。
记忆化搜索
记忆化
将搜索中会重复计算且结果相同的部分保留下来,作为一个状态,下一次再访问到这个状态时将子搜索结果返回,不需要重复计算。
通常用数组或map来进行记忆化,下标与dfs参数表对应。
要保证重复计算后的结果是相同的,否则会失真。
斐波那契数列
设F[1] = 1, F[2] = 1, F[n] = F[n-1] + F[n-2]
,求F[n]
,结果对1e9 + 7取模
代码例
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int p = 1e9 + 3, N = 1e6;
int dp[N];
int fbn(int n) {
if (n == 1 || n == 2)return 1;
if (dp[n] != -1)return dp[n];//dp[n]被计算后才返回dp[n](注释掉这一句就是没用记忆化搜索的方法)
return dp[n] = (fbn(n - 1) + fbn(n - 2)) % p;//先计算等号后dp[n]的值后返回dp[n]
//dp[n]没有被计算,先计算,后返回
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
memset(dp, -1, sizeof(dp));
int n; cin >> n;
cout << fbn(n) << '\n';
return 0;
}
例题
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 15;
int n, ans;
int vis[N][N];//表示被多少个皇后占用了,等于0表示可以放皇后
void dfs(int dep) {
if (dep == n + 1) {
ans++;
return;//递归出口
}
//遍历棋盘上的这一层
for (int i = 1; i <= n; i++) {
if (vis[dep][i])continue;//不为0,这个位置不能放,找下一个位置
//修改状态
for (int _i = 1; _i <= n; ++_i)vis[_i][i]++;//列加1,因为我们是一行行枚举的,所以行可以不用加1
//四个方向的米字,_i是横轴,_j是纵轴
for (int _i = dep, _j = i; _i >= 1 && _j >= 1; --_i, --_j)vis[_i][_j]++;
for (int _i = dep, _j = i; _i <= n && _j >= 1; ++_i, --_j)vis[_i][_j]++;
for (int _i = dep, _j = i; _i >= 1 && _j <= n; --_i, ++_j)vis[_i][_j]++;
for (int _i = dep, _j = i; _i <= n && _j <= n; ++_i, ++_j)vis[_i][_j]++;
dfs(dep + 1);//向下一层递归,一直递归到递归出口,答案加一,退出递归,准备恢复现场(会形成一个树形结构)
//恢复现场,准备下一次搜索
for (int _i = 1; _i <= n; ++_i)vis[_i][i]--;
for (int _i = dep, _j = i; _i >= 1 && _j >= 1; --_i, --_j)vis[_i][_j]--;
for (int _i = dep, _j = i; _i <= n && _j >= 1; ++_i, --_j)vis[_i][_j]--;
for (int _i = dep, _j = i; _i >= 1 && _j <= n; --_i, ++_j)vis[_i][_j]--;
for (int _i = dep, _j = i; _i <= n && _j <= n; ++_i, ++_j)vis[_i][_j]--;
}
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
//每一层必定只有一个皇后,可通过枚举每一层皇后的位置来搜索所有可能解
//层数到n+1时表示找到了一个可行解,不可行的解都到不了n+1层
cin >> n;
dfs(1);
cout << ans << '\n';
return 0;
}
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e5 + 9;
int n, a[N], dfn[N];
int idx;//表示此时的时间,全局变量默认为0
int mindfn = 1;//最小时间戳
int dfs(int x) {
dfn[x] = ++ idx;//此时的时间,打一个时间戳
//如果这里有时间戳
if (dfn[a[x]]) {
if (dfn[a[x]] >= mindfn)return dfn[x] - dfn[a[x]] + 1;//找到了一个环且合法
return 0;//不合法
}
//如果没有时间戳,继续向下递归找x所指向的那个a[x]
return dfs(a[x]);
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
//用时间戳(dfn)标记,将走过的地方标记一个时间戳,即第几步走到的
//如果再次走到一个用时间戳标记的点,则为找到一个环,就用此时间戳减去遇到的时间戳,得环的大小
//还需更新最小时间戳,以免走到之前构成过的地方,此时不能构成环
//走到走过的地方必须停下,根据时间戳的合法性更新最大值
cin >> n;
for (int i = 1; i <= n; ++i)cin >> a[i];//a[i]表示小朋友i最崇拜的那个小朋友,a[i]的值就是i的指向
int ans = 0;
//多个起点,从1开始遍历起点
for (int i = 1; i <= n; ++i) {
//如果i点没有时间戳,就表示没走过
if (!dfn[i]) {
mindfn = idx + 1;//更新最小时间戳
ans = max(ans, dfs(i));
}
}
cout << ans << '\n';
return 0;
}
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e3 + 5;
char mp[N][N];
int n, scc, col[N][N], ans;//scc是某个点的颜色,ans是剩余的岛屿数
bool vis[N];//bool数组定义为全局变量表时默认值为false
int dx[] = { 0,0,1,-1 };//表示偏移量,方便移动,x方向
int dy[] = { 1,-1,0,0 };//y方向
//题目保证了地图边缘全是海洋
//用dfs将不同岛屿染上不同颜色
void dfs(int x, int y) {
col[x][y] = scc;
//遍历上下左右方向,递归地找到与当前地块有联通的陆地,给它标上颜色
for (int i = 0; i < 4; ++i) {
int nx = x + dx[i], ny = y + dy[i];//next xy,下一个xy
if (col[nx][ny] || mp[nx][ny] == '.')continue;//是海洋,不可以过去,或者是四个方向中的上一点(已经有颜色的点),又递归回去了,陷入死循环
dfs(nx, ny);
}
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j)cin >> mp[i][j];
}
//枚举每一个位置
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (col[i][j] || mp[i][j] == '.')continue;//如果是海洋就不管,如果是陆地,就把目前的岛标上同一颜色,如果有颜色就不管(dfs的时候已经染过了)
scc++;//scc表示当前颜色编号,同时也表示了颜色的种类数,即岛屿数目
dfs(i, j);
}
}
//开始淹没
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j) {
if (mp[i][j] == '.')continue;
bool tag = true;
//这里有可能跳出地图边界,故在前面判定是不是海洋,是就跳过,而地图边界又一定是海洋,故不会超出数组范围
for (int k = 0; k < 4; ++k) {
int x = i + dx[k], y = j + dy[k];
if (mp[x][y] == '.')tag = false;//与海洋有相邻,被淹没
}
if (tag) {
//如果当前颜色,即当前的地块所属小岛没有出现过,表明这一小岛没有被淹,答案就要加1了
if (!vis[col[i][j]])ans++;
vis[col[i][j]] = true;//出现过了
}
}
}
cout << scc - ans << '\n';//淹没的岛屿个数
return 0;
}
本文作者:BreadCheese
本文链接:https://www.cnblogs.com/breadcheese/p/18014803
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步