天地转,光阴迫。|

BreadCheese

园龄:1年4个月粉丝:1关注:0

DFS-深度优先搜索

回溯法简介

回溯法一般使用DFS(深度优先搜索)实现,DFS是一种遍历或搜索图,树或图像等数据结构的算法。上述数据结构不保存下来就是回溯法。
常见的是搜索树,排列型搜索树(节点数一般为n!)与子集型搜索树(节点数一般为2n)。

DFS从起始点开始,沿着一条路尽可能深入,直到无法继续回溯到上一节点为止,继续搜索,直到遍历完整个树或图。DFS使用栈与递归管理节点,一般使用递归。

排列树

A

B1

C1

B2

C2

子集树

A

B1

C1

C2

B2

D1

D2

2n(n=2)

即4种方案。

回溯法模板

1

2

3

3

2

1至3的全排列,后略

//求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;
}

例题

N皇后问题

#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 中国大陆许可协议进行许可。

posted @   BreadCheese  阅读(37)  评论(0编辑  收藏  举报
  1. 1 転がる岩、君に朝が降る ASIAN KUNG-FU GENERATION
転がる岩、君に朝が降る - ASIAN KUNG-FU GENERATION
00:00 / 00:00
An audio error has occurred.

詞:後藤正文

曲:後藤正文

出来れば世界を僕は塗り変えたい

戦争をなくすような

大逸れたことじゃない

だけどちょっと

それもあるよな

それどころか

そんな僕に術はないよな

嗚呼

何を間違った

それさえもわからないんだ

初めから持ってないのに

胸が痛んだ

心絡まってローリング

凍てつく地面を転がるように

走り出した

评论
收藏
关注
推荐
深色
回顶
收起
点击右上角即可分享
微信分享提示