AtCoder Grand Contest 007

题目传送门:AtCoder Grand Contest 007

A - Shik and Stone

井号个数必须等于 \(N + M - 1\)

#include <cstdio>

int N, M, C;

int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N * M; ++i) {
		char s[2];
		scanf("%1s", s);
		C += *s == '#';
	}
	puts(C == N + M - 1 ? "Possible" : "Impossible");
	return 0;
}

B - Construct Sequences

\(a_i = N \cdot i\),并且 \(a_{p_i} + b_{p_i} + 1 = a_{p_{i + 1}} + b_{p_{i + 1}}\)。合理安排 \(b\) 的值即可。

#include <cstdio>

const int V = 500000000;
const int MN = 20005;

int N, A[MN];

int main() {
	scanf("%d", &N);
	for (int i = 1, x; i <= N; ++i) scanf("%d", &x), A[x] = i;
	for (int i = 1; i <= N; ++i) printf("%d%c", N * i, " \n"[i == N]);
	for (int i = 1; i <= N; ++i) printf("%d%c", V + A[i] - N * i, " \n"[i == N]);
	return 0;
}

C - Pushing Balls

一开始时,相邻两个洞与球之间的距离,是等差数列。令首项为 \(a\),公差为 \(b\)

根据期望的线性性,我们考虑求出第一次操作后,规模小了 \(1\) 的情况的相邻两个洞与球之间的距离的期望值。

一通推导发现还是等差数列,并且有公式:\(\left< a', b' \right> = \left< ((2 n + 2) a + 5 b) / (2 n), (n + 2) b / n \right>\)

所以每次求出第一次操作后时的代价,和操作后的 \(a, b\) 然后递归进规模减去 \(1\) 的情况即可。

#include <cstdio>

int N;
double A, B, Ans;

int main() {
	scanf("%d%lf%lf", &N, &A, &B);
	for (int i = N; i >= 1; --i) {
		Ans += A + (2 * i - 1) * B / 2;
		A = ((2 * i + 2) * A + 5 * B) / (2 * i);
		B = B * (i + 2) / i;
	}
	printf("%.10lf\n", Ans);
	return 0;
}

D - Shik and Game

一定是一直往右走,直到走到某只熊,然后往回走,到第一个还未被捡的金币处,等到金币被熊给出后,一直向右走到回头的位置。

我们考虑对着这个过程 DP。答案一定是 \(E\) 加一个值,这个值即是把所有熊划分成若干段,每段代价为 \(\max(2 (x_r - x_\ell), T)\)

\(\mathrm{f}[i]\) 表示把前 \(i\) 个分成若干段的最小总代价,朴素的 DP 是 \(\mathcal O (n^2)\) 的。

注意到对于每个 \(i\),一个前缀的 \(j\) 向它转移的代价是 \(2 (x_i - x_{j + 1})\),一个后缀的 \(j\) 向它转移的代价是 \(T\)

然而 \(\mathrm{f}[i]\) 显然是单调递增的,所以后缀的 \(j\) 直接取最前一个进行转移即可。

对于前缀的 \(j\),记 \(\mathrm{g}[j] = \mathrm{f}[j] - 2 x_{j + 1}\),于是就有 \(\mathrm{f}[i] \gets \mathrm{g}[j] + 2 x_i\)

前缀的长度是单调非严格递增的,可以双指针确定。

#include <cstdio>
#include <algorithm>

typedef long long LL;
const LL Infll = 0x3f3f3f3f3f3f3f3f;
const int MN = 200005;

int N, E, T, A[MN];
LL f[MN], g[MN];

int main() {
	scanf("%d%d%d", &N, &E, &T);
	for (int i = 1; i <= N; ++i) scanf("%d", &A[i]);
	g[0] = Infll;
	for (int i = 1, j = 1; i <= N; ++i) {
		while (2 * (A[i] - A[j]) > T) ++j;
		f[i] = std::min(f[j - 1] + T, 2 * A[i] + g[j - 1]);
		g[i] = std::min(g[i - 1], f[i - 1] - 2 * A[i]);
	}
	printf("%lld\n", E + f[N]);
	return 0;
}

E - Shik and Travel

(我们记 \(w_u\) 为节点 \(u\) 到其双亲节点的边的权值)

