引言
关于这个 D F S DFS DFS 的剪枝和优化确实难度是非常的大,从我这篇文章的思路和代码量上就能看出来不是一般的难度,而且难度不亚于 D P DP DP ,而且这个 D F S DFS DFS 也是花费了我三天的时间才基本把这几道例题给搞懂了,并且这种题就是没有固定的模型和套路,每个题都不一样,只有你多做题,这样在考场上才能想到这道题好像跟之前做过的题有点相似,然后再去套用模型。不可能短时间去创造一种算法,所以还是继续刷题吧,加油!
DFS之剪枝与优化
- 优化搜索顺序:大部分情况下,我们应该优先搜索分支较少的节点。比如背包问题应该先放体积大的物品
- 排除等效冗余:组合问题中, 1 , 2 , 3 1,2,3 1,2,3 与 1 , 3 , 2 1,3,2 1,3,2 是一样的,可以用一个 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,3和3,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 内部去不断更新它,另一种是采用迭代加深的方法。
- 关于小猫爬山和木棒这两个题,前者是枚举每个猫在其中遍历每个组,后者是枚举每个组在其中遍历每个木棍,前者是要求最小的组数,后者要求最短的木棒,前者求最值,后者求是否可行,前者是因为每个组可能装不满,后者是要每个组装满了才能装下一个组,所以怎么说呢,感觉自己还没有那么举一反三的能力,只能按模型去写对应的写法。