《算法竞赛进阶指南》0x28IDA*


A*算法的本质是带有估价函数的优先队列BFS算法,故A*算法有一个显而易见的缺点,就是需要维护一个二叉堆来存储状态及其估价,耗费空间较大,并且对堆进行一次操作也要花费O(logn)的时间。
A*算法的关键在于估价函数,估价函数也能与DFS结合,当然DFS也有一个缺点,一旦估价失误容易向下递归深入到一个不能产生最优解的分支,浪费许多时间。
综上,我们可以把估价函数与迭代加深的DFS相结合,我们设计一个估价函数,估算从每个状态到目标状态的步数,和A*算法相同,固件函数需要遵守“估计值不能大于未来实际步数”的准则。然后以迭代加深DFS的搜索框架为基础,把原来的深度限制加强为:若当前深度+未来估计步数>深度限制,则立即从当前分支回溯。

例题
acwing180.排书

先考虑每一步的决策数量:
当抽取长度为 i 的一段时,有 n − i + 1 n−i+1 ni+1种抽法,对于每种抽法,有 n − i n−i ni 种放法。另外,将某一段向前移动,等价于将跳过的那段向后移动,因此每种移动方式被算了两遍,所以每个状态总共的分支数量是:
∑ i = 1 n ( n − i ) ∗ ( n − i + 1 ) / 2 ≤ ( 15 ∗ 14 + 14 ∗ 13 + 13 ∗ 12 + . . . + 2 ∗ 1 ) = 560 \sum_{i=1}^{n}(n-i)*(n-i+1)/2 \leq (15*14+14*13+13*12+...+2*1)=560 i=1n(ni)(ni+1)/2(1514+1413+1312+...+21)=560
考虑在四步以内解决,最多有 56 0 4 560^4 5604 个状态,会超时。可以使用双向BFS或者IDA来优化。
我们用IDA
来解决此题。
估价函数:

估价函数需要满足:不大于实际步数在最终状态下,每本书后面的书的编号应该比当前书多1。
每次移动最多会断开三个相连的位置,再重新加入三个相连的位置,因此最多会将3个错误的连接修正,所以如果当前有 t o t tot tot 个连接,那么最少需要 ⌈ t o t / 3 ⌉ ⌈tot/3⌉ tot/3次操作。因此当前状态的估价函数可以设计成 f ( s ) = ⌈ t o t / 3 ⌉ f(s)=⌈tot/3⌉ f(s)=tot/3如果当前层数加上 f ( s ) f(s) f(s) 大于迭代加深的层数上限,则直接从当前分支回溯。

#include<iostream>
#include<cstring>
using namespace std;
#define MAX_N 15
int depth=0;
int t,n;
int q[MAX_N+5],w[5][MAX_N+5];
int f()
{
    int tot=0;
    for(int i=0;i<n-1;i++)
    if(q[i]!=q[i+1]-1)tot++;
    return (tot+2)/3;
}
bool dfs(int depth,int max_depth)
{
    if(f()+depth>max_depth)return false;
    if(!f())return true;
    for(int l=0;l<n;l++)
    {
        for(int r=l;r<n;r++)
        {
            for(int k=r+1;k<n;k++)
            {
                memcpy(w[depth],q,sizeof q);
                int x,y;
                for(x=r+1,y=l;x<=k;x++,y++)q[y]=w[depth][x];
                for(x=l;x<=r;x++,y++)q[y]=w[depth][x];
                if(dfs(depth+1,max_depth))return true;
                memcpy(q,w[depth],sizeof q);
            }
        }
    }
    return false;
}
int main()
{
    cin>>t;
    while(t--)
    {
        depth=0;
        cin>>n;
        for(int i=0;i<n;i++)cin>>q[i];
        while(depth<5&&!dfs(0,depth))depth++;
        if(depth==5)cout<<"5 or more"<<endl;
        else cout<<depth<<endl;
    }
    return 0;
}

acwing181.回转游戏
本题采用 IDA* 算法,即迭代加深的 A* 算法。

估价函数:

统计中间8个方格中出现次数最多的数出现了多少次,记为 k 次。
每次操作会从中间8个方格中移出一个数,再移入一个数,所以最多会减少一个不同的数。
因此估价函数可以设为 8−k剪枝:

记录上一次的操作,本次操作避免枚举上一次的逆操作。
如何保证答案的字典序最小?

由于最短操作步数是一定的,因此每一步枚举时先枚举字典序小的操作即可。
时间复杂度
假设答案最少需要 k步,每次需要枚举 7种不同操作(除了上一步的逆操作),因此最坏情况下需要枚举 7 k 7^k 7k 种方案。但加入启发函数后,实际枚举到的状态数很少。

