算法学习系列(四十五):DFS之剪枝与优化

引言

关于这个 D F S DFS DFS 的剪枝和优化确实难度是非常的大,从我这篇文章的思路和代码量上就能看出来不是一般的难度,而且难度不亚于 D P DP DP ,而且这个 D F S DFS DFS 也是花费了我三天的时间才基本把这几道例题给搞懂了,并且这种题就是没有固定的模型和套路,每个题都不一样,只有你多做题,这样在考场上才能想到这道题好像跟之前做过的题有点相似,然后再去套用模型。不可能短时间去创造一种算法,所以还是继续刷题吧,加油!


DFS之剪枝与优化

  • 优化搜索顺序:大部分情况下,我们应该优先搜索分支较少的节点。比如背包问题应该先放体积大的物品
  • 排除等效冗余:组合问题中, 1 , 2 , 3 1,2,3 123 1 , 3 , 2 1,3,2 132 是一样的,可以用一个 s t a r t start start 来规定初始
  • 可行性剪枝:发现当前状态不合法,退出
  • 最优性剪枝:如果发现当前方案不可能是最优解了,就退出

搜索顺序模型:
1.给一堆物品及其单个体积和单个篮子的容量,问最小的篮子数。解决方案:枚举每个物品,该物品要么加入到已有的篮子里,要么新开一个篮子。
2.给一堆物品及其单个体积,问刚好能把所有篮子都装满的最小篮子容量。解决方案:枚举每个篮子,然后在每个篮子中枚举每个物品。
3.给一堆物品,装进篮子里,但都有一定的限制。解决方案:枚举每个物品


一、小猫爬山

标签:搜索、深度优先搜索、DFS

思路1:首先要保证正确性,也就是枚举的所有方案必须是不重不漏的,然后才对其进行优化,不能本末颠倒了。我们可以按小猫的编号进行枚举,当前小猫要么新坐一辆缆车,要么从已有的缆车里挑一个坐。首先是优化搜索顺序,可以把小猫的重量从大到小排序,进行枚举,然后就是如果当前的方案已经不是最优解了,直接退出,再有就是如果当前缆车已经容不下当前小猫了,退出。
思路2:采用了迭代加深的方式,就是很简单的一种,看看代码就会了。因为这里递归里面枚举的是组数,并不是小猫本身,所以想的每个小猫按由大到小人为的排序顺序,想着用等效冗余剪枝是不行的。

题目描述:

翰翰和达达饲养了 N 只小猫,这天,小猫们要去爬山。

经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。

翰翰和达达只好花钱让它们坐索道下山。

索道上的缆车最大承重量为 W,而 N 只小猫的重量分别是 C1、C2……CN。

当然,每辆缆车上的小猫的重量之和不能超过 W。

每租用一辆缆车,翰翰和达达就要付 1 美元,所以他们想知道,最少需要付多少美元才能把这 N 只小猫都运送下山?

输入格式
第 1 行:包含两个用空格隔开的整数,N 和 W。

第 2..N+1 行:每行一个整数,其中第 i+1 行的整数表示第 i 只小猫的重量 Ci。

输出格式
输出一个整数,表示最少需要多少美元,也就是最少需要多少辆缆车。

数据范围
1≤N≤18,1≤Ci≤W≤108
输入样例:
5 1996
1
2
1994
12
29
输出样例:
2

示例代码1: 暴搜+剪枝优化

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second

const int N = 20;

int n, m;
int w[N];
int sum[N];
int ans = N;

void dfs(int u, int k)  // 正在选编号为u的小猫,当前组数为k k为实际已有的组数 
{
	if(k >= ans) return;  // 若u已经为n,并且k==ans的话,那么也非最优解 
	if(u == n)  // 说明前n个小猫已经加到组里了
	{
		ans = k;
		return;
	}
	
	for(int i = 1; i <= k; ++i)  // k为第k组 
	{
		if(w[u] + sum[i] <= m)
		{
			sum[i] += w[u];
			dfs(u+1,k);  // 这里不返回bool的原因是要找最值,而非判断是否可行 
			sum[i] -= w[u];	
		} 
	}
	
	sum[k+1] = w[u];
	dfs(u+1, k+1);
	sum[k+1] = 0;
}

int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	cin >> n >> m;
	for(int i = 0; i < n; ++i) cin >> w[i];
	sort(w, w + n, greater<int>());  // 优化搜索顺序
	
	dfs(0,0);
	
	cout << ans << endl;
	
	return 0;
}

示例代码2: 迭代加深+剪枝

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second

const int N = 20;

int n, m;
int w[N];
int sum[N];

