Typesetting math: 0%

AtCoder Grand Contest 012

题目传送门:AtCoder Grand Contest 012

A - AtCoder Group Contest

从大到小排序后选择 a2,a4,,a2Na2,a4,,a2N

#include <cstdio>
#include <algorithm>

typedef long long LL;
const int MN = 300005;

int N, A[MN];
LL Ans;

int main() {
	scanf("%d", &N), N *= 3;
	for (int i = 1; i <= N; ++i) scanf("%d", &A[i]);
	std::sort(A + 1, A + N + 1);
	for (int i = N / 3 + 1; i <= N; i += 2) Ans += A[i];
	printf("%lld\n", Ans);
	return 0;
}

B - Splatter Painting

注意到 d 的范围只有 10,考虑到一个 d=d0 的操作等价于它以及它周围所有点的 d=d01 的操作。

从大到小处理 d,每次把 d0 通过一个 O(N+M) 的操作推向 d01 然后再加入 d=d01 的操作。

时间复杂度为 O((N+M+Q)maxdi)

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

const int MN = 100005, MQ = 100005;

int N, M;
std::vector<int> G[MN];
int Q, qv[MQ], qd[MQ], qc[MQ];
int tag[MN], tmp[MN];

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);
	}
	scanf("%d", &Q);
	for (int i = 1; i <= Q; ++i) scanf("%d%d%d", &qv[i], &qd[i], &qc[i]);
	for (int j = 10; j >= 0; --j) {
		for (int u = 1; u <= N; ++u) tmp[u] = 0;
		for (int u = 1; u <= N; ++u) if (tag[u])
			for (int v : G[u]) tmp[v] = std::max(tmp[v], tag[u]);
		for (int u = 1; u <= N; ++u) tag[u] = std::max(tag[u], tmp[u]);
		for (int i = 1; i <= Q; ++i) if (qd[i] == j) tag[qv[i]] = std::max(tag[qv[i]], i);
	}
	for (int u = 1; u <= N; ++u) printf("%d\n", qc[tag[u]]);
	return 0;
}

C - Tautonym Puzzle

考虑 1k 的排列 p1,p2,,pk,若 s 序列为 [p1,p2,,pk,1,2,,k],则好子序列数量恰好为 p 中的非空递增子序列数量。

考虑一个 1(k1) 的排列 q,令其递增子序列(可以为空)的数量为 c,则:

  • p=[k]+q,则 p 的递增子序列(可以为空)的数量应为 c+1
  • p=q+[k],则 p 的递增子序列(可以为空)的数量应为 c×2

特别地,空序列的递增子序列(可以为空)的数量为 1

N 加上 1,我们要求即是一个排列 p 满足递增子序列(可以为空)的数量恰好为 N

根据上面的结论,不难想到一个按照 N 的二进制位从高到低进行递归构造的做法,s 的长度不超过 4log2n160

#include <cstdio>
#include <vector>

typedef long long LL;

std::vector<int> Solve(LL N) {
	if (N == 1) return std::vector<int>();
	if (N & 1) {
		auto v = Solve(N - 1);
		v.insert(v.begin(), (int)v.size() + 1);
		return v;
	}
	auto v = Solve(N / 2);
	v.push_back((int)v.size() + 1);
	return v;
}

int main() {
	LL N;
	scanf("%lld", &N);
	auto Ans = Solve(N + 1);
	printf("%d\n", 2 * (int)Ans.size());
	for (int x : Ans) printf("%d ", x);
	for (int i = 1; i <= (int)Ans.size(); ++i) printf("%d ", i);
	puts("");
	return 0;
}

D - Colorful Balls

我们考虑把每个球初始在的位置的下标写在这个球上,那么交换两个球的位置就可以看成交换两个球上的数。

如果两个球可以交换,那么我们在它们之间连边。显然我们只需对每个连通分量分别考虑即可。

