DFS之剪枝与优化
dfs和bfs都是对应一个搜索树,常用的剪枝策略:
①优化搜索顺序:大部分情况下,我们应该优先搜索分支较少的节点。
②排除等效冗余
③可行性剪枝
④最优性剪枝
⑤记忆化搜索(dp)
165. 小猫爬山
翰翰和达达饲养了 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≤10^8
输入样例:
5 1996
1
2
1994
12
29
输出样例:
2
cpp
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 20;
int n,m;
int w[N];
int sum[N];
int ans=N;
void dfs(int u,int k){
//最优性剪枝
if(k>=ans) return ;
if(u==n){
ans=k;
return;
}
for(int i=0;i<k;i++){
if(sum[i]+w[u]<=m){//可行性剪枝
sum[i]+=w[u];
dfs(u+1,k);
sum[i]-=w[u];//恢复现场
}
}
sum[k]=w[u];
dfs(u+1,k+1);
sum[k]=0;//恢复现场
}
int main(){
cin>>n>>m;
for(int i=0;i<n;i++) cin>>w[i];
//优化搜索顺序
sort(w,w+n);
reverse(w,w+n);
dfs(0,0);//从零号猫,当前车的数量是0开始搜索
cout<<ans<<endl;
return 0;
}
166. 数独
数独 是一种传统益智游戏,你需要把一个 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
解题思路
位运算优化:行,列,九宫格,都可以用9个二进制数表示(1表示可放,0表示不能放),只有三者取&后,如果仍为1,才能放进去.
cpp
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 9,M = 1<<N;
int ones[M],map[M];
int row[N],col[N],cell[3][3];
char str[100];
void init(){
for(int i=0;i<N;i++) row[i]=col[i]=(1<<N) - 1;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++){
cell[i][j]=(1<<N)-1;
}
}
void draw(int x,int y,int t,bool is_set)//is_set,可以填,就是dfs的过程,反之则是恢复现场
{
if(is_set) str[x*N+y] = '1'+t;
else str[x*N+y]='.';
int v=1<<t;
if(!is_set) v=-v;
row[x]-=v;
col[y]-=v;
cell[x/3][y/3]-=v;
}
int lowbit(int x)
{
return x&-x;
}
int get(int x,int y){//能填的数
return row[x] & col[y] & cell[x/3][y/3];
}
bool dfs(int cnt){
if(!cnt) return true;
//找个分支最小的进行遍历
int minv=10;
int 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)){
int t=map[lowbit(i)];//填的数字
draw(x,y,t,true);
if(dfs(cnt-1)) return true;
draw(x,y,t,false);
}
return false;
}
int main(){
for(int i=0;i<N;i++) map[1<<i] = i;
for(int i=0;i< 1<<N;i++){//每个二进制数里面有多少个1
for(int j=0;j<N;j++)
ones[i] += i>>j & 1;
}
while(cin>>str,str[0]!='e'){
init();
int cnt=0;
for(int i=0,k=0;i<N;i++){
for(int j=0;j<N;j++,k++){
if(str[k]!='.'){//统计需要多少个需要填的位置
int t=str[k]-'1';
draw(i,j,t,true);
}
else cnt++;
}
}
dfs(cnt);
puts(str);
}
return 0;
}
167. 木棒
乔治拿来一组等长的木棒,将它们随机地砍断,使得每一节木棍的长度都不超过 50个长度单位。
然后他又想把这些木棍恢复到为裁截前的状态,但忘记了初始时有多少木棒以及木棒的初始长度。
请你设计一个程序,帮助乔治计算木棒的可能最小长度。
每一节木棍的长度都用大于零的整数表示。
输入格式
输入包含多组数据,每组数据包括两行。
第一行是一个不超过 64的整数,表示砍断之后共有多少节木棍。
第二行是截断以后,所得到的各节木棍的长度。
在最后一组数据之后,是一个零。
输出格式
为每组数据,分别输出原始木棒的可能最小长度,每组数据占一行。
数据范围
数据保证每一节木棍的长度均不大于 50。
输入样例:
9
5 2 1 5 2 1 5 2 1
4
1 2 3 4
0
输出样例:
6
5
解题思路
① length | sum
②优先搜索顺序,从大到小枚举
③排除等效冗余,按照组合数的方式枚举,start存储下一给木棍从哪里开始; 如果当前木棍放到当前棒子中石板路,则直接掠过其他所有长度相等的木棍;如果是当前木棒的第一个木棍失败,则一定失败;如果放到最后一个失败,则也一定失败
cpp
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 70;
int n;
int w[N],sum,length;
bool st[N];
bool dfs(int u,int s,int start)//u代表当前的木棍的编号,s表示当前大棍的长度
{
if(u*length==sum) return true;
if(s==length) return dfs(u+1,0,0);//走下一步的搜索
for(int i=start;i<n;i++){
if(st[i]) continue;//用过了
if(s+w[i]>length) continue;//可行性剪枝
st[i]=true;//使用了这个木棍
if(dfs(u,s+w[i],i+1)) return true;
st[i]=false;//恢复现场
//剪树3-3
if(!s) return false ;
//剪枝3-4
if(s+w[i]==length) return false;
//剪枝3-2
int j=i;
while(j<n&&w[j]==w[i]) j++;
i=j-1;
}
return false;
}
int main(){
while(cin>>n,n){
memset(st,0,sizeof st);
sum=0;
for(int i=0;i<n;i++) cin>>w[i],sum+=w[i];
//优化搜索顺序
sort(w,w+n);
reverse(w,w+n);
length=1;
while(1){
//剪枝1
if(sum%length==0&&dfs(0,0,0)){
cout<<length<<endl;
break;
}
length++;
}
}
return 0;
}
168. 生日蛋糕
7月 17日是 Mr.W 的生日,ACM-THU 为此要制作一个体积为 Nπ的 M层生日蛋糕,每层都是一个圆柱体。
设从下往上数第 i层蛋糕是半径为 Ri,高度为 Hi的圆柱。
当 i<M时,要求 Ri>Ri+1且 Hi>Hi+1。
由于要在蛋糕上抹奶油,为尽可能节约经费,我们希望蛋糕外表面(最下一层的下底面除外)的面积 Q最小。
令 Q=Sπ ,请编程对给出的 N和 M,找出蛋糕的制作方案(适当的 Ri
和 Hi的值),使 S最小。
除 Q外,以上所有数据皆为正整数。
输入格式
输入包含两行,第一行为整数 N,表示待制作的蛋糕的体积为 Nπ。
第二行为整数 M,表示蛋糕的层数为 M。
输出格式
输出仅一行,是一个正整数 S(若无解则 S=0)。
数据范围
1≤N≤10000,
1≤M≤20
输入样例:
100
2
输出样例:
68
cpp
#include <cstring>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
const int N = 25, INF = 1e9;
int n, m;
int minv[N], mins[N];
int R[N], H[N];
int ans = INF;
void dfs(int u, int v, int s)
{
if (v + minv[u] > n) return;
if (s + mins[u] >= ans) return;
if (s + 2 * (n - v) / R[u + 1] >= ans) return;
if (!u)
{
if (v == n) ans = s;
return;
}
for (int r = min(R[u + 1] - 1, (int)sqrt(n - v)); r >= u; r -- )
for (int h = min(H[u + 1] - 1, (n - v) / r / r); h >= u; h -- )
{
int t = 0;
if (u == m) t = r * r;
R[u] = r, H[u] = h;
dfs(u - 1, v + r * r * h, s + 2 * r * h + t);
}
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= m; i ++ )
{
minv[i] = minv[i - 1] + i * i * i;
mins[i] = mins[i - 1] + 2 * i * i;
}
R[m + 1] = H[m + 1] = INF;
dfs(m, 0, 0);
if (ans == INF) ans = 0;
cout << ans << endl;
return 0;
}