Bootstrap

C++搜索算法

搜索

一、Flood Fill

flood fill算法通常是用于计处理连通块问题,由于其算法思路由一个连通块蔓延到周围的连通块,类似洪水蔓延,故得此名。

flood fill算法本质上还是bfs算法,将周围的连通块看作同一父结点的子结点,不难看出是一个树状结构。

模板如下:

//Flood Fill
bool vis[N][N];//标记一个点是否被搜索过
int mx[4]{0,-1,0,1},my[4]{-1,0,1,0};//偏移数组
int bfs(int i,int j)//从i,j开始搜
{
    queue<pair<int,int>>q;
    q.push({i,j});
    vis[i][j] = 1;
    while(!q.empty())
    {
        auto t = q.front();
        p.pop();
        
        for(int k = 0;k < 4;k++)//遍历周围的坐标,此处是四连通
        {
            int x = t.first + mx[k],y = t.second + my[k];
            
            //写判断条件,满足判断条件则跳过
            
            q.push({x,y});//将周围的连通块的坐标加入到队列中
        }
    }
    //返回结果
}

//在主函数中遍历每个坐标

当然,队列可以通过手写的方式实现,具体参考我的这篇博客:单调栈与单调队列

如题:https://www.acwing.com/problem/content/1100/

城堡问题:

    1   2   3   4   5   6   7  
   #############################
 1 #   |   #   |   #   |   |   #
   #####---#####---#---#####---#
 2 #   #   |   #   #   #   #   #
   #---#####---#####---#####---#
 3 #   |   |   #   #   #   #   #
   #---#########---#####---#---#
 4 #   #   |   |   |   |   #   #
   #############################
           (图 1)

   #  = Wall   
   |  = No wall
   -  = No wall

   方向:上北下南左西右东。

图1是一个城堡的地形图。

请你编写一个程序,计算城堡一共有多少房间,最大的房间有多大。

城堡被分割成 m∗n个方格区域,每个方格区域可以有0~4面墙。

注意:墙体厚度忽略不计。

输入格式

第一行包含两个整数 m 和 n,分别表示城堡南北方向的长度和东西方向的长度。

接下来 m 行,每行包含 n 个整数,每个整数都表示平面图对应位置的方块的墙的特征。

每个方块中墙的特征由数字 P 来描述,我们用1表示西墙,2表示北墙,4表示东墙,8表示南墙,P 为该方块包含墙的数字之和。

例如,如果一个方块的 P 为3,则 3 = 1 + 2,该方块包含西墙和北墙。

城堡的内墙被计算两次,方块(1,1)的南墙同时也是方块(2,1)的北墙。

输入的数据保证城堡至少有两个房间。

输出格式

共两行,第一行输出房间总数,第二行输出最大房间的面积(方块数)。

数据范围

1≤m,n≤50
0≤P≤15

输入样例:
4 7 
11 6 11 6 3 10 6 
7 9 6 13 5 15 5 
1 10 12 7 13 7 5 
13 11 10 8 10 12 13 
输出样例:
5
9

我们将每个面积视为一个连通块,然后根据题目表述,将左上右下分别标记为0123,每个坐标的值的二进制表示墙是否存在。如3,四位二进制表示围殴0011,第一位为1,表示存在左边的墙,第二位为1,表示存在上边的墙。由此我们可以得到下面的代码

#include<iostream>
#include<queue>
using namespace std;
const int N = 55;
typedef pair<int, int> PII;

int g[N][N];
bool vis[N][N];
int n, m;
int max_area, room_num;
int mx[4]{ 0,-1,0,1 }, my[4]{ -1,0,1,0 };

int bfs(int i, int j)
{
    queue<PII>q;
    q.push({ i,j });
    vis[i][j] = 1;
    int area = 0;

    while (!q.empty())
    {
        auto t = q.front();
        q.pop();
        area++;

        for (int k = 0; k < 4; ++k)
        {
            int x = t.first + mx[k], y = t.second + my[k];
            if (g[t.first][t.second] >> k & 1)continue;
            if (vis[x][y])continue;
            if (x < 0 || x >= n || y < 0 || y >= m)continue;

            q.push({ x,y });
            vis[x][y] = 1;
        }
    }
    return area;
}

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

    for(int i = 0;i < n;i++)
        for(int j = 0;j < m;j++)
            if (!vis[i][j])
            {
                max_area = max(max_area, bfs(i, j));
                room_num++;
            }
    cout << room_num << endl << max_area << endl;
    return 0;
}

二、最短路模型

最短路模型用于解常用来解决两点之间的最短路径问题,这里我们具体讨论一维数组和二维数组模拟的最短路问题。该类问题也是利用bfs算法进行的,搜索能够走到的点,计算到达目标点的最小层数。

模板如下:

int bfs()
{
    queue<int>q;
   	q.push(a);//将起始点加入队列
    //其他的初始化操作在这里进行
    
    while(!q.empty())
    {
        int t = q.front();//取出队列头部的元素
        q.pop();
        
        //判断条件
        q.push(b);//加入新的点
        
    }
    
    return c;//返回目标值
}

首先来看看一维数组的模拟,看题:https://www.acwing.com/problem/content/1102/

农夫知道一头牛的位置,想要抓住它。

