算法初探系列1 - 深度优先搜索之搜索枚举

相信大家都学过枚举算法叭,如果学过的话,就继续跟蒟蒻君往下学吧!

首先,请大家看一道题~

题目1:全排列

  • 描述
    输入n,输出1~n所有不重复的排列,即n的全排列。
  • 分析
    显而易见,这道题并不能用for循环枚举做,因为要用一个“n重循环”,而n是一个变量,所以无法用枚举算法。
    那该如何解决这道题呢?大家应该会想到,循环层数不定,就可以用递归来实现,这就是我们的深度优先搜索(dfs)算法的一个用途。
    dfs算法在递归的过程中,用for循环枚举,也就是说,在这里for循环的层数是由递归来决定的,而不是一个一个写。
  • 代码
#include 
using namespace std;
bool vis[11]; // 假设n最大为10,vis[i]表示第i个数在当前这个排列中是否被用过
int n, a[11]; // 表当前排列的结果 
void dfs(int stp) {	// stp是step的缩写,表示现在已经枚举到了第几步(所谓的第几层循环) 
	if (stp == n + 1) {	// 当n层循环都枚举完后,输出当前结果并且结束dfs函数 
		for (int i = 1; i <= n; ++i) {
			cout << a[i] << ' ';
		}
		cout << '\n'; // 不要忘记排列之间要用换行隔开 
		return ;
	}
	for (int i = 1; i <= n; ++i) {	// 枚举所有数,找到符合条件的数(没有被用过),然后放到当前数位上 
		if (!vis[i]) {
			vis[i] = 1;	a[stp] = i; // 先放到当前数位上,继续枚举,然后尝试其他数,这样的过程就是一个回溯算法 
			dfs(stp + 1);
			vis[i] = 0;
		}
	}
}
int main() {
	cin >> n;
	dfs(1); // 从第1个数开始枚举 
	return 0;
}

注释已经很清楚了,蒟蒻君就不再讲这道题啦。
蒟蒻君温馨提示:在搜索枚举的过程中,我们可以根据题目性质,对过程进行剪枝优化(在后几节课会讲)。但是在大部分题目中,dfs的时间复杂度会超,所以在搜索前,我们很有必要确定问题状态的总数。
下面给大家出一道练习题~

题目2:烤鸡

  • 背景
    猪猪hanke得到了一只鸡
  • 描述
    猪猪Hanke特别喜欢吃烤鸡(本是同畜牲,相煎何太急!)Hanke吃鸡很特别,为什么特别呢?因为他有10种配料(芥末、孜然等),每种配料可以放1—3克,任意烤鸡的美味程度为所有配料质量之和
    现在,Hanke想要知道,如果给你一个美味程度,请输出这10种配料的所有搭配方案
  • 输入格式
    一行,n<=5000
  • 输出格式
    第一行,方案总数
    第二行至结束,10个数,表示每种配料所放的质量
    按字典序排列。
    如果没有符合要求的方法,就只要在第一行输出一个“0”
  • 输入样例#1
    11
  • 输出样例#1
    10
    1 1 1 1 1 1 1 1 1 2
    1 1 1 1 1 1 1 1 2 1
    1 1 1 1 1 1 1 2 1 1
    1 1 1 1 1 1 2 1 1 1
    1 1 1 1 1 2 1 1 1 1
    1 1 1 1 2 1 1 1 1 1
    1 1 1 2 1 1 1 1 1 1
    1 1 2 1 1 1 1 1 1 1
    1 2 1 1 1 1 1 1 1 1
    2 1 1 1 1 1 1 1 1 1
  • 分析
    很显然,这道题可以用十重循环枚举,但是我们已经学过了最基本的dfs,何必要用十重循环枚举呢?这里蒟蒻君给大家一个模板,请大家写完题目,写完了可以到这里提交。
#include 
using namespace std;
int n, ans1, ans2[10000][10], arr[10], sum;
void dfs(int stp, int now) {
	// 请继续完成这个函数~ 
}

int main() {
	cin >> n; dfs(1, 0);
	cout << ans1 << '\n';
	for (int i = 1; i <= ans1; ++i) {
		for (int j = 1; j <= 10; ++j) {
			cout << ans2[i][j] << ' ';
		}
		puts("");
	}
	return 0;
}

  • 题解
    题目大意
    输入一个n,输出有多少种方案使得十个数之和为n,顺序不用视为两种,并且输出每一种方案。
    解题思路
    从第一层(第一个数组元素)开始,记录当前数组和。一直往下dfs,当最后达到临界时进行条件判断,满足条件的记录数据和存储数据。不满足的就return返回上一层。
    代码
#include 
using namespace std;
int n, ans1, ans2[10000][10], arr[10], sum;
void dfs(int stp, int now) {
	if (stp > 10) {
		if (now == n) {
			++ans1;
			for (int i = 1; i <= 10; ++i) {
				ans2[ans1][i] = arr[i];
			}
		}
		return ;
	}
	for (int i = 1; i <= 3; ++i) {
		if (now + i > n) {
			break;
		}
		arr[stp] = i;
		dfs(stp + 1, now + i);
		arr[stp] = 0;
	} 
}
int main() {
	cin >> n; dfs(1, 0);
	cout << ans1 << '\n';
	for (int i = 1; i <= ans1; ++i) {
		for (int j = 1; j <= 10; ++j) {
			cout << ans2[i][j] << ' ';
		}
		puts("");
	}
	return 0;
}

学到这里,大家应该已经发现一点规律了~

给大家列出基本的dfs框架~

void dfs(目前状态) {
	判断边界
	尝试每一种可能,跳出可以继续的 {
		继续搜索,尝试下一步 
	} 
}

当然,枚举不一定非要用for循环滴!这里给大家一个例子,希望大家能融会贯通~

题目3:n阶幻方

  • 题目
    输入n,输出n阶幻方的一个解。
  • 解题思路
    显然,学过幻方的小伙伴们都会想到用“萝卜法”(谐音)不会的小伙伴可以看这里。
  • 代码
#include 
using namespace std;
int n, a[45][45];
void dfs(int x, int y, int now) {	// 目前坐标和下一步要填的数
	if (now > n * n) {	// 所有数都填完了,输出,然后结束dfs函数 
		for (int i = 1; i <= n; ++i) {
			for (int j = 1; j <= n; ++j) {
				cout << a[i][j] << ' ';
			}
			cout << '\n';
		}
		return ;
	}
	// 直接模拟即可
	if (x == 1 && y != n) {
		a[n][y + 1] = now;
		dfs(n, y + 1, now + 1);
	} else if (y == n && x != 1) {
		a[x - 1][1] = now;
		dfs(x - 1, 1, now + 1);
	} else if (x == 1 && y == n) {
		a[x + 1][y] = now;
		dfs(x + 1, y, now + 1);
	} else {
		if (a[x - 1][y + 1] == 0) {
			a[x - 1][y + 1] = now;
			dfs(x - 1, y + 1, now + 1);
		} else {
			a[x + 1][y] = now;
			dfs(x + 1, y, now + 1);
		}
	}
}
int main() {
	cin >> n;
	a[1][(n + 1) / 2] = 1; // 特殊处理1 
	dfs(1, (n + 1) / 2, 2);
	return 0;
} 

看到这里,大家是不是已经会处理基本dfs的题目了?如果大家觉得有帮助的话,就继续期待蒟蒻君的第二篇吧!

你可能感兴趣的:(算法)