【暖*墟】#迭代加深# 埃及分数 + 编辑书稿

73 篇文章 0 订阅
7 篇文章 0 订阅

【迭代加深搜索】

迭代加深搜索经常用于理论上解答树深度上没有上界的问题,求出满足某些条件时的解即可。

比如在“埃及分数”问题中要求将一个分数a/b分解成为若干个形如1/d的加数之和,

而且加数越少越好,如果加数个数相同,那么最小的分数越大越好。下面总结一般流程:

(1)概述:通过限制每次dfs的最大深度进行的搜索。

令maxd表示最大的搜索深度,那么dfs就只能在0~maxd之间来进行,

如果在这个范围内找到了解,就退出大循环,否则maxd++,扩大搜索范围。

但可想而知,倘若没有 { 高效及时的退出无解 } 的情况,那么时间上的开销也是会比较大的。

这时就需要进行“剪枝”操作,及时地判断此时能否找到解。

对于迭代加深搜索,经常通过设计合适的“乐观估价函数”来判断能否剪枝。

设当前搜索的深度是cur,乐观估价函数是h(),那么当cur+h()>maxd时就需要剪枝。

(2)乐观估价函数:设想最优方案,能达到的最值,作为初始化搜索的起点。

简单的说就是从当前深度到找到最终的解“至少”还需要多少步

或者距离找到最终的解还需要扩展多少层。如果超出了当前限制的深度maxd,

说明当前限制的最大深度下是不可能找到解的,直接退出。比如像前面的“埃及分数”问题,

要拆分19/45这样的一个分数,假设当前搜索到了第三层,得到19/45=1/5+1/100...

那么根据题意此时最大的分数为1/101,而且如果需要凑够19/45,

需要(19/45-1/5-1/100)*101=23个1/101才行。

即从第3层还要向下扩展至少大于23层的深度才可能找到所有的解。

所以如果此时的maxd<23,就可以直接剪枝了。因此(a/b-c/d)/(1/e)便是本题的乐观估价函数。

注意,使用迭代加深搜索时要保证一定可以找到解,否则会无限循环下去。

下面给出“埃及分数”问题的代码以便更好地理解迭代加深搜索的过程:


【埃及分数】

#include<cmath>  
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<string>
#include<sstream>
#include<iostream>
using namespace std;

/*【迭代加深】从小到大枚举{深度上限}maxd,每次执行只考虑{深度不超过maxd}的结点
这样,只要解的深度有限,则一定可以在有限时间内枚举到。*/

/*【埃及分数】在古埃及,人们使用单位分数的和(即1/a,a是自然数)表示一切有理数
例如,2/3=1/2+1/6,但不允许2/3=1/3+1/3,因为在加数中不允许有相同的
对于一个分数a/b,表示方法有很多种,其中加数[少]的比加数[多]的好,如果加数[个数相同]
则{最小的分数越大越好}。例如,19/45=1/5+1/6+1/18是最优方案。
输入整数a,b(0<a<b<500),试编程计算最佳表达式  */

int a,b,maxd;//{深度上限}maxd
typedef long long LL;//定义longlong=>LL
const int maxn = 100 + 5;
LL v[maxn], ans[maxn];

LL gcd(LL a, LL b) { return b == 0 ? a : gcd(b, a%b); }//最大公约数
//b=0则输出a,b!=0则继续递归

inline int get_first(LL a, LL b) {//inline,自定义能变的
    return b/a+1;
}// 返回满足1/c <= a/b的{最小c}(便于剪枝和使最后数最小处理)

bool better(int d) {// 如果当前解v比目前最优解ans更优,更新ans
	for(int i = d; i >= 0; i--) 
	    if(v[i] != ans[i])   return ans[i] == -1 || v[i] < ans[i];
	//如果没考虑过ans或者有更优解,return true
  return false;
}

bool dfs(int d, int from, LL aa, LL bb) {
    if(d == maxd) {// 当前深度为d,分母不能小于from,{分数之和}恰好为aa/bb
	    if(bb % aa == 0 ) return false; // aa/bb必须是埃及分数
        v[d] = bb/aa;
		if(better(d)) memcpy(ans, v, sizeof(LL) * (d+1));
        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; 
		LL b2 = bb*i;  // 计算aa/bb - 1/i,设结果为a2/b2(剩余)
        LL a2 = aa*i - bb;
        LL g = gcd(a2, b2); // 以便约分
        if(dfs(d+1, i+1, a2/g, b2/g)) ok = true;
    }
    return ok;
}