农夫和牛都位于数轴上,农夫起始位于点 N,牛位于点 K。

农夫有两种移动方式:

  1. 从 X 移动到 X−1 或 X+1,每次移动花费一分钟
  2. 从 X 移动到 2∗X,每次移动花费一分钟

假设牛没有意识到农夫的行动,站在原地不动。

农夫最少要花多少时间才能抓住牛?

输入格式

共一行,包含两个整数N和K。

输出格式

输出一个整数,表示抓到牛所花费的最少时间。

数据范围

0≤N,K≤105

输入样例:
5 17
输出样例:
4
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N = 1e5 + 10;
typedef pair<int, int> PII;

int n, m;
int d[N];

int bfs()
{
	memset(d, -1, sizeof d);
	queue<int>q;
	q.push(n);
	d[n] = 0;

	while (!q.empty())
	{
		int t = q.front();
		q.pop();
		if (t == m)return d[m];

		int p;
		p = t + 1;
		if (p < N && d[p] == -1)
		{
			q.push(p);
			d[p] = d[t] + 1;
		}
		p = t - 1;
		if (p >= 0 && d[p] == -1)
		{
			q.push(p);
			d[p] = d[t] + 1;
		}
		p = t << 1;
		if (p < N && d[p] == -1)
		{
			q.push(p);
			d[p] = d[t] + 1;
		}
	}
	return d[m];
}

int main()
{
	cin >> n >> m;

	cout << bfs() << endl;
	return 0;
}

然后是二维数组的模拟:https://www.acwing.com/problem/content/190/

农民 John 有很多牛,他想交易其中一头被 Don 称为 The Knight 的牛。

这头牛有一个独一无二的超能力,在农场里像 Knight 一样地跳(就是我们熟悉的象棋中马的走法)。

虽然这头神奇的牛不能跳到树上和石头上,但是它可以在牧场上随意跳,我们把牧场用一个 x,y 的坐标图来表示。

这头神奇的牛像其它牛一样喜欢吃草,给你一张地图,上面标注了 The Knight 的开始位置,树、灌木、石头以及其它障碍的位置,除此之外还有一捆草。

现在你的任务是,确定 The Knight 要想吃到草,至少需要跳多少次。

The Knight 的位置用 K 来标记,障碍的位置用 * 来标记,草的位置用 H 来标记。

这里有一个地图的例子:

             11 | . . . . . . . . . .
             10 | . . . . * . . . . . 
              9 | . . . . . . . . . . 
              8 | . . . * . * . . . . 
              7 | . . . . . . . * . . 
              6 | . . * . . * . . . H 
              5 | * . . . . . . . . . 
              4 | . . . * . . . * . . 
              3 | . K . . . . . . . . 
              2 | . . . * . . . . . * 
              1 | . . * . . . . * . . 
              0 ----------------------
                                    1 
                0 1 2 3 4 5 6 7 8 9 0 

The Knight 可以按照下图中的 A,B,C,D… 这条路径用 55 次跳到草的地方(有可能其它路线的长度也是 5):

             11 | . . . . . . . . . .
             10 | . . . . * . . . . .
              9 | . . . . . . . . . .
              8 | . . . * . * . . . .
              7 | . . . . . . . * . .
              6 | . . * . . * . . . F<
              5 | * . B . . . . . . .
              4 | . . . * C . . * E .
              3 | .>A . . . . D . . .
              2 | . . . * . . . . . *
              1 | . . * . . . . * . .
              0 ----------------------
                                    1
                0 1 2 3 4 5 6 7 8 9 0

注意: 数据保证一定有解。

输入格式

第 1 行: 两个数,表示农场的列数 CC 和行数 RR。

第 2…R+1 行: 每行一个由 C 个字符组成的字符串,共同描绘出牧场地图。

输出格式

一个整数,表示跳跃的最小次数。

数据范围

1≤R,C≤150

输入样例:
10 11
..........
....*.....
..........
...*.*....
.......*..
..*..*...H
*.........
...*...*..
.K........
...*.....*
..*....*..
输出样例:
5
#include<iostream>
#include<queue>
using namespace std;
const int N = 160;
typedef pair<int, int> PII;

int n, m;
bool g[N][N];
int sx, sy, ex, ey;
bool vis[N][N];
int d[N][N];

int mx[8]{ -2,-2,-1,-1,1,1,2,2 }, my[8]{ -1,1,-2,2,-2,2,-1,1 };

int bfs()
{
	queue<PII>q;
	q.push({ sx,sy });
	vis[sx][sy] = 1;
	d[sx][sy] = 0;

	while (!q.empty())
	{
		PII t = q.front();
		q.pop();

		for (int i = 0; i < 8; i++)
		{
			int x = t.first + mx[i], y = t.second + my[i];
			if (x < 0 || y < 0 || x >= m || y >= n)continue;
			if (vis[x][y] || g[x][y])continue;

			q.push({ x,y });
			vis[x][y] = 1;
			d[x][y] = d[t.first][t.second] + 1;
			
			if(x == ex && y == ey)return d[x][y];
		}
	}
	return d[ex][ey];
}

