CodeForces Contest #1137: Round #545 (Div. 1)

比赛传送门:CF #1137

比赛记录:点我

每次都自闭的 div1 啊,什么时候才能上 IM 呢。


【A】Skyscrapers

题意简述:

有一个 \(n\times m\) 的矩阵 \(a_{ij}\)

对于每个 \((i,j)\)\(1\le i\le n\)\(1\le j\le m\)),你把第 \(i\) 行和第 \(j\) 列单独抽出,这样就有 \(n+m-1\) 个数被你抽出。

你可以对这些数重新标号为正整数,但是要满足第 \(i\) 行所有数的大小关系不变,第 \(j\) 列所有数的大小关系不变(两个限制相互独立)。

满足这个限制下,你希望最大的标号尽量小,对每个 \((i,j)\) 求出这个最小的最大标号。

题解:

因为行大小关系不变,列大小关系不变,我们考虑分别离散化每行每列,并统计每个数在行内和列内的排名。

\((i,j)\) 在行内排名为第 \(x\) 小,列内排名为第 \(y\) 小,它最优情况下就被标号为 \(\max(x,y)\),然后行内、列内比它大的数依次往大排。

由此写出代码:

#include <cstdio>
#include <algorithm>

const int MN = 1005;

int N, M, A[MN][MN];
int X[MN][MN], Y[MN][MN];
int B[MN], C1[MN], C2[MN], C;

int main() {
	scanf("%d%d", &N, &M);
	for (int i = 1; i <= N; ++i)
		for (int j = 1; j <= M; ++j)
			scanf("%d", &A[i][j]);
	for (int i = 1; i <= N; ++i) {
		for (int j = 1; j <= M; ++j) B[j] = A[i][j];
		std::sort(B + 1, B + M + 1), C1[i] = C = std::unique(B + 1, B + M + 1) - B - 1;
		for (int j = 1; j <= M; ++j) X[i][j] = std::lower_bound(B + 1, B + C + 1, A[i][j]) - B;
	}
	for (int j = 1; j <= M; ++j) {
		for (int i = 1; i <= N; ++i) B[i] = A[i][j];
		std::sort(B + 1, B + N + 1), C2[j] = C = std::unique(B + 1, B + N + 1) - B - 1;
		for (int i = 1; i <= N; ++i) Y[i][j] = std::lower_bound(B + 1, B + C + 1, A[i][j]) - B;
	}
	for (int i = 1; i <= N; ++i, puts(""))
		for (int j = 1; j <= M; ++j)
			printf("%d ", std::max(X[i][j], Y[i][j]) + std::max(C1[i] - X[i][j], C2[j] - Y[i][j]));
	return 0;
}

【B】Camp Schedule

题意简述:

有两个 01 串 \(s\)\(t\),要求重新排列 \(s\) 使得 \(t\) 在重新排列后的 \(s\) 中出现次数尽量多(位置相交的出现也算)。

题解:

贪心匹配 \(t\),随便用什么方法算出 \(t\) 的最长真公共前后缀,我用了 KMP,然后贪心能选就选。

#include <cstdio>
#include <cstring>

const int MN = 500005;

int N, M, c0, c1;
char s[MN], t[MN];
int p[MN];

int main() {
	scanf("%s%s", s + 1, t + 1);
	N = strlen(s + 1), M = strlen(t + 1);
	for (int i = 1; i <= N; ++i)
		if (s[i] == '0') ++c0;
		else ++c1;
	int k = 0;
	for (int i = 2; i <= M; ++i) {
		while (k && t[k + 1] != t[i]) k = p[k];
		if (t[k + 1] == t[i]) p[i] = ++k;
	}
	k = 0;
	for (int i = 1; i <= N; ++i) {
		int r = t[k + 1] - '0';
		int q = r ? c1 ? 1 : 0 : c0 ? 0 : 1;
		if (q == r) if (++k == M) k = p[M];
		--(q ? c1 : c0);
		printf("%d", q);
	}
	return 0;
}

【C】Museums Tour

题意简述:

一个 \(n\) 个点 \(m\) 条边的简单有向图,一周有 \(d\) 天。

每个点都有一个博物馆,给出博物馆 \(i\) 在一周的第 \(j\) 天的开门情况(\(1\le i\le n\)\(1\le j\le d\))。

这周的第 \(1\) 天,你从 \(1\) 号点开始,每次走一条边需要花费 \(1\) 天,如果当前点的博物馆开着,你就可以进去游览。

问在足够长的时间后,你最多可以游览多少个不同的博物馆(一个点的博物馆游览多次只算一次)。

题解:

