【算法】深度优先搜索(dfs)
突然发现机房里有很多人不会暴搜(dfs),所以写一篇他们能听得懂的博客(大概?)
PS:万能 yuechi ———— 大法师怎么能不会呢?!
若有错误,请 dalao 指出。
前置
我知道即使很多人都知道 dfs 是用递归来实现的,但免不了还是叨叨几句:
要有边界(不然你要递归到猴年马月……)
剪枝(即使是暴搜也不至于从头莽到尾)
别犯 sb 错误(debug 到心累,最后发现边界写错了 = =)
大概流程
严格来说,dfs 其实也是有一套固定的流程,毕竟
万物皆可板(bushi)
定义现在的状态(即搜索到了哪一个位置)
枚举可能的情况(如一个数可能是 \([0,9]\))
标记枚举到的情况已被用了(如一个数已经是偶数了,那下一个数就不能是偶数(这个视情况而定))
判断有无到达边界(如果到达就输出,没到就继续搜(用递归))
回溯(难点,下面举例来讲)
放几个例题来讲解一下
例题一
很多算法都是建立在 dfs 上的,先放一个裸题。
题目描述
一个的 \(n \times n\) 的跳棋棋盘,有 \(n\) 个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。
数据范围:\(n \in [6, 13]\) 。
分析
八皇后的题目我相信大家也不陌生,积护所有 dfs 入门的人都做过,但我还是来分析一下吧。
看过数据范围,就能确认眼神:一道 dfs 能做的题。
首先,很容易就能知道: \(n\) 个棋子一定是在不同行,不同列的,这是可以构成限制的。
要求每条对角线上只能有一个棋子,这不仅是限制,也是该题的难点所在,如果要优化可以从这里入手。
既然是搜(暴搜),那么就可以从第一行开始,到达最后一行结束(边界)。
代码实现
从第一行开始枚举行数,同时也枚举列数,并且记录下棋子放下的位置导致出现的限制。
| 变量 | 意义 |
|---|---|
| a | 存储答案 |
| b1 | 判断一个位置是否能放棋子 |
| b2 | 判断这个数有无被用(貌似没用) |
| t | 搜到的当前的行数 |
| 函数 | 意义 |
|---|---|
| fread | 快读 |
| bj | 标记位置不能用 |
| hy | 标记位置能用 |
| 搜完输出答案 | |
| search | dfs |
/**
*
author:Eiffel_A
*/
#include <iostream>
#include <iomanip>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <map>
#include <queue>
#define MAXN 100001
#define Mod 998244353
//-------------定义变量-------------
int n, s = 0;
int a[14], b1[14][14], b2[14];
//------------定义结构体------------
//-------------定义函数-------------
int fread() {
int x = 0, f = 0; char ch = getchar();
while (!isdigit(ch)) f |= (ch == '-'), ch = getchar();
while (isdigit(ch)) x = x * 10 + (ch ^ 48), ch = getchar();
return f ? -x : x;
}
void bj(int x, int y) { // 一个棋子放下后将对角线标记为不可用
for (int j = 1; j <= n; ++j) {
if (x + j >= 1 && x + j <= n && y + j >= 1 && y + j <= n && b1[x + j][y + j] == 0)
b1[x + j][y + j] = x;
if (x + j >= 1 && x + j <= n && y - j >= 1 && y - j <= n && b1[x + j][y - j] == 0)
b1[x + j][y - j] = x;
}
}
void hy(int x, int y) { // 将棋子回溯到未放下时将对角线标记为可用
for (int j = 1; j <= n; ++j) {
if (x + j >= 1 && x + j <= n && y + j >= 1 && y + j <= n && b1[x + j][y + j] == x)
b1[x + j][y + j] = 0;
if (x + j >= 1 && x + j <= n && y - j >= 1 && y - j <= n && b1[x + j][y - j] == x)
b1[x + j][y - j] = 0;
}
}
void print() { // 到达边界后输出
s++;
if (s <= 3) {
for (int i = 1; i <= n; ++i)
printf("%d ",a[i]);
printf("\n");
}
}
int search(int t) { // dfs
for (int i = 1; i <= n; ++i) // 枚举列数
if (!b2[i] && !b1[t][i]) { // 如果这一列还没有棋子且不在任何一条已放棋子的对角线上
a[t] = i; b2[i] = t; // 记录棋子位置,标记这列已用
bj(t, i); // 标记对角线已用
if (t == n) print(); // 如果到了最后一行,就输出
else search(t + 1); // 否则继续搜下一行
b2[i] = 0; // 回溯,这列还没用
hy(t, i); // 回溯,这个对角线还没用
}
}
//--------------主函数--------------
int main() {
n = fread();
search(1);
printf("%d", s);
return 0;
}
请不要在意我难看的马蜂和奇怪的变量名……
解释回溯
让我们想象一下:
当判断是否搜到边界时,
如果到了,则输出,然后回到 dfs 函数里;
但这时候仅仅只是找到了一种可行的摆放方法,还有许多方法还没开始搜,
所以我们要假装这个位置没有放过棋子,即退回放这个棋子之前,这样才能将这一列空出来,以便在其他行在这一列放棋子,找到更多的情况。
若没到边界,则又会进入新的一行,一直到到达边界为止,剩下的就与上一种情况一致了。
如果到现在还是没懂的话,那我举个栗子:
假如你正在走迷宫:emmmm 这个(我手画的……)

