JOI Open 2019 题解
题目传送门:LOJ「JOI Open 2019」。
三级跳 / 三段跳び / Triple Jump
考虑一组合法的 \(a, b, c\),如果在 \(a, b\) 之间存在一个下标 \(i\) 满足 \(A_i \ge A_a\) 或 \(A_i \ge A_b\),则显然选择 \(i\) 不更劣。
也就是说,如果只考虑这样的 \(a, b\):满足它们之间的数都小于 \(\min(A_a, A_b)\);答案也不会改变。
可以发现这样的 \(a, b\) 只有 \(\mathcal O (N)\) 组,也就是对于每个点来说,左边和右边第一个大于等于本身的数。
这 \(\mathcal O (N)\) 组 \(a, b\) 可以通过单调栈求出。
对于询问,考虑离线,按照左端点从大到小排序。把那些可行的 \(a, b\) 也记录在左端点(\(a\))上。
在进行扫描线的同时,对于每个右端点,维护一个 \(B\) 值,表示如果选它作为 \(c\),最大的 \(A_a + A_b\) 的值。
在处理询问前先加入这些新的 \(a, b\),相当于把所有在 \(2 b - a\) 右侧的位置的 \(B\) 值与 \(A_a + A_b\) 取 \(\max\)。
询问时则是询问 \([l + 2, r]\) 之间的所有位置的最大的 \(B_i + A_i\) 之和。
因为 \(B\) 只会变大,可以用线段树维护区间中最大的 \(A_i\) 以及最大的 \(B_i + A_i\),使用一个标记下传对 \(B\) 的取 \(\max\) 操作即可。
代码如下,时间复杂度为 \(\mathcal O ((N + Q) \log N)\):
#include <cstdio>
#include <algorithm>
#include <vector>
typedef long long LL;
const int Inf = 0x3f3f3f3f;
const int MN = 500005, MQ = 500005, MS = 1 << 20 | 7;
void chkmx(LL &x, LL y) { x = x < y ? y : x; }
int N, Q;
LL A[MN];
int stk[MN], tp;
std::vector<int> V[MN], W[MN];
int qr[MQ]; LL Ans[MQ];
#define li (i << 1)
#define ri (li | 1)
#define mid ((l + r) >> 1)
#define ls li, l, mid
#define rs ri, mid + 1, r
LL v2[MS], v3[MS], tg[MS];
inline void P(int i, LL x) { chkmx(tg[i], x), chkmx(v3[i], x + v2[i]); }
inline void Pushdown(int i) { if (tg[i]) P(li, tg[i]), P(ri, tg[i]), tg[i] = 0; }
void Build(int i, int l, int r) {
if (l == r) return v2[i] = v3[i] = A[l], void();
Build(ls), Build(rs);
v2[i] = v3[i] = std::max(v2[li], v2[ri]);
}
void Mdf(int i, int l, int r, int a, int b, LL x) {
if (r < a || b < l) return ;
if (a <= l && r <= b) return P(i, x);
Pushdown(i), Mdf(ls, a, b, x), Mdf(rs, a, b, x);
v3[i] = std::max(v3[li], v3[ri]);
}
LL Qur(int i, int l, int r, int a, int b) {
if (r < a || b < l) return 0;
if (a <= l && r <= b) return v3[i];
Pushdown(i);
return std::max(Qur(ls, a, b), Qur(rs, a, b));
}
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) scanf("%lld", &A[i]);
A[stk[tp = 1] = 0] = Inf;
for (int i = 1; i <= N; ++i) {
while (A[stk[tp]] < A[i]) V[stk[tp--]].push_back(i);
if (stk[tp]) V[stk[tp]].push_back(i);
stk[++tp] = i;
} while (tp) --tp;
scanf("%d", &Q);
for (int i = 1, x; i <= Q; ++i) scanf("%d%d", &x, &qr[i]), W[x].push_back(i);
Build(1, 1, N);
for (int i = N - 2; i >= 1; --i) {
for (int j : V[i]) if (j + j - i <= N) Mdf(1, 1, N, j + j - i, N, A[i] + A[j]);
for (int j : W[i]) Ans[j] = Qur(1, 1, N, i + 2, qr[j]);
}
for (int i = 1; i <= Q; ++i) printf("%lld\n", Ans[i]);
return 0;
}
汇款 / 送金 / Remittance
汇款的系数矩阵满秩,通过高斯消元(稀疏矩阵)解出每个房子要向下一个房子汇多少钱,在模意义下计算可以防止浮点数。
如果解出来是小数或者负数,一定不可能,求出模意义下的数值后这可以很好地判断。
如果初始钱数非零,但是最终总钱数为零,也不可能。
否则一定可行。
证明?不知道。反正其他做法(就是模拟 \(\log N\) 轮的做法)也没证明。
下面是代码,时间复杂度为 \(\mathcal O (N + \log \mathrm{MOD})\):
#include <cstdio>
typedef long long LL;
const int Mod = 1000000007;
const int MN = 1000005;
inline int qPow(int b, int e) {
int a = 1;
for (; e; e >>= 1, b = (LL)b * b % Mod)
if (e & 1) a = (LL)a * b % Mod;
return a;
}
inline int gInv(int b) { return qPow(b, Mod - 2); }
int N, A[MN], B[MN], X[MN];
int xk[MN], xb[MN];
int ok;
int main() {
scanf("%d", &N);
for (int i = 1; i <= N; ++i) scanf("%d%d", &A[i], &B[i]);
ok = 1;
for (int i = 1; i <= N; ++i) if (A[i] != B[i]) ok = 0;
if (ok) return puts("Yes"), 0;
xk[N] = 1, xb[N] = 0;
for (int i = N - 1; i >= 1; --i) {
xk[i] = 2 * xk[i + 1];
if (xk[i] >= Mod) xk[i] -= Mod;
xb[i] = (2ll * xb[i + 1] + B[i + 1] - A[i + 1] + Mod) % Mod;
}
X[N] = (2ll * xb[1] + B[1] - A[1] + Mod) * gInv((1 + 2 * (Mod - xk[1])) % Mod) % Mod;
for (int i = 1; i < N; ++i) X[i] = ((LL)xk[i] * X[N] + xb[i]) % Mod;
ok = 1;
for (int i = 1; i <= N; ++i)
if (X[(i + N - 2) % N + 1] - 2ll * X[i] != B[i] - A[i]) ok = 0;
if (!ok) return puts("No"), 0;
ok = 0;
for (int i = 1; i <= N; ++i) if (B[i]) ok = 1;
if (!ok) return puts("No"), 0;
puts("Yes");
return 0;
}
病毒实验 / ウイルス実験 / Virus Experiment
假定我们已知任意两个格子之间的关系:如果仅 \(u\) 感染了病毒,那么 \(v\) 在若干时刻后也会被感染 / 永远不会被感染。
显然这个图强连通分量缩点之后就是一个 DAG,我们需要考虑所有的无出度的强连通分量的大小。
然而我们并不已知这些关系,也无法存储。那么我们到底能够求什么?
我们可以写一个 BFS 来求出如果初始某格子被感染,最终会感染到哪些格子,这是如何做到的呢:
-
首先,一个格子是否会被感染,仅和它上下左右的格子是否会被感染有关。
-
我们用 \(16 \times \mathcal O (M)\) 的时间,预处理出上下左右四个格子是否被感染共 \(2^4 = 16\) 种情况下,被有病毒的风吹的最长时间。
那么每次拓展一个新格子的时候,就可以在 \(\mathcal O (1)\) 的时间内确定它周围的格子目前是否会被感染。
这样在每个点运行一遍 BFS 就可以得到 \(\mathcal O (R^2 C^2)\) 的时间复杂度。
考虑这样一个事实:如果初始 \(u\) 感染了,可以让 \(v\) 也感染,则 \(u\) 导致的感染人数一定不少于 \(v\) 导致的感染人数。
而且再考虑到这个关系是有传递性的:如果 \(u \to v\) 并且 \(v \to w\),那么一定有 \(u \to w\)。
这提示我们不一定每次运行 BFS 都需要运行“满”(不能再拓展新点),可以在某些条件下中断,以减少复杂度。
考虑维护这样的一些区域:每个区域中有一个关键点,表示从这个区域内的任何一点出发,都能到达这个关键点。
一开始每个格子自成一个区域,然后这些区域会逐渐合并:
我们任取一个区域,从它的关键点出发 BFS,一旦到达了一个不在本区域内的格子,就把该区域并入到达的区域中。
可以发现这样的过程是满足区域的性质的,但是复杂度仍然不变,考虑优化。
考虑求最小生成树的 Boruvka 算法,它保证每层的时间是 \(\mathcal O (n)\) 的,并且让连通块个数除以 \(2\),所以复杂度为 \(\mathcal O (n \log n)\)。
把这个思想运用到本题中:枚举每个区域,如果 BFS 到了新区域就合并,但是如果该区域已经被合并过了,就不进行 BFS 了。
可以发现,每次从一个区域进行 BFS 的时间复杂度为 \(\mathcal O (\mathrm{size})\),其中 \(\mathrm{size}\) 为该区域大小(因为只要找到了新格子就立刻退出)。
这样可以保证每层的总时间复杂度不会超过 \(\mathcal O (RC)\)。
并且也最多执行 \(\mathcal O (\log(RC))\) 层,这是因为:
如果某次 BFS 没有到达任何新区域,则它一定对应了 DAG 中没有出度的一个连通分量。
对于其它区域,它们要么找到一个还没被合并过的区域,这两个区域在这一层都不能用了,或者找到一个已经被合并过的区域。
前者让 \(2\) 个区域变成 \(1\) 个,后者让 \(1\) 个区域变成 \(0\) 个。
所以无论如何,每一层至少少掉一半的区域(除了对应最后的无出度的连通分量的区域),最多执行 \(\mathcal O (\log (RC))\) 层。
下面是代码,时间复杂度为 \(\mathcal O (M + RC \log(RC))\):
#include <cstdio>
#include <algorithm>
const int MK = 100005, MN = 805, MS = 640005;
const char dir[5] = "NSWE";
const int dx[4] = {-1, 1, 0, 0}, dy[4] = {0, 0, -1, 1};
int K, S[MK * 2], N, M, A[MN][MN], tim[16];
void Init() {
static char Str[MK];
scanf("%d%d%d%s", &K, &N, &M, Str + 1);
for (int i = 1; i <= K; ++i)
for (int j = 0; j < 4; ++j)
if (Str[i] == dir[j]) S[i] = S[K + i] = j;
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= M; ++j) {
scanf("%d", &A[i][j]);
if (A[i][j] == 0) A[i][j] = 100001;
}
for (int d = 1; d < 16; ++d) {
int lst = 0, mxl = 0;
for (int i = 1; i <= 2 * K; ++i) {
if (d >> S[i] & 1) mxl = std::max(mxl, ++lst);
else lst = 0;
}
if (mxl == 2 * K) mxl = 100000;
tim[d] = mxl;
}
}
int bx[MN][MN], by[MN][MN], tx[MN][MN], ty[MN][MN];
int col[MN][MN], tot, qx[MS], qy[MS], lb, rb;
inline bool chk(int x, int y) {
int stat = 0;
for (int d = 0; d < 4; ++d) {
int nx = x + dx[d], ny = y + dy[d];
if (nx < 1 || nx > N || ny < 1 || ny > M) continue;
if (col[nx][ny] == tot) stat |= 1 << d;
}
return A[x][y] <= tim[stat];
}
inline bool BFS(int sx, int sy) {
if (A[sx][sy] > 100000) {
tx[sx][sy] = sx;
ty[sx][sy] = sy;
rb = N * M + 1;
return 0;
}
col[sx][sy] = ++tot;
lb = rb = 1, qx[1] = sx, qy[1] = sy;
while (lb <= rb) {
int x = qx[lb], y = qy[lb]; ++lb;
for (int d = 0; d < 4; ++d) {
int nx = x + dx[d], ny = y + dy[d];
if (nx < 1 || nx > N || ny < 1 || ny > M) continue;
if (col[nx][ny] == tot) continue;
if (!chk(nx, ny)) continue;
int nbx = bx[nx][ny], nby = by[nx][ny];
if (nbx != sx || nby != sy) {
int ntx = tx[nbx][nby], nty = ty[nbx][nby];
if (ntx) {
tx[sx][sy] = ntx, ty[sx][sy] = nty;
return 1;
}
tx[sx][sy] = tx[nbx][nby] = nbx;
ty[sx][sy] = ty[nbx][nby] = nby;
return 1;
}
col[nx][ny] = tot;
++rb, qx[rb] = nx, qy[rb] = ny;
}
}
tx[sx][sy] = sx;
ty[sx][sy] = sy;
return 0;
}
int ans, cnt;
int main() {
Init();
ans = N * M + 1;
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= M; ++j)
bx[i][j] = i, by[i][j] = j;
while (1) {
int ok = 0;
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= M; ++j)
tx[i][j] = ty[i][j] = 0;
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= M; ++j) {
int x = bx[i][j], y = by[i][j];
if (tx[x][y]) continue;
int ret = BFS(x, y);
if (ret) ok = 1;
}
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= M; ++j) {
int bi = bx[i][j], bj = by[i][j];
bx[i][j] = tx[bi][bj];
by[i][j] = ty[bi][bj];
}
if (!ok) break;
}
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= M; ++j)
tx[i][j] = ty[i][j] = 0;
for (int i = 1; i <= N; ++i)
for (int j = 1; j <= M; ++j) {
int x = bx[i][j], y = by[i][j];
if (tx[x][y]) continue;
BFS(x, y);
if (ans > rb) cnt = ans = rb;
else if (ans == rb) cnt += rb;
}
printf("%d\n%d\n", ans, cnt);
return 0;
}