int main()
{
	cin >> n >> m;
	for(int i = 0;i < m;i++)
		for (int j = 0; j < n; j++)
		{
			char c;
			cin >> c;
			if (c == '*')g[i][j] = 1;
			else
			{
				if (c == 'K')
					sx = i, sy = j;
				else if (c == 'H')
					ex = i, ey = j;
			}
		}
		
	cout << bfs() << endl;
	return 0;
}

同理:易得三维数组模拟的最短路模型

三、多源BFS

在我们之前接触的bfs算法当中,都是从一个点开始进行遍历,但是在多源bfs中,是从多个点开始比遍历的。

多源bfs的处理方法并不复杂,单源bfs在初始化的时候将起点存入队列,而多元bfs就是初始化的收将多个点存入队列中。

这没什么模板的,直接看题吧,如题:https://www.acwing.com/problem/content/175/

给定一个 N 行 M 列的 0101 矩阵 A,A[i] [j] 与 A[k] [l] 之间的曼哈顿距离定义为:

dist(A[i] [j],A[k] [l])=|i−k|+|j−l|dist(A[i] [j],A[k] [l])=|i−k|+|j−l|

输出一个 NN 行 MM 列的整数矩阵 BB,其中:

B[i] [j]=min1≤x≤N,1≤y≤M,A[x] [y]=1dist(A[i] [j],A[x] [y])

输入格式

第一行两个整数 N,M。

接下来一个 N 行 M 列的 01 矩阵,数字之间没有空格。

输出格式

一个 N 行 M 列的矩阵 B,相邻两个整数之间用一个空格隔开。

数据范围

1≤N,M≤1000

输入样例:
3 4
0001
0011
0110
输出样例:
3 2 1 0
2 1 0 0
1 0 0 1
#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
typedef pair<int, int> PII;
const int N = 1010;

int n, m;
bool g[N][N];
int d[N][N];
queue<PII>q;

void bfs()
{
	int mx[4]{ 0,-1,0,1 }, my[4]{ -1,0,1,0 };
	while (!q.empty())
	{
		PII t = q.front();
		q.pop();

		for (int i = 0; i < 4; i++)
		{
			int x = t.first + mx[i], y = t.second + my[i];

			if (x < 0 || y < 0 || x >= n || y >= m)continue;
			if (d[x][y] != -1)continue;

			d[x][y] = d[t.first][t.second] + 1;
			q.push({ x,y });
		}
	}
}

int main()
{
	memset(d, -1, sizeof d);
	cin >> n >> m;
	for (int i = 0; i < n; i++)
		for (int j = 0; j < m; j++)
		{
		    char a;
			cin >> a;
			if (a == '1')
			{
			    g[i][j] = 1;
				q.push({ i,j });
				d[i][j] = 0;
			}
		}

	bfs();

	for (int i = 0; i < n; i++)
	{
		for (int j = 0; j < m; j++)
			cout << d[i][j] << ' ';
		cout << endl;
	}
	return 0;
}

四、最小步数模型

顾名思义,最小步数模型就是完成一个状态到另一个状态的转变的最小步骤数量。

如题:https://www.acwing.com/problem/content/1109/

Rubik 先生在发明了风靡全球的魔方之后,又发明了它的二维版本——魔板。

这是一张有 88 个大小相同的格子的魔板:

1 2 3 4
8 7 6 5

我们知道魔板的每一个方格都有一种颜色。

这 8 种颜色用前 8 个正整数来表示。

可以用颜色的序列来表示一种魔板状态,规定从魔板的左上角开始,沿顺时针方向依次取出整数,构成一个颜色序列。

对于上图的魔板状态,我们用序列 (1,2,3,4,5,6,7,8) 来表示,这是基本状态。

这里提供三种基本操作,分别用大写字母 A,B,C 来表示(可以通过这些操作改变魔板的状态):

A:交换上下两行;
B:将最右边的一列插入到最左边;
C:魔板中央对的4个数作顺时针旋转。

下面是对基本状态进行操作的示范:

A:

8 7 6 5
1 2 3 4

B:

4 1 2 3
5 8 7 6

C:

1 7 2 4
8 6 3 5

对于每种可能的状态,这三种基本操作都可以使用。

你要编程计算用最少的基本操作完成基本状态到特殊状态的转换,输出基本操作序列。

注意:数据保证一定有解。

输入格式

输入仅一行,包括 8 个整数,用空格分开,表示目标状态。

输出格式

输出文件的第一行包括一个整数,表示最短操作序列的长度。

如果操作序列的长度大于0,则在第二行输出字典序最小的操作序列。

数据范围

输入数据中的所有数字均为 1 到 8 之间的整数。

输入样例:
2 6 8 4 5 7 3 1
输出样例:
7
BCABCCB

通过理解阅读题意,可以知道一共有三种转换方式,我们将三种转换方式写成函数,然后直接调用即可。

#include<bits/stdc++.h>
using namespace std;

int g[2][4];
unordered_map<string,int>dist;
unordered_map<string,pair<char,string>>cnt;
queue<string>q;

void get(string start)//将字符串转换为二维数组,方便进行转换操作
{
	for(int i = 0;i < 4;i++)g[0][i] = start[i] - '0';
	for(int i = 3,j = 4;~i;i--,j++)g[1][i] = start[j] - '0';
}

