一.前言
说起搜索,想必大家都耳熟能详了吧。爆搜就不说了。这里重点说一下,搜索题目的千变万化。万变不离其中
二.热身运动:多个搜索的配合使用
何为多个搜索的配合使用呢?也就是一道题目不仅需要一次搜索,而是多次搜索的配合使用,这就要看你的技术了,不仅是搜索的技术,还有识题的技术。
1.例题:路径规划(path)
题目描述
有n个点,m条无向边,有A,B两个人,初始时刻A在点1,B在点2,他们要走到点n去。A每走一条边,要消耗B单位能量,B每走一条边,要消耗E单位能量。如果A,B相伴走,则只消耗P单位的能量。请问A,B走到点n,最少要消耗多少能量?
输入数据保证1和n,2和n连通。
输入
第一行包含整数B,E,P,N和M,所有的整数都不超过40000,N>=3.
接下来M行,每行两个整数,表示该无向边连接的两个顶点。
输出
一个整数,表示最小要消耗的能量。
样例输入
4 4 5 8 8
1 4
2 3
3 4
4 7
2 5
5 6
6 8
7 8
样例输出
22
2.解题思路
看了这道题,大家是不是很懵?但是既然我的主题都已经给出来了:多个搜索的配合使用。那么,这道题一定就要用到多次搜索呀!
首先,我们来分析题目:有A,B两个人,如果他们两个相遇了,说明他们两个下一步的计划就是一起从这个相遇的点走到终点,因为这个点到终点的最短路这有一条,并且在这条路上他们的花费体力是P。好了,我们现在何不枚举这个相遇的点呢?假设两人到i相遇,最终的总最短路就是A从点1到i的最短路,加上B从2到i的最短路,再加上i到n的最短路,耗废的体力值就直接算就行了。我们现在的任务就是让这三条最短路加起来耗废的体力值最小,就需要把每个点都枚举一遍,答案就出来了。至于最短路的计算,就直接3次BFS搜索,记录从三个起点(1,2,n)到每一个点的最短路径就行了呀!
哈哈,怎么样,这就是多次搜索的灵活配合题。
3.代码
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
#define M 40005
#define INF 0x3f3f3f3f
#define min(a, b) a < b ? a : b
int B, E, P, n, m, t, dis[5][M], ans = INF;
bool flag[M];
vector <int> G[M];
inline void bfs (int start){
t ++;
memset (flag, 0, sizeof(flag));
memset (dis[t], INF, sizeof(dis[t]));
int f1, f2;
queue <int> que;
f1 = start;
dis[t][f1] = 0;
que.push(f1);
while (!que.empty()){
f1 = que.front();
que.pop();
int siz = G[f1].size();
for (register int i = 0; i < siz; i ++){
f2 = G[f1][i];
if (dis[t][f1] + 1 < dis[t][f2]){
dis[t][f2] = dis[t][f1] + 1;
if (!flag[f2]){
flag[f2] = 1;
que.push(f2);
}
}
}
}
}
int main (){
scanf ("%d %d %d %d %d", &B, &E, &P, &n, &m);
int v, u;
for (int i = 1; i <= m; i ++){
scanf ("%d %d", &v, &u);
G[u].push_back(v);
G[v].push_back(u);
}
bfs (1);//三次BFS找最短路径
bfs (2);
bfs (n);
for (int i = 1; i <= n; i ++){
ans = min (ans, dis[1][i] * B + dis[2][i] * E + dis[3][i] * P);//枚举每一个点,找最小值
}
printf ("%d\n", ans);
return 0;
}
这只是热身运动,下面还有更难的:
三.小李飞刀:可不仅是搜索
学搜索不要学的太死板了,往往搜索的题目它并不单独出现,而是需要其他的算法与搜索算法联合起来才能解决。
1.例题:滑雪场的高度差
题目描述
滑雪场可以看成M x N的网格状山地(1 <= M,N <= 500),每个网格是一个近似的平面,具有水平高度值在0 .. 1,000,000,000米的范围内。
某些网格被指定为关键网格。当两个相邻网格之间的高度差的绝对值不超过某个参数D时,就可以相互到达。相邻关系是指某个格子的东、西、南、北的格子。
显然,当D不断减小时,原本可以相互到达的相邻格子就不能到达了。
滑雪赛的组委会想知道,为了保证各个关键网格之间彼此连通,最小的D是多少?
输入
第1行:2个整数M和N
接下来M行,每行N个整数,表示各网格的高度
接下来M行,每行N个0或者1,1表示关键网格
输出
第1行:1个整数,表示最小的D
样例输入
3 5
20 21 18 99 5
19 22 20 16 26
18 17 40 60 80
1 0 0 0 1
0 0 0 0 0
0 0 0 0 1
样例输出
21
2.解题思路
这道题目,有的人一拿到题目就说爆搜,从一个关键点开始,向周围BFS,直到搜索到其他所有的关键点为止。但这些点可是都有权值(高度)的!你能保证你每次搜到的点,你现在记录的到它需要付出的代价就最小吗?这可不是走迷宫。
当然,你可以用最小生成树,但我在这里就要用BFS把它做出来。
从题目中可以知道,最后付出的代价是个定值,而且我还可以知道这个定值的大小范围,就是0~最高的点。那么,为何不用二分的方法来确定这个代价呢?除此之外,我们还知道,二分中一定有判断mid是否合理的标准,那么这个标准是什么呢?想必大家都想到了:就是付出这个代价是否能从每一个关键点到另外其他所有的关键点。这个任务就可以交给BFS来完成了。我在具体说一下BFS的内部结构:先一波常规操作,主要在判断是否可以到达下一个点上,需要把这个点的高度与下一个点的高度作差,如果在指定付出代价的范围内,就可以到下一个点。
还不理解?请看下图:
3.代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
#define M 505
#define INF 0x3f3f3f3f
#define max(a, b) a > b ? a : b
int n, m, v[M][M], t[M][M], ii, jj, tmp, maxn;
int dir[4][2] = {{1, 0}, {0, 1}, {-1, 0}, {0, -1}};
bool flag[M][M];
struct node {
int x, y;
};
inline void Read (int &x){
int f = 1; x = 0; char c = getchar();
while (c < '0' || c > '9') {if (c == '-') f = -1; c = getchar();}
while (c >= '0' && c <= '9') {x = x * 10 + c - 48; c = getchar();}
x *= f;
}
inline bool pd (int tox, int toy){
if (tox > n || tox < 1 || toy > m || toy < 1)
return 0;
return 1;
}
inline int fabs (int a){
if (a < 0)
return -a;
return a;
}
inline bool bfs (int high){
int sum = 1;
queue <node> f;
node p1, p2;
p1.x = ii;
p1.y = jj;
for (register int i = 1; i <= n; i ++)
for (register int j = 1; j <= m; j ++)
flag[i][j] = 0;
flag[ii][jj] = 1;
f.push(p1);
while (!f.empty()){
p1 = f.front();
f.pop();
for (int i = 0; i <= 3; i ++){
int tox = p1.x + dir[i][0];
int toy = p1.y + dir[i][1];
if (pd(tox, toy) && fabs(v[tox][toy] - v[p1.x][p1.y]) <= high && !flag[tox][toy]){//小于代价才符合要求
if (t[tox][toy])
sum ++;
flag[tox][toy] = 1;
p2.x = tox;
p2.y = toy;
f.push(p2);
}
}
}
if (sum >= tmp)
return 1;
return 0;
}
int main (){
Read (n);
Read (m);
for (register int i = 1; i <= n; i ++)
for (register int j = 1; j <= m; j ++){
Read (v[i][j]);
maxn = max (maxn, v[i][j]);
}
for (register int i = 1; i <= n; i ++)
for (register int j = 1; j <= m; j ++){
Read (t[i][j]);
if (t[i][j])
ii = i, jj = j, tmp ++;
}
int l = 0, r = maxn, mid;
while (l < r){//二分确定付出的最小代价
int mid = (l + r) / 2;
if (bfs (mid))//BFS判断
r = mid;
else
l = mid + 1;
}
printf ("%d\n", r);
return 0;
}
这道题就把你难住啦?别慌,还有更难得呢:
三.登峰造极:难以实践的最小生成树
图论搜索有时特别好识别出来,但是你能把你想的方法用代码实践出来吗?
1.题目:篱笆
题目描述
农夫FJ的奶牛们有空旷恐惧症,所以,FJ打算在他的农场围上篱笆。他的农场是一个矩形区域。左上角的坐标是(0,0),右下角的坐标是(A,B),FJ修建了n(0<=n<=2000)个竖直的篱笆,其横坐标分别为a1,a2,a3,……,an,其中0<ai<A,,每一个篱笆从(ai,0)到(ai,B)也修建了m个水平的篱笆,其纵坐标为b1,b2,b3,……,bm,其中0<bi<B,每一个篱笆从(0,bi)到(A,bi)。这些篱笆把整个农场分成(n+1)*(m+1)个区域。
不幸的是FJ忘了在篱笆上装门了。这导致奶牛无法在不同的区域之间移动。于是他决定将某些篱笆拆掉。现在要使得所有的区域都联通,请问最少要拆掉多长的篱笆。
比如下面这个篱笆
+---+--+
| | |
+---+--+
| | |
| | |
+---+--+
可以这么拆:
+---+--+
| |
+---+ +
| |
| |
+---+--+
输入
第一题包含四个数A,B,n,m。(0<=A,B<=1000000000).
接下来有两行,第二行n个数,表示a1,a2,……,an,表示竖直的n个篱笆的横坐标,第三行m个数,表示b1,b2,b3,……,bm,表示m个水平的篱笆的纵坐标。
输出
最少要拆除的篱笆的总长度。结果可能超过int。请用long long int。
样例输入
15 15 5 2
2
5
10
6
4
11
3
样例输出
44
2.解题思路
大家都看出来了吗?这就是一道最小生成树用kruskal。如何一个最小生成树呢?从题目中给出的图中可以看出,整个图被分成很多个小格,而且每个小格之间都有篱笆在阻隔,还知道这每一条篱笆的长度。说到这里,题目就已经很清楚了,何不把每个格子看成每一个点,每一条栅栏就是边呢?然后再把整个图如此抽象出来,就是一个连通图呀!再把每个点对应编号,如下图:
连通图:
哇,好复杂,是不是?但大家不要忘了其本质:最小生成树。
好,现在我们有了思路,再来谈一些具体的东西吧。kruscal算法的两大操作,就是:①对每一条边进行排序;②运用并查集找出最短路。在这里,我们不用担心并差集的问题,它是一定不会超时的,但是对每一条边进行排序就是一个重点了。我们来算一下,此题最多有多少条边:,是多少?八百万条边呀!你能保证你的排序不超时?所以,此题还要引进离散化。大家不难从我的第一个图中发现规律,对于相同的一行或一列,边的长度都相等。所以每一行、每一列我们只需拿出一条代表边出来与其他边进行排序就行了。如此,我们就可把整个图变化一下:
现在,我们来讲一下kruscal的内部操作:横边和竖边分别设两个变量,表示第几条横边(比如x和y),每次取出最小的一条边,分两种情况:①取出的是横边,那么代有这条边的第一个格子的编号就是,至于原因,大家可以从上图找出规律;②取出的是竖边,那么代表这条边的第一个格子的编号就是y。取出之后,就遍历这一行或这一列,把没有连通的格子连通起来。最后的花费总和直接输出就行了。
3.代码
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
#define M 2005
#define INF 0x3f3f3f3f
int A, B, n, m, a[M], b[M], fa[M * M];
long long ans, lena[M], lenb[M];
inline void MakeSet (int x){//并查集基本操作
for (int i = 1; i <= x; i ++)
fa[i] = i;
}
inline int FindSet (int x){
if (fa[x] != x)
fa[x] = FindSet (fa[x]);
return fa[x];
}
inline void UnionSet (int x, int y, int value){
int u = FindSet (x), v = FindSet (y);
if (u != v){
fa[u] = v;
ans += value;
}
}
int main (){
scanf ("%d %d %d %d", &A, &B, &n, &m);
for (int i = 1; i <= n; i ++)
scanf ("%d", &a[i]);
for (int i = 1; i <= m; i ++)
scanf ("%d", &b[i]);
sort (a + 1, a + 1 + n);
sort (b + 1, b + 1 + m);
a[++ n] = A;
b[++ m] = B;
for (int i = 1; i <= n; i ++)
lena[i] = a[i] - a[i - 1];
for (int i = 1; i <= m; i ++)
lenb[i] = b[i] - b[i - 1];
sort (lena + 1, lena + 1 + n);
sort (lenb + 1, lenb + 1 + m);
MakeSet (n * m);
int x = 1, y = 1;
while (x <= n || y <= m){
if (lena[x] < lenb[y] && x <= n){
int k = (x - 1) * m + 1;//求出第一个格子的编号
for (int i = 1; i < m; i ++){
UnionSet (k, k + 1, lena[x]);
k ++;
}
x ++;
}
else{
int k = y;
for (int i = 1; i < n; i ++){
UnionSet (k, k + m, lenb[y]);
k += m;
}
y ++;
}
}
printf ("%lld\n", ans);
return 0;
}
四.总结
通过这几道题,让我深刻认识到了搜索的灵活性,爆搜并没有这么简单!这有灵活应用所有所学算法,融为一起,才能登峰造极呀!