搜索算法——IDA*

搜索算法—— I D A ∗ IDA* IDA

算法介绍

  • I D A ∗ IDA* IDA是基于深度优先搜索进行优化的一种算法,又被称作为迭代加深搜索,是一种启发式搜索算法。
  • 什么叫做迭代加深呢?实际上深搜会搜索很多层,几乎是呈指数级增长的,但是实际上可以通过限制搜索深度来优化。在深度没有上限的情况下,先预先估计一个较小的深度 k k k,首先搜索 K K K层,如果没有找到解,深度加 1 1 1,从头开始搜索到 k + 1 k+1 k+1层,不断的迭代加深,知道找到解为止
  • 由于搜索深度是一层一层逐渐增加的,所以一定可以保证找到最优解的时候深度是最小的(每一层深度增加的时候,都要从头开始搜,但搜索的复杂度是呈指数级别增长的,所以对于下一层搜索,前面的工作可以忽略不计)

例如下图,一层层加深深度,直到找到解为止

k = 1 k = 1 k=1
搜索算法——IDA*_第1张图片
k = 2 k = 2 k=2
搜索算法——IDA*_第2张图片
k = 3 k = 3 k=3
搜索算法——IDA*_第3张图片

找到最优解

  • 总的来说迭代加深搜索类似于广搜的思想,但是空间上和深度优先搜索相当。适用于深度没有明确上限的情况。
  • 实际操作中,要求进行剪枝,因为深搜的时间开销很大,即使限制了深度,还需要在无解的情况下及时退出。怎么做呢?需要使用一个估价函数(和A*中的相当)

估价函数:

f ( n ) = g ( n ) + h ( n ) f(n) = g (n) + h(n) f(n)=g(n)+h(n)

其中, g ( n ) g(n) g(n)表示现在已经走的步数, h ( n ) h(n) h(n)表示从当前到达最优解的步数的一个估计**( h ( n ) h(n) h(n)一定要小于等于到达最优解的一个实际的值)**,如果 f ( n ) > m a x d e p f(n)>maxdep f(n)>maxdep,即大于当前设定的最大深度,说明当前深度限制下找不到解,直接退出即可

总的来说,IDA*是在限制搜索深度下,加入估价函数进行剪枝,然后不断迭代加深的一种算法,算法保证找到答案时,搜索深度是最小的,部分情况下可以代替广搜。题目一定要保证有解,否则会无限递归下去


题目训练

骑士精神

题目链接

在一个 5 × 5 5 \times 5 5×5的棋盘上有 12 12 12个白色的骑士和 12 12 12个黑色的骑士, 且有一个空位。在任何时候一个骑士都能按照骑士的走法(它可以走到和它横坐标相差为 1 1 1,纵坐标相差为 2 2 2或者横坐标相差为 2 2 2,纵坐标相差为 1 1 1的格子)移动到空位上。

给定一个初始的棋盘,怎样才能经过移动变成如下目标棋盘:

搜索算法——IDA*_第4张图片

为了体现出骑士精神,他们必须以最少的步数完成任务。

输入格式

第一行有一个正整数 T     ( T ≤ 10 ) T \,\,\,(T \leq 10) T(T10),表示一共有 N N N组数据。

接下来有 T T T 5 × 5 5 \times 5 5×5的矩阵, 0 0 0表示白色骑士, 1 1 1表示黑色骑士, ∗ * 表示空位。两组数据之间没有空行。

输出格式

对于每组数据都输出一行。如果能在 15 15 15步以内(包括 15 15 15步)到达目标状态,则输出步数,否则输出 − 1 -1 1

样例输入

2
10110
01*11
10111
01001
00000
01011
110*1
01110
01010
00100

样例输出

7
-1

思路

这道题思路比较简单,首先移动黑白棋子很麻烦,比较复杂,所以直接移动空白的位置,然后不断和最终状态比较,主要这道题的估价函数选择的是当前的棋盘和最终的棋盘有多少个不一样的棋子。直接上代码:

