AtCoder Grand Contest 013

题目传送门:AtCoder Grand Contest 013

A - Sorted Arrays

从前往后贪心取尽量长的合法前缀。

#include <cstdio>

int N, Ans;

int main() {
	scanf("%d", &N);
	int lst = 0, sgn = 0;
	for (int i = 1; i <= N; ++i) {
		int x;
		scanf("%d", &x);
		if (!lst) ++Ans;
		else if (!sgn) sgn = x > lst ? 1 : x < lst ? -1 : 0;
		else if (sgn == 1) {
			if (x < lst) ++Ans, sgn = 0;
		} else {
			if (x > lst) ++Ans, sgn = 0;
		}
		lst = x;
	}
	printf("%d\n", Ans);
	return 0;
}

B - Hamiltonish Path

考虑我们有一条经过 \(k\) 个点的路径 \(\{ u, a_2, a_3, \ldots , a_{k - 1}, v \}\),且其中 \(a_1 = u\) 以及 \(a_k = v\)

若此时 \(v\) 还未满足相邻的点都在路径中的限制,任取一个相邻的不在路径中的点 \(w\),将 \(w\) 变为 \(v\) 的下一个点,即 \(a_{k + 1} = w\)

注意这样的过程一定会在有限步内结束,因为每次会多取一个点,而当取满所有 \(N\) 个点时,限制一定会被满足。

此时 \(v\) 已经满足其限制,对于 \(u\) 来说同理,只不过是将新点变为 \(u\) 的前一个点。用双端队列容易在 \(\mathcal O (N + M)\) 的复杂度内维护。

#include <cstdio>
#include <algorithm>
#include <vector>

const int MN = 100005;

int N, M;
std::vector<int> G[MN];
int vis[MN], A[MN], B[MN], C, D;

int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1, x, y; i <= M; ++i) {
		scanf("%d%d", &x, &y);
		G[x].push_back(y);
		G[y].push_back(x);
	}
	A[C = 1] = 1;
	B[D = 1] = 1;
	vis[1] = 1;
	while (1) {
		int u = A[C], w = 0;
		for (int v : G[u]) if (!vis[v]) w = v;
		if (!w) break;
		vis[w] = 1, A[++C] = w;
	}
	while (1) {
		int u = B[D], w = 0;
		for (int v : G[u]) if (!vis[v]) w = v;
		if (!w) break;
		vis[w] = 1, B[++D] = w;
	}
	printf("%d\n", C + D - 1);
	for (int i = C; i >= 2; --i) printf("%d ", A[i]);
	for (int i = 1; i <= D; ++i) printf("%d%c", B[i], " \n"[i == D]);
	return 0;
}

C - Ants on a Circle

这是某一道经典题的变形,经典题为:

  • 在一根长度为 \(L\) 的棍子上,有 \(N\) 只蚂蚁,位置互不相同且朝向左或右,其中蚂蚁的行为与本题相同。

  • 求出 \(T\) 时刻后每只蚂蚁的位置(如果蚂蚁从一端掉下请求出它从哪一段掉下)。

这道经典题的做法是:

  • 假设棍子的左右两端都是无限长的,这样暂时不需要考虑掉下的情况。

  • 在两只蚂蚁碰撞时,我们假设蚂蚁其实没有掉头,而是直接穿过彼此。

  • 这样我们可以在 \(\mathcal O (N)\) 的复杂度内,直接算出 \(T\) 时刻后哪些位置还存在蚂蚁,将这些位置排序,花费 \(\mathcal O (N \log N)\)

  • 注意到在任意时刻,无论是有发生碰撞还是没有发生碰撞,所有蚂蚁之间的相对位置都不会改变。

  • 所以初始时在最左侧的蚂蚁,在最终时刻的位置一定是最小的那个位置,以此类推。

  • 最后如果位置超出了原棍子的范围,那么蚂蚁就是掉下了棍子。

在本题中也是类似的,我们假设蚂蚁是穿过了彼此,这样可以求出 \(T\) 时刻后哪些位置上还存在蚂蚁。