首先观察到必然是先走到其中一个子树,把这个子树全走完,再去另一个子树。要不然就会有一条边被经过四次或以上。

我们考虑先二分答案 \(\mathrm{ans}\),然后去判断是否存在每次从叶子跳到另一个叶子时距离不超过 \(\mathrm{ans}\) 的路径。

这引出一个树形 DP:令 \(\mathrm{f}[u][x][y]\) 表示从 \(u\) 出发到每个 \(u\) 子树中的叶子再回到 \(u\),第一次距离为 \(x\),最后一次距离为 \(y\) 是否可行。

合并两个孩子时就可以枚举进入子树的先后顺序,以及两边 DP 值为 \(1\) 的状态进行合并转移。

也就是:\(\mathrm{f}[v_1][a][b]\)\(\mathrm{f}[v_2][c][d]\) 如果都为 \(1\),并且 \(b + c + w_{v_1} + w_{v_2} \le \mathrm{ans}\),就可以转移到 \(\mathrm{f}[u][a + w_{v_1}][d + w_{v_2}]\)

但是这样状态数太多了,我们考虑记 \(S_u = \{ (a, b) \mid \mathrm{f}[u][a][b] = 1 \}\),然后继承子节点的集合 \(S_{v_1}\)\(S_{v_2}\) 进行转移。

当然这样状态数还是很多,我们考虑 \((a, b), (a', b') \in S\),并且 \(a \le a'\)\(b \le b'\),则 \((a', b')\) 是可以被忽略的。

我们只保留在这样的条件下不会被忽略的 \((a, b)\) 对,如果有 \((a_1, b_1)\)\((a_2, b_2)\) 两对,且 \(a_1 < a_2\),则有 \(b_1 > b_2\)

合并子树时考虑 \(v_1\) 给出的一对 \((a, b)\),对于 \(v_2\) 给出的 \((c, d)\),如果 \(b + c \le \mathrm{ans} - w_{v_1} - w_{v_2}\),就可转移到 \((a + w_{v_1}, d + w_{v_2})\)

或者如果 \(a + d \le \mathrm{ans} - w_{v_1} - w_{v_2}\),也可转移到 \((c + w_{v_1}, b + w_{v_2})\)

例如第一种转移,要让转移到的 \(d\) 尽量小,也就是 \(c\) 尽量大,但是如果 \(c\) 太大就不满足条件了,所以要选满足条件的尽量大的 \(c\)

而第二种就是选满足条件的尽量大的 \(d\)

所以每个 \((a, b)\) 只会导出最多两个数对贡献给双亲结点。

也就是:\(|S_u| \le 2 \min(|S_{v_1}|, |S_{v_2}|)\)

根据重链剖分那套结论,我们就有 \(\displaystyle \sum_u |S_u| = \mathcal O (N \log N)\)

只要保证转移时的复杂度是线性的即可,显然双指针即可做到线性,注意用归并排序。

总复杂度 \(\mathcal O (N \log N \log \mathrm{ans})\)

#include <cstdio>
#include <algorithm>

typedef long long LL;
const int MN = 1 << 17 | 7;

int N, p[MN], v[MN], d[MN];

struct dat {
	LL x, y;
	dat() { x = y = 0; }
	dat(LL a, LL b) { x = a, y = b; }
} tmp1[MN], tmp2[MN];
std::vector<dat> f[MN];
int vis[MN];
inline bool check(LL w) {
	for (int i = 1; i <= N; ++i)
		if (d[i]) f[i].clear(), vis[i] = 0;
	for (int u = N; u >= 2; --u) if (vis[p[u]]) {
		int a = u, c = p[u], b = vis[c];
		if (f[a].size() > f[b].size()) std::swap(a, b);
		int na = f[a].size(), nb = f[b].size(), k1 = 0, k2 = 0;
		LL t = w - v[a] - v[b];
		for (int i = 0, j1 = -1, j2 = 0; i < na; ++i) {
			while (j1 + 1 < nb && f[a][i].y + f[b][j1 + 1].x <= t) ++j1;
			while (j2 < nb && f[a][i].x + f[b][j2].y > t) ++j2;
			if (j1 >= 0) tmp1[++k1] = dat(f[a][i].x + v[a], f[b][j1].y + v[b]);
			if (j2 < nb) tmp2[++k2] = dat(f[b][j2].x + v[b], f[a][i].y + v[a]);
		}
		int d1 = 1, d2 = 1;
		while (d1 <= k1 || d2 <= k2) {
			dat g = d2 > k2 || (d1 <= k1 && tmp1[d1].x <= tmp2[d2].x) ? tmp1[d1++] : tmp2[d2++];
			if (f[c].empty()) f[c].push_back(g);
			else if (f[c].back().x == g.x && f[c].back().y >= g.y) f[c].back() = g;
			else if (f[c].back().y > g.y) f[c].push_back(g);
		}
		if (f[c].empty()) return 0;
	} else vis[p[u]] = u;
	return 1;
}

