突然发现机房里有很多人不会暴搜(dfs),所以写一篇他们能听得懂的博客(大概?)

PS:万能 yuechi ———— 大法师怎么能不会呢?!

若有错误,请 dalao 指出。

前置

我知道即使很多人都知道 dfs 是用递归来实现的,但免不了还是叨叨几句:

  • 要有边界(不然你要递归到猴年马月……)

  • 剪枝(即使是暴搜也不至于从头莽到尾)

  • 别犯 sb 错误(debug 到心累,最后发现边界写错了 = =)

大概流程

严格来说,dfs 其实也是有一套固定的流程,毕竟

万物皆可板(bushi)

  1. 定义现在的状态(即搜索到了哪一个位置)

  2. 枚举可能的情况(如一个数可能是 \([0,9]\))

  3. 标记枚举到的情况已被用了(如一个数已经是偶数了,那下一个数就不能是偶数(这个视情况而定))

  4. 判断有无到达边界(如果到达就输出,没到就继续搜(用递归))

  5. 回溯(难点,下面举例来讲)

放几个例题来讲解一下

例题一

很多算法都是建立在 dfs 上的,先放一个裸题。

题目描述

一个的 \(n \times n\) 的跳棋棋盘,有 \(n\) 个棋子被放置在棋盘上,使得每行、每列有且只有一个,每条对角线(包括两条主对角线的所有平行线)上至多有一个棋子。

数据范围:\(n \in [6, 13]\) 。

分析

八皇后的题目我相信大家也不陌生,积护所有 dfs 入门的人都做过,但我还是来分析一下吧。

看过数据范围,就能确认眼神:一道 dfs 能做的题。

首先,很容易就能知道: \(n\) 个棋子一定是在不同行,不同列的,这是可以构成限制的。

要求每条对角线上只能有一个棋子,这不仅是限制,也是该题的难点所在,如果要优化可以从这里入手。

既然是搜(暴搜),那么就可以从第一行开始,到达最后一行结束(边界)。

代码实现

从第一行开始枚举行数,同时也枚举列数,并且记录下棋子放下的位置导致出现的限制。

变量 意义
a 存储答案
b1 判断一个位置是否能放棋子
b2 判断这个数有无被用(貌似没用)
t 搜到的当前的行数
函数 意义
fread 快读
bj 标记位置不能用
hy 标记位置能用
print 搜完输出答案
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++

练习题

  1. 洛谷 八皇后 Checker Challenge(例题一)

  2. 洛谷 数的划分(例题二)

  3. 一堆 慢慢刷

