搜索

Part 1 剪枝

剪枝,就是减小搜索树规模、尽早排除搜索树中不必要的分支的一种手段。

有以下几种常见剪枝方法:

1.优化搜索顺序

搜索顺序并不是固定的。不同的搜索顺序构成的搜索树大小也不同。

2.排除等效冗杂

在搜索过程中,如果我们能判定从当前节点沿不同分支达到同样的效果,那么只需要搜索其中的一条分支。

3.可行性剪枝

在搜索过程中,如果发现该节点已经无法到达边界,就执行回溯来结束不必要的搜索。

4.最优性剪枝

对于一些求最优解的问题,如果当前解已经劣于已知最优解,就执行回溯。因为接下来的操作不可能会更新最优解。

5.记忆化搜索

对于一些会重复访问同一状态的问题,采用记忆化的方式,当再次访问到这一状态时直接返回。
(可以看看这题 P1278 单词游戏 )

例题P1120 小木棍


很好想到可以暴搜,只要枚举小木棒的初始长度 \(length\) 再一个个拼,判断是否能拼出来就可以了。

先考虑搜索的状态,设\(dfs(now,len,last)\),其中 \(now\) 表示当前在拼第几根原始木棍\(len\) 表示当前木棍已经拼了多长,\(last\) 表示上一根小木棍的编号。

可以做出一些最基础的优化。
首先枚举范围应该是 \(max_{1\leq i\leq n}a_i\)\(\leq length\leq\)\(\sum_{i=1}^{n}{a_i}\)(原因自己去想)
其次枚举的每一个 \(length\) 都应该是木棍长度总和的约数。那么原始木棒的根数就应该是\(\sum_{i=1}^{n}a_i/length\)

但显而易见,循环里面再套搜索,时间复杂度是非常高的。因此需要做出剪枝:
1.优化搜索顺序
按小木棍的长度从大到小的顺序进行搜索。优先选择更长的小木棍可以减小搜索树大小。
2.排除等效冗杂
(1)保证加入的小木棍是递减的。因为先拼一根短木棍再拼一根长木棍的效果和先拼一根长木棍再拼一根短木棍效果 是一样的。
(2)对于当前正在拼的原始木棒,可以记录当前小木棍长度,如果当前小木棍失败,那么同样长度的也一定会失败。
(3)如果在当前原始木棍拼第一根小木棍就失败,直接回溯。因为这跟小木棍不可能和任何其他的木棍拼成完整 的木棍,最后一定会剩。
(4)如果在当前原始木棍拼玩最后一根小木棍后在接下来的搜索中失败,直接回溯。因为已经保证了小木棍的长度是 递减的,而用长木棍的情况一定比用几个短木棍拼成等长的长木棍更优。因为短木棍比长木棍更加“灵活”。

#include <bits/stdc++.h>
using namespace std;
bool cmp(int a,int b){return a>b;}
int n,a[70],length,cnt,maxlen,sum,ans;
int used[70];
int dfs(int now,int len,int last){
	if(now>cnt)return 1;
	if(len==length)return dfs(now+1,0,0);
	int fail=0;
	for(int i=last+1;i<=n;i++){
		if(!used[i]&&len+a[i]<=length&&fail!=a[i]){
			used[i]=1;
			fail=a[i];
			if(dfs(now,len+a[i],i))return 1;
			used[i]=0;
			if(len==0||len+a[i]==length)return 0;
		}
	}
	return 0;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		sum+=a[i];
		maxlen=max(maxlen,a[i]);
	}
	sort(a+1,a+1+n,cmp);
	for(length=maxlen;length<=sum;length++){
		if(sum%length)continue;
		cnt=sum/length;
		memset(used,0,sizeof(used));
		if(dfs(1,0,0)){printf("%d",length);return 0;}
	}
}

Part 2 迭代加深

迭代加深就是通过限制搜索深度,来避免在一棵过大子树上浪费过多时间的情况的思想。

注意:只有能确保答案在较浅层时才能使用迭代加深DFS!!!

例题P1763 埃及分数


题目已经明确表述了加数少的比加数多的好,因此可以使用迭代加深限制加数的个数,并且可以通过按照加数递减的方法来优化搜索顺序。设 \(dfs(nume,deno,last,lim)\) 其中 \(nume\)\(deno\) 分别表示剩下的分子和分母,\(last\) 表示上一个分母,\(lim\) 表示限制的加数个数。

#include <bits/stdc++.h>
#define int long long
using namespace std;
int a,b,lim,t[1005],ans[1005];
bool flag=false;

void dfs(int num,int nume,int deno,int last){
	if(num==lim+1){
		if(nume==0&&t[lim]<ans[lim])flag=1,memcpy(ans,t,sizeof(t));
		return;
	}
	if(deno*(lim+1-num)/nume>ans[lim])return;
	for(int i=max(last,deno/nume);i<=deno*(lim+1-num)/nume;i++){
		t[num]=i;
		dfs(num+1,nume*i-deno,deno*i,i+1);
	} 
}

signed main(){
	scanf("%d%d",&a,&b);
	while(!flag){
		memset(ans,0x7f,sizeof(ans));
		lim++;
		dfs(1,a,b,1);
	}
	for(int i=1;i<=lim;i++)printf("%lld ",ans[i]);
	return 0;
}

Part 3 双向搜索

双向搜索就是从初态和终态分别出发,产生两颗深度减半的搜索树,在中间交汇得到最终的答案

在一些初态和终态都已明确的搜索中,可以尝试采用双向搜索。

例题[P5195 USACO05DEC]Knights of Ni S


题目中贝茜和骑士的位置都已经明确,那么使用双向BFS,分别从贝茜和骑士的位置开始搜索,当第一次出现一个既能被贝茜到达,又能被骑士到达的灌木丛时,当前所花费的时间就是任务完成的最短时间。

Part 4 A*

A*算法就是在优先队列BFS的基础上,再设计一个估价函数,从而可以更快地扩展到最优解

f(x) 为当前已花费代价,\(g(x)\) 为当前状态到目标状态的估计代价,\(h(x)\) 为当前状态到目标状态的真实代价。
那么存入优先队列的就应该是 \(f(x)+g(x)\) ,并且要保证 \(g(x)\leq h(x)\) ,因为一旦估价大于真实值,那么原本的最优解就有可能被压在堆中无法取出。因此,\(A^*\) 算法的关键就在于设计合理的 \(g(x)\)\(g(x)\) 越接近于 \(h(x)\) 代码的效率就越高。

例题P1379 八数码难题


通过观察可以发现,对于任意一个状态,移动的次数至少是每个数字当前位置到最终位置的曼哈顿距离之和。设当前状态为 \(now\) ,最终状态为 \(end\) ,那么估价函数 \(g(x)\) 就等于所有数字在当前状态与最终状态的曼哈顿距离之和。即:

\[g(now)=\sum_{num=1}^9(|nowx_{num}-endx_{num}|+|nowy_{num}-endy_{num}|) \]

Part 5 IDA*

IDA*以迭代加深DFS为框架,再加上估价函数来减少搜索量。

\(f(x)\) 为当前深度,\(g(x)\) 为未来估计步数,\(lim\) 为深度限制,那么原来深度限制就变为当 \(f(x)+g(x)>lim\) 时,立即回溯。

例题P2324 [SCOI2005]骑士精神


不难想到对于每一个状态,最理想的情况就是接下来的每一步都能使黑马或白马移到对应的位置。那么估价函数 \(g(x)\) 就等于当前状态与最终状态位置不同的个数。

posted @ 2022-11-09 20:32  ChenJHen  阅读(217)  评论(0)    收藏  举报