拖拉机

拖拉机

干了一整天的活,农夫约翰完全忘记了他把拖拉机落在田地中央了。

他的奶牛非常调皮,决定对约翰来场恶作剧。

她们在田地的不同地方放了$N$捆干草,这样一来,约翰想要开走拖拉机就必须先移除一些干草捆。

拖拉机的位置以及$N$捆干草的位置都是二维平面上的整数坐标点。

拖拉机的初始位置上没有干草捆。

当约翰驾驶拖拉机时,他只能沿平行于坐标轴的方向(北,南,东和西)移动拖拉机,并且拖拉机必须每次移动整数距离。

例如,驾驶拖拉机先向北移动$2$单位长度,然后向东移动$3$单位长度。

拖拉机无法移动到干草捆占据的位置。

请帮助约翰确定他需要移除的干草捆的最小数量,以便他能够将拖拉机开到二维平面的原点。

输入格式

第一行包含三个整数:$N$以及拖拉机的初始位置$\left( x,y \right)$

接下来$N$行,每行包含一个干草捆的位置坐标$\left( x,y \right)$

输出格式

输出约翰需要移除的干草捆的最小数量。

数据范围

$1 \leq N \leq 50000$,

$1 \leq x,y \leq 1000$

输入样例:

1 7 6 3
2 6 2
3 5 2
4 4 3
5 2 1
6 7 3
7 5 4
8 6 4

输出样例:

1

 

解题思路

  首先先审题,题目给出的干草捆的坐标范围虽然是$1 \sim 1000$,但并不代表整个图的被限制在$1000 \times 1000$的矩阵中,整个图其实是无限大的,点还可以走到$1001$以外的地方。类似于这样的效果。

  但事实上我们不可能去搜索一个无限大的图。因为障碍物的坐标范围为$1 \leq x,y \leq 1000$,因此我们可以给$1000 \times 1000$的矩阵扩大一圈(扩大的一圈没有障碍物),$0 \sim 1001$,也就是只用搜$1001 \times 1001$的矩阵就可以了。

  接着我们抽象出一个图的模型。首先,每一个格子就是一个点。边的话,就是每个格子上下左右四个方向,相邻的格子可以看作是有边相连的。然后不同的是这题要的并不是边权,而是点权(其实两者差不了多少)。如果一个格子是障碍物的话,那么这个格子的点权是$1$,否则是$0$。

  然后我们可以把原问题转化为一个求最短路的问题。

  首先要证明这两个问题是等价的。对于任意一个从起点到终点的走法,原问题中移走障碍物的一种走法就对于一条路径,并且移走障碍物的数量就是这条路径的权值和。所以原问题的每一个方案就对应一条从起点到终点的路径,反过来每一条路径对应原问题的一个方案,而且路径长度就是移走障碍物的数量。所以要求原问题的最优解,等价于求最短路。

  当时写这题的话完全没有这种抽象的意识,完全没有去这么想,所以写不出来。

  然后我们发现,每条边的权重只有$0$和$1$。如果全部边的权重为$1$,我们可以用bfs求最短路。而只有$0$和$1$的话就用双端队列广搜。

  其实双端队列广搜本质就是$dijkstra$算法,不过由于边权只有$0$和$1$的特殊性,把堆变成了双端队列,时间复杂度为$O \left( n \right)$。

  原理是,从一个点扩展的时候另一个点$x$的时候,如果边权为$0$,则把$x$插队头;如果边权为$1$,则把$x$插队尾。

  下面证明为什么可以用双端队列代替堆。

  首先是证明双端队列中所有的点到起点的距离大小最多只有两种,而且如果有两种的话他们的大小只相差$1$。比如,如果有两种不同的距离,那么双端队列前部分的点到起点的距离都是$l$,后部分的点到起点的距离都是$l+1$。如果只有一种距离,那么双端队列中的点到起点的距离都是$l$。

 

  用归纳法。初始时,双端队列只有一个起点,显然成立。假设当双端队列的大小为$n-1$时成立,证明当双端队列的大小为$n$时也成立。对于任意一个大小为$n$的双端队列,因为我们每次只能往队列插入一个元素,所以它是由大小为$n-1$的双端队列扩展出来的。因为对于长度为$n-1$的双端队列条件是成立的,所以到起点的距离大小不同的点最多只有两段。然后我们每次扩展都是从队头取出元素的,若从队头取出的元素到起点的距离为$l$,扩展的结果只有两种:一种是边权为0,扩展的结果就为$l+0=l$,插入队头;另一种是边权为$1$,扩展的结果就为$l+1$,插入队尾。插完后我们会发现,双端队列中还是最多只有两种不同的距离。前面是队列中有两种不同距离的证明,只有一种距离的证明同理。

  所以可以证明对于任何大小的双端队列,队列中所有的点到起点的距离大小最多只有两种,而且前一段的点到起点的距离为$l$,后一段的点到起点的距离为$l+1$

  也就意味着,任何时刻队列中所有的点,到起点的距离是升序的,所以队头的点到起点的距离一定是最小值,所以可以起到堆的效果。

  代码的写法和$dijkstra$的很像,只不过这里把优先队列改成了双端队列,边是根据曼哈顿距离来选择的。

  AC代码如下:

 1 #include <cstdio>
 2 #include <cstring>
 3 #include <deque>
 4 #include <algorithm>
 5 using namespace std;
 6 
 7 typedef pair<int, int> PII;
 8 
 9 const int N = 1010;
10 
11 int graph[N][N], dist[N][N];
12 bool vis[N][N];
13 
14 int bfs(int sx, int sy) {
15     deque<PII> dq;
16     dq.push_back({sx, sy});
17     memset(dist, 0x3f, sizeof(dist));
18     dist[sx][sy] = 0;
19     
20     int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
21     
22     while (!dq.empty()) {
23         PII t = dq.front();
24         dq.pop_front();
25         
26         if (vis[t.first][t.second]) continue;
27         vis[t.first][t.second] = true;
28         if (vis[0][0]) break;
29         
30         for (int i = 0; i < 4; i++) {
31             int x = t.first + dx[i], y = t.second + dy[i];
32             
33             // 点要满足在合法的位置,而且距离变小(入队不需要判重)
34             if (x >= 0 && x < N && y >= 0 && y < N && dist[x][y] > dist[t.first][t.second] + graph[x][y]) {
35                 dist[x][y] = dist[t.first][t.second] + graph[x][y];
36                 if (graph[x][y]) dq.push_back({x, y});  // 边权为0加入队头
37                 else dq.push_front({x, y});             // 边权为1加入队尾
38             }
39         }
40     }
41     
42     return dist[0][0];
43 }
44 
45 int main() {
46     int n, sx, sy;
47     scanf("%d %d %d", &n, &sx, &sy);
48     while (n--) {
49         int x, y;
50         scanf("%d %d", &x, &y);
51         graph[x][y] = 1;
52     }
53     
54     printf("%d", bfs(sx, sy));
55     
56     return 0;
57 }

 

参考资料

  AcWing 2019. 拖拉机(寒假每日一题2022):https://www.acwing.com/video/3656/

posted @ 2022-02-09 16:38  onlyblues  阅读(48)  评论(0编辑  收藏  举报
Web Analytics