迭代加深
当答案的层数较低,并且搜索的分支较多时,如果直接搜索会消耗很多时间。这时候可以进行多次搜索,每次搜索可以限制一个深度,如果我们在当前深度下搜索不到答案,就增加深度限制,重新搜索一边答案,这样“迭代”且“加深”的过程称为迭代加深。但他的缺点也很明显,每次需要重新搜索一遍,所以在答案的层数比较深的时候不建议使用。
【例题】Addition Chains (poj2248)
需要求一个长度(长度为m)最小的序列
a
a
满足
2、a[m]=m
2
、
a
[
m
]
=
m
3、a[1]<a[2]<.....a[m−1]<a[m]
3
、
a
[
1
]
<
a
[
2
]
<
.
.
.
.
.
a
[
m
−
1
]
<
a
[
m
]
4、对于每个a[k](2≤k≤m)都存在a[k]=a[i]+a[j](1≤i,j≤k−1,i,j可以相等)
4
、
对
于
每
个
a
[
k
]
(
2
≤
k
≤
m
)
都
存
在
a
[
k
]
=
a
[
i
]
+
a
[
j
]
(
1
≤
i
,
j
≤
k
−
1
,
i
,
j
可
以
相
等
)
其中
n(n≤100)
n
(
n
≤
100
)
是给定的值
搜索思路大概是枚举每个k进行搜索,把i,j作为分支填写到a[k]上进行搜索。
套路剪枝一下,倒序枚举,a[i]+a[j]判重。
由于n比较小,且要满足第四条条件,所以长度m的长度不会太大,所以深度不会太大,由于所有小于k的位置都可以作为i,j所以搜索的分支比较多。那么我们就可以使用迭代加深来优化速度。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cstdlib>
using namespace std;
int n;
int a[110];
bool v[110][110];
int dep;
bool flag;
void dfs(int k)
{
if(flag) return;
if(k==dep+1) return;
if(a[k-1]>n) return;
if(a[k-1]==n)
{
flag=true;
for(int i=1;i<k-1;i++) printf("%d ",a[i]);
printf("%d\n",a[k-1]);
return;
}
memset(v[k],false,sizeof(v[k]));
for(int i=k-1;i>=1;i--)
{
if(a[i]+a[i]<a[k-1]) break;
for(int j=i;j>=1;j--)
{
if(a[i]+a[j]<a[k-1]) break;
if(v[k][a[i]+a[j]]==false)
{
v[k][a[i]+a[j]]=true;
a[k]=a[i]+a[j];
dfs(k+1);
a[k]=0;
}
}
}
}
int main()
{
while(scanf("%d",&n)&&n)
{
dep=1; flag=false;
a[1]=1;
while(1)
{
dfs(2); //2<=k<=m
if(flag) break;
dep++;
}
}
}
双向搜索
双向搜索又名折半搜索。当搜索的复杂度在指数级的时候,我们可以通过将指数折半的方法降低搜索复杂度。
具体做法是从初态和终态出发各搜索一半状态,产生两颗深度减半的搜索树,两颗树交汇在一起形成最终答案,将
O(nk)
O
(
n
k
)
降低到
O(nk/2+nk/2+1)
O
(
n
k
/
2
+
n
k
/
2
+
1
)
的复杂度。
其实对于这样的指数级复杂度,如果指数除以2后可以接受的话,可以考虑双向搜索。
【例题】灯
传送门
双向搜索的例题,首先状压灯的开关状态。我们知道起点状态时全灭,终点状态是全亮,那么就可以分成两半搜索,搜到相反数即可。假设有四个灯,我们把灯分成左右两部分,从0000~1101我们只按左边的按钮,1111~0010只按右边的,那加起来就是答案,可以更新最小步数。
那么对于一个点相连的所有点,我们可以对一个点x记录一个二进制f[x],将他相连的点(包括他自己)记为1,搜索的时候记录一个当前状态now,每次开关x的时候就可以让now异或f[x],就可以得到开关这个点可以达到的状态啦。
#include<cstdio>
#include<iostream>
#include<cstring>
#include<algorithm>
#include<map>
using namespace std;
typedef long long ll;
const int INF=2147483647;
ll bin[40];
ll f[40],ed;
bool half=false;
int cnt,minn;
map<ll,int>s;//到now这个状态需最少要多少步,折半后第二次加上第一次的值
void dfs(int x,ll now,int step)
{//当前在x这个点,当前状态为now,当前走了step步
if(x==cnt+1)
{
if(now==ed)
minn=min(minn,step);
else
{
if(!half)//没过半
{
int t=s[now];
if(t==0 || step<t) s[now]=step;//维护最少需要多少步
}
else//过半,看看now和其他合并能否到达最终状态
{
int t=s[ed-now];
if(t!=0) minn=min(minn,t+step);
}
}
return;
}
dfs(x+1,now^f[x],step+1);
dfs(x+1,now,step);
}
int main()
{
int n,m;scanf("%d%d",&n,&m);
f[1]=bin[1]=1; for(int i=2;i<=n+1;i++) f[i]=bin[i]=bin[i-1]*2;
ed=bin[n+1]-1;
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
f[x]+=bin[y],f[y]+=bin[x];
}
minn=INF;
half=false;cnt=n/2; dfs(1,0,0);
half=true; cnt=n; dfs(n/2+1,0,0);
printf("%d\n",minn);
return 0;
}
【例题】送礼物(tyvj1340)
在N个数中选若干个数,使得他们加起来小于等于W且最接近W,输出这个最接近W的值。
( N<=45 W<=2^31-1)
可以看到W是很大的,所以不用背包来做。
搜索的话,对于一个物品,朴素的有选和不选两种选择,那么这样的复杂度是
O(2N)
O
(
2
N
)
,但如我们上面所说,指数级复杂度如果折半可以接受的话,可以考虑双向搜索。我们发现
O(222+223)
O
(
2
22
+
2
23
)
是可以接受的,所以我们可以考虑折半搜索。
首先,我们第一遍搜索出前一半数中选出若干个可以达到的0~W之间的所有重量值,把他们存放在一个有序数组A中。
然后,第二遍在后一半选出若干个数,记录他们的重量和now,对于一个重量和now在A中二分查找<=W-now中最大的一个t,然后用t+now更新答案。
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#include<climits>
#include<set>
using namespace std;
typedef long long ll;
const int N=50;
const int INF=INT_MAX;
int W;
int n,a[N];
int A[1<<25],tmp=0;
int cnt;
int maxx;
bool half;
int cmp(int a,int b){return a>b;}
int erfen(int val)
{
int l=1,r=tmp,ans;
while(l<=r)
{
int mid=(l+r)/2;
if(A[mid]<=val)
{
ans=A[mid];
l=mid+1;
}
else
r=mid-1;
}
return ans;
}
void dfs(int x,int now)
{
if(x==cnt+1)
{
if(!half)
A[++tmp]=now;
else
{
int t=erfen(W-now);
maxx=max(maxx,t+now);
}
return;
}
if((ll)now+a[x]<=W) dfs(x+1,now+a[x]);
dfs(x+1,now);
}
int main()
{
scanf("%d%d",&W,&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
tmp=0; maxx=0;
sort(a+1,a+n+1,cmp);
half=false; cnt=n/2; dfs(1,0);
sort(A+1,A+tmp+1);
half=true; cnt=n; dfs(n/2+1,0);
printf("%d\n",maxx);
return 0;
}