千变万化搜索

一.前言

说起搜索,想必大家都耳熟能详了吧。爆搜就不说了。这里重点说一下,搜索题目的千变万化。万变不离其中

二.热身运动:多个搜索的配合使用

何为多个搜索的配合使用呢?也就是一道题目不仅需要一次搜索,而是多次搜索的配合使用,这就要看你的技术了,不仅是搜索的技术,还有识题的技术。

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

样例输出

44

 2.解题思路

大家都看出来了吗?这就是一道最小生成树用kruskal。如何一个最小生成树呢?从题目中给出的图中可以看出,整个图被分成很多个小格,而且每个小格之间都有篱笆在阻隔,还知道这每一条篱笆的长度。说到这里,题目就已经很清楚了,何不把每个格子看成每一个点,每一条栅栏就是边呢?然后再把整个图如此抽象出来,就是一个连通图呀!再把每个点对应编号,如下图:

 连通图:

哇,好复杂,是不是?但大家不要忘了其本质:最小生成树。

好,现在我们有了思路,再来谈一些具体的东西吧。kruscal算法的两大操作,就是:①对每一条边进行排序;②运用并查集找出最短路。在这里,我们不用担心并差集的问题,它是一定不会超时的,但是对每一条边进行排序就是一个重点了。我们来算一下,此题最多有多少条边:n*m* 2,是多少?八百万条边呀!你能保证你的排序不超时?所以,此题还要引进离散化。大家不难从我的第一个图中发现规律,对于相同的一行或一列,边的长度都相等。所以每一行、每一列我们只需拿出一条代表边出来与其他边进行排序就行了。如此,我们就可把整个图变化一下:

现在,我们来讲一下kruscal的内部操作:横边和竖边分别设两个变量,表示第几条横边(比如x和y),每次取出最小的一条边,分两种情况:①取出的是横边,那么代有这条边的第一个格子的编号就是(x-1)*m+1,至于原因,大家可以从上图找出规律;②取出的是竖边,那么代表这条边的第一个格子的编号就是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;
}

四.总结

通过这几道题,让我深刻认识到了搜索的灵活性,爆搜并没有这么简单!这有灵活应用所有所学算法,融为一起,才能登峰造极呀!

猜你喜欢

转载自blog.csdn.net/weixin_43908980/article/details/85320513