你走到了终点:这样(橡皮开路)

但是你的要求是找出所有能到达终点的路,仅仅只有一条是不够的,
所以你得退回去:

(当然也可以退到其他地方)
这样你就可以找另一条道路:

所以回溯大概就是这么一个过程~~
dfs (\(t\)) 每一层 dfs 可以用变量 \(t\) 来标记,可以把 \(t\) 看做是下标(反正我这么理解)
如果 \(t == 1\) 就说明这一层 dfs 是在 \(1\) 这个点的,以此类推。
这样回溯就会很好理解啦~~
优化
这个代码是我刚刚学 dfs 时写的,只不过又被我扒了出来改了改马蜂罢了……
如果你像我这份代码这样判断一条对角线有无占用,那么当你把代码交上去后,你就会惊喜地发现:
你 T 啦~~
大概是反复调用标记和回溯函数的问题……
所以要优化的说~~
然后经过我深(cha)思(kan)熟(ti)虑(jie)后发现了一个好方法:
我们可以再开一个 \(c\) 数组和一个 \(d\) 数组,然后把 \(b1\) 和 \(b2\) 数组去掉,改成 \(b\) 数组 。
众所周知,如果一个点的坐标是 \((x,y)\) 且独一无二,那么 \(x + y\) 和 \(x - y + n\) (\(n\) 是总行数)就是独一无二的。
这样就可以表达出对角线啦~~~
int search(int t) {
for (int i = 1; i <= n; ++i)
if (!b[i] && !c[t + i] && !d[t - i + n]) {
a[t] = i; b[i] = 1;
c[t + i] = 1; d[t - i + n] = 1;
if (t == n) print();
else search(t + 1);
b[i] = 0;
c[t + i] = 0; d[t - i + n] = 0;
}
}
例题二
题目描述
将整数 \(n\) 分成 \(k\) 份,且每份不能为空,任意两个方案不相同(不考虑顺序)。
例如:\(n=7\),\(k=3\),下面三种分法被认为是相同的。
\(1,1,5\) 或 \(1,5,1\) 或 \(5,1,1\)
问有多少种不同的分法。
数据范围:\(n\in (6,200]\),\(k\in [2,6]\)
分析
几乎与上一题一样,无非只是把条件和枚举的东西变了一下而已 = =
PS:下面的代码是错的,而且还删了几个头文件(貌似 pd 函数写错了,不过这不重要)
代码实现
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
//------------定义结构体------------
//-------------定义变量-------------
int n, k, s, v = 0, w = 0;
int a[10], b[10];
map<int,int> hh;
//-------------定义函数-------------
bool pd() { // 错的
w = 0; memcpy(b, a, sizeof(a));
sort(b, b + 10);
for (int j = 9; j >= 10 - k; --j)
w *= 10, w += b[j];
if (!hh[w]) v++, hh[w] = 1;
}
int search(int t) {
if (t == k && s) a[t] = s, pd(); // 判断到边界后是否满足条件,若满足,则输出
else {
for (int i = a[t - 1]; i <= n; ++i) { // 保证下一个数大于等于上一个数,防止重复
if (!i) continue; // 如果 i 为零,则不算入答案
if (i < s) { // 保证各数字之和不大于 n
a[t] = i; // 记录 i
s -= a[t]; // 减去加数
search(t + 1); // 继续搜
s += a[t]; // 回溯,假装没用过这个加数
}
}
}
}
//--------------主函数--------------
int main() {
cin >> n >> k;
s = n;
search(1);
printf("%d", v);
return 0;
}
依旧是很久以前写的代码,被我扒拉出来改改马蜂贴了上来……
Q:为什么把错的代码放了上来?
A:是因为我 懒得改 只想让你们了解思路就行了
优化
经查实,如果你按照这个思路(即 pd 函数写对)交了上去
你会惊喜得发现:
你又 T 啦~~
这时候就又需要优化剪枝了,我们可以这样想:
既然是求 \(k\) 个数,又知道这 \(k\) 个数的和,那么只需要求 \(k - 1\) 个数,最后一个数减出来就好辣。
直接减出来了数,就不用判断所有数加起来是否等于 \(n\) 。
只需要判断减出来的数是否大于之前的数(判重)。
这下正解代码就出来啦~~
int search(int t) {
if (t == k && s >= a[t - 1]) ++v;
if (t != k)
for (int i = a[t - 1]; i <= n; ++i) {
if (!i) continue;
if (i < s) {
a[t] = i;
s -= a[t];
search(t + 1);
s += a[t];
}
}
}
后言
我相信两道例题已经足够讲明白了,就不举第三个例子了 (其实只是我不想写了而已)
祝所有人 noip2020 rp++
练习题
【算法】深度优先搜索(dfs)的更多相关文章
- 【算法入门】深度优先搜索(DFS)
深度优先搜索(DFS) [算法入门] 1.前言深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法.它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解 ...
- 深度优先搜索 DFS 学习笔记
深度优先搜索 学习笔记 引入 深度优先搜索 DFS 是图论中最基础,最重要的算法之一.DFS 是一种盲目搜寻法,也就是在每个点 \(u\) 上,任选一条边 DFS,直到回溯到 \(u\) 时才选择别的 ...
- 深度优先搜索DFS和广度优先搜索BFS简单解析(新手向)
深度优先搜索DFS和广度优先搜索BFS简单解析 与树的遍历类似,图的遍历要求从某一点出发,每个点仅被访问一次,这个过程就是图的遍历.图的遍历常用的有深度优先搜索和广度优先搜索,这两者对于有向图和无向图 ...
- 利用广度优先搜索(BFS)与深度优先搜索(DFS)实现岛屿个数的问题(java)
需要说明一点,要成功运行本贴代码,需要重新复制我第一篇随笔<简单的循环队列>代码(版本有更新). 进入今天的主题. 今天这篇文章主要探讨广度优先搜索(BFS)结合队列和深度优先搜索(DFS ...
- 深度优先搜索DFS和广度优先搜索BFS简单解析
转自:https://www.cnblogs.com/FZfangzheng/p/8529132.html 深度优先搜索DFS和广度优先搜索BFS简单解析 与树的遍历类似,图的遍历要求从某一点出发,每 ...
- 算法与数据结构基础 - 深度优先搜索(DFS)
DFS基础 深度优先搜索(Depth First Search)是一种搜索思路,相比广度优先搜索(BFS),DFS对每一个分枝路径深入到不能再深入为止,其应用于树/图的遍历.嵌套关系处理.回溯等,可以 ...
- 图的深度优先搜索(DFS)和广度优先搜索(BFS)算法
深度优先(DFS) 深度优先遍历,从初始访问结点出发,我们知道初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接 ...
- 算法总结—深度优先搜索DFS
深度优先搜索(DFS) 往往利用递归函数实现(隐式地使用栈). 深度优先从最开始的状态出发,遍历所有可以到达的状态.由此可以对所有的状态进行操作,或列举出所有的状态. 1.poj2386 Lake C ...
- 深度优先搜索(DFS)
[算法入门] 郭志伟@SYSU:raphealguo(at)qq.com 2012/05/12 1.前言 深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法.它的思想是从一 ...
- HDU(搜索专题) 1000 N皇后问题(深度优先搜索DFS)解题报告
前几天一直在忙一些事情,所以一直没来得及开始这个搜索专题的训练,今天做了下这个专题的第一题,皇后问题在我没有开始接受Axie的算法低强度训练前,就早有耳闻了,但一直不知道是什么类型的题目,今天一看,原 ...
随机推荐
- 10分钟带你入门git到github
git的产生背景 开局先来一个故事吧,故事看完如果不想看枯燥无味的指令,没关系我已经把这篇文章的内容录制成了一个视频,点击文末阅读原文就可以观看.或者说你已经熟练掌握git的使用了,可以直接跳到总结部 ...
- 大白话Java多线程,小白都能看的懂的哦
什么是线程 说到线程我们应该先了解下什么是进程,下面这个图片大家应该都比较熟悉吧. 我们看到的这些单独运行的程序就是一个独立的进程,进程之间是相互独立存在的.我们上面图中的360浏览器.百度云盘等等都 ...
- 痞子衡嵌入式:Farewell, 我的2020
-- 题图:苏州大学老校门 2020年的最后一天,痞子衡驱车300多公里从苏州赶回了苏北老家(扬州某边陲小镇),连镇铁路虽然新通车了,解决了苏南苏北多年的铁路不直通问题,但奈何痞子衡老家小镇离最近的火 ...
- Maven安装配置和IDEA创建Maven项目
maven 一个项目架构管理工具(约定大于配置) 1.配置 M2_HOME:指向maven bin目录 以后bootstrop要用 MAVEN_HOME:指向maven目录 path:指向maven ...
- label_form
表单: action "URL" 如果为空,则本form接收 指定接收方 disabled 指定该标签是否可用 method "net" "http& ...
- 如果你的 HTML 里全是 div,那就要小心了
做前端开发的同学都知道,一个网页的基本组成部分是 HTML,JavaScript 和 CSS.开发人员通常更关注 JavaScript 和 CSS ,实践着各种语言规范和设计模式.对于 HTML 的关 ...
- STM32 HAL库之串口详细篇
一.基础认识 (一) 并行通信 原理:数据的各个位同时传输 优点:速度快 缺点:占用引脚资源多,通常工作时有多条数据线进行数据传输 8bit数据传输典型连接图: 传输的数据是二进制:11101010, ...
- CTF常见编码及加解密(超全)
@ 目录 前言 常见CTF编码及加解密 补充 ASCII编码 base家族编码 MD5.SHA1.HMAC.NTLM等类似加密型 1.MD5 2.SHA1 3.HMAC 4.NTLM 5.类似加密穷举 ...
- 【LeetCode】365.水壶问题
题目描述 解题思路 思路一:裴蜀定理-数学法 由题意,每次操作只会让桶里的水总量增加x或y,或者减少x或y,即会给水的总量带来x或y的变化量,转为数字描述即为:找到一对整数a,b使得下式成立: ax+ ...
- Java进阶专题(二十一) 消息中间件架构体系(3)-- Kafka研究
前言 Kafka 是一款分布式消息发布和订阅系统,具有高性能.高吞吐量的特点而被广泛应用与大数据传输场景.它是由 LinkedIn 公司开发,使用 Scala 语言编写,之后成为 Apache 基金会 ...