对于一个连通分量,考虑它的任意一棵生成树,不难发现仅通过树边上的交换操作就可以到达任意排列所对应的状态。

也就是说对于一个连通分量对应回原序列的下标位置,其任意一个颜色排列均可以被达到。

所以方案数是一个每个颜色的出现次数构成的多重组合数。

朴素的实现的边数是 O(N2) 的,接下来的问题在于减少边数,使得连通分量的性质仍然保留。

对于每种颜色,按照重量从小到大对这种颜色的所有球排序,记颜色为 c 的所有球中重量最小的为 min[c]

那么对于同种颜色球之间的连边 uv,如果没有经过 min[c],则转化为 umin[c]v 绝不会更劣。

也就是对于每种颜色 c,每个非 min[c] 的球尝试向 min[c] 连边即可。

对于非同种颜色的球之间的连边,令 kmin[c] 的重量最小的颜色 c

则如果两非颜色 k 的球 u,v 之间连边,转化为 umin[k]v 绝不会更劣。

而如果 u,v 之一的颜色为 k,令 kmin[c] 的重量次小的颜色 c

  • 如果 u,v 的颜色均不为 k,转化为 umin[k]v 绝不会更劣。
  • 如果 u,v 中其一颜色为 k,另一颜色为 k,不妨设 u 的颜色为 k,转化为 umin[k]min[k]v 绝不会更劣。

于是,我们仅需处理出 kk,然后将每个与 kk 不同颜色的球向 min[k]min[k] 连边即可。

总边数控制在 O(N),然后运行 DFS 找出这张图的每个连通分量,计算多重组合数贡献给答案即可。

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

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

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;
}

int Fac[MN], iFac[MN];
inline void Init(int N) {
	Fac[0] = 1;
	for (int i = 1; i <= N; ++i) Fac[i] = (LL)Fac[i - 1] * i % Mod;
	iFac[N] = qPow(Fac[N], Mod - 2);
	for (int i = N; i >= 1; --i) iFac[i - 1] = (LL)iFac[i] * i % Mod;
}

int N, X, Y, p1, p2, C;
std::vector<int> V[MN], G[MN];
int index[MN], col[MN];
inline void Ins(int x, int y) {
	G[x].push_back(y);
	G[y].push_back(x);
}

int vis[MN], buk[MN], stk[MN], tp;
void DFS(int u) {
	vis[u] = 1, ++buk[col[u]], stk[++tp] = u;
	for (int v : G[u]) if (!vis[v]) DFS(v);
}

int main() {
	scanf("%d%d%d", &N, &X, &Y);
	Init(N);
	for (int i = 1, x, y; i <= N; ++i) {
		scanf("%d%d", &x, &y);
		V[x].push_back(y);
	}
	for (int i = 1; i <= N; ++i) if (!V[i].empty()) {
		std::sort(V[i].begin(), V[i].end());
		if (!p1) p1 = i;
		else {
			if (!p2 || V[p2][0] > V[i][0]) p2 = i;
			if (V[p1][0] > V[p2][0]) std::swap(p1, p2);
		}
		int s = V[i].size();
		index[i] = C += s;
		for (int j = 0; j < s; ++j) col[C - j] = i;
	}
	for (int i = 1; i <= N; ++i) if (!V[i].empty()) {
		int s = V[i].size(), c = index[i];
		for (int j = 1; j < s && V[i][0] + V[i][j] <= X; ++j) Ins(c, c - j);
		if (p1 && i != p1)
			for (int j = 0; j < s && V[p1][0] + V[i][j] <= Y; ++j) Ins(index[p1], c - j);
		if (p2 && i == p1)
			for (int j = 1; j < s && V[p2][0] + V[i][j] <= Y; ++j) Ins(index[p2], c - j);
	}
	int Ans = 1;
	for (int i = 1; i <= N; ++i) if (!vis[i]) {
		DFS(i);
		int s = 0;
		for (int x = 0; tp; --tp) if (buk[x = col[stk[tp]]])
			s += buk[x], Ans = (LL)Ans * iFac[buk[x]] % Mod, buk[x] = 0;
		Ans = (LL)Ans * Fac[s] % Mod;
	}
	printf("%d\n", Ans);
	return 0;
}