int main() {
    int kase = 0;
    while(cin >> a >> b) {
    int ok = 0;
    for(maxd = 1; maxd <= 100; maxd++) {//
		memset(ans, -1, sizeof(ans));
        if(dfs(0, get_first(a, b), a, b)) { ok = 1; break; }
    }
    cout << "Case " << ++kase << ": ";
    if(ok) {
        cout << a << "/" << b << "=";
        for(int i = 0; i < maxd; i++) cout << "1/" << ans[i] << "+";
        cout << "1/" << ans[maxd] << "\n";  } 
	else cout << "No solution.\n";
    }
	return 0;
}

【编辑书稿】(UVa 11212)

你有一篇由n(2≤n≤9)个自然段组成的文章,希望将它们排列成1,2,…, n。

可以用Ctrl+X(剪切)和Ctrl+V(粘贴)快捷键来完成任务。

每次可以剪切一段连续的自然段,粘贴时按照顺序粘贴。

注意,剪贴板只有一个,所以不能连续剪切两次,只能剪切和粘贴交替。

例如,为了将{2,4,1,5,3,6}变为升序,可以剪切1将其放到2前,然后剪切3将其放到4前。

再如,对于排列{3,4,5,1,2},将{3,4,5}放在{1,2}后,或者将{1,2}放在{3,4,5}前。

【分析】典型的状态空间搜索问题。

“状态”是1~n的排列,初态是输入,终态是1, 2, 3,…, n。因为n≤9,排列最多有9!=362880个。

虽然这个数字不算大,但是每个状态的后继状态也比较多(有很多剪切和粘贴的方式),很多选手使用了一些“加速策略”。

策略1:每次只剪切一段连续的数字。例如,不要剪切2 4这样数字不连续的片段。 ×

策略2:假设剪切片段的第一个数字为a,最后一个数字为b,要么把这个片段粘贴到a-1的下一个位置,要么粘贴到b+1的前一个位置。 ×

策略3:永远不要“破坏”一个已经连续排列的数字片段。例如,不能把1 2 3 4中的2 3剪切出来。 ×

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),

读者不妨自行验证上面的3种策略是否可以得到这组数据的正确答案。

本题可以用IDA*算法求解。不难发现n≤9时最多只需要8步,因此深度上限为8。IDA*的关键在于启发函数

考虑后继不正确的数字个数h,可以证明每次剪切时h最多减少3,因此当3d+h>3maxd时可以剪枝,其中maxd为深度限制。

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
 
const int maxn = 10;
 
int n,a[maxn];
 
bool ans_sort(){
    for(int i = 0;i < n-1;i++){
        if(a[i] >= a[i]+1)return false;//判断是否有序
    }
    return true;
}
 
int h(){
    int cnt =  0;
    for(int i = 0;i < n-1; i++)
        if(a[i]+1 != a[i+1])cnt++;//如果他加1等于他的下一个,则这个有序,否则无序,然后统计
    if(a[n-1] != n)cnt++;//判断最后一个数是否等于n
    return cnt;
}
 
bool dfs(int d,int maxd){
    if(d*3 + h() >= maxd*3)return false;//启发式函数估计
    if(ans_sort())return true;//判断是否顺序正确
    int o[maxn],b[maxn];
    memcpy(o,a,sizeof(a));
    for(int i = 0;i < n; i++){//设置剪切文本的最左边下标
        for(int j = i;j < n; j++){//设置剪切文本的最右边下标
            int cnt = 0;
            for(int k = 0;k < n;j++)
                if(k < i || k > j)b[cnt++] = a[k];//将有顺序的接在一起
            for(int k = 0;k <= cnt; k++){
                int cnt2 = 0;
                for(int p = 0;p < k; p++)a[cnt2++] = b[p];//插入文本之前的文本
                for(int p = i;p <= j;p++)a[cnt2++] = o[p];//插入的文本
                for(int p = k;p < cnt;p++)a[cnt2++] = b[p];//插入文本之后的文本
 
                if(dfs(d+1,maxd))return true;//深搜
                memcpy(a,o,sizeof(a));//还原a数组方便下次递归
            }
 
        }
    }
    return false;
}
 
int solve(){
    int max_ans = 5;//运用数学思维估计,老实说我也不懂为什么是5,不懂可以改成8.
    for(int maxd = 1;maxd < 5;maxd++){
        if(dfs(0,maxd))return maxd;//迭代加深搜
    }
    return max_ans;
}
 
int main(){
    while(scanf("%d",&n) == 1 && n){
        int kase = 1;
        for(int i = 0;i < n; i++)scanf("%d",&a[i]);
        printf("Case %d: %d\n",kase++,solve());
    }
}




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值