进阶式 Search 学习笔记

博客食用效果更佳

进阶式搜索,顾名思义,很难。

进阶式搜索分为:IDS(迭代加深深度优先搜索),A*(启发式广搜),IDA*(启发式迭代加深搜索),折半搜索等几类。

我们先来了解概念:

1. IDS。

顾名思义,IDS 其实就是在 DFS 的基础上增加一层循环进行迭代。那它迭代的是什么呢?

我们对 DFS 的最大搜索深度进行迭代,这也就代表着 IDS 适合解决那些搜索深度接近无限或宽度接近无限的题目。

由于迭代加深的过程中搜索到的每一层节点都与之前的节点数呈指数差距,因此不需要考虑迭代过程中带来的时间浪费,毕竟对于最后一次搜索来说,之前的所有搜索都显得很微不足道。

2. A*。

首先我们得知道启发式是什么。对于启发式的概念,我们有几个设定如下:

  1. \(g()\) 表示从起点状态到当前状态的代价。

  2. \(h()\)估价函数),用于估计当前状态到目标状态的代价,一般不要求 \(100\%\) 的准确,最重要的函数。

  3. \(f() = g() + h()\)估值函数),用来估计从起点状态经过当前状态再到达终点状态的代价。

在此,我们假设 \(H()\) 为从当前状态到达终点状态的真实代价,那么就有以下判定:

  1. \(h() = 0\) 此时时间复杂爆炸,启发式搜索退化为爆搜。

  2. \(h() < H()\) 此时启发式搜索一定能找到最优解,但不一定很快。

  3. \(h() = H()\) 此时启发式搜索的效率达到顶峰,可以直达最优解而不会考虑其它无用的解,不过实现较难。

  4. \(h() > H()\) 此时启发式搜索有可能找到最优解,即启发式搜索的正确性无法得到保障。

综上所述,不难得出:在设计 \(h()\) 时需要防止 \(h() > H()\) 的情况发生,适当地将估价函数设计得保守一点,但也不能过于保守,\(h()\)\(H()\) 越接近,求解的速度就越快。

回归正题,A* 就是在 priority_queue 维护的 BFS 上加一个估价函数,优先拓展 \(h()\) 最小的状态。

易证得:如果估价函数正确,那么在第一次到达终点状态时即为最优解。

3. IDA*。

理解了 A* 那 IDA* 就很简单了。

同 A* 的几个定义,IDA* 就只是在 DFS 的基础上添加 \(h()\) 估价函数,当 \(f() > maxdep\)(maxdep 表示迭代加深搜索限制的最大层数)时直接舍去当前状态进行剪枝。

即只搜 \(f() \le maxdep\) 的状态。

接下来做几道例题:

洛谷P1379 八数码难题

这是很明显的一道搜索题,深搜广搜皆可,笔者在这里只讨论广搜。

既然是广搜那么我们可以考虑使用 A* 进行求解。

为了方便求解,我们假设移动的是空格。

对于 \(h()\) 我们可以设它为不在应有位置的数字个数,注意需去除空格的贡献,否则会 WA。

我们用字符串来表示状态再使用一个 map 来记录到某一状态是否曾经到达过。

不难写出代码。

按这个思路可以 AC 但由于使用了 STL 并且估价函数不够优秀,总用时 \(1.29s\),就算开启 O2 也达到了 \(447ms\),要不是数据比较水不开 O2 可能都无法通过本题。

考虑寻找一个更优的 \(h()\)。我们可以用每一个数与其最终位置的曼哈顿距离之和来当做 \(h()\)

由于在移动一个数到应有位置的过程中其它数字也会变动。所以对于此定义 \(h() \le H()\) 恒成立。

代码
#include<iostream>
#include<queue>
#include<map>
#include<cmath>
#include<cstring>
using namespace std;
const string ed="123804765";
int h(string u){
	int sum=0;
	for(int i=0;i<9;i++){
		if(u[i]=='0') continue;
		int j=ed.find(u[i]),x=i/3,y=i%3,x2=j/3,y2=j%3;
		sum+=abs(x-x2)+abs(y-y2);
	}
	return sum;
}
struct node{
	int f,s;
	string now;
	bool operator < (const node &x)const{
		return f>x.f;
	}
};
priority_queue<node> q;
map<string,bool> used;
const int dx[4]={0,0,1,-1},dy[4]={1,-1,0,0};
int main(){
	#ifdef ytxy
	freopen("in.txt","r",stdin);
	#endif
	ios::sync_with_stdio(0);
	string a;
	cin>>a;
	q.push(node{h(a),0,a});used[a]=1;
	while(!q.empty()){
		node u=q.top();q.pop();
		string now=u.now;
		if(now==ed){
			cout<<u.s;return 0;
		}
		int ux,uy,id;
		for(int i=0;i<9;i++)
			if(now[i]=='0') ux=i/3+1,uy=i%3+1,id=i;
		for(int i=0;i<4;i++){
			int vx=ux+dx[i],vy=uy+dy[i];
			if(vx<1||vx>3||vy<1||vy>3) continue;
			int id2=(vx-1)*3+vy-1;
			swap(now[id],now[id2]);
			if(!used[now]){
				used[now]=1;
				q.push(node{h(now)+u.s+1,u.s+1,now});
			}
			swap(now[id],now[id2]);
		}
	}
}

