HDU6992. Lawn of the Dead
题目链接:HDU6992. Lawn of the Dead
题意:
有一天,一个僵尸来到了“Lawn of the Dead”草坪,这个草坪可以看成\(n\times m\)的网格。一开始,他站在左上角的格子里,即\((1,1)\)。
他一次只能从\((i,j)\)向下走一步到\((i+1,j)\)或者向右走一步到\((i,j+1)\)。
网格中有\(k\)个雷,第\(i\)个雷在\((x_i,y_i)\)上。为了防止被炸到,他不能走到有雷的那个格子当中。
问:有多少个格子他能到达?(包括起始的格子)
数据范围:\(2\leq n,m,k\leq 10^5\)
分析:
由于僵尸只能向下向右走,所以走\(i\)步可以走到\(x+y=i+2\)这条直线上。故而可以考虑将网格依据步数分层。为了防止常数项对我们分析产生困扰,我们直接认为\((x,y)\)在第\(x+y\)层上,在相同的层上,所需的步数是相同的。
假如我们知道第\(i\)层所有点的可达情况,可以知道第\(i+1\)层的可达情况。如果这个过程我们能快速进行,那么这道题也基本上解决了。
我们来仔细研究一下转移的过程。
为了方便描述和写代码,我们用第\(x\)层第\(y\)列来表征位置。具体来说,原来坐标为\((x,y)\)(即第\(x\)行,第\(y\)列),我们用坐标\([x+y,y]\)(即第\(x+y\)层,第\(y\)列)来表示。(即小括号表示原坐标,方括号表示新坐标)
若不考虑边界,对于一个点\((i,j)\),若\((i-1,j)\)和\((i,j-1)\)都不可达,那么\((i,j)\)也不可达,逆命题也成立。用新坐标表示即为,对于一个点\([i+j,j]\),若\([i+j-1,j-1]\)和\([i+j-1,j]\)不可达,则\([i+j,j]\)也不可达,逆命题也成立。不妨令\(k=i+j\),则结论变为若\([k-1,j-1]\)和\([k-1,j]\)不可达,则\([k,j]\)也不可达,逆命题也成立。从而可以将结论进行推广:
- 若\([k-1,j],[k-1,j+1],[k-1,j+2],...,[k-1,j+m]\)不可达,则\([k,j+1],[k,j+2],...,[k,j+m]\)不可达。
举个例子,在图上可以表示为
将新坐标的第一维看成行,第二维看成列
X表示不可达,.表示可达
.XXXXX..XXXXXXXXXXX.XXXX...(前一层)
..XXXX...XXXXXXXXXX..XXX...(后一层)
感性理解会发现,所有连续不可达的区间(下面简称“区间”),每向后一层,这个区间的第一个不可达点都会变成可达点。
我们再考虑边界
// 这个是原坐标表示
对于左边界而言,若(i-1,j)不可达,(i,j)不可达
...........
...........
X..........
X..........
X..........
X..........
对于右边界而言,它单独本身不会影响到下一层
...........
...........
..........X
...........
...........
...........
对于上边界而言,若(i,j-1)不可达,(i,j)不可达
....XXXXXXX
...........
...........
...........
...........
...........
对于下边界而言,它单独本身不会影响到下一层
...........
...........
...........
...........
...........
...X.......
看到边界讨论的结果,为了简单,我们不妨,考虑在左边界和上边界(对于原坐标表示而言)加一圈雷
// 这个是原坐标表示
XXXXXXXXXXXXXXXXXX
X.................
X.................
X.................
X.................
X.................
X.................
X.................
X.................
这样就可以将边界的结论和内部的结论统一
// 这个是原坐标表示
左边界
XXXXXXXXXXXXXXXXXX
X.................
X.................
X.................
X.................
Xx................
Xx................
Xx................
Xx................
上边界
XXXXXXXXXXXXXXXXXX
X.......xxxxxxxxxx
X.................
X.................
X.................
X.................
X.................
X.................
X.................
对于这个结论(所有连续的不可达的区间,每向后一层,这个区间的第一个不可达点都会变成可达点),我们如何快速转移呢?由于雷相对于格子数较少,可以考虑维护每个区间第一个不可达点的位置和这一层的答案。对于每一层,先根据这一层的雷进行更新,同时更新这一层答案并累加进最终答案,然后对下一层进行一次不可达点变成可达点的更新。(一定要注意边界的变化)
复杂度比较玄学,因为多数情况下区间数不会很多,就算区间数很多,也不可能每一层区间数都很多。(有没有大佬可以分析一下这个算法的复杂度)
代码:
#include <algorithm>
#include <cstring>
#include <iostream>
#include <set>
#include <vector>
typedef long long Lint;
using namespace std;
const int maxn = 1e5 + 10;
struct Bomb {
int x, y;
void read() { cin >> x >> y; }
bool operator<(const Bomb& rhs) const {
return x + y < rhs.x + rhs.y || x + y == rhs.x + rhs.y && y < rhs.y;
}
} bombs[maxn];
int st[maxn << 1];
void solve() {
int n, m, k;
cin >> n >> m >> k;
for (int i = 0; i < k; i++) bombs[i].read();
// 对炸弹按层进行排序
sort(bombs, bombs + k);
// st[i]存储当前层,第i列的可达情况
memset(st, 0, sizeof(st));
set<int> S; // S存储当前层每个区间第一个不可达点的位置
vector<int> tmp; // 用于辅助更新S的临时数组
// 左边界加雷
st[0] = 1;
S.insert(0);
int j = 0; // 考虑到哪一个雷的指针
Lint res = 0, ans = 0; // ans为当前层答案,res为最终答案
for (int i = 3; i <= n + m; i++) {
if (i <= m) { // 当层数<=m时,说明这一层存在上边界,需要给上边界加雷
// 层数>m时,右边界之外的列号是定值,st数组的值一定为0,无需更新
st[i] = 1;
if (st[i - 1] == 0) S.insert(i);
}
if (i > n && st[i - n - 1]) {
// 当层数>n时,说明不存在左边界,而存在下边界
// 由于下边界列号不是定值
// 如果这一层的下边界之外是不可达点,需要更新成可达点,以免发生问题
st[i - n - 1] = 0;
if (S.count(i - n - 1)) {
S.erase(i - n - 1);
if (st[i - n] == 1) S.insert(i - n);
}
}
// 考虑这一层的雷
while (j < k && bombs[j].x + bombs[j].y == i) {
if (!st[bombs[j].y]) {
st[bombs[j].y] = 1;
ans++;
if (st[bombs[j].y - 1] == 0) S.insert(bombs[j].y);
if (st[bombs[j].y + 1] == 1) S.erase(bombs[j].y + 1);
}
j++;
}
// 累加答案
res += ans;
// 根据结论更新S
tmp.clear();
for (auto it : S)
if (it) tmp.push_back(it); // 左边界的列号不改变,不需要更新
for (auto it : tmp) {
S.erase(it);
st[it] = 0;
// 由于上边界的雷没有计算到贡献中,此种情况不需要-1
if (it != i) ans--;
if (st[it + 1] == 1) S.insert(it + 1);
}
// 细节,下一层可能会把上一层上边界(列号会改变)的雷带进来,ans需要+1
if (i <= m && st[i] == 1) ans++;
}
cout << (Lint)n * m - res << endl;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(0);
int T;
cin >> T;
while (T--) solve();
return 0;
}