E - Camel and Oases

k=log2V+1,则骆驼可以跳跃 k 次。而骆驼的储水量,包括初始的那次,依次为 v0vk,其中 vj=V/2j

我们对每个 j,预处理在每个位置向左向右走,跨过长度不超过 vj 的段,能走到的区间的左右端点。这部分复杂度为 O(NlogV)

考虑骆驼能到达到每个位置的条件,应该是能将 n 个位置划分成 k+1 个连续段,使得每一段对应一个互不相同的 vj0jk),必须满足每一段内的位置都能在其对应的 vj 的限制下通过走路能互相到达。

而骆驼从一个起点出发能到达每个位置,就是这个起点必须在 v0 所在的段内。

我们考虑枚举起点,求出它只通过走路能到达的区间的左右端点 ,r,那么该区间的左右两边就可以分别考虑。

枚举在左侧需要的 j 的集合 S{1,2,,k},则右侧能够使用的集合就是 {1,2,,k}S

我们通过状压 DP 可以处理出 f[S] 表示仅通过集合 S 中的 j,最长可以覆盖到多长的前缀位置。同理定义后缀的 DP 数组 fr

那么如果 f[S] 的位置在 右侧,并且 fr[{1,2,,k}S] 的位置在 r 左侧,这个起点就是能到达每个位置的。

DP 的时间复杂度为 O(k2k)=O(VlogV)

最后实现时不能在每个位置上枚举 S,那样复杂度是 O(NV) 的。

而应该先枚举 S 然后处理出 f[S] 在每个位置右侧时 fr[{1,2,,k}S] 最左能到的位置,然后利用这个信息再进行枚举。

总时间复杂度为 O((N+V)logV)

#include <cstdio>
#include <algorithm>

const int MN = 200005, MM = 18;

int N, V, A[MN], v[MM], M;
int L[MM + 1][MN], R[MM + 1][MN];
int fl[1 << MM], fr[1 << MM];
int val[MN];

int main() {
	scanf("%d%d", &N, &V);
	for (int i = 1; i <= N; ++i) scanf("%d", &A[i]);
	for (int x = V; x; ) v[M++] = x /= 2;
	for (int j = 0; j <= M; ++j) {
		int nv = j ? v[j - 1] : V;
		L[j][0] = L[j][1] = 1, R[j][N + 1] = R[j][N] = N;
		for (int i = 2; i <= N; ++i) L[j][i] = A[i] - A[i - 1] > nv ? i : L[j][i - 1];
		for (int i = N - 1; i >= 1; --i) R[j][i] = A[i + 1] - A[i] > nv ? i : R[j][i + 1];
	}
	fl[0] = 0, fr[0] = N + 1;
	for (int S = 1; S < 1 << M; ++S) {
		fl[S] = 0, fr[S] = N + 1;
		for (int j = 0; j < M; ++j) if (S >> j & 1)
			fl[S] = std::max(fl[S], R[j + 1][fl[S ^ 1 << j] + 1]),
			fr[S] = std::min(fr[S], L[j + 1][fr[S ^ 1 << j] - 1]);
	}
	for (int i = 0; i <= N; ++i) val[i] = N + 2;
	for (int S = 0; S < 1 << M; ++S)
		val[fl[S]] = std::min(val[fl[S]], fr[((1 << M) - 1) ^ S]);
	for (int i = N - 1; i >= 0; --i) val[i] = std::min(val[i], val[i + 1]);
	for (int i = 1; i <= N; ++i) puts(val[L[0][i] - 1] <= R[0][i] + 1 ? "Possible" : "Impossible");
	return 0;
}