但是蚂蚁之间的相对位置无法比较显然地确定,我们仅知道蚂蚁在环上的相对位置是与原来循环同构的。

我们考虑:一开始第 \(i\) 只蚂蚁携带着一个显示着数值 \(i\) 的电子屏,两只蚂蚁碰撞时看作它们穿过了彼此,但电子屏上的数值交换。

也就是说任意时刻蚂蚁携带的电子屏上的数值,就是实际上该蚂蚁的编号(我们假设每只蚂蚁编号从不改变,仅有显示屏变化)。

我们考虑,碰撞时,左侧的蚂蚁携带的电子屏上的数值实际上是增加了 \(1\),除非数值为 \(N\),那样必然是与携带着数值 \(1\) 的蚂蚁碰撞。

同理右侧的蚂蚁携带的数值减少了 \(1\),除非数值为 \(1\),那样会变成 \(N\)

但是,如果在模 \(N\) 的剩余系下考虑,就仅仅是左侧的蚂蚁携带的数值增加 \(1\),右侧的蚂蚁携带的数值减少 \(1\) 而已。

最终取所在剩余类中,在 \([1, N]\) 范围内的元素作为真实值即可。

此时也就是要求每只蚂蚁与反向的蚂蚁碰撞的次数,如果该蚂蚁是朝顺时针方向(正方向)的,其编号增加碰撞次数,否则减少。

这可以通过在另一朝向的所有蚂蚁中计算绕过了几圈,并对剩余的不足一圈的蚂蚁执行二分查找得出。

时间复杂度为 \(\mathcal O (N \log N)\)

#include <cstdio>
#include <algorithm>

typedef long long LL;
const int MN = 100005;

int N, L, T, X[MN], W[MN], Y[MN], Id[MN];
int X1[MN], X2[MN], C1, C2;
int Ans[MN];

inline LL Q1(LL v) {
	LL w = (v % L + L) % L, k = (v - w) / L;
	return std::upper_bound(X1, X1 + C1, w) - X1 + k * C1;
}
inline LL Q2(LL v) {
	LL w = (v % L + L) % L, k = (v - w) / L;
	return std::upper_bound(X2, X2 + C2, w) - X2 + k * C2;
}

int main() {
	scanf("%d%d%d", &N, &L, &T);
	for (int i = 0; i < N; ++i) {
		scanf("%d%d", &X[i], &W[i]);
		if (W[i] == 1) X1[C1++] = X[i];
		if (W[i] == 2) X2[C2++] = X[i];
	}
	for (int i = 0; i < N; ++i) {
		Y[i] = X[i];
		if (W[i] == 1) {
			Y[i] = (Y[i] + T) % L;
			Id[i] = (i + Q2(X[i] + 2ll * T) - Q2(X[i])) % N;
		}
		if (W[i] == 2) {
			Y[i] = (Y[i] - T % L + L) % L;
			Id[i] = ((i - Q1(X[i] - 1) + Q1(X[i] - 2ll * T - 1)) % N + N) % N;
		}
		Ans[Id[i]] = Y[i];
	}
	for (int i = 0; i < N; ++i) printf("%d\n", Ans[i]);
	return 0;
}

D - Piling Up

我们假设初始时红砖的数量为 \(x_0\),则蓝砖的数量应为 \(N - x_0\)

同时,我们记第 \(i\) 次操作后的红砖的数量为 \(x_i\)

