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;
}