#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 5
using namespace std;

const int aim[N][N] = { //最终的位置
    {1, 1, 1, 1, 1},
    {0, 1, 1, 1, 1},
    {0, 0, 2, 1, 1},
    {0, 0, 0, 0, 1},
    {0, 0, 0, 0, 0}
};
int mat[N][N];
int dx[8] = {1, 1, -1, -1, 2, 2, -2, -2};
int dy[8] = {2, -2, 2, -2, 1, -1, 1, -1};
int sx, sy;
bool flag;

int calc () { //计算估价函数,计算当前矩阵位置和目标矩阵有多少个是不一样的
    int res = 0;
    for (int i = 0; i < N; i++) 
        for (int j = 0; j < N; j++) {
            if (mat[i][j] != aim[i][j]) res ++;
        }
    return res;
}

void dfs (int step, int dep, int x, int y, int lax, int lay) {
    if (step == dep) { //达到我们设置的深度dep 
        if(calc () == 0) flag = 1; //如果出现了最终状态,flag标记为1
        return ;
    }
    for (int i = 0; i < 8; i++) {
        int nx = dx[i] + x;
        int ny = dy[i] + y;
        if (nx < 0 || nx >= N || ny < 0 || ny >= N) continue;
        if (lax == nx && lay == ny) continue; //这一步剪枝是为了防止往回走
        swap (mat[x][y], mat[nx][ny]);
        if (step + calc () <= dep) // 估价函数剪枝
            dfs (step+1, dep, nx, ny, x, y);     
        swap (mat[x][y], mat[nx][ny]);
    }
}

int solve () {
    flag = 0;
    if (!calc()) return 0;
    for (int i = 1; i <= 15; i++) { //深度设置从1到15
        dfs (0, i, sx, sy, -1, -1);
        if (flag)
            return i;
    }
    return -1;
}

int main () {
    int T;
    char c;
    scanf ("%d", &T);
    while (T --) {
        for (int i = 0; i < N; i++) {
            c = getchar();
            for (int j = 0; j < N; j++) {
                c = getchar();
                if (c == '*') {
                    mat[i][j] = 2;
                    sx = i, sy = j;
                }
                else mat[i][j] = c - 48;
            }
        }
        printf ("%d\n", solve());
    }
    return 0;
}

埃及分数

题目链接

在古埃及,人们使用单位分数的和(形如$ \frac{1}{a}$的, a a a是自然数)表示一切有理数。如: 2 3 = 1 2 + 1 6 \frac{2}{3} = \frac{1}{2} + \frac{1}{6} 32=21+61,但不允许 2 3 = 1 3 + 1 3 \frac{2}{3} = \frac{1}{3} + \frac{1}{3} 32=31+31,因为加数中有相同的。对于一个分数 a c \frac{a}{c} ca ,表示方法有很多种,但是哪种最好呢?首先,加数少的比加数多的好,其次,加数个数相同的,最小的分数越大越好。如:
19 45 = 1 3 + 1 12 + 1 180 . . . . . . 19 45 = 1 5 + 1 6 + 1 18 \frac{19}{45} = \frac{1}{3} + \frac{1}{12} + \frac{1}{180} \\ ... \\ ... \\ \frac{19}{45} = \frac{1}{5} + \frac{1}{6} + \frac{1}{18} \\ 4519=31+121+1801......4519=51+61+181
最好的是最后一种。可能有多个最优解如:
51 211 = 1 4 + 1 36 + 1 633 + 1 3798 51 211 = 1 6 + 1 9 + 1 633 + 1 3798 \frac{51}{211} = \frac{1}{4} + \frac{1}{36} + \frac{1}{633} + \frac{1}{3798} \\ \frac{51}{211} = \frac{1}{6} + \frac{1}{9} + \frac{1}{633} + \frac{1}{3798} \\ 21151=41+361+6331+3798121151=61+91+6331+37981
由于方法一与方法二中,最小的分数相同,因此二者均是最优解。

