7.6 迭代加深搜索

迭代加深搜索是一个应用范围很广的算法,不仅可以像回溯法那样找一个解,也可以像状态空间搜索那样找一条路径
迭代加深搜索最经典的例子就是埃及分数
这道题目理论上可以用回溯法求解,但是解答树非常恐怖,不仅深度没有明显的上界,而且加数的选择在理论上也是无限的,也就是说,BFS可能会无限拓宽,DFS会无限往深处
解决方案是采用迭代加深搜索(iterative deepening):从小到大枚举深度上限maxd,每次执行只考虑深度不超过maxd的结点,这样只要解的深度有限,则一定可以在有限的时间内枚举到
其实更像是约束条件下的DFS或者是BFS

对于可以使用回溯法求解,但是解答树的深度没有明显上限的题目,可以考虑使用迭代加深搜索(iterative deepening)

深度上限maxd还可以用来“剪枝”,按照坟墓递增的顺序进行扩展,如果扩展到i层时,前i个分数之和为c/d,而第i个分数为1/e,则接下来至少还需要(a/b-c/d)/(1/e)个分数,总和才能达到a/b,因此这边最关键的时我们可以依据该种思想来推断出该子树至少还需要多少次拓展才可能得到答案
也就是说有时我们可以根据ID的每个结点来判断出至少还需要拓展多少次才可以得到答案,因此可以选择性放弃一些过长的子树拓展,实现剪枝
注意,这里的估计都是乐观的,即设深度上限maxd,当前结点n的深度为g(n),乐观估价函数为h(n),则当g(n)+h(n)>maxd时应该剪枝,这样的算法就是IDA*
其实就是ID和A的结合,通过引入启发式信息,即估值函数来加速当前的搜索
如果可以设计出一个乐观估价函数,预测从当前结点至少还需要扩展几层结点才有可能得到解,则迭代加深搜索编程了IDA
算法

或者说只要存在估值函数,对于当前状态进行预测,那么就有A*算法的思想,而迭代加深搜索就是ID

点击查看代码
//本题的主框架就是一个简单的循环 
int ok = 0;
for(maxd = 1; ; maxd++) {
  memset(ans, -1, sizeof(ans));
  if(dfs(0, get_first(a, b), a, b)) { ok = 1; break; }//从第一层开始逐层搜索直到最优解的出现 
} 
//其中get_first(a, b)是满足1/c <= a/b的最小c。迭代加深搜索过程如下,约分的原理详见第十章(欧拉) 
//如果当前解v比目前最优解ans更优,更新ans 
bool better(int d) {
  for(int i = d; i >= 0; i--) if(v[i] != ans[i]) {
  	return ans[i] == -1 || v[i] < ans[i];//因为上面的深度的限制,因此如果在第i层得到答案,那么搜索树在搜索完第i层后,将不会再往后,因此我们首先需要一个初始化过程
//ans数组的初始元素为-1,因此有前面的判断条件,因为存储的是分母,因此越小即越大所以有后面的刷新 
  }
  return false;
}

//当前深度为d,分母不能小于from,分数之和恰好为aa/bb
 
bool dfs(int d, int from, LL aa, LL bb) {
  if(d == maxd) {
  	if(bb % aa) return false;// aa/bb必须是埃及分数 也就是说aa必须为bb的因数 
  	v[d] = bb/aa;
  	if(better(d)) memcpy(ans, v, sizeof(LL)*(d+1));//注意是d+1,不是d,从0开始的问题 
  	return true;
  }
  bool ok = false;
  from = max(from, get_first(aa, bb)); //枚举的起点 
  for(int i = from; ; i++) {
  	//剪枝,如果剩下的maxd+1-d个分数全部都是1/i,加起来仍然不能超过aa/bb,则无解 
  	if(bb * (maxd+1-d) <= i * aa) break; 
  	v[d] = i;
  	//计算aa/bb - 1/i,设结果为a2/b2 
  	LL b2 = bb*i;
  	LL a2 = aa*i - bb;
  	LL g = gcd(a2, b2);//以便约分 
  	if(dfs(d+1, i+1, a2/g, b2/g)) ok = true;
  }
  return ok;
}
//注意这边作者最巧妙的地方在于使用整数替代了浮点数可能存在误差的坏情况
//这边需要注意对于分数形式的判断,有时候未必不能化浮点数为整型 