观察第 \(i + 1\) 次操作时,可能执行的四种情况:

  1. 操作 RR:先拿出一块红砖,然后塞入一红一蓝两砖,再拿出一块红砖
    此操作仅能在 \(x_i > 0\) 时被执行,并且 \(x_{i + 1} = x_i - 1\)\(\boldsymbol{x}\) 减少 \(\boldsymbol{1}\))。
  2. 操作 RB:先拿出一块红砖,然后塞入一红一蓝两砖,再拿出一块蓝砖
    此操作仅能在 \(x_i > 0\) 时被执行,并且 \(x_{i + 1} = x_i\)不变)。
  3. 操作 BR:先拿出一块蓝砖,然后塞入一红一蓝两砖,再拿出一块红砖
    此操作仅能在 \(x_i < N\) 时被执行,并且 \(x_{i + 1} = x_i\)不变)。
  4. 操作 BB:先拿出一块蓝砖,然后塞入一红一蓝两砖,再拿出一块蓝砖
    此操作仅能在 \(x_i < N\) 时被执行,并且 \(x_{i + 1} = x_i + 1\)\(\boldsymbol{x}\) 增加 \(\boldsymbol{1}\))。

对于一种操作序列,根据上面的 \(x_i\) 关于 \(x_0\) 的表达式以及不等式,我们可以确定出 \(x_0\) 对应的区间(或为空集,此时序列不合法)。

我们考虑直接进行 DP:令 \(\mathrm{f}[i][j]\) 表示 \(x_i = j\) 时前 \(i\) 次操作的方案数,初始化 \(\mathrm{f}[0][\ast] = 1\),答案就是 \(\mathrm{f}[N][\ast]\) 之和,转移显然。

错误!这样计算会使得一个合法的操作序列被统计多次,也就是上面的 \(x_0\) 可能的区间内至少包含两个整数的情况。

我们不妨考虑必须在 \(x_0\) 可能到达的最小值处统计一个操作序列。这意味着 \(x_0\) 哪怕再减小 \(1\),操作序列都将变得不合法。

体现在 DP 过程中,即是存在一个时刻 \(j\) 值曾经到达过 \(0\),或者为 \(1\) 时执行了 RB 操作。

所以我们更换状态定义,令 \(\mathrm{f}[i][j]\) 表示 \(x_i = j\) 时前 \(i\) 次操作的方案数,限制是 \(j\) 从未到达过最小值。

同时定义 \(\mathrm{g}[i][j]\) 表示类似含义,但限制是 \(j\) 到达过至少一次最小值。

转移仍然显然,答案即为 \(\mathrm{g}[N][\ast]\) 之和。时间复杂度为 \(\mathcal O (N M)\)

#include <cstdio>

typedef long long LL;
const int Mod = 1000000007;
const int MN = 3005;

int N, M;
int f[2][MN], g[2][MN], Ans;

int main() {
	scanf("%d%d", &N, &M);
	int o = 0;
	for (int i = 1; i <= N; ++i) f[o][i] = 1;
	g[o][0] = 1;
	for (int i = 1; i <= M; ++i) {
		o ^= 1;
		for (int j = 0; j <= N; ++j) f[o][j] = g[o][j] = 0;
		for (int j = 0; j <= N; ++j) {
			if (j) {
				f[o][j] = (f[o][j] + f[o ^ 1][j - 1]) % Mod;
				g[o][j] = ((LL)g[o][j] + g[o ^ 1][j - 1] + g[o ^ 1][j]) % Mod;
				if (j == 1) g[o][j] = (g[o][j] + f[o ^ 1][j]) % Mod;
				else f[o][j] = (f[o][j] + f[o ^ 1][j]) % Mod;
			}
			if (j < N) {
				g[o][j] = ((LL)g[o][j] + g[o ^ 1][j + 1] + g[o ^ 1][j]) % Mod;
				if (!j) g[o][j] = (g[o][j] + f[o ^ 1][j + 1]) % Mod;
				else f[o][j] = ((LL)f[o][j] + f[o ^ 1][j + 1] + f[o ^ 1][j]) % Mod;
			}
		}
	}
	for (int j = 0; j <= N; ++j) Ans = (Ans + g[o][j]) % Mod;
	printf("%d\n", Ans);
	return 0;
}

E - Placing Squares

寻求组合意义:

  • \(N + 1\) 个间隔(包含位置为 \(0\)\(N\) 的间隔)中放置若干个隔板。

  • 其中位置 \(0\)\(N\) 必须放置隔板,且有 \(M\) 个位置禁止放置隔板。

  • 对于 \(N\) 个格子,每个格子中可以放球,蓝球或者红球。

  • 特别地需要满足:在相邻两个隔板间的每个格子中,蓝球的数量恰好为 \(1\),红球的数量也恰好为 \(1\)