有两种做法,一种比较简单,一种比较复杂,我写的是复杂的。

考虑 \(\mathrm{f}[i][j]\) 表示一周的第 \(j\) 天在 \(i\) 号点,最终能游览多少个博物馆。

但是图上有环,转移比较困难。于是考虑强连通分量缩点。这样形成一个 DAG。

观察可以得到,一个缩点后的强连通分量,是拥有自己的“周期”的,即若第 \(j\) 天在这个强连通分量中,第 \(j+x\) 天也能在同一个点上,最小的正整数 \(x\) 便是周期。

这个周期一定是 \(d\) 的因数,因为 \(d\) 是所有强连通分量的总周期,周期可以这样计算:

  • 周期 \(\mathrm{period}\) 初始化为 \(d\)

  • 考虑这个强连通分量的 DFS 树,记录每个点的深度 \(\mathrm{dep}\)

  • 对于每一条边 \(u\to v\),令 \(\mathrm{period}=\gcd(\mathrm{period},\mathrm{dep}[u]-\mathrm{dep}[v]+1)\)

关于证明,只能感性理解。

这之后就可以 DP 了,DP 的状态变为 \(\mathrm{f}[i][j]\) 表示 \(i\) 号强连通分量中的时间戳为 \(j\) 的答案。

因为涉及到强连通分量内所有点的时间戳,需要对每个强连通分量设置一个基准时间戳以便互相转化,所幸我们可以用 \(\mathrm{dep}\bmod \mathrm{period}\) 来当作节点的相对时间戳,基准时间戳为 \(0\)

DP 时需要处理一个 \(\mathrm{cnt}[j]\) 数组表示当强连通分量时间戳为 \(j\) 时这个强连通分量内开启的不同博物馆个数。DP 时要格外注意不同强连通分量中时间戳的转换。

时间复杂度 \(\mathcal{O}((n+m)d)\)

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

const int MN = 100005, MD = 55;

int N, M, D;
std::vector<int> G[MN], T[MN];
char s[MD]; int w[MN][MD];

int dfn[MN], low[MN], stk[MN], instk[MN], tp, dfc;
int dep[MN], bel[MN], per[MN], tic[MN], itic[MN], bcnt;
void Tarjan(int u) {
	low[u] = dfn[u] = ++dfc;
	stk[++tp] = u, instk[u] = 1;
	for (auto v : G[u]) {
		if (!dfn[v]) {
			dep[v] = dep[u] + 1, Tarjan(v);
			low[u] = std::min(low[u], low[v]);
		}
		else if (instk[v]) low[u] = std::min(low[u], dfn[v]);
	}
	if (low[u] == dfn[u]) {
		++bcnt;
		for (int x = 0; x != u; --tp) {
			x = stk[tp];
			bel[x] = bcnt;
			T[bcnt].push_back(x);
			instk[x] = 0;
		}
	}
}

int f[MN][MD];

int main() {
	scanf("%d%d%d", &N, &M, &D);
	for (int i = 1; i <= M; ++i) {
		int u, v;
		scanf("%d%d", &u, &v);
		G[u].push_back(v);
	}
	for (int i = 1; i <= N; ++i)
		if (!dfn[i]) dep[i] = 1, Tarjan(i);
	for (int i = 1; i <= bcnt; ++i) per[i] = D;
	for (int u = 1; u <= N; ++u)
		for (auto v : G[u]) if (bel[u] == bel[v])
			per[bel[u]] = std::__gcd(per[bel[u]], std::abs(dep[u] - dep[v] + 1));
	for (int i = 1; i <= N; ++i)
		tic[i] = dep[i] % per[bel[i]], itic[i] = tic[i] ? per[bel[i]] - tic[i] : 0;
	for (int i = 1; i <= N; ++i) {
		scanf("%s", s);
		for (int j = 0; j < D; ++j) w[i][j] = s[j] - '0';
	}
	for (int id = 1; id <= bcnt; ++id) {
		int pr = per[id];
		static int cnt[MD];
		for (int j = 0; j < pr; ++j) cnt[j] = 0;
		for (int j = 0; j < pr; ++j) {
			for (auto u : T[id]) for (int k = 0; k < D; k += pr)
				if (w[u][(k + tic[u] + j) % D]) { ++cnt[j]; break; }
		}
		for (int j = 0; j < pr; ++j) {
			f[id][j] = cnt[j];
			for (auto u : T[id]) {
				int ticv = (j + tic[u] + 1) % pr;
				for (auto v : G[u]) if (bel[v] != id) {
					for (int k = 0; k < D; k += pr)
						f[id][j] = std::max(f[id][j], cnt[j] + f[bel[v]][(itic[v] + k + ticv) % per[bel[v]]]);
				}
			}
		}
	}
	printf("%d\n", f[bel[1]][itic[1]]);
	return 0;
}

