[AcWing算法提高课]之搜索 双端队列广搜+双向广搜+迭代加深+双向深搜(C++题解)

[AcWing算法提高课]之搜索 双端队列广搜+双向广搜+迭代加深+双向深搜(C++题解),第1张

目录

(一)双端队列广搜

1)电路维修

(二)双向广搜

1)子串变换

(三)迭代加深

1)加成括号

(四)双向DFS

1)送礼物


(一)双端队列广搜 1)电路维修

 

 

 

 这个证明是真的待学习

目前我所在的问题:为什么要用双端队列,一个放队头,一个放队尾

题解来自:AcWing 175. 电路维修 ----解释dxdy和ixiy偏移数组 - AcWing

/*
抽象成一个最短路,横纵坐标和为奇数的点,永远走不到
每一步的长度为0或1,1表示需要反转一下
dx,dy确定点的坐标,ix,iy确定线的坐标
在读入数据的时候,我们输入的是线,但是我们的BFS是按照点来做的
所以向四个方向扩展时,每一个点要找对应4个方向的线,看看走到对角线上点的位置
需要0还是1 

*/

#include
#include
#include
#include//双端队列

#define x first
#define y second

using namespace std;

typedef pair PII;

const int N=510, M=N*N;

int n, m;
char g[N][N];
int dist[N][N];//注意起点是从0,0开始的
bool st[N][N];