bool dfs(int u, int k, int depth)  // 正在选第u个小猫,当前组数为k
{
	if(u == n) return true;
	
	for(int i = 0; i < k; ++i)
	{
		if(w[u] + sum[i] <= m)
		{
			sum[i] += w[u];
			if(dfs(u+1,k,depth)) return true;
			sum[i] -= w[u];
		}
	}
	
	if(k >= depth) return false;  // 此时k个缆车不够
	sum[k] = w[u];
	if(dfs(u+1,k+1,depth)) return true;
	sum[k] = 0;
	return false;
}

int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	cin >> n >> m;
	for(int i = 0; i < n; ++i) cin >> w[i];
	sort(w, w + n, greater<int>());
	
	int depth = 1;
	while(!dfs(0,0,depth)) depth++;
	cout << depth << endl;
	
	return 0;
}

二、木棒

标签:搜索、DFS、剪枝

思路:首先这道题是找最小的木棒长度,也就是组数是无所谓的,只要能凑出来每组最小的容量即可。之前求最小组数的思路是,枚举每一个物品,然后这件物品,要么加入到当前任意一个组里,要么新开一个组,在此基础上进行剪枝优化。而这道题是从小到大枚举容量,如果当前容量恰好能装满每一个组,那就是答案了。然后在每一个组的基础上枚举每一件物品,放入该组里,如果当前组满了,然后再开下一个组,然后在此进行剪枝优化。首先是优化搜索顺序,先从大到小排序,然后最最小的也就是最长的那根木棍了。然后因为是每个组都要枚举每一件物品,所以避免出现组合型冗余,规定一个 s t a r t start start 枚举顺序,因为之前的已经枚举过了,如果继续从 0 0 0 开始,那么还要枚举重复的,比如说: 1 , 2 , 3 和 3 , 1 , 2 1,2,3和3,1,2 1,2,33,1,2 之类的方案。然后就是可行性剪枝了,就是如果当前组装不下,那就不装了,另外如果当前木棍是第一个装的,但没有满足要求,那么该类方案一定没有解,因为如果该木棍是下一组的,可以通过在组内排顺序把它变成第一个,那么上一次没有满足要求,这次也就不会满足要求了。其次如果该木棍是最后一根刚好装满了,但是之后的方案却没有满足,那么该方案之后的所有都不会满足的,因为如果满足要求了,那么肯定会有几根木棍凑出来最后一根,因为这两长度是一样的,所以可以替换,但因为之前的最后一根已经不满足了,所以肯定不会有满足要求的情况存在。

题目描述:

乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50 个长度单位。

然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。

请你设计一个程序,帮助乔治计算木棒的可能最小长度。

每一节木棍的长度都用大于零的整数表示。

输入格式
输入包含多组数据,每组数据包括两行。

第一行是一个不超过 64 的整数,表示砍断之后共有多少节木棍。

第二行是截断以后,所得到的各节木棍的长度。

在最后一组数据之后,是一个零。

输出格式
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。

数据范围
数据保证每一节木棍的长度均不大于 50。

输入样例:
9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0
输出样例:
6
5

示例代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second

const int N = 100;

int n;
int w[N];
bool st[N];
int sum, len;

bool dfs(int u, int cur, int start)  // 当前u组里,目前的容量为cur,从start开始
{
	if(u * len == sum) return true;  // 说明前u个组里已经满了,并且长度已经达到总和
	if(cur == len) return dfs(u+1,0,0);  // 当前组已经满了,新开下一个组
	
	for(int i = start; i < n; ++i)  // 排序冗余
	{
	    if(st[i]) continue;  // 可行性剪枝 只能用一次
		if(cur + w[i] <= len)  // 可行性剪枝 不能超过容量
		{
			st[i] = true;
			if(dfs(u,cur+w[i],i+1)) return true;
			st[i] = false;
		}
		
		if(!cur || cur + w[i] == len) return false;  // 可行性剪枝
		
		int j = i + 1;
		while(j < n && w[i] == w[j]) j++;
		i = j - 1;
	}
	return false;
}

int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	while(cin >> n, n)
	{
		sum = 0, len = 0; 
		for(int i = 0; i < n; ++i) 
		{
			cin >> w[i];
			sum += w[i];
			len = max(len,w[i]);
		}
		
		sort(w, w + n, greater<int>());
		
		memset(st, 0, sizeof st);
		while(1)
		{
			if(sum % len == 0 && dfs(0,0,0))
			{
				cout << len << endl;
				break;
			}
			len++;
		}
	} 
	
	return 0;
}

三、数独

标签:搜索、深度优先搜索、DFS

