Problem Description
One day, a zombie came to the Lawn of the Dead, which can be seen as an n×m grid. Initially, he stood on the top-left cell, which is (1,1).
Because the brain of the zombie was broken, he didn't have a good sense of direction. He could only move down from (i,j) to (i+1,j) or right from (i,j) to (i,j+1) in one step.
There were k "lotato mines" on the grid. The i-th mine was at (xi,yi). In case of being destroyed, he would never step into a cell containing a "lotato mine".
So how many cells could he possibly reach? (Including the starting cell)
Input
The first line contains a single integer t (1≤t≤20), denoting the number of test cases.
The first line of each test case contains three integers n,m,k (2≤n,m,k≤105) --- there was an n×m grid, and there were k "lotato mines" on the grid.
Each of the following k lines contains 2 integers xi,yi (1≤xi≤n,1≤yi≤m) --- there was a "lotato mine" at (xi,yi). It's guaranteed that there was no "lotato mine" at (1,1) and no mines were in the same cell.
It is guaranteed that ∑n≤7⋅105,∑m≤7⋅105.
Output
For each test case, output the number of cells he could possibly reach.
Sample Input
1
4 4 4
1 3
3 4
3 2
4 3
Sample Output
10
HintThe cells that the zombie might reach are
(1,1), (1,2), (2,1), (2,2), (2,3), (2,4), (3,1), (3,3), (4,1), (4,2).
據說還有set+二分/並查集/排序遍歷等各種神仙做法QAQ
首先正難則反,不妨算出來無法到達的點的數目,再用總數去減。首先一個點不可達當且僅當其上方和左方不可達,所以考慮按行一行一行處理。每一行會被該行的雷分成若干個區間。對於第i行每個區間\([l, r]\)(設左右端點包含雷),找到從第i - 1行第\(l + 1\)個位置開始的從左往右的最長連續不可達區域(設為\([l + 1,x]\))那么對應第i行的\([l + 1,x]\)也不可達,而第i行\([x+1,r]\)是可達的。因此考慮用線段樹維護每行每個位置往右的最長連續不可達區域的長度。怎么維護?利用線段樹的區間修改和區間查詢功能!將一個連續區間從0變為1就代表該連續區間不可達。線段樹每個節點維護兩個變量 :val代表這個區間的1的數量,lmax代表這個區間從左端點開始最長的連續的1的長度(這里可以參考藍書線段樹這一章的CH4301這道題,維護的思路比較類似)。
整個代碼的流程是這樣的:先把信息讀入,每行的雷的縱坐標扔進對應的vector排序。然后遍歷每一行每個被兩顆雷夾起來的區間\([l + 1, r - 1]\)(這里的區間不含雷),然后查詢上一行這個區間左端點往右的最長連續不可達區域的長度,並更新當前行這個區間里的不可達區域同時更新不可達位置的數量。因為當前行只和上一行有關,因此實際上開兩棵線段樹即可(滾動數組思想)。最后用總數減去不可達位置的數量再減去雷數就是答案了。
注意懶標記的打法,要么設置一個變量表示是否打標記了,要么就設置tag為-1表示沒有打標記。因為這個題區間賦值的緣故,tag為0也應該表示打了標記!還有一些小細節見代碼和注釋QWQ
#include <bits/stdc++.h>
#include <stdio.h>
#define N 200005
using namespace std;
int n, m, k;
vector<int> p[N];
struct SegmentTree {
int p, l, r, val, lmax, tag;//lmax為從這個節點對應的區間的左端點開始的最長連續1的長度(最長不超過區間長度)
} t[2][(N << 2)];
void build(int x, int p, int l, int r) {
t[x][p].l = l, t[x][p].r = r;
t[x][p].tag = -1;
if(l == r) {
t[x][p].val = t[x][p].lmax = 0;
t[x][p].tag = -1;//!!!不能初始化為0,因為此處tag有清零的作用
return;
}
int mid = (l + r) >> 1;
build(x, 2 * p, l , mid);
build(x, 2 * p + 1, mid + 1, r);
t[x][p].val = t[x][2 * p].val + t[x][2 * p + 1].val;
t[x][p].lmax = 0;
}
void spread(int x, int p) {
if(t[x][p].tag != -1) {//打了標記
t[x][2 * p].val = (t[x][2 * p].r - t[x][2 * p].l + 1) * t[x][p].tag;
t[x][2 * p].lmax = (t[x][2 * p].r - t[x][2 * p].l + 1) * t[x][p].tag;//不要忘了更新 tag為0就直接把子樹的lmax清零
t[x][2 * p + 1].val = (t[x][2 * p + 1].r - t[x][2 * p + 1].l + 1) * t[x][p].tag;
t[x][2 * p + 1].lmax = (t[x][2 * p + 1].r - t[x][2 * p + 1].l + 1) * t[x][p].tag;
t[x][2 * p].tag = t[x][p].tag;
t[x][2 * p + 1].tag = t[x][p].tag;
t[x][p].tag = -1;//清除標記
}
}
void modify(int x, int p, int L, int R, int v) {
if(t[x][p].l >= L && t[x][p].r <= R) {
t[x][p].val = (t[x][p].r - t[x][p].l + 1) * v;
t[x][p].lmax = (t[x][p].r - t[x][p].l + 1) * v;
t[x][p].tag = v;
return;
}
spread(x, p);
int mid = (t[x][p].l + t[x][p].r) >> 1;
if(L <= mid) modify(x, 2 * p, L, R, v);
if(R > mid) modify(x, 2 * p + 1, L, R, v);
t[x][p].val = t[x][2 * p].val + t[x][2 * p + 1].val;//val可以直接維護
if(t[x][2 * p].val == (t[x][2 * p].r - t[x][2 * p].l + 1)) t[x][p].lmax = t[x][2 * p].val + t[x][2 * p + 1].lmax;
//如果左子樹表示的區間全部不可達,那么當前節點的lmax就是左子樹區間長度+右子樹的lmax
else t[x][p].lmax = t[x][2 * p].lmax;
//否則就是左子樹的lmax(因為這樣左右子樹之間必然有可達位置,就不用管右子樹了
return;
}
int query(int x, int p, int l, int r) {
if(t[x][p].l == l && t[x][p].r == r) {//直接覆蓋要查詢的區間
return t[x][p].lmax;
}
spread(x, p);
int mid = (t[x][p].l + t[x][p].r) >> 1;
if(r <= mid) return query(x, 2 * p, l, r);//注意這里要查詢的區間不變,因為還沒真正查詢!!!!
if(l > mid) return query(x, 2 * p + 1, l, r);
int tmp = query(x, 2 * p, l, mid);//首先查詢左子樹,如果左子樹表示的區間全部不可達再查詢右子樹,否則直接返回即可
if(tmp == mid - l + 1) return tmp + query(x, 2 * p + 1, mid + 1, r);//這里的l和r要變!因為查詢區間已經被分割為兩個待查詢的子區間了!
else return tmp;
}
signed main() {
int T;
cin >> T;
while (T--) {
scanf("%d%d%d", &n, &m, &k);
build(0, 1, 1, m);
build(1, 1, 1, m);
for (int i = 0; i <= n + 10; i++) p[i].clear();
for (int i = 1; i <= k; i++) {
int u, v;
scanf("%d%d", &u, &v);
p[u].push_back(v);
}
//滾動數組的思想,開兩棵線段樹即可
long long ans = 0;
modify(0, 1, 1, m, 1);//先把第0行的邊界更新
for (int i = 1; i <= n; i++) {
sort(p[i].begin(), p[i].end());
p[i].push_back(m + 1);//邊界,便於處理
int lst = 0;//lst表示同行中前一個地雷的位置 每行一開始是0
modify((i & 1), 1, 1, m, 0);//先把之前的信息清零
for (int j = 0; j < p[i].size(); j++) {
int x = p[i][j];
if(i == 1 && lst == 0) {//第一行一開始的一部分一定可達,更新lst后忽略即可
lst = x;
continue;
}
int lpos = lst + 1, rpos = x - 1, len;
if(lpos <= rpos) {
len = query(1 ^ (i & 1), 1, lpos, rpos);
ans += 1ll * len;
} else len = 0;
if(!(lpos - 1 == 0 && lpos - 1 + len == 0)) //最左邊的邊界不要標記
modify(i & 1, 1, lpos - 1, lpos - 1 + len, 1);//把左側的地雷也標記上
lst = x;//不要忘記更新
}
}
cout << 1ll * n * m - ans - 1ll * k << endl;//總數減去不可達的格子減去地雷數
}
return 0;
}