Editing a Book:
本题可以采用IDA*来解决,笔者直接无脑暴力搜索,因为时间较为充裕,所以还是可以过去的

点击查看笔者代码
#include<iostream>
#include<cstring>
using namespace std;

int n, maxd, R[10];

bool getD() {
  cin >> n;
  if(!n) return false;
  int pre = 0;
  for(int i = 0; i < n; i++) {
  	cin >> R[pre];
  	pre = R[pre];
  } 
  R[pre] = 0; 
  return true;
}

bool check() {
  int pos = 0;
  while(R[pos]) { 
  	if(pos > R[pos]) return false;
  	pos = R[pos];
  }
  return true;
}

int cnt() {
  int sum = 0, pos = 0, cnt = 0; 
  while(R[pos]) { 
  	if(pos > R[pos]) cnt++;
  	else { sum += (cnt+1)/2; cnt = 0; }
  	pos = R[pos]; 
  }
  return sum + (cnt+1)/2; 
}

int getPos(int order) {
  int pos = 0;
  for(int i = 0; i < order; i++) pos = R[pos];
  return pos;
}

void connect(int from, int to) {
  from = getPos(from); to = getPos(to);
  
}

void print() {
  int pre = 0;
  while(R[pre]) {
  	cout << R[pre] << " ";
  	pre = R[pre];
  }
  cout << endl;
}

bool dfs(int d) { 
  if(d==maxd) { 
    if(check()) return true;
    return false;
  } 
  if(cnt()+d > maxd) return false; 
  bool ok = false; 
  for(int i = 1; i <= n; i++) {
  	for(int j = i; j <= n; j++) {  
  	  int from = getPos(i-1), sta = getPos(i), des = getPos(j), to = getPos(j+1), pre = 0;
  	  R[from] = R[des]; 
  	  do{
  	    R[des] = R[pre];
		R[pre] = sta;
		if(dfs(d+1)) { ok = true; break;}	
		R[pre] = R[des]; 
		pre = R[pre]; 
	  } while(pre);
	  if(ok) return ok;
	  R[from] = sta;
	  R[des] = to;
	}
	if(ok) return ok;
  }
  return ok;
}

int main() {
  int kase = 0;
  while(getD()) {
    for(maxd = 0; ; maxd++) { 
      if(dfs(0)) { cout << "Case " << ++kase << ": " << maxd << endl; break; }
	}
  }
  return 0;
} 

本题是典型的状态空间搜索问题,状态就是1-n个排列,初始状态是输入,终止状态是1,2,。。。,n,排列最多有9!=362880个,因为每个状态的后继状态也比较多,所以仍然有超时的危险,比赛时选手或采用一些加速策略
1:每次只剪切一段连续的数字,例如不要剪切24这样数字不连续的片段
2:假设剪切片段的第一个数字为a,最后一个数字为b,要么把这个片段粘贴到a-1的下一个位置,要么粘贴到b+1的前一个位置
3:永远不要破坏一个已经连续排列的数字片段,例如不能把1234中的23剪切出来
3种策略都能缩小状态空间,但他们并不都是正确的,很多程序无法得到5 4 3 2 1的正确结果,答案是3步而不是4步
5 4 3 2 1 -> 3 2 5 4 1 -> 3 4 1 2 5 -> 1 2 3 4 5
本题可以用IDA算法求解,不难发现n<=9的时候,最多只需要8步,因此深度上限为8.IDA的关键在于启发函数。考虑后续不正确的数字个数h,可以证明每次剪切时h最多减少3,因此当3d+h>3maxd的时候可以剪枝,其中d为当前深度,maxd为深度限制
至于为什么最多只有3个数字可以正确,是因为剪切的时候,最多改变了三个数字的后继节点,因此最多有三个正确,这边要善于观察

posted @   banyanrong  阅读(194)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek “源神”启动!「GitHub 热点速览」
· 我与微信审核的“相爱相杀”看个人小程序副业
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 如何使用 Uni-app 实现视频聊天(源码,支持安卓、iOS)
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
点击右上角即可分享
微信分享提示