不难验证,对于一种放置隔板的方案,放球的方案数恰好为 \(\displaystyle \prod_{i = 1}^{k} {(a_i)}^2\),其中 \(k\) 为正方形个数,\(a_i\) 为第 \(i\) 个正方形的边长。

此时我们即是要统计上述放置隔板和球的方案数。

我们可以令 \(\mathrm{f}[i][j]\) 表示仅考虑了前 \(i\) 个格子和前 \(i + 1\) 个间隔(也就是考虑到第 \(i\) 个格子右侧的间隔)时,且当最后一个隔板的右边的球数恰好为 \(j\) 时的放置方案数。

显然 \(j\) 的取值为 \([0, 2]\)。我们容易写出 \(\mathrm{f}[i][\ast]\)\(\mathrm{f}[i - 1][\ast]\) 转移的式子,有两种转移,取决于第 \(i\) 个格子右侧是否禁止了放置隔板。

注意到这两种转移关于 \(\mathrm{f}[i]\) 看作向量后,都是线性变换,可以被表示为矩阵的形式。

那么也就是有 \(N\)\(\boldsymbol{A}\) 矩阵连乘,然而实际上其中有 \(\mathcal O (M)\) 个被替换成了 \(\boldsymbol{B}\) 矩阵,求一向量乘矩阵的结果。

显然使用矩阵快速幂可以做到 \(\mathcal O (M \log N)\) 的时间复杂度。

#include <cstdio>

typedef long long LL;
const int Mod = 1000000007;

#define F(i) for (int i = 0; i < 3; ++i)
struct Mat {
	int a[3][3];
	Mat() {}
	Mat(int v) {
		F(i) F(j) a[i][j] = (i == j) * v;
	}
	int * operator [] (int i) {
		return a[i];
	}
	friend Mat operator * (Mat a, Mat b) {
		Mat c(0);
		F(i) F(k) F(j) c[i][j] = (c[i][j] + (LL)a[i][k] * b[k][j]) % Mod;
		return c;
	}
};

int N, M;
Mat A, B, Ans(1);

int main() {
	A[0][0] = 2, A[0][1] = 1, A[0][2] = 1;
	A[1][0] = 2, A[1][1] = 1, A[1][2] = 0;
	A[2][0] = 1, A[2][1] = 1, A[2][2] = 1;
	B[0][0] = 1, B[0][1] = 0, B[0][2] = 0;
	B[1][0] = 2, B[1][1] = 1, B[1][2] = 0;
	B[2][0] = 1, B[2][1] = 1, B[2][2] = 1;
	scanf("%d%d", &N, &M);
	int lst = 0;
	for (int i = 1, x; i <= M; ++i) {
		scanf("%d", &x);
		int e = x - lst - 1;
		Mat C = A;
		for (; e; e >>= 1, C = C * C)
			if (e & 1) Ans = C * Ans;
		Ans = B * Ans;
		lst = x;
	}
	int e = N - lst - 1;
	Mat C = A;
	for (; e; e >>= 1, C = C * C)
		if (e & 1) Ans = C * Ans;
	Ans = B * Ans;
	printf("%d\n", Ans[2][0]);
	return 0;
}

F - Two Faced Cards

\(N\) 加上 \(1\),此时有 \(X\) 牌堆中仅有 \(N - 1\) 张牌,而 \(Y\) 牌堆中有 \(N\) 张牌,而 \(Z\) 牌堆中仍有 \(Q\) 张牌。

注意到我们只关心 \(X, Z\) 牌堆中的数值,相对于 \(Y\) 牌堆中的数值的相对大小关系。

我们先把牌堆 \(Y\) 中的牌变为 \(\{1, 2, 3, \ldots , N\}\),然后用二分查找将 \(X, Z\) 牌堆中的数值进行变换,不改变相对于 \(Y\) 的大小关系。

