DFS进阶技巧--迭代加深,IDA*和双向DFS

1.迭代加深

深度优先搜索每次选定一个分支,不断深入直到递归边界才回溯.当搜索树的分支特别多,但答案处于较浅层的节点上,就会导致浪费许多时间.

所以我们通过指定搜索的最大深度,逐步加深,保证递归层数不大.
这种方法确实会重复搜索浅层节点,但是前 n − 1 n - 1 n1层的节点数量之和在最坏情况下等于第 n n n层节点数 - 1(满二叉树),分支增多则小于最后一层节点数,故总数量级仍为最后一层节点数,可以接受.
这个算法需要保证答案在浅层节点,有些题目里面会有相应的提示,比如"如果10步以内搜不到答案就算无解 "此类.

AdditionChains

满足如下条件的序列 X(序列中元素被标号为 123…m)被称为“加成序列”:
X[1]=1
X[m]=n
X[1]<X[2]<<X[m−1]<X[m]
对于每个 k(2≤k≤m)都存在两个整数 i 和 j (1≤i,j≤k−1,i 和 j 可相等),使得 X[k]=X[i]+X[j]。
你的任务是:给定一个整数 n,找出符合上述条件的长度 m 最小的“加成序列”。
如果有多个满足要求的答案,只需要找出任意一个可行解。 n <= 100

在最坏情况下, n = 100 n = 100 n=100时要搜100层,但是在较好的情况下,由于满足 X [ k ] = X [ i ] + X [ j ] X[k]=X[i]+X[j] X[k]=X[i]+X[j],i 和 j 可相等,故可以每次翻倍, 1 , 2 , 4 , 8 , 16 , 32 , 64 , 128 1 ,2 ,4 ,8 ,16 ,32 ,64 ,128 1,2,4,8,16,32,64,128,8层就能搜到答案.所以答案的层数应该不深.迭代加深搜索,
这里有两个剪枝的技巧:
1.由于要满足 X [ k ] = X [ i ] + X [ j ] X[k]=X[i]+X[j] X[k]=X[i]+X[j],i和j我们从大到小枚举,优先选择较大的数可以使得后续的分支较少,让我们更快搜到答案.(顺序)
2.假设现在序列为1,2,3,4,第5位可以取5 = 1 + 4 = 2 + 3,会有冗余搜索,开一个st数组判断当前数(5)有没有被枚举过,若枚举过就不在dfs一层,因为之前已经dfs过了.

代码

/*
 * @Author: ACCXavier
 * @Date: 2022-01-28 22:29:01
 * @LastEditTime: 2022-01-29 09:20:03
 * Bilibili:https://space.bilibili.com/7469540
 * @keywords: DFS迭代加深
 * 逐步增加搜索层数
 */

#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 110;
int path[N];
int n;
//当前层,要搜到的层(从1开始的)
bool dfs(int u,int depth){
    if(u == depth){
        return path[u - 1] == n;
    }
    bool st[N] = {0};
    for(int i = u - 1; i >= 0; -- i){
        for(int j = i; j >= 0; -- j){//j > i组合搜索
            int s = path[i] + path[j];
                     //保证严格单调递增
            if(s > n || s <= path[u - 1] || st[s])continue;
            st[s] = true;
            path[u] = s;
            if(dfs(u + 1,depth))return true;
        }
    }
    return false;
}
int main()
{
    path[0] = 1;
    
    while(cin >> n,n){
        int depth = 1;
        while(!dfs(1,depth)) ++ depth;//false说明层数不够
        for(int i = 0; i < depth; ++ i)cout << path[i] << ' ';
        cout << endl;
        
    }
}

2.IDA*

dfs有两种分类.1是在图上进行寻找广义最短路,称之为内部搜索,例如迷宫;另一种是将一个图设为一个状态,每次操作对整个图进行变换,寻找最短的变换次数,称之为外部搜索,例如八数码,.

IDA*是一种基于迭代加深的启发式算法,一般是对外部搜索问题进行处理.对从初始状态操作到 s t a t e state state对应的操作步数为 u u u,我们要算估价函数 f ( u ) f(u) f(u), f ( u ) f(u) f(u)是我们估计的 s t a t e state state到目标状态的操作距离,这个操作距离在dfs中就是搜索层数.