string reget()//将二维数组转换回字符串
{
	string res;
	for(int i = 0;i < 4;i++)res += g[0][i] + '0';
	for(int i = 3;~i;i--)res += g[1][i] + '0';
	return res;
}

string m0(string start)//操作1
{
	get(start);
	for(int i = 0;i < 4;i++)
		swap(g[0][i],g[1][i]);
	return reget();
}

string m1(string start)//操作2
{
	get(start);
	for(int i = 3;i;i--)
		swap(g[0][i],g[0][i-1]),swap(g[1][i],g[1][i - 1]);
	return reget();
}

string m2(string start)//操作3
{
	get(start);
	int t = g[0][1];
	g[0][1] = g[1][1];
	g[1][1] = g[1][2];
	g[1][2] = g[0][2];
	g[0][2] = t;
	return reget();
}

void bfs(string start,string end)
{
	if(start == end)return ;
	
	q.push(start);
	dist[start] = 0;
	
	while(q.size())
	{
		string t = q.front();
		q.pop();
		
		string m[3];
		m[0] = m0(t);
		m[1] = m1(t);
		m[2] = m2(t);
		
		for(int i = 0;i < 3;i++)
		{
			if(!dist.count(m[i]))
			{
				dist[m[i]] = dist[t] + 1;
				cnt[m[i]] = {'A' + i,t};
				q.push(m[i]);
				if(m[i] == end)return;
			}
		}
	}
}

int main()
{
	string start,end;
	for(int i = 0;i < 8;i++)
	{
		int a;
		cin >> a;
		end += a + '0';
	}
	start = "12345678";
	
	bfs(start,end);
	
	cout << dist[end] << endl;
	
	if(dist[end] > 0)
	{
    	string ans;
    	while(end != start)
    	{
    		ans += cnt[end].first;
    		end = cnt[end].second;
    	}
    	reverse(ans.begin(),ans.end());
    	cout << ans << endl;
	}
	
	return 0;
}

五、双端队列广搜

如题:https://www.acwing.com/problem/content/177/

达达是来自异世界的魔女,她在漫无目的地四处漂流的时候,遇到了善良的少女翰翰,从而被收留在地球上。

翰翰的家里有一辆飞行车。

有一天飞行车的电路板突然出现了故障,导致无法启动。

电路板的整体结构是一个 R 行 C 列的网格(R,C≤500),如下图所示。

电路.png

每个格点都是电线的接点,每个格子都包含一个电子元件。

电子元件的主要部分是一个可旋转的、连接一条对角线上的两个接点的短电缆。

在旋转之后,它就可以连接另一条对角线的两个接点。

电路板左上角的接点接入直流电源,右下角的接点接入飞行车的发动装置。

达达发现因为某些元件的方向不小心发生了改变,电路板可能处于断路的状态。

她准备通过计算,旋转最少数量的元件,使电源与发动装置通过若干条短缆相连。

不过,电路的规模实在是太大了,达达并不擅长编程,希望你能够帮她解决这个问题。

注意:只能走斜向的线段,水平和竖直线段不能走。

输入格式

输入文件包含多组测试数据。

第一行包含一个整数 T,表示测试数据的数目。

对于每组测试数据,第一行包含正整数 R 和 C,表示电路板的行数和列数。

之后 R 行,每行 C 个字符,字符是"/""\"中的一个,表示标准件的方向。

输出格式

对于每组测试数据,在单独的一行输出一个正整数,表示所需的最小旋转次数。

如果无论怎样都不能使得电源和发动机之间连通,输出 NO SOLUTION

数据范围

1≤R,C≤500,
1≤T≤5

输入样例:
1
3 5
\\/\\
\\///
/\\\\
输出样例:
1
样例解释

样例的输入对应于题目描述中的情况。

只需要按照下面的方式旋转标准件,就可以使得电源和发动机之间连通。

电路2.png

#include<bits/stdc++.h>
using namespace std;
const int N = 510;
typedef pair<int,int> PII;

int n,m;
char str[N][N];
int dist[N][N];
bool vis[N][N];

int mx[4]{-1,-1,1,1},my[4]{-1,1,1,-1};
int mi[4]{-1,-1,0,0},mj[4]{-1,0,0,-1};
char cs[]{"\\/\\/"};

int bfs()
{
	memset(dist,0x3f,sizeof dist);
	memset(vis,0,sizeof vis);
	dist[0][0] = 0;
	deque<PII>q;
	q.push_back({0,0});
	
	while(q.size())
	{
		PII t = q.front();
		q.pop_front();
		
		if(vis[t.first][t.second])continue;
		vis[t.first][t.second] = 1;
		
		for(int i = 0;i < 4;i++)
		{
			int x = t.first + mx[i],y = t.second + my[i];
			if(x < 0 || y < 0 || x > n || y > m)continue;
			
			int xi = t.first + mi[i],yj = t.second + mj[i];
			int w = (str[xi][yj] != cs[i]);
			int d = dist[t.first][t.second] + w;
			if(dist[x][y] > d)
			{
				dist[x][y] = d;
				
				if(w)q.push_back({x,y});
				else q.push_front({x,y});
			}
		}
	}
	return dist[n][m];
}