【算法】深度优先搜索(dfs)的更多相关文章

  1. 【算法入门】深度优先搜索(DFS)

    深度优先搜索(DFS) [算法入门] 1.前言深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法.它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解 ...

  2. 深度优先搜索 DFS 学习笔记

    深度优先搜索 学习笔记 引入 深度优先搜索 DFS 是图论中最基础,最重要的算法之一.DFS 是一种盲目搜寻法,也就是在每个点 \(u\) 上,任选一条边 DFS,直到回溯到 \(u\) 时才选择别的 ...

  3. 深度优先搜索DFS和广度优先搜索BFS简单解析(新手向)

    深度优先搜索DFS和广度优先搜索BFS简单解析 与树的遍历类似,图的遍历要求从某一点出发,每个点仅被访问一次,这个过程就是图的遍历.图的遍历常用的有深度优先搜索和广度优先搜索,这两者对于有向图和无向图 ...

  4. 利用广度优先搜索(BFS)与深度优先搜索(DFS)实现岛屿个数的问题(java)

    需要说明一点,要成功运行本贴代码,需要重新复制我第一篇随笔<简单的循环队列>代码(版本有更新). 进入今天的主题. 今天这篇文章主要探讨广度优先搜索(BFS)结合队列和深度优先搜索(DFS ...

  5. 深度优先搜索DFS和广度优先搜索BFS简单解析

    转自:https://www.cnblogs.com/FZfangzheng/p/8529132.html 深度优先搜索DFS和广度优先搜索BFS简单解析 与树的遍历类似,图的遍历要求从某一点出发,每 ...

  6. 算法与数据结构基础 - 深度优先搜索(DFS)

    DFS基础 深度优先搜索(Depth First Search)是一种搜索思路,相比广度优先搜索(BFS),DFS对每一个分枝路径深入到不能再深入为止,其应用于树/图的遍历.嵌套关系处理.回溯等,可以 ...

  7. 图的深度优先搜索(DFS)和广度优先搜索(BFS)算法

    深度优先(DFS) 深度优先遍历,从初始访问结点出发,我们知道初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接 ...

  8. 算法总结—深度优先搜索DFS

    深度优先搜索(DFS) 往往利用递归函数实现(隐式地使用栈). 深度优先从最开始的状态出发,遍历所有可以到达的状态.由此可以对所有的状态进行操作,或列举出所有的状态. 1.poj2386 Lake C ...

  9. 深度优先搜索(DFS)

    [算法入门] 郭志伟@SYSU:raphealguo(at)qq.com 2012/05/12 1.前言 深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法.它的思想是从一 ...

  10. HDU(搜索专题) 1000 N皇后问题(深度优先搜索DFS)解题报告

    前几天一直在忙一些事情,所以一直没来得及开始这个搜索专题的训练,今天做了下这个专题的第一题,皇后问题在我没有开始接受Axie的算法低强度训练前,就早有耳闻了,但一直不知道是什么类型的题目,今天一看,原 ...

随机推荐

  1. java ListNode 链表

    链表是一种数据结构:由数据和指针构成,链表的指针指向下一个节点. java ListNode 链表 就是用Java自定义实现的链表结构. 基本结构: class ListNode { //类名 :Ja ...

  2. 在搜索引擎中输入汉字就可以解析到对应的域名,请问如何用LoadRunner进行测试。

    建立测试计划,确定测试标准和测试范围 设计典型场景的测试用例,覆盖常用业务流程和不常用的业务流程等 根据测试用例,开发自动测试脚本和场景: 录制测试脚本:新建一个脚本(Web/HTML协议):点 ...

  3. zigzag压缩算法

    前文 Base 128 Varints 编码(压缩算法) 介绍了Base 128 Varints这种对数字传输的编码,了解到了这种编码方式是为了最大程度压缩数字的.但是,在前文里,我们只谈论到了正数的 ...

  4. 使用 SOS 对 Linux 中运行的 .NET Core 进行问题诊断

    目录 说明 准备一个方便的学习环境 2.x 配置内容 3.x 配置内容 工具介绍 lldb sos plugin 1. attach 到进程上进行调试 2. 分析core dump文件 SOS 案例分 ...

  5. Asp.Net Core 应用配置

    五种读取方式 五种读取方式依赖于 IConfiguration 和 IConfigurationRoot 对象 一.初级写法 //不区分大小写 string connectionString = _c ...

  6. Liunx运维(十二)-Liunx系统常用内置命令

    文档目录: 一.Liunx内置命令概述 二.LIunx常用内置命令实例 1.help查看内置命令帮助2.查看内置命令使用方法3.":" 占位符4. "." 与s ...

  7. loj #6179. Pyh 的求和 莫比乌斯反演

    题目描述 传送门 求 \(\sum\limits_{i=1}^n\sum\limits_{j=1}^m \varphi(ij)(mod\ 998244353)\) \(T\) 组询问 \(1 \leq ...

  8. MySQL 标识符到底区分大小写么——官方文档告诉你

    最近在阿里云服务器上部署一个自己写的小 demo 时遇到一点问题,查看 Tomcat 日志后定位到问题出现在与数据库服务器交互的地方,执行 SQL 语句时会返回 指定列.指定名 不存在的错误.多方查证 ...

  9. 【模拟】P1143进制转换

    题目相关 题目描述 请你编一程序实现两种不同进制之间的数据转换. 输入格式 共三行,第一行是一个正整数,表示需要转换的数的进制n(2≤n≤16),第二行是一个n进制数,若n>10则用大写字母A- ...

  10. WixVersionControl Wix项目版本控制

    原文链接:https://www.swack.cn/wiki/001565675133949eff0d3d5a51f48288cf6d8248905e28f000/001569821278313e6b ...