洛谷P2324 [SCOI2005]骑士精神

与上题类似,本题也可以使用启发式搜索进行求解,笔者在这里仅讨论 IDA*

我们将 \(h()\) 定义为不在正确位置的旗子数量,这是一个非常保守的估计,但也足够通过本题。

我们选择迭代移动步数再进行 DFS 求解,利用 \(h()\) 剪枝。

当目前步数加上 \(h()\) 的值后大于已经得到的答案时就进行剪枝,直接舍去当前状态。

代码
#include<iostream>
using namespace std;
const int Ans[6][6]={0,0,0,0,0,0,0,1,1,1,1,1,0,0,1,1,1,1,0,0
,0,2,1,1,0,0,0,0,0,1,0,0,0,0,0,0};
int T,a[6][6],dep;
bool ok;
const int dx[]={1,1,-1,-1,2,2,-2,-2};
const int dy[]={2,-2,2,-2,1,-1,1,-1};
int h(){
	int sum=0;
	for(int i=1;i<=5;i++) for(int j=1;j<=5;j++)
		if(a[i][j]!=Ans[i][j]) sum++;
	return sum;
}
void dfs(int d,int x,int y){
	if(d==dep){
		if(!h()) ok=1;
		return ;
	}
	for(int i=0;i<8;i++){
		const int vx=x+dx[i],vy=y+dy[i];
		if(vx<1||vx>5||vy<1||vy>5) continue;
		swap(a[vx][vy],a[x][y]);
		if(h()+d<=dep) dfs(d+1,vx,vy);
		swap(a[vx][vy],a[x][y]);
	}
}
int main(){
	#ifdef ytxy
	freopen("in.txt","r",stdin);
	#endif
	ios::sync_with_stdio(0);
	cin>>T;
	while(T--){
		ok=0;char c;int sx,sy;
		for(int i=1;i<=5;i++) for(int j=1;j<=5;j++){
			cin>>c;
			if(c=='*') a[i][j]=2,sx=i,sy=j;
			else a[i][j]=c-'0';
		}
		if(!h()){
			cout<<'0'<<'\n';continue;
		}
		for(dep=1;dep<=15;dep++){
			dfs(0,sx,sy);
			if(ok) break;
		}
		if(!ok) cout<<-1<<'\n';
		else cout<<dep<<'\n';
	}
}

洛谷P1120 小木棍

这是一道好题,码量并不大,但有挑战性。不过曾经数据出问题了,现在没有问题了。

先观察题面,题目没有给出原来的木棍数量也没给出原木棍被分成了几段,这也就意味着如果暴搜的话复杂度将会爆炸,不可能通过本题。

既然这样,自然要请出我们的 IDS 了。

考虑枚举原木棍的数量或原木棍的长度来进行搜索。

先考虑枚举数量,在最坏情况下,所有给出的木棍长度相同,需要从 \(n\) 开始枚举,我们可以由此算出原木棍的长度。

再考虑枚举长度,最坏情况下所有木棍拼接在一起才是答案,但一般很难卡到让它跑满,由于所有的原木棍长度相等,我们也可以由此计算出原来的木棍数量。

显而易见,我们需要从最长的木棍的长度开始枚举原木棍们的长度,在此不多赘述。

当枚举长度到 \(\dfrac{\sum\limits_{i=1}\limits^{n} a_i}{2}\) 后就可以直接退出,如果到目前还没有解就直接输出所有木棍的长度,因为大于 \(\dfrac{\sum\limits_{i=1}\limits^{n} a_i}{2}\) 却小于 \(\sum\limits_{i=1}\limits^{n} a_i\) 的长度的木棍是不可能拼出来的。

综上所述,无论先考虑数量还是先考虑长度,结论相同,因为它们可以互推,时间复杂度相差不大。

代码
#include<iostream>
using namespace std;
const int N(70);
int n,a[N],sum,mx,mn;
void dfs(int num,int Long,int need,int lst){
	if(num==0){
		cout<<need;exit(0);
	}
	if(Long==need){
		dfs(num-1,0,need,mx);return ;
	}
	for(int i=lst;i>=mn;i--){
		if(a[i]&&Long+i<=need){
			a[i]--;
			dfs(num,Long+i,need,i);
			a[i]++;
			if(Long==0||Long+i==need) return ;
		}
	}
}
int main(){
	#ifdef ytxy
	freopen("in.txt","r",stdin);
	#endif
	ios::sync_with_stdio(0);
	cin.tie(0),cout.tie(0);
	cin>>n;
	for(int i=1,k;i<=n;i++){
		cin>>k;
		a[k]++;
		sum+=k;
		mx=max(k,mx);mn=min(mn,k);
	}
	int mxdep=sum/2;
	for(int i=mx;i<=mxdep;i++)
		if(sum%i==0) dfs(sum/i,0,i,mx);
	cout<<sum;
}
posted @ 2022-07-09 22:19  JR_ytxy  阅读(60)  评论(0)    收藏  举报