#include<iostream>
using namespace std;
int q[30];
int op[8][7]={
    {0,2,6,11,15,20,22},
    {1,3,8,12,17,21,23},
    {10,9,8,7,6,5,4},
    {19,18,17,16,15,14,13},
    {23,21,17,12,8,3,1},
    {22,20,15,11,6,2,0},
    {13,14,15,16,17,18,19},
    {4,5,6,7,8,9,10},
};
int center[8]={6,7,8,11,12,15,16,17};
int opposite[8]={5,4,7,6,1,0,3,2};
int path[100];
int f()
{
    int sum[4]={0};
    for(int i=0;i<8;i++)
    sum[q[center[i]]]++;
    int s=0;
    for(int i=1;i<=3;i++)s=max(s,sum[i]);
    return 8-s;
}
void operate(int x)
{
    int t = q[op[x][0]];
    for (int i = 0; i < 6; i ++ ) q[op[x][i]] = q[op[x][i + 1]];
    q[op[x][6]] = t;
}
bool dfs(int depth, int max_depth, int last)
{
    if (depth + f() > max_depth) return false;
    if (!f()) return true;

    for (int i = 0; i < 8; i ++ )
    {
        if (opposite[i] == last) continue;
        operate(i);
        path[depth] = i;
        if (dfs(depth + 1, max_depth, i)) return true;
        operate(opposite[i]);
    }

    return false;
}
int main()
{
    while (scanf("%d", &q[0]), q[0])
    {
        for (int i = 1; i < 24; i ++ ) scanf("%d", &q[i]);
        int depth = 0;
        while (!dfs(0, depth, -1))
        {
            depth ++ ;
        }
        if (!depth) printf("No moves needed");
        for (int i = 0; i < depth; i ++ ) printf("%c", 'A' + path[i]);
        printf("\n%d\n", q[6]);
    }

    return 0;
}

acwing182.破坏正方形
先将每个正方形的所有边的编号预处理出来。

这一部分要耐心观察原图形找规律,可以发现每个正方形上下两组边是公差为1的等差数列,只要求出数列的首项即可;左右两组边是公差为 2n+1的等差数列,同理求出首项即可。
然后问题变成最少选出多少边,使得每个正方形中至少被选出一条边。
这是一个经典的重复覆盖问题,可以用 Dancing Links 求解。
这里我们不适用DLX这个数据结构,直接求解。

估价函数:
枚举所有未被删掉的正方形,将其所有边全部删掉,只记删除一条边。这样估计出的值一定不大于真实值,满足IDA*对估价函数的要求。其实这也是Dancing Links求解重复覆盖问题时的估价函数。

搜索顺序优化:
找出最小的未被删除的正方形,依次枚举删除每条边。

#include <vector>
#include <cstdio>
#include <cstring>

using namespace std;

const int N = 61;      // 网格最大是 5 * 5 的,其中最多会有 5 * (5 + 1) * 2 = 60 个正方形,所以要开到 61

int n, idx;            // n 为网格规模,idx 为正方形数量
int max_depth;         // IDA* 的 max_depth
vector<int> square[N]; // 存每个正方形边上的火柴的编号
bool used[N];          // 存每个火柴是否已经被破坏

// 新加一个左上角坐标为 (r, c),边长为 len 的正方形
void add(int r, int c, int len)
{
    int d = n << 1 | 1;  // 由于用到的 2n + 1 比较多,这里先用一个变量代替掉 2n + 1
    vector<int> &s = square[idx];
    s.clear(); // 有多组测试数据,需要上一组数据的内容清空
    for (int i = 0; i < len; i ++ )
    {
        s.push_back(1 + r * d + c + i);               // 上边第 i 个
        s.push_back(1 + (r + len) * d + c + i);       // 下边第 i 个
        s.push_back(1 + n + r * d + c + i * d);       // 左边第 i 个
        s.push_back(1 + n + r * d + c + i * d + len); // 右边第 i 个
    }
    idx ++ ;
}

// 判断正方形 s 是否完整
bool check(vector<int> &s)
{
    for (int i = 0; i < s.size(); i ++ )
        if (used[s[i]]) return false; // 如果其中有一条边已经被破坏了,那么说明不完整
    return true; // 如果每条边都没被破坏,说明完整
}

// 估价函数
int f()
{
    static bool backup[N]; // 由于要改动 used,需要先新建一个备份数组
    memcpy(backup, used, sizeof used); // 将 used 复制到备份数组中
    int res = 0;
    for (int i = 0; i < idx; i ++ ) // 枚举所有正方形
        if (check(square[i]))       // 如果某个正方形是完整的,
        {
            res ++ ;                // 那么 res ++ ,并将该正方形所有的边都删去
            for (int j = 0; j < square[i].size(); j ++ )
                used[square[i][j]] = true;
        }
    memcpy(used, backup, sizeof used); // 复制回来
    return res;
}

// IDA*
bool dfs(int depth)
{
    if (depth + f() > max_depth) return false;
    for (int i = 0; i < idx; i ++ ) // 枚举所有的正方形
        if (check(square[i]))       // 如果第 i 个正方形还没被破坏
        {
            // 那么枚举该正方形的所有边编号,去掉该边并继续爆搜
            for (int j = 0; j < square[i].size(); j ++ )
            {
                used[square[i][j]] = true;
                if (dfs(depth + 1)) return true;
                used[square[i][j]] = false;
            }
            // 如果每条边都爆搜不成功,那么说明删掉 max_depth 个火柴无法破坏该正方形
            return false;
        }
    return true; // 如果所有的正方形都被破坏了,返回 true
}

int main()
{
    int T;
    scanf("%d", &T);
    while (T -- )
    {
        scanf("%d", &n), idx = 0; // 初始化 idx
        memset(used, false, sizeof used); // 初始化 used
        for (int len = 1; len <= n; len ++ ) // 枚举 len, r, c,预处理每个正方形
            for (int r = 0; r + len <= n; r ++ )
                for (int c = 0; c + len <= n; c ++ )
                    add(r, c, len);
        int k;
        scanf("%d", &k);
        while (k -- )  // 读入所有已经被破坏的边
        {
            int x;
            scanf("%d", &x);
            used[x] = true;
        }
        max_depth = 0; // IDA*
        while (!dfs(0)) max_depth ++ ;
        printf("%d\n", max_depth);
    }
    return 0;
}

  • 9
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值