估价函数性质:要保证 f ( u ) < = s t a t e f(u)<= state f(u)<=state到终态的距离.等号一般只在已经到达终态时成立
作用:能够剪枝.找广义最小距离时,当 u + f ( u ) > u + f(u) > u+f(u)>当前距离时,则直接return.因为 u + f ( u ) > = u + T r u e d i s t a n c e ( u ) u + f(u) >= u + Truedistance(u) u+f(u)>=u+Truedistance(u),继续搜不可能找到距离更小的路.

简单的说就是我们开一个估计函数这个估计函数的返回值小于等于真实值这样我们就可以利用这个进行最优化减枝
如果估计函数都无法完成那么真实值一定也无法完成可以直接剪掉

Booksort

给定 n 本书,编号为 1∼n。
在初始状态下,书是任意排列的。
在每一次操作中,可以抽取其中连续的一段,再把这段插入到其他某个位置。
我们的目标状态是把书按照 1∼n 的顺序依次排列。
求最少需要多少次操作。
如果最少操作次数大于或等于 **5** 次,则输出 5 or more。

本题可以详细的估算暴搜的复杂度
计数每次将 i i i本书移动的所有情况次数
在这里插入图片描述暴力搜索 O ( 56 0 4 ) O(560^4) O(5604),使用IDA*,估价函数的取法及原理如下

图中蓝色段为要挪动的书,挪动位置标出,这次挪动改变了三个元素的后继元素,分别用红,绿,蓝标出了后继的改变.

也就是说一次挪动改变3个后继,设总共不正确后继为 t o t tot tot,则最少需要操作 ⌈ t o t 3 ⌉    =    ⌊ t o t + 2 3 ⌋    \lceil \frac{\mathrm{tot}}{3} \rceil \,\,=\,\,\lfloor \frac{\mathrm{tot}+2}{3} \rfloor \,\, 3tot=3tot+2次,将其设为估价函数

代码

/*
 * @Author: ACCXavier
 * @Date: 2022-01-30 19:51:40
 * @LastEditTime: 2022-01-30 21:01:19
 * Bilibili:https://space.bilibili.com/7469540
 * @keywords: DFS 中等
我们开一个估计函数这个估计函数的返回值小于等于真实值这样我们就可以利用这个进行最优化减枝
如果估计函数都无法完成那么真实值一定也无法完成可以直接剪掉
由于步数很小答案可能会很大所以用迭代加深优化搜索
枚举选择的长度以及放的位置
选择书后不用往前放,因为会对应前面的一种选法,重复
这题由于每次我们最多改变3个后继的位置的关系所以假设我们有tot个错误前后关系那么(tot+2)/3就是我们最多要操作的次数
我们可以用这个(tot+2)/3来当成估计函数
 */
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;
const int N = 15;
int q[N];
int w[5][N];//暂存每层u的状态
int n;
int f(){//不正确的后继数量
    int cnt = 0;
    for(int i = 0; i < n - 1; ++ i){
        if(q[i + 1] != q[i] + 1)cnt ++;
    }
    return (cnt + 2)/3;//对3上取整
}

bool dfs(int u,int depth){
    if(u + f() > depth)return false;//
    if(!f())return true;//为0表示已经排好
    
    for(int len = 1; len <= n; ++ len){//移动len长度的书
        for(int l = 0; l + len - 1 < n; ++ l){//左端点从0开始,右端点就要-1
            int r = l + len - 1;

            for(int k = r + 1; k < n; ++ k){//移动到第k位数字的后面,从r + 1位开始
                //memcpy(str1, str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。
                memcpy(w[u],q,sizeof q);//暂时存储
                
                int x,y;

                //y       x 
                //l______r____k____
                for(x = r + 1,y = l;x <= k;x ++,y ++)q[y] = w[u][x];
                //再拷贝前面的一部分,y继续加
                for(x = l; x <= r; ++ x, ++ y)q[y] = w[u][x];
                if(dfs(u + 1,depth))return true;
                memcpy(q,w[u],sizeof q);//回溯
            }
        }
    }
    return false;//! bool类型默认返回值true,很容易造成搜索结果错误!
    //!除了void,都记得写return!
}
int main()
{
    int T;
    cin >> T;
    while(T --){
        cin >> n;
        for(int i = 0; i < n; ++ i){
            cin >> q[i];
        }
        
        int depth = 0;//0次对应已经排好序
        while(depth < 5 && !dfs(0,depth))depth ++;
        
        if(depth == 5)puts("5 or more");
        else printf("%d\n",depth);
    }
    return 0;
}