int main()
{
	int t;
	cin >> t;
	while(t--)
	{
		cin >> n >> m;
		for(int i = 0;i < n;i++)scanf("%s",&str[i]);
		
		if(n + m & 1)puts("NO SOLUTION");
		else cout << bfs() << endl;
	}
	return 0;
}

六、双向广搜

在bfs中,我们通常从起点开始发散搜索,一直搜到终点为止。有时这样搜索会因为时间复杂度的缘故被卡,这时就可以使用双向广搜了,从起点和终点同时开始搜索,直到中间相遇,可以有效地降低算法的时间复杂度。

比如这题:https://www.acwing.com/problem/content/192/

#include<bits/stdc++.h>
using namespace std;
#define q_s queue<string>
#define um_si unordered_map<string,int>

string A,B;
string a[6],b[6];
int n;

int extend(q_s& q,um_si& da,um_si& db,string a[6],string b[6])
{
	int d = da[q.front()];
	while(q.size() && da[q.front()] == d)
	{
		string t = q.front();
		q.pop();
		
		for(int i = 0;i < n;i++)
			for(int j = 0;j < t.size();j++)
				if(t.substr(j,a[i].size()) == a[i])
				{
					string r = t.substr(0,j) + b[i] + t.substr(j + a[i].size());
					if(db.count(r))return da[t] + db[r] + 1;
					if(da.count(r))continue;
					da[r] = da[t] + 1;
					q.push(r);
				}
	}
	return 11;
}

int bfs()
{
	q_s qa,qb;
	um_si da,db;
	qa.push(A),qb.push(B);
	da[A] = db[B] = 0;
	
	int step = 0;
	while(qa.size() && qb.size())
	{
		int t;
		if(qa.size() < qb.size())t = extend(qa,da,db,a,b);
		else t = extend(qb,db,da,b,a);
		
		if(t <= 10)return t;
		if(++step == 10)return -1;
	}
	return -1;
}

int main()
{
	cin >> A >> B;
	while(cin >> a[n] >> b[n])n++;
	
	int t = bfs();
	if(t == -1)puts("NO ANSWER!");
	else cout << t << endl;
	return 0;
}

七、A*算法

A* 搜索算法,即A star search algorithm,简称A* 算法。 是一种在图形平面上对于多个节点的路径求出最低通过成本的算法。是对图的遍历和最佳优先搜索算法,也是对BFS的改进。其思想在于为每个状态建立启发函数,用启发函数制约搜索沿着最有效的方向行进。

定义起点** s s s** ,终点** t t t** ,从起点(初始状态)开始的距离函数 g ( x ) g(x) g(x) ,到终点(最终状态)的距离函数 h ( x ) , h ∗ ( x ) h(x),h^{*}(x) h(x),h(x) ,以及每个点的估价函数 f ( x ) = g ( x ) + h ( x ) f(x) = g(x) + h(x) f(x)=g(x)+h(x)

A*算法每次从优先队列中取出一个 ** f f f**最小的元素,然后更新相邻的状态。

如果 h ≤ h ∗ h \le h^* hh,则 A* 算法能找到最优解。

上述条件下,如果** h h h**满足三角形不等式,则 A*算法不会将重复结点加入队列。

当时 h = 0 h = 0 h=0,A*算法变为Dijkstra;当并且边权1为时变为 BFS。

如题:https://www.acwing.com/problem/content/180/

#include<bits/stdc++.h>
using namespace std;
const int N = 1010,M = 20010;
#define PII pair<int,int>
#define PIII pair<int,PII>

int n,m;
int e[M],ne[M],d[M],h[N],rh[N],idx;
int S,T,K;
int dist[N],cnt[N];

void add(int* h,int a,int b,int c)
{
	e[idx] = b,d[idx] = c,ne[idx] = h[a],h[a] = idx++;
}

void dijkstra()
{
	memset(dist,0x3f,sizeof dist);
	priority_queue<PII,vector<PII>,greater<PII>>q;
	q.push({0,T});
	dist[T] = 0;
	
	while(q.size())
	{
		auto t = q.top();
		q.pop();
		int p = t.second;
		if(cnt[p])continue;
		cnt[p] = 1;
		
		for(int i = rh[p];~i;i = ne[i])
		{
			int j = e[i];
			if(dist[j] > dist[p] + d[i])
			{
				dist[j] = dist[p] + d[i];
				q.push({dist[j],j});
			}
		}
	}
}

int a_star()
{
	priority_queue<PIII,vector<PIII>,greater<PIII>>q;
	memset(cnt,0,sizeof cnt);
	q.push({dist[S],{0,S}});
	
	while(q.size())
	{
		auto t = q.top();
		q.pop();
		
		int p = t.second.second,distance = t.second.first;
		if(cnt[p] >= K)continue;
		cnt[p]++;
		if(cnt[T] == K)return distance;
		
		for(int i = h[p];~i;i = ne[i])
		{
			int j = e[i];
			if(cnt[j] < K)
				q.push({distance + d[i] + dist[j],{distance + d[i],j}});
		}
	}
	return -1;
}

int main()
{
	memset(h,-1,sizeof h);
	memset(rh,-1,sizeof rh);
	cin >> n >> m;
	while(m--)
	{
		int a,b,c;
		scanf("%d%d%d",&a,&b,&c);
		add(h,a,b,c),add(rh,b,a,c);
	}
	scanf("%d%d%d",&S,&T,&K);
	if(S == T)K++;
	
	dijkstra();
	
	cout << a_star() << endl;
	return 0;
}

