HDU6992/ 2021“MINIEYE杯”中国大学生算法设计超级联赛(4)Lawn of the Dead(线段树/好题)详细题解
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;
}