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;
}
posted @ 2020-05-15 00:03  粉兔  阅读(1059)  评论(0编辑  收藏  举报