此时 \(X, Z\) 牌堆中的数值的值域为 \([1, N + 1]\)

接下来考虑这个小问题:

  • 给定两个序列 \(a_1 \sim a_n\)\(b_1 \sim b_n\),问能否将 \(a, b\) 任意进行排列,使得最后对于任意的 \(i\) 均有 \(a_i \le b_i\) 被满足。

显然我们可以对 \(a, b\) 从小到大排序然后依次检查。但是本题中我们采用这样一个特殊的方法:

  • 令数组 \(\mathrm{s}\) 初始为全 \(0\)
  • 对于每个 \(a_i\),将 \(\mathrm{s}[a_i], \mathrm{s}[a_i + 1], \mathrm{s}[a_i + 2], \ldots\) 全部加上 \(1\),即执行一个后缀加。
  • 对于每个 \(b_i\),将 \(\mathrm{s}[b_i], \mathrm{s}[b_i + 1], \mathrm{s}[b_i + 2], \ldots\) 全部减去 \(1\),即执行一个后缀减。
  • 最终 \(\mathrm{s}\) 数组中的每个元素必须都大于等于 \(0\)

不难验证这个方法的正确性。回到原问题:

因为要最大化取正面的牌数,我们假设 \(X\) 牌堆中的所有牌初始时都是取正面的,也就是取了 \(A_i\)

然后 \(Y\) 牌堆中的牌依次为 \(C_i\)。所以可以用一次前缀和计算出此时的 \(\mathrm{s}\) 数组(注意值域只有 \(N\),所以 \(\mathrm{s}\) 的有意义下标为 \([1, N]\))。

然而此时 \(\mathrm{s}\) 数组一定不满足全部元素 \(\ge 0\) 的性质。我们有若干补救措施:

  1. 对于 \(X\) 牌堆中的所有牌,如果 \(B_i < A_i\),我们可以花费 \(1\) 代价把它翻成反面,令 \(\mathrm{s}\) 中下标在 \([B_i, A_i)\) 中的元素加 \(1\)
  2. 对于 \(Z\) 牌堆中的一张牌(此时是处理 \(Q\) 个询问),选择 \(x = D_i \text{ or } E_i\),令 \(\mathrm{s}\) 中下标在 \([x, N]\) 中的元素加 \(1\)

对于 \(x = D_i \text{ or } E_i\),可以两种情况都询问一次,然后合并,所以变成仅仅是给定一个 \(x\),下标 \([x, N]\)\(1\)

我们把 \([x, N]\)\(1\) 的操作撤销掉,现在变成仅使用 \([B_i, A_i)\)\(1\) 的操作,使得:

  • 对于 \(i < x\)\(\mathrm{s}[i] \ge 0\);且对于 \(i \ge x\)\(\mathrm{s}[i] \ge -1\)

小结一下,此时有一个有正有负的数组 \(\mathrm{s}[1 \sim N]\),有若干区间 \([B_i, A_i)\),和一个不确定的数 \(x\)
我们需要选若干区间把 \(\mathrm{s}\) 对应区间内元素加上 \(1\) 然后使得 \(\mathrm{s}[i] \ge -[i \ge x]\),选择的区间个数越少越好。

从后往前考虑 \(\mathrm{s}\) 中的元素,当遇到第一个 \(\le -2\) 的元素时,考察包含它的所有区间,对于左端点最左的区间,选取它是不劣的。

这样一直下去直到 \(\mathrm{s}\) 中的所有元素都 \(\ge -1\) 为止,此时还有一些区间没有被使用。

注意如果在这个过程中,有一个需要被区间覆盖的点上的区间已经用光了,则所有答案均为 \(-1\)

然后考虑对所有可能的 \(x\) 都计算答案。例如如果前 \(p\)\(\mathrm{s}\) 中的元素都大于等于 \(0\),则 \(\mathrm{Ans}[1 \sim (p + 1)]\) 为上一步选取的区间数。