int main() {
	scanf("%d", &N);
	for (int i = 2; i <= N; ++i)
		scanf("%d%d", &p[i], &v[i]), d[p[i]] = 1;
	for (int i = 1; i <= N; ++i) if (!d[i]) f[i].resize(1);
	LL lb = 0, rb = (N - 1ll) << 17, mid, ans = -1;
	while (lb <= rb) {
		mid = (lb + rb) >> 1;
		if (check(mid)) ans = mid, rb = mid - 1;
		else lb = mid + 1;
	}
	printf("%lld\n", ans);
	return 0;
}

F - Shik and Copying String

我们观察转移的过程:

最终在 \(T\) 中的连续段,我们在它们上画线,并顺着相同的字母延伸下来回到 \(S_0\)

为了使得线条的高度最小,我们考虑这样一个贪心做法:

\(T\) 中从后往前贪心,对于每个同字母连续段 \([\ell, r]\),找到在 \(\ell\)\(\ell\) 之前的,最近的还未被画线的相同字母的 \(S_0\) 的位置 \(j\)

\(r\) 画到 \(\ell\),然后贪心地从 \(\ell\) 开始,保证不和之前的线以及 \(S_0\) 相交,尽量地往下画,如果不行了再往左画,直到画到 \(S_0[j]\) 上方:

这样似乎只能对一个 \(T = S_k\) 进行 check,于是导出一个二分答案的做法,不过我们也可以轻松地将其改写为直接求答案的做法:

通过维护当前每个位置的最高的线的高度 \(h\),我们可以在 \(\mathcal O (N^2)\) 的时间内维护这个贪心并求出答案。

也就是:一开始 \(h\) 全为 \(0\);假设处理的是 \([\ell, r]\) 以及 \(j\),令 \(i\) 取在 \(j \sim r - 1\) 中的 \(h[i]\) 都变成 \(h[i + 1] + 1\)

最终 \(h\) 数组的最大值就是答案。

为了得到 \(\mathcal O (n)\) 的做法,注意到我们每次是让一个区间的值往左复制了一位,然后加上 \(1\)

因为是从后往前处理的,不需要管后缀,所以我们可以直接当成是把一个后缀往左复制了一位然后加上 \(1\)

而且每次往左的那个后缀的位置还都是非严格递减的。

使用一个队列来维护这些往左了的位置,注意到新加入的后缀往左操作会影响到旧的位置,记一个全局标记维护影响,细节见代码。

#include <cstdio>
#include <algorithm>

const int MN = 1000005;

int N, Ans;
char S[MN], T[MN];
int bias, que[MN], head, tail;

int main() {
	scanf("%d%s%s", &N, S + 1, T + 1);
	int ok = 1;
	for (int i = 1; i <= N; ++i) if (S[i] != T[i]) ok = 0;
	if (ok) return puts("0"), 0;
	head = 1, tail = 0;
	for (int i = N, j = N; i >= 1; --i) {
		if (j > i) j = i;
		while (head <= tail && que[head] + bias > i) ++head;
		Ans = std::max(Ans, tail - head + 2);
		if (i == 1 || T[i - 1] != T[i]) {
			while (j && S[j] != T[i]) --j;
			if (!j) return puts("-1"), 0;
			--bias;
			que[++tail] = j - bias;
			--j;
		}
	}
	printf("%d\n", Ans);
	return 0;
}
posted @ 2020-07-15 09:16  粉兔  阅读(389)  评论(0编辑  收藏  举报