八、DFS之连通性模型

在图中判断一个连通块相关的问题就是利用DFS进行处理,比如两点是否处于同一连通块的问题就可以使用该算法进行判断,但是该算法无法求出两点之间的最短距离。

**模板:**就是基础的DFS,根据题目进行修改。

如题:https://www.acwing.com/problem/content/1114/

一天Extense在森林里探险的时候不小心走入了一个迷宫,迷宫可以看成是由 n∗n 的格点组成,每个格点只有2种状态,.#,前者表示可以通行后者表示不能通行。

同时当Extense处在某个格点时,他只能移动到东南西北(或者说上下左右)四个方向之一的相邻格点上,Extense想要从点A走到点B,问在不走出迷宫的情况下能不能办到。

如果起点或者终点有一个不能通行(为#),则看成无法办到。

注意:A、B不一定是两个不同的点。

输入格式

第1行是测试数据的组数 k,后面跟着 k 组输入。

每组测试数据的第1行是一个正整数 n,表示迷宫的规模是 n∗n 的。

接下来是一个 n∗n 的矩阵,矩阵中的元素为.或者#

再接下来一行是 4 个整数 ha,la,hb,lb,描述 AA 处在第 ha 行, 第 la 列,B 处在第 hb 行, 第 lb 列。

注意到 ha,la,hb,lb 全部是从 0 开始计数的。

输出格式

k行,每行输出对应一个输入。

能办到则输出“YES”,否则输出“NO”。

数据范围

1≤n≤100

输入样例:
2
3
.##
..#
#..
0 0 2 2
5
.....
###.#
..#..
###..
...#.
0 0 4 0
输出样例:
YES
NO

就不多说了:

#include<bits/stdc++.h>
using namespace std;
const int N = 105;

int t,n;
bool vis[N][N];
int sx,sy,ex,ey;
char st[N][N];
int mx[4]{0,-1,0,1},my[4]{-1,0,1,0};

bool dfs(int x,int y)
{
    if(st[x][y] == '#')return 0;
	if(x == ex && y == ey)return 1;
	
	vis[x][y] = 1;
	for(int i = 0;i < 4;i++)
	{
		int a = x + mx[i],b = y + my[i];
		
		if(a < 0 || a >= n || b < 0 || b >= n)continue;
		if(vis[a][b])continue;
		if(st[a][b] == '#')continue;
		
		if(dfs(a,b))return 1;
	}
	
	return 0;
}

int main()
{
	scanf("%d",&t);
	while(t--)
	{
		memset(vis,0,sizeof vis);
		scanf("%d",&n);
		for(int i = 0;i < n;i++)
			cin >> st[i];
		
		scanf("%d%d%d%d",&sx,&sy,&ex,&ey);
		if(dfs(sx,sy))puts("YES");
		else puts("NO");
	}
	return 0;
}

九、DFS之搜索算法

该算法就是将DFS与搜索算法结合,没什么好说的,看看例题理解即可:

例题:https://www.acwing.com/problem/content/1118/

马在中国象棋以日字形规则移动。

请编写一段程序,给定 n∗m 大小的棋盘,以及马的初始位置 (x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。

输入格式

第一行为整数 TT,表示测试数据组数。

每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标 n,m,x,y。

输出格式

每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,若无法遍历棋盘上的所有点则输出 0。

数据范围

1≤T≤9,
1≤m,n≤9,
0≤x≤n−1,
0≤y≤m−1

输入样例:

1
5 4 0 0

输出样例:

32

#include<bits/stdc++.h>
using namespace std;
const int N = 10;

int t,n,m;
bool vis[N][N];
int mx[8]{-1,-2,-2,-1,1,2,2,1},my[8]{-2,-1,1,2,2,1,-1,-2};
int all_num,ans;

void dfs(int x,int y,int num)
{
	if(num == all_num)
	{
		ans++;
		return ;
	}
	vis[x][y] = 1;
	for(int i = 0;i < 8;i++)
	{
		int a = x + mx[i],b = y + my[i];
		if(a < 0 || a >= n || b < 0 || b >= m)continue;
		if(vis[a][b])continue;
		dfs(a,b,num + 1);
	}
	vis[x][y] = 0;
}

int main()
{
	cin >> t;
	while(t--)
	{
	    int x,y;
	    memset(vis,0,sizeof vis);
	    ans = 0;
		cin >> n >> m >> x >> y;
		all_num = n * m;
		dfs(x,y,1);
		cout << ans << endl;
	}
	return 0;
}

十、DFS之剪枝与优化

在DFS的基础上对不符合要求的搜索路径提前结束搜索,以达到提高DFS搜索的效率。

如题:https://www.acwing.com/problem/content/167/

翰翰和达达饲养了 N 只小猫,这天,小猫们要去爬山。

经历了千辛万苦,小猫们终于爬上了山顶,但是疲倦的它们再也不想徒步走下山了(呜咕>_<)。

翰翰和达达只好花钱让它们坐索道下山。

索道上的缆车最大承重量为 W,而 N 只小猫的重量分别是C1、C2……CN。

当然,每辆缆车上的小猫的重量之和不能超过 W。

每租用一辆缆车,翰翰和达达就要付 1 美元,所以他们想知道,最少需要付多少美元才能把这 N 只小猫都运送下山?

输入格式

第 1 行:包含两个用空格隔开的整数,N 和 W。

第 2…N+1 行:每行一个整数,其中第 i+1 行的整数表示第 i 只小猫的重量 Ci。

输出格式

输出一个整数,表示最少需要多少美元,也就是最少需要多少辆缆车。

数据范围

1≤N≤18,

1≤Ci≤W≤108

输入样例:
5 1996
1
2
1994
12
29
输出样例:
2
#include<bits/stdc++.h>
using namespace std;
const int N = 20;

int n,m;
int cat[N],num[N];
int ans;

bool cmp(int a,int b)
{
	return a > b;
}

void dfs(int u,int k)
{
	if(k >= ans)return;//对该路径剪枝结束搜索
	if(u == n)
	{
		ans = k;
		return;
	}
	
	for(int i = 0;i < k;i++)
	{
		if(cat[u] + num[i] <= m)
		{
			num[i] += cat[u];
			dfs(u + 1,k);
			num[i] -= cat[u];
		}
	}
	
	num[k] = cat[u];
	dfs(u + 1,k + 1);
	num[k] = 0;
 }

int main()
{
	cin >> n >> m;
	for(int i = 0;i < n;i++)
		cin >> cat[i];
		
	sort(cat,cat + n,cmp);
		
	dfs(1,0);
	
	cout << ans << endl;
	return 0;
}

十一、迭代加深

迭代加深搜索是对dfs搜索的一种优化,通常的dfs搜索都会搜索到每条路径的末尾才会回溯,但是迭代加深搜索时会规定一个深度,每一条路径搜索到该深度时就会回溯,如果该深度不能搜索到答案,则将深度加深。

如题:https://www.acwing.com/problem/content/172/

满足如下条件的序列 XX(序列中元素被标号为 1、2、3…m)被称为“加成序列”:

  1. X[1]=1
  2. X[m]=n
  3. X[1]<X[2]<…<X[m−1]<X[m]
  4. 对于每个 k(2≤k≤m)都存在两个整数 i 和 j (1≤i,j≤k−1,i 和 j 可相等),使得 X[k]=X[i]+X[j]。

你的任务是:给定一个整数 n,找出符合上述条件的长度 m 最小的“加成序列”。

如果有多个满足要求的答案,只需要找出任意一个可行解。

输入格式

输入包含多组测试用例。

每组测试用例占据一行,包含一个整数 n。

当输入为单行的 0 时,表示输入结束。

输出格式

对于每个测试用例,输出一个满足需求的整数序列,数字之间用空格隔开。

每个输出占一行。

数据范围

1≤n≤100

输入样例:
5
7
12
15
77
0
输出样例:
1 2 4 5
1 2 4 6 7
1 2 4 8 12
1 2 4 5 10 15
1 2 4 8 9 17 34 68 77
#include<bits/stdc++.h>
using namespace std;
const int N = 110;

int n;
int path[N];

bool dfs(int u,int deep)
{
	if(u == deep)return path[u - 1] == n;
	
	bool vis[N] = {0};
	
	for(int i = u - 1;~i;i--) 
		for(int j = i;~j;j--)
		{
			int t = path[i] + path[j];
			if(t <= path[u - 1] || t > n || vis[t])continue;
			vis[t] = 1;
			path[u] = t;
			if(dfs(u + 1,deep)) return 1;
		}
	
	return 0;
}

int main()
{
	path[0] = 1;
	while(cin >> n,n)
	{
		int deep = 1;
		while(!dfs(1,deep))deep++;
		
		for(int i = 0;i < deep;i++)cout << path[i] << ' ';
		cout << endl;
	}
	return 0;
}

十二、双向DFS

双向DFS,先搜索一半的空间,然后在另外一半中搜索答案。

如题:https://www.acwing.com/problem/content/173/

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

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

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

输入格式

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

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

输出格式

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

数据范围

1≤N≤46,

1≤W,G[i]≤2A31−1

输入样例:
20 5
7
5
4
18
1
输出样例:
19
#include<bits/stdc++.h>
using namespace std;
const int N = 1 << 24;
typedef long long ll;

int n,w;
int g[50];
int weight[N],cnt;
int deep,ans;

bool mape(int a,int b)
{
    return a > b;
}

void dfs_1(int u,int s)
{
    if(u == deep)
    {
        weight[cnt++] = s;
        return;
    }
    
    if((ll)s + g[u] <= w)dfs_1(u + 1,s + g[u]);
    dfs_1(u + 1,s);
}

void dfs_2(int u,int s)
{
    if(u == n)
    {
        int l = 0,r = cnt - 1;
        while(l < r)
        {
            int mid = l + r + 1 >> 1;
            if((ll)s + weight[mid] <= w)l = mid;
            else r = mid - 1;
        }
        if((ll)s + weight[l] <= w)ans = max(ans,s + weight[l]);
        return;
    }
    
    if((ll)s + g[u] <= w)dfs_2(u + 1,s + g[u]);
    dfs_2(u + 1,s);
}

int main()
{
    cin >> w >> n;
    for(int i = 0;i < n;i++)
        cin >> g[i];
   	sort(g,g + n,mape);
   
    deep = n >> 1;
    dfs_1(0,0);
    
    sort(weight,weight + cnt);
    int t = 1;
    for(int i = 1;i < cnt;i++)
        if(weight[i] != weight[i - 1])
            weight[t++] = weight[i];
    cnt = t;
    
    dfs_2(deep,0);
    
    cout << ans << endl;
    return 0;
}

十三、IDA*

IDA*就是迭代加深和A *算法的结合。

如题:https://www.acwing.com/problem/content/182/

给定 n 本书,编号为 1∼n。

在初始状态下,书是任意排列的。

在每一次操作中,可以抽取其中连续的一段,再把这段插入到其他某个位置。

我们的目标状态是把书按照 1∼n 的顺序依次排列。

求最少需要多少次操作。

输入格式

第一行包含整数 T,表示共有 T 组测试数据。

每组数据包含两行,第一行为整数 n,表示书的数量。

第二行为 n 个整数,表示 1∼n 的一种任意排列。

同行数之间用空格隔开。

输出格式

每组数据输出一个最少操作次数。

如果最少操作次数大于或等于 5 次,则输出 5 or more

每个结果占一行。

数据范围

1≤n≤15

输入样例:
3
6
1 3 4 6 2 5
5
5 4 3 2 1
10
6 8 5 3 4 7 2 9 1 10
输出样例:
2
3
5 or more
#include<bits/stdc++.h>
using namespace std;
const int N = 20;

int t,n;
int id[N];
int num[5][N];

int f()
{
	int res = 0;
	for(int i = 0;i + 1< n;i++)
		if(id[i + 1] != id[i] + 1)res++;
	return (res + 2) / 3;
}

bool check()
{
	for(int i = 0;i + 1 < n;i++)
		if(id[i + 1] != id[i] + 1)
			return 0;
	return 1;
}

bool ida(int u,int deep)
{
    if(u + f() > deep)return 0;
	if(check())return 1;
    
    for(int len = 1;len < n;len++)
		for(int l = 0;l + len - 1 < n;l++)
		{
			int r = l + len - 1;
			
			for(int i = r + 1;i < n;i++)
			{
				memcpy(num[u],id,sizeof id);
				
				int x,y;
				for(x = r + 1,y = l;x <= i;x++,y++)
					id[y] = id[x];
				for(x = l;x <= r;x++,y++)id[y] = num[u][x];
				if(ida(u + 1,deep))return 1;
				
				memcpy(id,num[u],sizeof id);
			}
		}
    return 0;
}
int main()
{
    cin >> t;
    while(t--)
    {
        cin >> n;
        for(int i = 0;i < n;i++)
            cin >> id[i];
        
        int deep = 0;
        while(deep < 5 && !ida(0,deep))deep++;
        
        if(deep >= 5)puts("5 or more");
        else cout << deep << endl;
    }
    return 0;
}

的一段,再把这段插入到其他某个位置。

我们的目标状态是把书按照 1∼n 的顺序依次排列。

求最少需要多少次操作。

输入格式

第一行包含整数 T,表示共有 T 组测试数据。

每组数据包含两行,第一行为整数 n,表示书的数量。

第二行为 n 个整数,表示 1∼n 的一种任意排列。

同行数之间用空格隔开。

输出格式

每组数据输出一个最少操作次数。

如果最少操作次数大于或等于 5 次,则输出 5 or more

每个结果占一行。

数据范围

1≤n≤15

输入样例:
3
6
1 3 4 6 2 5
5
5 4 3 2 1
10
6 8 5 3 4 7 2 9 1 10
输出样例:
2
3
5 or more
#include<bits/stdc++.h>
using namespace std;
const int N = 20;

int t,n;
int id[N];
int num[5][N];

int f()
{
	int res = 0;
	for(int i = 0;i + 1< n;i++)
		if(id[i + 1] != id[i] + 1)res++;
	return (res + 2) / 3;
}

bool check()
{
	for(int i = 0;i + 1 < n;i++)
		if(id[i + 1] != id[i] + 1)
			return 0;
	return 1;
}

bool ida(int u,int deep)
{
    if(u + f() > deep)return 0;
	if(check())return 1;
    
    for(int len = 1;len < n;len++)
		for(int l = 0;l + len - 1 < n;l++)
		{
			int r = l + len - 1;
			
			for(int i = r + 1;i < n;i++)
			{
				memcpy(num[u],id,sizeof id);
				
				int x,y;
				for(x = r + 1,y = l;x <= i;x++,y++)
					id[y] = id[x];
				for(x = l;x <= r;x++,y++)id[y] = num[u][x];
				if(ida(u + 1,deep))return 1;
				
				memcpy(id,num[u],sizeof id);
			}
		}
    return 0;
}
int main()
{
    cin >> t;
    while(t--)
    {
        cin >> n;
        for(int i = 0;i < n;i++)
            cin >> id[i];
        
        int deep = 0;
        while(deep < 5 && !ida(0,deep))deep++;
        
        if(deep >= 5)puts("5 or more");
        else cout << deep << endl;
    }
    return 0;
}
;