从前往后考虑 \(\mathrm{s}\) 中的元素,当遇到第一个等于 \(-1\) 的元素时,考察包含它的所有区间,对于右端点最右的区间,选取它是不劣的。

每选取一个区间就可以更新一些 \(x\) 的答案了。

注意如果在这个过程中,有一个需要被区间覆盖的点上的区间已经用光了,则目前还没更新的 \(x\) 的答案就为 \(-1\)

求出每个 \(x\) 对应的最少选取的区间数后,对于每个询问即可枚举是选择 \(D_i\) 还是 \(E_i\) 然后计算答案了。

当需要求出一个需要被区间覆盖的点上的左端点最左(右同理)的区间时,可以使用优先队列维护(std::priority_queue)。

时间复杂度为 \(\mathcal O ((N + Q) \log N)\)

#include <cstdio>
#include <algorithm>
#include <queue>

const int MN = 100005, MQ = 100005;

int N, Q, nA[MN], nB[MN], nC[MN], qD[MQ], qE[MQ];
inline void tr(int &x) { x = std::lower_bound(nC + 1, nC + N + 1, x) - nC; }

int A[MN], B[MN], used[MN];
std::vector<int> R[MN];
std::priority_queue<std::pair<int, int>> pq;
int Ans[MN];

int main() {
	scanf("%d", &N), ++N;
	for (int i = 1; i < N; ++i) scanf("%d%d", &nA[i], &nB[i]);
	for (int i = 1; i <= N; ++i) scanf("%d", &nC[i]);
	scanf("%d", &Q);
	for (int i = 1; i <= Q; ++i) scanf("%d%d", &qD[i], &qE[i]);
	std::sort(nC + 1, nC + N + 1);
	for (int i = 1; i < N; ++i) tr(nA[i]), tr(nB[i]);
	for (int i = 1; i <= Q; ++i) tr(qD[i]), tr(qE[i]);
	for (int i = 1; i < N; ++i) ++A[nA[i]];
	for (int i = 1; i <= N; ++i) A[i] += A[i - 1] - 1;
	for (int i = 1; i < N; ++i)
		if (nA[i] > nB[i]) R[nA[i] - 1].push_back(i);
		else used[i] = 1;
	for (int i = N, S = 0; i >= 1; --i) {
		for (int j : R[i]) pq.push({-nB[j], j});
		S += B[i];
		while (A[i] + S < -1) {
			while (!pq.empty() && -pq.top().first > i) pq.pop();
			if (pq.empty()) {
				while (Q--) puts("-1");
				return 0;
			}
			auto p = pq.top(); pq.pop();
			int j = p.second, l = nB[j], r = nA[j] - 1;
			++S, ++B[r], --B[l - 1], used[j] = 1, ++Ans[1];
		}
	}
	for (int i = N; i >= 1; --i) A[i] += B[i] += B[i + 1];
	for (int i = 1; i <= N; ++i) B[i] = 0, R[i].clear();
	for (int i = 1; i < N; ++i) if (!used[i]) R[nB[i]].push_back(i);
	for (int i = 1, S = 0; i <= N; ++i) {
		Ans[i + 1] = Ans[i];
		for (int j : R[i]) pq.push({nA[j] - 1, j});
		S += B[i];
		while (A[i] + S == -1) {
			while (!pq.empty() && pq.top().first < i) pq.pop();
			if (pq.empty()) {
				for (int j = i; j <= N; ++j) Ans[j + 1] = N + 1;
				goto where;
			}
			auto p = pq.top(); pq.pop();
			int j = p.second, l = nB[j], r = nA[j] - 1;
			++S, ++B[l], --B[r + 1], ++Ans[i + 1];
		}
	}
	where:
	for (int i = 1; i <= Q; ++i) {
		int ans = -1;
		ans = std::max(ans, N - Ans[qD[i]]);
		ans = std::max(ans, N - Ans[qE[i]] - 1);
		printf("%d\n", ans);
	}
	return 0;
}
posted @ 2020-08-02 00:52  粉兔  阅读(874)  评论(0编辑  收藏  举报