忽然发现机房里有不少人不会暴搜(dfs),因此写一篇他们能听得懂的博客(大概?)ios
如有错误,请 dalao 指出。c++
我知道即便不少人都知道 dfs 是用递归来实现的,但免不了仍是叨叨几句:git
要有边界(否则你要递归到猴年马月……)算法
剪枝(即便是暴搜也不至于从头莽到尾)数组
别犯 sb 错误(debug 到心累,最后发现边界写错了 = =)函数
严格来讲,dfs 其实也是有一套固定的流程,毕竟优化
万物皆可板(bushi)spa
定义如今的状态(即搜索到了哪个位置)debug
枚举可能的状况(如一个数多是 \([0,9]\))code
标记枚举到的状况已被用了(如一个数已是偶数了,那下一个数就不能是偶数(这个视状况而定))
判断有无到达边界(若是到达就输出,没到就继续搜(用递归))
回溯(难点,下面举例来说)
不少算法都是创建在 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++