F - Prefix Median

a 从小到大排序。我们先考虑 ai 互不相同的情况,分析此时 bi 需要满足的条件。

首先有 aibia2Ni,这是显然的,极值在全选最小或最大的 2i1 个元素时取到。

还有一个很关键的性质,因为每次只添加两个新元素,所以 bi 相比 bi1,在选 2i1 个数时的集合内,相对位置只会变化最多 1

形式化地说就是,对于 j<i,不允许出现 bj 的值在 bibi+1 之间的情况,也就是不允许 bi<bj<bi+1bi>bj>bi+1

我们可以证明上述两个条件就是充要条件。具体的证明过程是倒序考虑:

每次删除 ai 中的两个数,使得 bi1i<N)在剩下的 2N3 个数中的相对位置在 [i,2Ni2] 内。

也就是剩下的 b1bN1 还必须满足第一个条件(第二个条件显然会满足)。

具体过程在此不详细展开,主要思路是根据 bN1aN 的大小关系分成三类来讨论。

对于 ai 可能相同的情况,也是类似的,仍然是 aibia2Ni,且不允许 bi<bj<bi+1bi>bj>bi+1,注意不取等号。

那么我们考虑按照 bN,bN1,,b1 的顺序倒序依次确定 b 中的元素。

考虑到第二个条件,也就是如果我们按照 bNb1 的顺序,其中“跳过”了某个元素 x,即 bi+1<x<bi(或反过来),则 x 不能在之后的 b 序列中再出现了。

f[i][a][b] 表示考虑了 bNbi 了,即合法区间是 ai1a2Ni+1,且此时小于 bi可以被选择的不同元素个数为 a,大于 bi可以被选择的不同元素个数为 b 时,b1bi1 的方案数。

转移时加入 ai1a2Ni+1,并据此更新 a,b 的值,枚举选取 (1+a+b) 个数中的哪一个,总时间复杂度 O(N4)

答案即为 f[N][0][0]

#include <cstdio>
#include <cstring>
#include <algorithm>

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

int N, A[MN * 2];

int f[MN][MN * 2][MN * 2];
int DP(int i, int a, int b) {
	if (i == 1) return 1;
	if (~f[i][a][b]) return f[i][a][b];
	int na = a + (A[i - 1] != A[i]);
	int nb = b + (A[2 * N - i + 1] != A[2 * N - i]);
	LL v = DP(i - 1, na, nb);
	for (int j = 1; j <= na; ++j) v = (v + DP(i - 1, na - j, nb + 1)) % Mod;
	for (int j = 1; j <= nb; ++j) v = (v + DP(i - 1, na + 1, nb - j)) % Mod;
	return f[i][a][b] = v % Mod;
}

int main() {
	scanf("%d", &N);
	for (int i = 1; i <= 2 * N - 1; ++i) scanf("%d", &A[i]);
	std::sort(A + 1, A + 2 * N);
	memset(f, -1, sizeof(f));
	printf("%d\n", DP(N, 0, 0));
	return 0;
}
posted @   粉兔  阅读(299)  评论(0编辑  收藏  举报
编辑推荐:
· .NET 原生驾驭 AI 新基建实战系列:向量数据库的应用与畅想
· 从问题排查到源码分析:ActiveMQ消费端频繁日志刷屏的秘密
· 一次Java后端服务间歇性响应慢的问题排查记录
· dotnet 源代码生成器分析器入门
· ASP.NET Core 模型验证消息的本地化新姿势
阅读排行:
· ThreeJs-16智慧城市项目(重磅以及未来发展ai)
· .NET 原生驾驭 AI 新基建实战系列(一):向量数据库的应用与畅想
· Ai满嘴顺口溜,想考研?浪费我几个小时
· Browser-use 详细介绍&使用文档
· 软件产品开发中常见的10个问题及处理方法
点击右上角即可分享
微信分享提示