TheRotationGame

如下图所示,有一个 # 形的棋盘,上面有 1,2,3 三种数字各 8 个。
在这里插入图片描述
给定 8 种操作,分别为图中的 A∼H。

这些操作会按照图中字母和箭头所指明的方向,把一条长为 7 的序列循环移动 1 个单位。

例如下图最左边的 # 形棋盘执行操作 A 后,会变为下图中间的 # 形棋盘,再执行操作 C 后会变成下图最右边的 # 形棋盘。

给定一个初始状态,请使用最少的操作次数,使 # 形棋盘最中间的 8 个格子里的数字相同。

输入格式
输入包含多组测试用例。
每个测试用例占一行,包含 24 个数字,表示将初始棋盘中的每一个位置的数字,按整体从上到下,同行从左到右的顺序依次列出。
输入样例中的第一个测试用例,对应上图最左边棋盘的初始状态。
当输入只包含一个 0 的行时,表示输入终止。
输出格式
每个测试用例输出占两行。
第一行包含所有移动步骤,每步移动用大写字母 A∼H 中的一个表示,字母之间没有空格,如果不需要移动则输出 No moves needed。
第二行包含一个整数,表示移动完成后,中间 8 个格子里的数字。

如果有多种方案,则输出字典序最小的解决方案。
输入样例:

1 1 1 1 3 2 3 2 3 1 3 2 2 3 1 2 2 2 3 1 2 1 3 3
1 1 1 1 1 1 1 1 2 2 2 2 2 2 2 2 3 3 3 3 3 3 3 3
0

输出样例:

AC
2
DDHH
2

一样用IDA*,此时估价函数为 f ( ) = 8 − 中 间 8 格 最 多 数 字 的 个 数 f() = 8 - 中间8格最多数字的个数 f()=88.因为每次对一条操作能改变中间8格的一格(看起来是改了两格,但数字只变化了一个),故 f ( ) < = T r u e d i s t a n c e ( ) f() <= Truedistance() f()<=Truedistance(),可以用.

代码

/*
 * @Author: ACCXavier
 * @Date: 2022-01-30 20:12:57
 * @LastEditTime: 2022-01-31 10:50:48
 * Bilibili:https://space.bilibili.com/7469540
 * @keywords: DFS 中等
 * 

*/
/*
        0     1
        0     1
        2     3
7 4  5  6  7  8  9  10   2
        11    12
6 13 14 15 16 17 18 19   3
        20    21
        22    23
        5      4
*/

#include <cstring>
#include <iostream>
#include <algorithm>

using namespace std;

const int N = 24;
//如果op里面用从1开始的编号,那么opposite和center也改成从1开始,q从1输入即可
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 opposite[8] = {5, 4, 7, 6, 1, 0, 3, 2};
int center[8] = {6, 7, 8, 11, 12, 15, 16, 17};

int q[N];
int path[100];

int f()//8 - cnt(最多的数字),<=最小操作步数
{
    static int sum[4];
    memset(sum, 0, sizeof sum);
    for (int i = 0; i < 8; i ++ ) sum[q[center[i]]] ++ ;

    int maxv = 0;
    for (int i = 1; i <= 3; i ++ ) maxv = max(maxv, sum[i]);

    return 8 - maxv;//!8 - maxv,距离
}

void operate(int x)//操作第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;//最后一位
}
//当前第u层,上限depth,最后一个操作的编号
bool dfs(int u, int depth, int last)
{
    if (u + f() > depth) return false;
    if (f() == 0) return true;

    for (int i = 0; i < 8; i ++ )
        if (last != opposite[i])//不能做上一步操作的逆操作,无用功
        {
            operate(i);
            path[u] = i;
            if (dfs(u + 1, depth, i)) return true;
            operate(opposite[i]);//回溯
        }

    return false;
}

