DFS & DFS 剪枝优化
Basic
01 先搜节点少的分支
如果搜进来一个大分支而答案不在此分支就会浪费大量时间
02 可行性剪枝
已经白扯了就 return
判断当前是否合法
03 最优性剪枝
如果此分支确定不是最优解(差于已有解)就 return
04 记忆化搜索
记录之前搜过的 Data ,避免重复搜
Question 01 [ACP1682 红与黑]
模板题,注意长宽输入顺序与常见顺序相反
Code
#include<bits/stdc++.h>
using namespace std;
const int N=35;
int n,m,sx,sy,dx[6]={0,1,0,0,-1},dy[6]={0,0,1,-1,0},ans;
char mp[N][N];
void dfs(int x,int y){
++ans;
mp[x][y]='#';
for(int i=1;i<=4;i++){
int xx=x+dx[i];
int yy=y+dy[i];
if(xx<1||yy<1||xx>n||yy>m)continue;
if(mp[xx][yy]=='#')continue;
dfs(xx,yy);
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
while(true){
ans=0;
cin>>m>>n;
if(n==0&&m==0)return 0;
for(int i=1;i<=n;i++){
cin>>mp[i]+1;
for(int j=1;j<=m;j++)if(mp[i][j]=='@')sx=i,sy=j,mp[i][j]='.';
}
dfs(sx,sy);
cout<<ans<<"\n";
}
}
Question 02 [ACP1685 马走日]
[dfs +回溯]模板
注意使用下述写法需要特殊标记起点
因为使用了回溯
所以不需要 memset 了
Code
#include<bits/stdc++.h>
using namespace std;
const int N=10;
int n,m,x,y,st[N][N],ans;
const int dx[10]={0,1,1,2,2,-1,-1,-2,-2},dy[10]={0,-2,2,-1,1,-2,2,-1,1};
void dfs(int x,int y,int step){
if(step==n*m){
++ans;
return;
}
for(int i=1;i<=8;i++){
int xx=x+dx[i],yy=y+dy[i];
if(xx<0||yy<0||xx>=n||yy>=m)continue;
if(st[xx][yy])continue;
st[xx][yy]=true;
dfs(xx,yy,step+1);
st[xx][yy]=false;
}
}
int main(){
int T;
scanf("%d",&T);
while(T--){
ans=0;
scanf("%d%d%d%d",&n,&m,&x,&y);
st[x][y]=1;
dfs(x,y,1);
st[x][y]=0;
printf("%d\n",ans);
}
return 0;
}
Question 03 [ACP2512 小猫爬山]
这题 luogu 只有橙
但卡了我 1e9+7 秒
显然是我太菜了
Answer
记录当前每一个车的已有载重
用每一只猫去选择坐哪一辆车
dfs 的两维分别是猫的下标和已有的花费
首先先枚举"蹭"上哪一辆车
最终再统计一下单开一辆车的最优答案
本题使用了优化技巧
Basic 01 和 Basic 03
经作者测试如果不是先从大到小进行排序
很难在一秒内通过
dfs 的部分还加入了最优性剪枝
记录局部最优答案与当前答案进行对比
如果发现当前答案已超过局部最优答案就直接舍弃掉该分支
Code
#include<bits/stdc++.h>
using namespace std;
const int N=20;
int cat[N],n,lim,car[N],ans=1000;
void dfs(int cat_id,int money){
if(money>ans)return;
if(cat_id>n){ans=min(ans,money);return;}
for(int i=1;i<=money;i++){
if(cat[cat_id]+car[i]<=lim){
car[i]+=cat[cat_id];
dfs(cat_id+1,money);
car[i]-=cat[cat_id];
}
}
car[money+1]=cat[cat_id];
dfs(cat_id+1,money+1);
car[money+1]=0;
}
bool cmp(int a,int b){return a>b;}
int main(){
scanf("%d%d",&n,&lim);
for(int i=1;i<=n;i++)scanf("%d",&cat[i]);
sort(cat+1,cat+n+1,cmp);
dfs(1,0);
printf("%d",ans);
return 0;
}
Question 04 [ACP2513 Sudoku]
题面就是个简简单单的数独
主体还是dfs+回溯
然而细节...
01 二进制优化
用一个九位无符号整数表示数独中哪几个数可以选择
example (000110010) 表示 在该范围内2,5,6三个数字可以选择而其他数字不可以
而要把行列和单个方格的信息结合起来
只需要使用与的位运算即可
将信息取出可以使用 lowbit 和预处理的 log 数组结合实现
为了实现第二步我们还需要预处理出每个二进制数之中数字一的个数 (即限制数)
至于用数组......我只能说你可以试试
02 Basic 01 顺序优化
容易知道如果当前一步限制多的更容易被选出
而如果有 9 个限制即证明当前分支已经无解
可以直接 return 掉(Basic 02 可行性剪枝)
同时需要将 DFS 设为 bool 类型记录是否匹配成功
我们找到限制最多的位置
对其进行讨论 DFS 即为更优选择
03 回溯
这本来并不难,当然在这道题里就是另一说了
我们记录的限制只有行,列和方格3种
(不要记录单个方格! 回溯直接就把你 taskkill 了!)
很明显一行一列或者一个方格不会出现两个相同的数
所以在放置/删除数的时候修改状态值
只需要暴力的加或者减即可
04 输入
本题在 Accoders 上抽象的读入方式令人暖心
必须特殊处理
接下来的就是开始码......
Code
#include<bits/stdc++.h>
using namespace std;
const int N=10,sqrtN=4,squN=82,Size=9,Max=511;
int mp[N][N],one[520],line[N],colume[N],square[sqrtN][sqrtN];
int logBin(int x){
if(x==1)return 1;
if(x==2)return 2;
if(x==4)return 3;
if(x==8)return 4;
if(x==16)return 5;
if(x==32)return 6;
if(x==64)return 7;
if(x==128)return 8;
if(x==256)return 9;
puts("Shit!");
return -1;
}
void place(int value,int x,int y,int on){
if(abs(on)!=1){
puts("Illegal value!");
return;
}
if(on==1) mp[x][y]=value; else mp[x][y]=-1;
line[x]-=on*(1<<(value-1));
colume[y]-=on*(1<<(value-1));
square[(x-1)/3+1][(y-1)/3+1]-=on*(1<<(value-1));
}
int lowbit(int x){
return x&-x;
}
bool dfs(int todo){
if(todo==0){
for(int i=1;i<=9;i++)for(int j=1;j<=9;j++)printf("%d",mp[i][j]);
puts("");
return true;
}
int minx=-1,miny=-1,minnum=10,lim,key;
for(int i=1;i<=9;i++)for(int j=1;j<=9;j++){
if(mp[i][j]!=-1)continue;
lim=line[i]&colume[j]&square[(i-1)/3+1][(j-1)/3+1];
if(one[lim]==0)return false;
if(one[lim]<minnum)minnum=one[lim],minx=i,miny=j,key=lim;
}
while(key){
place(logBin(lowbit(key)),minx,miny,1);
if(dfs(todo-1))return true;
place(logBin(lowbit(key)),minx,miny,-1);
key-=lowbit(key);
}
return false;
}
int main(){
int todo;
for(int i=0,tmp;i<=Max;i++){
tmp=i;
while(tmp){
if(tmp&1)one[i]++;
tmp>>=1;
}
}
char input_data[squN];
while(true){
for(int i=1;i<=9;i++)line[i]=Max,colume[i]=Max;
for(int i=1;i<=3;i++)for(int j=1;j<=3;j++)square[i][j]=Max;
todo=0;
scanf("%s",input_data);
if(input_data[0]=='e')return 0;
for(int i=0;i<=80;i++){
if(isdigit(input_data[i])){
place(input_data[i]-'0',i/9+1,i%9+1,1);
}else{
++todo;
mp[i/9+1][i%9+1]=-1;
}
}
dfs(todo);
}
return 0;
}
Question 05 [ACP Addition Chains]
本题采用迭代加深 + 优化搜索顺序
AC 上还需要输出数链长度加三个空格
(题面不写数据写,只能说不愧是他)
Solution
爆搜 DFS 肯定不行
最多 10000 的 depth 直接挂了
宽搜 BFS 也白扯
一次存一个数组
正所谓你不 MLE 谁 MLE
所以
我们要使用新算法 [迭代加深]
即限定深度的 DFS,简称 ID
(没错,就是加上启发式搜索的 IDA*)
每次将深度 ++
用 DFS 的空间复杂度达到与 BFS 相似的效果
于是有了以下的 Code
memCode
#include<bits/stdc++.h>
using namespace std;
int n,lable[90099];
bool st[90099];
bool dfs(int depth,int lim,int mini){
if(depth==lim){
for(int i=1;i<depth;i++)if(st[n-lable[i]])return true;
return false;
}
int rim=min(n,mini<<1);
for(int i=mini+1;i<=rim;i++){
for(int j=1;j<depth;j++){
if(st[i-lable[j]]==true){
st[i]=1;
lable[depth]=i;
if(dfs(depth+1,lim,i))return true;
st[i]=0;
break;
}
}
}
return false;
}
int main(){
lable[0]=true;
while(true){
cin>>n;
st[0]=1,st[1]=1,lable[1]=1;
if(n==0)return 0;
if(n==1){puts("1");continue;}
for(int i=2;i<=n;i++){
for(int i=2;i<=n;i++)st[i]=0,lable[i]=0;
if(dfs(2,i,1)){
printf("%d ",i);
for(int j=1;j<i;j++)printf("%d ",lable[j]);
printf("%d\n",n);
break;
}
}
}
return 0;
}
这个 Code 正确性是有的
(只不过 AC 没有 SPJ 无法评测)
但是 n>300 就基本"坐下"了
思考如何优化
01 优化搜索顺序
搜索时从大向小遍历
数据1000 5310ms => 1140ms
02 最优性剪枝
判断之后每步都乘二能否达到ans
数据10000 跑不出来 => 2000ms
Code
#include<bits/stdc++.h>
using namespace std;
int n,lable[90099];
bool st[90099];
bool dfs(int depth,int lim,int mini){
if(depth==lim){
for(int i=1;i<depth;i++)if(st[n-lable[i]])return true;
return false;
}
if(lable[depth-1]*1<<(lim-depth+1)<n)return false;
int rim=min(n,mini<<1);
for(int i=mini+1;i<=rim;i++){
for(int j=1;j<depth;j++){
if(i<=ans[j])break;
if(st[i-lable[j]]==true){
st[i]=1;
lable[depth]=i;
if(dfs(depth+1,lim,i))return true;
st[i]=0;
break;
}
}
}
return false;
}
int main(){
lable[0]=true;
while(true){
cin>>n;
st[0]=1,st[1]=1,lable[1]=1;
if(n==0)return 0;
if(n==1){puts("1");continue;}
for(int i=2;i<=n;i++){
for(int i=2;i<=n;i++)st[i]=0,lable[i]=0;
if(dfs(2,i,1)){
printf("%d ",i);
for(int j=1;j<i;j++)printf("%d ",lable[j]);
printf("%d\n",n);
break;
}
}
}
return 0;
}
Question 06 [ACP2514 木棒][P1120 小木棍]
疯狂剪枝题
裸的思路很简单
枚举木棍长度(可行性剪枝处理边界)
dfs 开两位维护组数和当前组的长度
开标记数组并进行回溯
简单加一个搜索顺序剪枝从大到小排序
类似 Question 03
于是有了以下代码
luogu Code 33pts TLE
#include<bits/stdc++.h>
using namespace std;
const int N=70;
int n,lable[N],tot,Lim;
bool st[N];
inline bool cmp(int a,int b){return a>b;}
bool dfs(int group,int len){
if(group==0)return true;
if(len==Lim)return dfs(group-1,0);
for(int i=1;i<=n;i++){
if(st[i]||len+lable[i]>Lim)continue;
st[i]=true;
if(dfs(group,len+lable[i]))return true;
st[i]=false;
}
return false;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++)scanf("%d",&lable[i]),tot+=lable[i];
sort(lable+1,lable+n+1,cmp);
for(Lim=lable[1];Lim<=tot;Lim++){
if(tot%Lim!=0)continue;
if(dfs(tot/Lim,0))printf("%d",Lim),exit(0);
}
return 0;
}
这么裸的代码肯定无法 AC,考虑优化
# 01 避免冗余搜索 33pts -> 36pts (8.33 s)
如果当前木棍失败
则判断下一根木棍长度与该木棍长度是否相同
显然如果原来会失败
换一根相同长度的木棍也会失败
特判可以节省时间
# 02 二分查找 36pts == 36pts (8.20 s)
发现一开始木棍长度会过长
需要不断判断
考虑将其二分维护
# 03 玄学优化 36pts == 36pts (8.15 s)
如果当前木棍是本组的最后一根木棍(将本组长度补成 Lim)且最终 DFS 失败了
那么不要犹豫,果断 return false
因为如果不放这一根长木棍就要放多根短木棍
肯定是不如放长木棍容易成功的
# 04 优化搜索边界 36pts -> 57pts (5.32 s)
发现如果是在上一组的基础上接木棍 i 的话
可以直接从 i+1 根搜
当然如果新开组就要从第一根搜索了
# 05 常数优化 57pts -> 57pts (5.32 s)
将第一步的优化预处理出来
# 07 玄学优化二 57pts -> 87pts (809 ms)
每组第一根如果失败则一定失败
因为这根木棍终归是要放的
和后面的木棍凑不了
则只能和前面的木棍凑
# 08 Getchar_Unlocked (813 ms)
加个快读结果还慢了
删掉!
# 09 优化边界判断 (694 ms)
就是把边界的递归减少一层
# 09 卡常 (690 ms)
DFS i 定义在外部
还差 11ms!
# 10 Inline (687 ms)
# 11 ... (685 ms)
# 12 AC (510 ms)
发现第二步和第四步可以结合
不需要 max 函数
然后就 AC 了
AC_Code [luogu]
#include<bits/stdc++.h>
using namespace std;
const int N=70;
int n,lable[N],tot,Lim,jump[N];
bool st[N];
inline bool cmp(int a,int b){return a>b;}
inline int Binary_Get(int l,int r,int less_than){
int mid;
while(l<r){
mid=(l+r)>>1;
if(lable[mid]<=less_than){
r=mid;
}else{
l=mid+1;
}
}
return l;
}
bool dfs(int group,int len,int pos){
int i=1;
if(len==Lim){
if(group==1)return true;
for(;i<=n;i++)if(!st[i])break;
st[i]=true;
if(dfs(group-1,lable[i],i+1))return true;
st[i]=false;
return false;
}
int from_pos=Binary_Get(pos,n,Lim-len);
for(i=from_pos;i<=n;i++){
if(st[i]||len+lable[i]>Lim)continue;
st[i]=true;
if(dfs(group,len+lable[i],i+1))return true;
st[i]=false;
if(len+lable[i]==Lim||len==0)return false;
i=jump[i];
}
return false;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&lable[i]);
tot+=lable[i];
}
sort(lable+1,lable+n+1,cmp);
for(int i=n;i>=1;i--)if(lable[i]==lable[i+1])jump[i]=jump[i+1];else jump[i]=i;
for(Lim=lable[1];Lim<=tot>>1;Lim++){
if(tot%Lim!=0)continue;
if(dfs(tot/Lim,0,1)){printf("%d",Lim);return 0;}
}
printf("%d",tot);
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 从 Windows Forms 到微服务的经验教训
· 李飞飞的50美金比肩DeepSeek把CEO忽悠瘸了,倒霉的却是程序员
· 超详细,DeepSeek 接入PyCharm实现AI编程!(支持本地部署DeepSeek及官方Dee