给出 a , b a,b a,b,编程计算最好的表达方式。保证最优解满足:最小的分数 ≥ 1 1 0 7 \ge \frac{1}{10^7} 1071

输入格式

一行两个整数,分别为 a a a b b b的值。

输出格式

输出若干个数,自小到大排列,依次是单位分数的分母。

输入样例

19 45

输出样例

5 6 18

思路

首先题目要求分解出来的值都是分子为 1 1 1的数,且分母在逐渐增大,并且分数不能有相同的情况,同时要满足最小的分数最大

显然对于一个分数,首先要找到的是第一个比它小的分数(这个分数分子要为 1 1 1)

假设对于分数 x y \frac{x}{y} yx,有 x y = 1 y x ≥ 1 y x + 1 \frac{x}{y} = \frac{1}{\frac{y}{x}} \geq \frac{1}{\frac{y}{x} + 1} yx=xy1xy+11

因此每次我们可以选择第一个比当前小的分数作为当前的分母,然后从 y x + 1 \frac{y}{x}+1 xy+1每次增加一位 i = y / x + 1 , i + + i = y/x+1, i++ i=y/x+1,i++,来进行搜索,需要注意的是,当

( 1 / i ) ∗ ( d e p − d + 1 ) ≤ x / y (1/i) * (dep -d+1) \leq x/y (1/i)(depd+1)x/y 等价于 y ∗ ( d e p − d + 1 ) ≤ x ∗ i y * (dep - d +1) \leq x * i y(depd+1)xi的时候就需要剪枝

其中 i i i表示当前分母, d e p dep dep表示设置的最大深度, d d d表示当前以及有的分数的数量, x x x表示当前的分子, y y y表示当前的分母。那么就是如果剩下所有的分数都使用 1 / i 1/i 1/i,最后的和仍然小于等于 x / y x/y x/y,说明肯定无解,因为后面的分数一定是小于 1 / i 1/i 1/i

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
#define max(x, y) x>y?x:y
#define N 105
using namespace std;

ll ans[N], v[N], a, b;
bool flag;

ll gcd (ll x, ll y) {
	return y ? gcd (y, x % y) : x;
}

ll getFirst (ll x, ll y) { //返回比x/y 第一个小的分数的分母,分子为1
	return y / x + 1;
}

bool judge (int d) { //判断当前深度下 数组v的结果是否优于ans
	for (int i = d; i >= 0; i --) {
		if (ans[i] == -1) return true;
		else if (ans[i] > v[i]) return true;
		else if (ans[i] < v[i]) return false;
	}
	return false;
}

void dfs (int step, int dep, ll x, ll y, ll pre) { //x表示分子,y表示分母
	if (step == dep) {
		if(y % x) return ; //如果最后不能除进 直接返回
		v[dep] = y / x;
		if (judge (dep)) { //更新答案
			for (int i = dep; i >= 0; i--) 
				ans[i] = v[i];
			flag = 1;
		}
		return ;
	}
	ll now = max (getFirst (x, y), pre); //为了保证当前新的分母比上一个分母要大
	for (ll i = now; ; i++) { //从第一个比x/y小的分母为now的开始搜索
		if ( y * (dep - step + 1) <= x * i ) break; //剪枝
		v[step] = i;
		ll xx = x*i - y; //计算 x/y - 1/i xx表示新的分子,yy表示新的分母
		ll yy = y*i;
		ll r = gcd (xx, yy);
		dfs (step+1, dep, xx / r, yy / r, i + 1); //新的分母至少要比上一个大1为所以是i+1
	}
}

int main () {
	cin >> a >> b;
	memset (ans, -1, sizeof (ans)); //初始化数组
	for (int i = 1; i < N; i++) {
		dfs (0, i, a, b, getFirst (a, b));
		if (flag) {
			for (int j = 0; j <= i; j++)
				printf("%d ", ans[j]);
			break;
		}
	}
	return 0;
}

题目练习POJ 2870

你可能感兴趣的:(搜索)