int main()
{
    while (cin >> q[0], q[0])
    {
        for (int i = 1; i < 24; i ++ ) cin >> q[i];

        int depth = 0;
        while (!dfs(0, depth, -1)) depth ++ ;

        if (!depth) printf("No moves needed");
        else
        {
            for (int i = 0; i < depth; i ++ ) printf("%c", 'A' + path[i]);
        }
        printf("\n%d\n", q[6]);
    }

    return 0;
}

3.双向DFS

从两个方向搜索,搜索树会变小.如图
在这里插入图片描述

送礼物

达达帮翰翰给女生送礼物,翰翰一共准备了 N 个礼物,其中第 i 个礼物的重量是 G[i]。

达达的力气很大,他一次可以搬动重量之和不超过 W 的任意多个物品。

达达希望一次搬掉尽量重的一些物品,请你告诉达达在他的力气范围内一次性能搬动的最大重量是多少。

输入格式
第一行两个整数,分别代表 W 和 N。

以后 N 行,每行一个正整数表示 G[i]。

输出格式
仅一个整数,表示达达在他的力气范围内一次性能搬动的最大重量。

数据范围
1 ≤ N ≤ 46 , 1≤N≤46, 1N46,
1 ≤ W , G [ i ] ≤ 2 31 − 1 1≤W,G[i]≤2^{31}−1 1W,G[i]2311
输入样例:

20 5
7
5
4
18
1

输出样例:

19

是个背包问题,但是物品质量太大,以背包 O ( M N ) O(MN) O(MN)的复杂度无法处理.
本题先预处理搜一部分所有的物品组合情况,再暴搜后一部分.
后一部分搜出 w w w权重后,在前面寻找最大的 u u u使得 u + w < = W u + w <= W u+w<=W,前部分暴搜后排序,即可用二分.
前后搜相当于两个方向dfs.
时间复杂度详细分析如下,先尝试对半分:
在这里插入图片描述
恰当的选区分界点可以降低搜索计算量,本题选 k = n / 2 + 2 k = n / 2 + 2 k=n/2+2,注意当 n < = 2 n<=2 n<=2时要特判.

代码

/*
 * @Author: ACCXavier
 * @Date: 2022-01-29 16:26:07
 * @LastEditTime: 2022-01-29 17:07:40
 * Bilibili:https://space.bilibili.com/7469540
 * @keywords: 双向DFS
 * 将物品分两份,前一部分k暴搜出所有方案,再搜后一部分二分<=m的最大值
 */
#include <iostream>
#include <cstring>
#include <algorithm>

using namespace std;

typedef long long ll;

const int N = 46;
int n,m;//物品数量和承受重量
int w[N];//每件物品的质量
int k;//前一部分k个
int weight[1 << 25],//前k部分的所有方案
    cnt = 1;//已有一种方案,即不选,放在0位,后续搜索的时候都是不选就是只选前半部分,前面也不选那就是全不选
int ans;

//物品序号(从0开始),当前质量和
void dfs1(int u,int s){
    if(u == k){//从0开始,所以u == k表示已经处理了0~u-1共k个物品
    //!搜到k 不是 n
        weight[cnt ++ ] =  s;
        return;
    }
    if((ll)s + w[u] <= m)dfs1(u + 1,s + w[u]);
    dfs1(u + 1,w1);
}
//物品序号(从0开始),当前质量和
void dfs2(int u,int s){
    if(u >= n){//写>是因为k若取n/2+2可能大于n,等同于边界特判
    //找<=m的最大数,前驱
        int l = 0,r = cnt - 1;
        while(l < r){
            int mid = l + r + 1 >> 1;
            if((ll)sum + weight[mid] <= m)l = mid;
            else r = mid - 1;
        }
        ans = max(ans,sum + weight[l]);
        return;
    }
    dfs2(u + 1,sum);
    if((ll)sum + w[u] <= m)dfs2(u + 1,sum + w[u]);
}
int main(){
    cin >> m >> n;
    for(int i = 0; i < n; ++ i){
        cin >> w[i];
    }
    sort(w,w + n,greater<int>());
    k = n / 2 + 2;
    dfs1(0,0);
    //!只需要排序到cnt
    sort(weight,weight + cnt);//二分要用weight,不要倒序排序
    cnt = unique(weight,weight + cnt) - weight;

    dfs2(k,0);
    cout << ans << endl;
    return 0;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值