NAC2022
Contents
G. Rafting Trip
题意
给定一个 $n \times m$ 的网格,每个网格要么是河流,要么是陆地。
如果是河流,那么它一定是上下左右四个方向之一,代表走到这个位置,下一个位置就会被冲到对应方向一格的位置。
如果是陆地,要么是空地,要么是一个观光点。
我们可以选定从一个点出发,顺着河流的流向前进,当我们走到陆地/场地外/一个走过的点时,将会结束旅程。
旅程中,如果我们在河流上,并且邻居有观光点,那么我们可以记录这个观光点。
对于所有的出发点,求最优的出发点,使得旅程中记录到的观光点最多(每个观光点不能被重复记录)。
其中,$n,m \leq 500$。
题解
注意到如果是河流,只会有上下左右四个方向。所以每个点只有至多1个出边。这说明什么?基环树!(更准确的说是基环森林)。
虽然这题是有向边,但处理基环树我们直接当无向边做就可以了。
所以我们先建无向图,只考虑河流,陆地就直接忽略。
然后我们先找到基环树中的环,然后缩点,这样就得到一个树了。以环作为根,跑 dfs 即可。
如果没有环,就取一个没有出边的点作为根。
• 这样看起来 dfs 的过程是跑了反向的边,但实际上为了统计这个观光点,必须按照反向的顺序来。
• 缩点后不用重新建图,建一个新点即可,注意新点向被缩的点的邻居们连边,要check一下不能连到环上的点。
基环树找环-注意事项
然后是需要特别注意的基环树找环问题。
注意到这个题目,基环树可能会有大小为 $2$ 的环,这样就会有重边了。
所以我们在第一次dfs找环时,不能直接记录 parent 的 vertex。
而是应该记录 parent 的边,并且用前向星建图。
举个例子,1->2, 2->1。
如果我们用 if (par[v] == u) continue;
的话,到了 2 时就会直接忽略掉 1。然后回到 1 的时候,由于有重边,1会发现 vis[2] = 1;
,这样看起来 1 是cycle的终点,2是cycle的起点,实际上是反过来的!
所以正确的写法是:
vector<int> cycle, comp; // component: 储存这个联通分量的所有点
void dfs1(int u, int in_edge) { // 这里的参数是 e的编号!
vis[u] = 1;
comp.push_back(u);
for (int e = head[u]; e; e = edges[e].nxt) {
if (e == (in_edge ^ 1)) continue; // 特意处理了大小为2的环,注意这里 (in_edge^1) 需要加括号!
int v = edges[e].to;
if (vis[v]) {
if (!cycle.size()) { // 只跑一个cycle!因为有重边!
int c = u;
while (c != v) {
cycle.push_back(c);
c = pre[c];
}
cycle.push_back(c);
for (int j : cycle) iscycle[j] = 1;
}
} else {
pre[v] = u;
dfs1(v, e); // 注意这里参数是 e
}
}
}
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 500+5;
const int maxm = (500*500+105) * 2;
int n,m;
char grid[maxn][maxn];
int id[maxn][maxn];
bool river[maxn][maxn], site[maxn][maxn];
bool ok(int i, int j) {
return i >= 1 && i <= n && j >= 1 && j <= m && river[i][j];
}
vector<int> adj[maxm];
vector<int> adj_site[maxm];
int d[4][2] = {{1,0}, {-1,0}, {0,1}, {0,-1}};
struct Edge {
int to, nxt;
} edges[maxm<<1];
int head[maxm], ecnt = 2;
void addEdge(int u, int v) {
Edge e = {v, head[u]};
edges[ecnt] = e;
head[u] = ecnt++;
}
bool vis[maxm], iscycle[maxm];
int pre[maxm], outdeg[maxm];
vector<int> cycle, comp; // component: 储存这个联通分量的所有点
void dfs1(int u, int in_edge) {
vis[u] = 1;
comp.push_back(u);
for (int e = head[u]; e; e = edges[e].nxt) {
if (e == (in_edge ^ 1)) continue; // 特意处理了大小为2的环,注意这里 (in_edge^1) 需要加括号!
int v = edges[e].to;
if (vis[v]) {
if (!cycle.size()) {
int c = u;
while (c != v) {
cycle.push_back(c);
c = pre[c];
assert(c != 0);
}
assert(c == v);
cycle.push_back(c);
for (int j : cycle) iscycle[j] = 1;
}
} else {
pre[v] = u;
dfs1(v, e); // 注意这里参数是 e
}
}
}
int cid = 0;
int cnt[maxm], res = 0, ans = 0;
void dfs2(int u, int p) {
for (int v : adj_site[u]) {
if (!cnt[v]) res++;
cnt[v]++;
}
ans = max(ans, res);
for (int v : adj[u]) {
if (v == p || iscycle[v]) continue; // 已经是cycle的说明被缩点缩掉了,删去
dfs2(v, u);
}
// 回溯
for (int v : adj_site[u]) {
cnt[v]--;
if (!cnt[v]) res--;
}
}
void solve(int u) {
cycle.clear();
comp.clear();
dfs1(u, 0);
int rt = -1;
if (cycle.size()) {
// 有环
rt = ++cid;
for (int v : cycle) {
for (int j : adj_site[v]) {
adj_site[rt].push_back(j);
}
for (int nv : adj[v]) {
if (!iscycle[nv]) { // 只考虑不是cycle上的点
adj[rt].push_back(nv);
}
}
}
iscycle[rt] = 1;
sort(adj[rt].begin(), adj[rt].end());
adj[rt].resize(unique(adj[rt].begin(), adj[rt].end()) - adj[rt].begin());
sort(adj_site[rt].begin(), adj_site[rt].end());
adj_site[rt].resize(unique(adj_site[rt].begin(), adj_site[rt].end()) - adj_site[rt].begin());
} else {
for (int v : comp) {
if (!outdeg[v]) {
rt = v;
break;
}
}
assert(rt != -1);
}
dfs2(rt, 0);
}
int main() {
cin >> n >> m;
for (int i = 1; i <= n; i++) {
string s; cin >> s;
for (int j = 1; j <= m; j++) {
grid[i][j] = s[j-1], id[i][j] = ++cid;
if (grid[i][j] != '.' && grid[i][j] != '#') river[i][j] = 1;
if (grid[i][j] == '#') site[i][j] = 1;
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
int u = id[i][j];
int op;
if (grid[i][j] == 'v') op = 0;
if (grid[i][j] == '^') op = 1;
if (grid[i][j] == '>') op = 2;
if (grid[i][j] == '<') op = 3;
int ni = i + d[op][0], nj = j + d[op][1];
if (ok(ni, nj)) {
int v = id[ni][nj];
addEdge(u,v);
addEdge(v,u);
adj[u].push_back(v);
adj[v].push_back(u);
outdeg[u]++; // outdeg = 0的就是树的根
}
// adj_site (only consider river's neighbor)
if (river[i][j]) {
for (int o = 0; o < 4; o++) {
int ni = i + d[o][0], nj = j + d[o][1];
if (site[ni][nj]) {
adj_site[u].push_back(id[ni][nj]);
}
}
}
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) {
if (river[i][j]) {
int u = id[i][j];
if (!vis[u]) solve(u);
}
}
}
cout << ans << endl;
}
L. Triangular Logs
题意
二维空间中有 $n$ 个点,每个点 $(x_i,y_i)$ 有一个权值 $w_i$。
现在有 $q$ 个询问,每次询问 $x_l,x_r,y_l,y_r$,回答:
在 $[x_l,x_r] \times [y_l,y_r]$ 这个矩阵中,是否存在三个点,使得这三个点的权值能够形成一个三角形?
其中,$n,q \leq 10^5, x_i,y_i,w_i \in [1,10^9]$。
题解
首先给一个结论:
每次询问,只需要从矩阵中,取 $\log(10^9)$ 个点,就足够了。
证明:如果矩阵中,取了 $m>\log(10^9)$ 个点,仍然没找到三角形,那么,将这些点的权值进行排序:
$$w_1,w_2,w_3,w_4,w_5,…,w_m$$
我们可以得到 $w_1+w_2 < w_3$,那么 $2w_1 < w_3$,同理 $w_2+w_3<w_4$,那么 $2w_2<w_4$。
所以 $\forall i, 2w_i < w_{i+2}$,说明这个数组的元素是几何增长的,所以长度不可能超过 $\log(10^9)$。
于是每次询问,我们只要从中找到 $\log(10^9)$ 个点,怎么维护呢?
我们 $x$ 坐标用动态开点线段树维护,而每棵线段树里层维护一个 set,按照 $y$ 坐标的大小排序,这样可以通过 lower_bound()
来找到 $[y_l,y_r]$ 的。
线段树的层数为 $\log(10^9)$,而每个点最坏情况下都会在每一层出现,所以总时空复杂度就是 $O(n\log n * \log(10^9))$。
• 最后注意!这个题不能用二维线段树!
考虑一个例子:$[1,2] \times [1,2]$ 这个矩阵里刚好有 $4$ 个点 $(1,1),(1,2),(2,1),(2,2)$。
我们如果考虑 $x \in [1,2]$,那么这刚好对应一棵线段树,里面储存的是 $y \in [1,2]$ 的,但这棵线段树只能储存 $2$ 个值,所以会丢失一些信息!
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+5;
const int N = 1e9;
struct Point {
int x, y, val;
bool operator<(const Point& others) const {
return y < others.y;
}
} a[maxn];
// 区间查询
// 单点修改
struct SegNode {
multiset<Point> s;
int lc, rc;
};
int cid = 0;
const int M = 80;
vector<Point> vec;
struct SegX {
SegNode tr[maxn*40];
void update(int& cur, int l, int r, int x, int y, int val) {
if (!cur) cur = ++cid;
tr[cur].s.insert(Point {x, y, val});
if (l == r) {
return;
}
int mid = (l+r) >> 1;
if (x <= mid) update(tr[cur].lc, l, mid, x, y, val);
if (x > mid) update(tr[cur].rc, mid+1, r, x, y, val);
}
void query(int cur, int l, int r, int xl, int xr, int yl, int yr) {
if (!cur) return;
if (vec.size() > M) return;
if (xl <= l && xr >= r) {
// 寻找 [yl, yr] 之间的
auto pl = tr[cur].s.lower_bound(Point {0, yl, 0});
auto pr = tr[cur].s.lower_bound(Point {0, yr+1, 0});
while (pl != pr) {
if (pl->y > yr) break;
vec.push_back(*pl);
if (vec.size() > M) return;
pl = next(pl);
}
return;
}
int mid = (l+r) >> 1;
if (xl <= mid) query(tr[cur].lc, l, mid, xl, xr, yl, yr);
if (xr > mid) query(tr[cur].rc, mid+1, r, xl, xr, yl, yr);
}
} tr;
int RT = 0;
void query(int xl, int xr, int yl, int yr) {
vec.clear();
tr.query(RT, 1, N, xl, xr, yl, yr);
}
void update(int x, int y, int val) {
tr.update(RT, 1, N, x, y, val);
}
int n,q;
int main() {
cin >> n >> q;
for (int i = 1; i <= n; i++) {
cin >> a[i].x >> a[i].y >> a[i].val;
update(a[i].x, a[i].y, a[i].val);
}
while (q--) {
int xl, yl, xr, yr; cin >> xl >> yl >> xr >> yr;
query(xl, xr, yl, yr);
int good = 0;
sort(vec.begin(), vec.end(), [](auto a, auto b) {
return a.val < b.val;
});
for (int i = 0; i < vec.size(); i++) {
if (i >= 2 && vec[i-2].val + vec[i-1].val > vec[i].val) {
good = 1;
break;
}
}
cout << good << "\n";
}
}