dfs
搜索
dfs
重在理解回溯
还有注意死循环
水题——啊会搜索了感人
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
int n,m,a[20],c[20];
int ans;
bool cmp(int a,int b){
return a>b;
}
void dfs(int now,int cnt){//第now只猫,用了cnt个缆车
if(now==n+1) {ans=min(cnt,ans);return;}
if(cnt>ans) return;
for(int i=1;i<=cnt;i++){
if(c[i]+a[now]>m) continue;
c[i]+=a[now];now++;
dfs(now,cnt);
now--;c[i]-=a[now];
}
cnt++;c[cnt]+=a[now];now++;
dfs(now,cnt);
now--;c[cnt]-=a[now];cnt--;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
sort(a+1,a+1+n,cmp);
ans=n;
dfs(1,1);
printf("%d\n",ans);
return 0;
}
剪枝
dfs 优秀在于它可以剪枝降低复杂度
•可行性剪枝:如果判断当前节点下面的叶子节点的状态一定不合法,剪枝
•最优性剪枝:如果当前节点下面叶子的答案一定不会比当前最优的答案优,剪枝。
经典例题:
例1:斗地主
啊啊啊啊。。。T飞了。。。发现for(1~n)写成了while(n--)...
阿西吧好不容易过了还有加强版毒瘤数据卡我
放一个ac了普通版的吧。。。天知道4小时血与泪
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int T,n,mi,tot;
int a,b,s[20];
int san(){
int res=0;
for(int i=2;i<=16;i++){
if(s[i]==4) res++;
if(s[i]==3) res++;
if(s[i]==2) res++;
if(s[i]==1) res++;
}
return res;
}
//四带二,双顺子,单顺子,三代二,三代一,四张,三张,两张(王),一张
inline void dfs(int num,int ans){
if(ans>mi) return ;
if(num==n){ mi=ans;return; }
tot=san();
if(tot+ans<mi) mi=tot+ans;//贪心——散打
int cnt=0;
for(int i=3;i<=13;cnt=0,i++){//三顺子
for(int j=i;j<=14;j++)
if(s[j]>=3) cnt++;
else break;
if(cnt>=2){
for(int j=i;j<=i+cnt-1;j++) s[j]-=3;
dfs(num+cnt*3,ans+1);
for(int j=i;j<=i+cnt-1;j++) s[j]+=3;
}
}
cnt=0;
for(int i=3;i<=12;cnt=0,i++){//双顺子
for(int j=i;j<=14;j++)
if(s[j]>=2){
cnt++;
if(cnt>=3){
for(int j=i;j<=i+cnt-1;j++) s[j]-=2;
dfs(num+cnt*2,ans+1);
for(int j=i;j<=i+cnt-1;j++) s[j]+=2;
}
}
else break;
}
cnt=0;//单顺子
for(int i=3;i<=10;cnt=0,i++){
for(int j=i;j<=14;j++)
if(s[j]>=1){
cnt++;
if(cnt>=5){
for(int j=i;j<=i+cnt-1;j++) s[j]--;
dfs(num+cnt,ans+1);
for(int j=i;j<=i+cnt-1;j++) s[j]++;
}
}
else break;
}
for(int i=2;i<=14;i++){
if(s[i]==4){
for(int j=2;j<=14;j++)// 4 d 2
if(s[j]>=2&&j!=i)
for(int k=j+1;k<=14;k++)
if(s[k]>=2&&i!=k){
s[i]-=4;s[j]-=2;s[k]-=2;
dfs(num+8,ans+1);
s[i]+=4;s[j]+=2;s[k]+=2;
}
for(int j=2;j<=15;j++)// 4 d 1
if(s[j]>=1&&j!=i)
for(int k=j+1;k<=16;k++)
if(s[k]>=1&&i!=k){
s[i]-=4;s[j]-=2;s[k]-=2;
dfs(num+6,ans+1);
s[i]+=4;s[j]+=2;s[k]+=2;
}
}
if(s[i]>=3){
for(int j=2;j<=14;j++)//3 d 2
if(s[j]>=2&&j!=i){
s[i]-=3;s[j]-=2;dfs(num+5,ans+1);s[i]+=3;s[j]+=2;
}
for(int j=2;j<=16;j++)//3 d 1
if(s[j]>=1&&j!=i){
s[i]-=3; s[j]-=1;dfs(num+4,ans+1);s[i]+=3;s[j]+=1;
}
}
}
cnt=0;
if(s[15]==1&&s[16]==1){//炸弹
s[15]--;s[16]--;
dfs(num+2,ans+1);
s[15]++;s[16]++;
}
}
int main(){
// freopen("1.in","r",stdin);
// freopen("1.out","w",stdout);
T=read();n=read();
while(T--){
memset(s,0,sizeof(s));
mi=n;
for(int i=1;i<=n;i++){//注意不能写while(n--)。。。n不可变
a=read();b=read();
if(a==1) s[14]++;//A > K
else if(a==0&&b==1) s[15]++;
else if(a==0&&b==2) s[16]++;
else s[a]++;
}
dfs(0,0);
printf("%d\n",mi);
}
return 0;
}
例2:生日蛋糕
dfs+剪枝
1.if(now_v+lastv[c-num]>v) return;现在的体积+剩下最小的体积($第i层是r=i ,h=i $) 若大于 V ,剪掉
2.if(now_s+lasts[c-num]>ans) return;同理
3.if($ now_v+rrh*(c-num)<v $) return;
现在的体积加上剩下最大的体积(每一层都和这层一样)若小于V,剪掉
预处理出剩下最小的体积、面积
for(int i=1;i<=c;i++){
lasts[i]=lasts[i-1]+2*i*i;
lastv[i]=lastv[i-1]+i*i*i;
}
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
int v,c,ans=0x3f3f3f3f;
int lasts[16];
int lastv[16];
bool f;
void dfs(int r,int h,int now_v,int now_s,int num){
if(now_v+lastv[c-num]>v) return;
if(now_s+lasts[c-num]>ans) return;
if(now_v+r*r*h*(c-num)<v) return;
if(num==c) {if(now_v==v)ans=min(ans,now_s);f=1;return;}
for(int i=r-1;i>=c-num;i--)
for(int j=h-1;j>=c-num;j--)
dfs(i,j,i*i*j+now_v,2*i*j+now_s,num+1);
}
int main(){
scanf("%d%d",&v,&c);
for(int i=1;i<=c;i++){
lasts[i]=lasts[i-1]+2*i*i;
lastv[i]=lastv[i-1]+i*i*i;
}
for(int r=c;r<=30;r++)
for(int h=c;h<=30;h++)
dfs(r,h,r*r*h,r*r+2*r*h,1);
if(f) printf("%d\n",ans);
else puts("0");
return 0;
}
例3:小木棍
剪枝好题
1.对木棍长 从大到小的排序,先加入长的棍,再加入短的棍,相较于先加入短的棍后加入长的棍,搜索数大大减小
2.枚举长度只用枚举 sum 的因数
3.记录上一次枚举的小木棍的长度(f),如果上一次不成功(回溯了)那么下一次这个肯定也不行
每次到了一根新大木棒,f=1
4.if(length==0) return false; 此句的意思是因为所有木棒长度相同皆等价,倘若第一根木棒就匹配失败(就是扫完了所有小木棍都不能连成这个木棒),其它木棒也会失败
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
using namespace std;
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int n,a[111],sum;
int len,x;//木棒长度
bool cmp(int a,int b){return a>b;}
bool vis[111];
bool dfs(int num,int length,int last){
if(num>=x) return 1;
if(length==len) return dfs(num+1,0,1);
int f=0;//3
for(int j=last;j<=n;j++){
if(!vis[j]&&len>=a[j]+length&&f!=a[j]){
vis[j]=1;
if(dfs(num,length+a[j],j+1)) return 1;
vis[j]=0;
f=a[j];
if(length==0 || length+a[j]==len) return 0;
}
}
return 0;
}
int main(){
n=read();
for(int i=1;i<=n;i++){
a[i]=read();
if(a[i]>50) {--i,--n;continue;}
sum+=a[i];
}
sort(a+1,a+1+n,cmp);//1
for(len=a[1];len<=sum;len++){//2
if(sum%len) continue;
memset(vis,0,sizeof(vis));
x=sum/len;
if(dfs(1,0,1)){
printf("%d\n",len);break;
}
}
return 0;
}
例4:切蛋糕
\(n\)块蛋糕,\(m\)个人,可以切蛋糕,但他不能把两块蛋糕拼起来,但是他又不会给任何人两块蛋糕,问最多可以满足多少人。
Solution
二分答案+贪心
蛋糕从大到小排序,先大蛋糕塞,然后再用小蛋糕,切的话也尽量切小蛋糕
先喂吃的多的人,因为我们是检验答案,这样能减少搜索状态,蛋糕消耗得快。
#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long ll;
const int N=10005;
const int M=400005;
inline int read() {
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return f*x;
}
int n,m;
int cake[N],tmp[N],p[M],sc,s[M];
int waste;
bool dfs(int x,int num,int lim) {//人的编号,蛋糕编号,
if(!x) return 1;
if(sc-waste<s[lim]) return 0;
for(int i=num;i<=n;i++) {
if(tmp[i]<p[x]) continue;
tmp[i]-=p[x];
if(tmp[i]<p[1]) waste+=tmp[i];//
if(p[x]==p[x-1]) {
if(dfs(x-1,i,lim)) return 1;
}
else if(dfs(x-1,1,lim)) return 1;
//回溯
if(tmp[i]<p[1]) waste-=tmp[i];
tmp[i]+=p[x];
}
return 0;
}
int main() {
n=read();
for(int i=1;i<=n;i++) cake[i]=read(),sc+=cake[i];
sort(cake+1,cake+1+n);
m=read();
for(int i=1;i<=m;i++) p[i]=read();
sort(p+1,p+1+m);
while(sc<p[m]) m--;
for(int i=1;i<=m;i++) s[i]=s[i-1]+p[i];
int l=0,r=m,ans;
while(l<=r) {
int mid=(l+r)>>1;
waste=0;
memcpy(tmp,cake,sizeof(tmp));
if(dfs(mid,1,mid)) ans=mid,l=mid+1;
else r=mid-1;
}
printf("%d\n",ans);
return 0;
}
迭代加深搜索
详见蓝书P108
简而言之,如果知道答案在层数较浅的地方,我们可以限制搜索层数,进行迭代加深搜索。
缺点是会重复搜搜过状态,优点在于当搜索树随层数规模增长飞快时,该方案可飞快
采用 bool 类型返回值时,是一种存在性搜索,并不一定能够得到最优解
例:Addition Chains
http://poj.org/problem?id=2248 poj数据规模较小
优化1:显然倒序枚举更有可能得到可行解
if(a[i]+a[j]>n) continue;
if(a[i]+a[j]<=a[now-1]) break;
两条可行性剪枝
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
int n,dep,a[10005],ff=1;
bool dfs(int now){
if(now==dep+1) return a[dep]==n;
for(int i=now-1;i>=1;i--)
for(int j=i;j>=1;j--){
if(a[i]+a[j]>n) continue;
if(a[i]+a[j]<=a[now-1]) break;
a[now]=a[i]+a[j];
if(dfs(now+1)) return 1;
a[now]=0;
}
return 0;
}
int main(){
while(scanf("%d",&n)&&n){
memset(a,0,sizeof(a));
a[1]=1;
for(dep=1;;dep++)
if(dfs(2)) break;
for(int i=1;i<dep;i++)
printf("%d ",a[i]);
printf("%d\n",a[dep]);
}
return 0;
}
然鹅UVA这道题数据范围并不友好
第三个剪枝
对于该数列,有性质 $ f[n]<=2*f[n-1]\(,所以 如果a[now] *\) 2^{(dep-now-1) }<n$ 的话,后面显然不能得到解,break掉
#include<iostream>
#include<cstdio>
using namespace std;
int n,dep,a[10007];
bool dfs(int now){
if(now==dep+1)
return a[dep]==n;
for(int i=now-1;i>=1;i--){
for(int j=i;j>=1;j--){
a[now]=a[i]+a[j];
int b=a[now];
for(int k=now+1;k<=dep;k++) b*=2;
if(b<n) break;
if(dfs(now+1)) return 1;
a[now]=0;
}
}
return 0;
}
int main(){
while((~scanf("%d",&n))&&n){
a[1]=1;
if(n==1){
printf("1\n");
continue;
}
for(dep=1;;dep++){
if(dfs(2)){
for(register int i=1;i<dep;i++)
printf("%d ",a[i]);
printf("%d\n",a[dep]);
break;
}
}
}
return 0;
}