// 19-03-10 00:18 ~ 19-03-10 00:54

而相对简单的做法是,将每个点在一周的每天拆成不同的点。这样有 \(n\times d\) 个点,每个点记作 \(\langle u,j\rangle\)\(u\) 号点在第 \(j\) 天拆出的点(\(1\le u\le n\)\(0\le j<d\))。每个点的权值为 \(0\)\(1\),取决于当天此点的博物馆是否开启。

对于一条边 \(u\to v\),将其拆成 \(d\) 条边,连接 \(\langle u,j\rangle\)\(\langle v,(j+1)\bmod d\rangle\)

这样再强连通分量缩点后,形成一个 DAG。每个缩点后的强连通分量权值为这个强连通分量中开启的不同博物馆个数,直接求最长路即可。

为什么不会重复统计?考虑重复统计的情况,即存在一条路径使得 \(\langle u,j_1\rangle\) 可以到达 \(\langle u,j_2\rangle\) 但不在同一个强连通分量中,这是不可能的,因为单独看这条路径经过的顶点编号序列,沿着这个序列再重复走 \(d-1\) 遍一定能回到 \(\langle u,j_1\rangle\)。这意味着如果同一顶点拆出的两点弱连通,它们必然也强连通,所以不需要担心重复统计的情况。

代码就不给了,比较简单。不过这个做法似乎空间翻好几番,要小心处理。


【D】Cooperative Game

题意简述:

这是一道交互题。

有一张 \(t+c\)\(1\le t,c\)\(t+c\le 1000\))个点的有向图,每个点只有一条出边。这张图由一个长度为 \(t\) 的链和一个长度为 \(c\) 的环构成。

即从起点(链的顶端)走 \(t-1\) 条边即可访问到链的尾端,从链的尾端再走 \(1\) 条边即可进入环中,记环上的这个点(从链进入的第一个点)为 \(\mathrm{finish}\)

你在起点(链的顶端)有 \(10\) 枚棋子,每次你可以令一部分棋子走一条边,然后交互库会告诉你哪些棋子在同一个节点内,哪些棋子在不同节点内。

你需要在不超过 \(3(t+c)\) 次操作后让所有棋子到达 \(\mathrm{finish}\) 并结束程序。

题解:

思路非常巧妙的一题。其实只需要 \(3\) 枚棋子即可完成任务。

考虑先移动 \(2\) 枚棋子,类似 Floyd 判圈算法,以及 Pollad-rho 算法的判圈过程,我们令一枚棋子速度为 \(2\),另一枚速度为 \(1\)

它们一定会在环上相遇,并且相遇时,较慢的棋子还没有走完一整个环,令相遇的点为 \(\mathrm{meet}\)

假设从 \(\mathrm{finish}\) 开始,走 \(x\) 条边第一次到达 \(\mathrm{meet}\),较慢的棋子走了 \(\mathrm{slow}\) 步,较快的棋子走了 \(\mathrm{fast}\) 步(显然 \(\mathrm{fast}=2\times\mathrm{slow}\)):
\(\mathrm{slow}=t+x\)\(2\times\mathrm{slow}=\mathrm{fast}=t+k\times c+x\)\(k\in\mathbb{Z}^+\)),整理得:
\(2(t+x)=t+k\times c+x\),即 \(t+x=k\times c\),即 \(t\equiv c-x\pmod{c}\)

\(c-x\) 即为 \(\mathrm{meet}\) 在环上剩余的步数,即从 \(\mathrm{meet}\)\(c-x\) 步又到达 \(\mathrm{finish}\)
所以有剩余的步数加上若干倍的 \(c\) 等于 \(t\)。也就是说,如果这时开始,我们每次让所有棋子同时走一条边,最终在 \(t\) 步之后就都会到达 \(\mathrm{finish}\),这正是我们要求的。

于是有了下面的代码:

#include <cstdio>

int t;
inline void gett() {
	fflush(stdout);
	scanf("%d", &t);
	for (int x = t; x; --x) scanf("%*s");
}

int main() {
	do {
		puts("next 0 1"), gett();
		puts("next 0"), gett();
	} while (t == 3);
	do {
		puts("next 0 1 2 3 4 5 6 7 8 9"), gett();
	} while (t == 2);
	puts("done");
	return 0;
}

【E】Train Car Selection

posted @ 2019-03-10 22:54  粉兔  阅读(738)  评论(3编辑  收藏  举报