思路:这道题就是枚举每一个空格,如果最后没有空格了,说明正确直接返回。每一行每一列和每一个田字格可以用一个 9 9 9 位的二进制数来表示,每一位为 1 1 1 说明当前位是空位。然后优化顺序,可以每次找到最少可能的格子来先枚举,可以用与运算行列和当前的田字格,来知道当前格子的 9 9 9 位二进制状态,然后可以提前打表,知道这个数是有几个 1 1 1 组成的,然后枚举该格子的多种可能,然后可以用 l o w b i t lowbit lowbit 来知道最后一位 1 1 1 对应的数,然后在可以通过提前打表知道该数所对应的位,然后暴搜各种可能即可。

题目描述:

数独是一种传统益智游戏,你需要把一个 9×9 的数独补充完整,使得数独中每行、每列、每个 3×3 的九宫格内数字 
1∼9 均恰好出现一次。

请编写一个程序填写数独。

输入格式
输入包含多组测试用例。

每个测试用例占一行,包含 81 个字符,代表数独的 81 个格内数据(顺序总体由上到下,同行由左到右)。

每个字符都是一个数字(1−9)或一个 .(表示尚未填充)。

您可以假设输入中的每个谜题都只有一个解决方案。

文件结尾处为包含单词 end 的单行,表示输入结束。

输出格式
每个测试用例,输出一行数据,代表填充完全后的数独。

输入样例:
4.....8.5.3..........7......2.....6.....8.4......1.......6.3.7.5..2.....1.4......
......52..8.4......3...9...5.1...6..2..7........3.....6...1..........7.4.......3.
end
输出样例:
417369825632158947958724316825437169791586432346912758289643571573291684164875293
416837529982465371735129468571298643293746185864351297647913852359682714128574936

示例代码:

#include <bits/stdc++.h>

using namespace std;

typedef long long LL;
typedef pair<int,int> PII;
#define x first
#define y second

const int N = 9, M = 1 << 9;

int ones[M], mmap[M];
int col[N], row[N], cell[3][3];
char str[100];

void init()
{
	for(int i = 0; i < N; ++i) col[i] = row[i] = (1 << 9) - 1;
	for(int i = 0; i < 3; ++i)
	{
		for(int j = 0; j < 3; ++j)
		{
			cell[i][j] = (1 << 9) - 1;
		}
	}
}

void draw(int x, int y, int t, bool is_set)
{
	if(is_set) str[x * N + y] = t + '1';  // 这里写1,是因为之后 -v 的缘故 
	else str[x * N + y] = '.';
	
	int v = 1 << t;
	if(!is_set) v = -v;
	
	row[x] -= v;  // v得是从1开始的 已经设置了说明当前该位的1要变为0 
	col[y] -= v;
	cell[x/3][y/3] -= v;
}

int get(int x, int y)
{
	return row[x] & col[y] & cell[x/3][y/3];
}

int lowbit(int x)
{
	return x & -x;
}

bool dfs(int cnt)
{
	if(!cnt) return true;
	
	int minv = 10, x, y;
	for(int i = 0; i < N; ++i)
	{
		for(int j = 0; j < N; ++j)
		{
			if(str[i * N + j] == '.')
			{
				int state = get(i,j);
				if(ones[state] < minv)  // 从里面找能填的最小的 
				{
					minv = ones[state];
					x = i, y = j;
				}
			}
		}
	}
	
	int state = get(x,y);
	for(int i = state; i; i -= lowbit(i))  // 枚举1的次数 
	{
		int t = mmap[lowbit(i)];  
		draw(x,y,t,true);
		if(dfs(cnt-1)) return true;
		draw(x,y,t,false); 
	}
	return false;
}

int main()
{
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	for(int i = 0; i < N; ++i) mmap[1 << i] = i;
	for(int i = 0; i < M; ++i)
	{
		for(int j = 0; j < N; ++j)
		{
			if(i >> j & 1) ones[i]++;
		}
	}
	
	while(cin >> str, str[0] != 'e')
	{
		init();
		int cnt = 0;
		for(int i = 0; i < N; ++i)
		{
			for(int j = 0; j < N; ++j)
			{
				if(str[i * N + j] != '.')
				{
					int t = str[i * N + j] - '1';
					draw(i,j,t,true);
				}
				else cnt++;
			}
		}
		
		dfs(cnt);
		
		cout << str << endl; 
		
	}
	
	return 0;
}

四、总结

  • 暴搜求最值一般就是两种方法,一种是定义一个全部变量, d f s dfs dfs 内部去不断更新它,另一种是采用迭代加深的方法。
  • 关于小猫爬山和木棒这两个题,前者是枚举每个猫在其中遍历每个组,后者是枚举每个组在其中遍历每个木棍,前者是要求最小的组数,后者要求最短的木棒,前者求最值,后者求是否可行,前者是因为每个组可能装不满,后者是要每个组装满了才能装下一个组,所以怎么说呢,感觉自己还没有那么举一反三的能力,只能按模型去写对应的写法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

lijiachang030718

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值