int bfs()
{
    //由于有多组输入,所以每次都要初始化
    memset(dist, 0x3f, sizeof dist);
    memset(st, 0, sizeof st);
    dist[0][0]=0;
    deque q;
    q.push_back({0, 0});

    char cs[]="\\/\\/";// 表示某个点可选的走到4个点的花费为0字符,\/\/,顺时针
    int dx[]={-1, -1, 1, 1}, dy[]={-1, 1, 1, -1};//表示可以走到的四个点的坐标
    int ix[]={-1, -1, 0, 0}, iy[]={-1, 0, 0, -1};//  把格子上的点和线分开看
    //i,j位置的点,对应的走到对角线上距离为0的线,在g中的坐标应该是
    //左上:i-1,j-1,右上:i-1, j 右下:i,j 左下:i,j-1
    //这个在bfs从一个点往下继续扩展时会用到

    while(q.size())
    {
        PII t=q.front();
        q.pop_front();//删掉对头

        if(st[t.x][t.y]) continue;//扩展过了,就直接跳过,
        //所有边的权重不同,所以某些点可能需要被扩展多次。

// 这道题目可以看成是特殊的dijkstra算法,在dijkstra算法中, // 某些点可能会被多次扩展,但第一次从优先队列中d出的节点的 // 距离一定就是最小值了,所以需要在出队的时候来判重 st[t.x][t.y]=true; for(int i=0; i<4; i++) { int a=t.x+dx[i], b=t.y+dy[i]; if(a<0 || a>n || b<0 || b>m) continue; //判断g中四个方向是什么字符,即看一下走到对角线花费是1还是0 int ca=t.x+ix[i], cb=t.y+iy[i]; //g中的实际方向与理论方向相同,则加0,不同则加1 int d=dist[t.x][t.y]+(g[ca][cb] != cs[i]); //每个点可能会被扩展多次,第一次被扩展到的时候不一定是最优解。

//有可能a,b由第一个节点扩展而来,但是距离加了1,但是从第二个节点扩展而来就加0, //所以第二次扩展的d竟然小于第一次扩展而来的d // 只有这个点第一次从堆中出来的时候,也就是当前点是当前堆中最小值时, // 它的值才一定是最优解。

这就是一个简化版的dijkstra算法。

if(d>T; while(T--) { scanf("%d%d", &n, &m); for(int i=0; i

 

#include 
#include 
#include 
#include 

using namespace std;
using T = pair;

const int N = 510, inf = 0x3f;

char g[N][N];
int st[N][N], dist[N][N];
//dxdy表示坐标点的方向,因为只能斜着走,所以每次偏移量是|2|
int dx[4] = {-1, -1, 1, 1}, dy[4] = {-1, 1, 1, -1};
//ixiy代表每次从坐标点跨越格点所经过的字符(\或/) 的偏移
/*
进一步解释为:第一个方格的\线所代表的坐标为(0,0) 其跨越了坐标点(0,0)到(1,1)
即dxdy为坐标点偏移 ixiy为格点偏移
*/
int ix[4] = {-1, -1, 0, 0}, iy[4] = {-1, 0, 0, -1};
int k, n, m;

int bfs()
{
    //多组测试数据 每次都要初始化
    memset(st, 0, sizeof st);
    memset(dist, 0x3f, sizeof dist);
    //起始点要初始化为0
    dist[0][0] = 0;

    //根据四个方向偏移 所能走到的四种电路的形状 分别为: 左上\ 右上 / 右下 \ 左下 / 这里要转义
    char cs[5] = "\\/\\/";

    deque q;
    q.push_front({0, 0});

    while(q.size()){
        auto t = q.front();
        q.pop_front();

        int a = t.first, b = t.second;
        /*注意
            双端队列中存储的是坐标点的坐标,即dxdy偏移下的坐标
            所以最终会走到右下角的那个 '点' 不是格子 所以走到(n, m)时返回
        */
        if(a == n && b == m) return dist[n][m];

        if(st[a][b]) continue;
        st[a][b] = true;

        for(int i = 0; i < 4; i ++ ){
            //坐标点偏移
            int u = a + dx[i], v = b + dy[i];
            if(u < 0 || v < 0 || u > n || v > m) continue;

            //格点偏移 即经过坐标点偏移后所经过的格点
            int ga = a + ix[i], gb = b + iy[i];
            //检查所经过的格点是不是我们想要的、连通的那种格点
            int w = (g[ga][gb] != cs[i]);
            //如果是 则距离算为0 如果不是,即需要旋转一下才能连通 则距离算为1
            int d = dist[a][b] + w;
            //如果新扩展的坐标点可以被更新 就入队
            if(d < dist[u][v]){
                dist[u][v] = d;
                //由双端队列广搜的解法 如果扩展的这条边为1 就加入队尾 如果是0就加入对头
                if(!w) q.push_front({u, v});
                else q.push_back({u, v});
            }
        }
    }
    return -1;
}

int main()
{
    cin >> k;
    while(k -- )
    {
        cin >> n >> m;
        for(int i = 0; i < n; i ++ )
            for(int j = 0; j < m; j ++ )
                cin >> g[i][j];

        //这一步是为什么 参考yls的视频讲解
        if(n + m & 1) puts("NO SOLUTION");
        else printf("%d\n", bfs());
    }
    return 0;
}

作者:djn
链接:https://www.acwing.com/solution/content/82459/
来源:AcWing
著作权归作者所有。

商业转载请联系作者获得授权,非商业转载请注明出处。


(二)双向广搜 1)子串变换

 

 核心思想:

 实现流程:

        全局和main函数

  1. 定义string A,B 为全局变量,并在main函数中进行 输入
  2. 由于题目说了,最多只有6个规则,所以在全局开一个大小为6的数组 string a[ ]  string b[ ],表示 a->b 的规则,并在main函数用while循环输入,并用一个变量n,来记录规则的总数
  3. 在main函数中调用bfs函数

        bfs函数实现

  1. 特判:如果string A==string B,那么证明此时不需要规则变换,return 0,表示需要变换的步数为0
  2. 定义queue qa,qb 来记录 变换后的 string A 和 string B,那么这两个队列的初始情况就是A入qa队 ,B入qb队,各自代表它们的初始状态
  3. 定义unordered_map 来 记录 由 由初始的 string A 到现在哈希表中的 string键,距离为 int:多少步的意思,B同理
  4. 理解了 (3),那么就能明白 哈希表的 初始化的意义,将初始状态存入哈希表,那么因为它并灭有改变,所以赋值为 0
  5. while 循环 继续的条件 ,两个队列均不为空,由上述的 (2)可知,qa和qb代表的是变换后的string A,B,  那么只要它们有一个为空,那么都是不能完成的变换的
  6. 然后关于扩展,应该选择扩展数少的进行扩展,因为此时它的分支更少
  7. 扩展的同时记录步数,并根据步数的情况进行判断

        extend函数实现

  1. 取出队头元素,并删除
  2. while 循环继续的条件 队列不空
  3. 枚举 : 外层 i从string t的哪一个位置开始枚举 ,内层 j 枚举规则
  4. 判断相等
  5. 得到变换后的string
  6. 如果db的 中的stirng 也存在 同样的string 那么就 相当于两个方向会师,返回步数
  7. 如果曾经在 da中出现 ,那么就 continue
  8. da 记录距离+1
  9. 入队

 代码实现:

#include
#include
#include
#include
#include
#include 
using namespace std;

const int N = 6;
int n;
string a[N], b[N];
// 扩展函数
// 参数:扩展的队列,到起点的距离,到终点的距离,规则,规则
//返回值:满足条件的最小步数
int extend(queue& q, unordered_map& da, unordered_map& db,
    string a[], string b[])
{
    // 取出队头元素
    string t = q.front();
    q.pop();

    for (int i = 0; i < t.size(); i++)  // t从哪里开始扩展
        for (int j = 0; j < n; j++) // 枚举规则
            //如果t这个字符串的一段= 规则,比如= xyz,才可以替换
            if (t.substr(i, a[j].size()) == a[j])
            {
                // 变换之后的结果state:前面不变的部分+ 变化的部分 + 后面不变的部分
                // 比如abcd ,根据规则abc--> xu,变成 xud,这里的state就是xud
                string state = t.substr(0, i) + b[j] + t.substr(i + a[j].size());
                // state状态是否落到b里面去,两个方向会师,返回最小步数
                if (db.count(state)) return da[t] + 1 + db[state];
                // 如果该状态之前已扩展过,
                if (da.count(state)) continue;
                da[state] = da[t] + 1;
                q.push(state);
            }
    return 11;

}
// 从起点和终点来做bfs
int bfs(string A, string B)
{
    if (A == B) return 0;
    queue qa, qb; // 两个方向的队列
    //每个状态到起点的距离da(哈希表),每个状态到终点的距离db哈希表
    unordered_map da, db;
    // qa从起点开始搜,qb从终点开始搜
    qa.push(A), da[A] = 0; // 起点A到起点的距离为0
    qb.push(B), db[B] = 0; // 终点B到终点B的距离为0

    // qa和qb都有值,说明可以扩展过来,否则说明是不相交的
    while (qa.size() && qb.size())
    {
        int t; // 记录最小步数
        // 哪个方向的队列的长度更小一些,空间更小一些,从该方向开始扩展,
        // 时间复杂度比较平滑,否则有1个点会超时
        if (qa.size() <= qb.size()) t = extend(qa, da, db, a, b);
        else t = extend(qb, db, da, b, a);
        // 如果最小步数在10步以内

        if (t <= 10) return t;
    }

    return 11; // 如果不连通或者最小步数>10,则返回大于10的数

}

int main()
{
    string A, B;
    cin >> A >> B;
    // 读入扩展规则,分别存在a数组和b数组
    while (cin >> a[n] >> b[n]) n++;
    int step = bfs(A, B);
    if (step > 10) puts("NO ANSWER!");
    else cout << step << endl;
}

(三)迭代加深 1)加成括号

 

 题目概述:

给定一个n,求处满足最小序列长度为m的序列

该序列满足以下条件: 

(1)单调递增

(2)后一个数可由 前面的两个数 用加法表示 出来 (一个数可以被使用多次)

(3)第一项一定为1

(4)第m项一定为n

核心思路:

  1. 不难想到要用dfs进行暴力搜索
  2. 确定参数--->>(int u,int k) -->其中u表示的是当前层数(当前已有项数的)k表示(最大层数)
  3.  dfs的返回类型为bool 值,如果为false,那么就证明 构造失败,需要更新最大层数,即k++
  4. 如果为true,那么就证明构造成功,打印path数组  (记录路径)

剪枝优化:

  1. 开一个bool 数组排除 等效冗余:如果 这个数之前构造过了,那么就continue
  2. 从后向前枚举:剪枝顺序优化,因为 从小往大数 有很多分支 ,从大往小 的分支 就会少很多
  3. 排除不合理的情况:因为这个序列是单调递增的,所以枚举出来的这个数 一定要大于 它的前一项,并且要小于 n(因为n是最后一项的数)

完整代码:

#include
#include
#include
using namespace std;

const int N = 110;
int n;
int path[N];

bool dfs(int u, int k)//u表示当前层数 k表示最大的层数
{
	if (u == k) return path[u - 1] == n;

	bool st[N] = { 0 };//通过bool 数组排除等效冗余

	for (int i = u - 1; i >= 0; i--)//从后向前枚举:剪枝顺序优化的一种
	{
		for (int j = i; j >= 0; j--)
		{
			int s = path[i] + path[j];
			if (s > n || s <= path[u - 1] || st[s]) continue;//path一定是递增的
			
			st[s] = true;
			path[u] = s;

			if (dfs(u + 1, k)) return true;
		}
	}
	return false;
}

int main()
{
	path[0] = 1;
	while (cin >> n, n)
	{
		int k = 1;
		while (!dfs(1, k)) k++;//如果不满足,那么就增加最大的层数
		for (int i = 0; i < k; i++)
		{
			cout << path[i] << " ";
		}
		cout << endl;
	}

	return 0;
}

(四)双向DFS 1)送礼物

 

 核心思路:

  1. 将物品件数砍半(k=n/2)
  2. dfs1枚举 前半部分(从0开始到k)
  3. dfs2枚举后半部分(从k到n)
  4. 在dfs2中记录最大的res

剪枝优化:

  1. 在dfs1之前,先逆向排序存放物品重量的数组,使得后序扩展的分支减少
  2. 在dfs1中,用weight存 枚举到 第k件物品的所有方案数
  3. 在dfs2之前,将weight数组进行排序(正序)(为了二分)
  4. 在dfs2之前,将weight数组进行去重(为了二分)
  5. 子在dfs2中 如果枚举到了第 n件物品,那么就二分查找满足 的weight[i],并记录

完整代码:

#include 
#include 
#include 

using namespace std;

typedef long long LL;

const int N = 1 << 24;

int n, m, k;
int g[50], weights[N];
int cnt = 0;
int ans;


void dfs1(int u, int s)
{
    if (u == k)//枚举完了一半的
    {
        weights[cnt++] = s;//记录方案数
        return;
    }

    if ((LL)s + g[u] <= m) dfs1(u + 1, s + g[u]);//满足条件-->>选
    dfs1(u + 1, s);//不选
}


void dfs2(int u, int s)
{
    if (u == n)
    {
        int l = 0, r = cnt - 1;
        while (l < r)//二分查找:二段性是第一段的末端:因为不能超过限制,限制的一方在右边
        {
            int mid = l + r + 1 >> 1;
            if (weights[mid] + (LL)s <= m) l = mid;
            else r = mid - 1;
        }
        if (weights[l] + (LL)s <= m) ans = max(ans, weights[l] + s);

        return;
    }

    if ((LL)s + g[u] <= m) dfs2(u + 1, s + g[u]);//选
    dfs2(u + 1, s);//不选
}


int main()
{
    cin >> m >> n;
    for (int i = 0; i < n; i++) cin >> g[i];

    sort(g, g + n);
    reverse(g, g + n);//逆向:剪枝顺序优化

    k = n / 2;  // 防止 n = 1时,出现死循环
    dfs1(0, 0);//第一次

    sort(weights, weights + cnt);//正向

    //去重
    int t = 0;
    for (int i = 1; i < cnt; i++)
        if (weights[i] != weights[i - 1])
            weights[t++] = weights[i];
    cnt = t;

    dfs2(k, 0);//从一半开始枚举

    cout << ans << endl;

    return 0;
}

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/673305.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-19
下一篇 2022-04-19

发表评论

登录后才能评论

评论列表(0条)