AtCoder Beginner Contest 004
A
输出 \(N \times 2\) 。
B
题意
输入一个 \(4 \times 4\) 的矩形,输出它旋转 \(180\) 度的结果。
题解
只讨论 \(N \times N\) 的正方形,正方形的置换一般是通过两种操作实现的:
以下是笛卡尔坐标系。
通常优先从置换群角度考虑会容易。
再否则,考虑构造朴素的全等三角形。
再否则,角度比较麻烦的的情况考虑计算几何,坐标 rotate 存在公式。
这里只举几个置换群例子。
置换 \(f\) ,上下的镜面翻转 h[i][j] = g[N + 1 - i][j] :
置换 \(g\) ,左右的镜面翻转 h[i][j] = g[i][N + 1 - j] :
置换 \(h\) ,沿 y = x 翻转 h[i][j] = g[j][i] :
这 \(3\) 个置换复合后可以得到一些操作。
比如:\(h \circ g \circ V\) 可以实现顺时针旋转 \(90\) 度。其中 \(h \circ g\) 为 h[i][j] = g[N + 1 - j][i] 。
比如:\(h \circ f \circ V\) 可以实现顺时针旋转 \(180\) 度。其中 \(h \circ g\) 为 h[i][j] = g[N + 1 - i][N + 1 - j] 。
view
const int N = 4;
std::vector<std::vector<char> > g(N + 1, std::vector<char>(N + 1));
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
std::cin >> g[i][j];
}
}
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= N; j++) {
std::cout << g[N + 1 - i][N + 1 - j] << " \n"[j == N];
}
}
C
题意
一开始有长度为 \(6\) 的序列 \(1, 2, 3, 4, 5, 6\) ,从 \(1\) 开始编号。执行 \(N\) 次操作 \(swap(a_{i \bmod 5 + 1}, a_{i \bmod 5 + 2})(i = 0, 1, 2, \cdots, N - 1)\) 。
询问 \(N\) 次操作后的序列。
\(0 \leq N \leq 10^{9}\)
题解
对于循环意义下的 \(swap\) ,每个循环是一个相等的置换。
模意义下的循环节 \(s\) 不超过模数 \(mod\) ,即 \(s \leq mod\) 。
可以找到 \(s = n - 1\) 。进一步观察到 \(swap(a_{i}, a_{(i + 1) \bmod n})(i = 0, 1, 2, \cdots, n - 2)\) 构成的置换是循环左移一位。不妨令这个置换为 \(f\) 。
根据 \(\forall f, \exists x, f^{x} \circ v = v\) 。一个简洁的观察是:
显然地 \(n(n - 1)\) 次操作后序列不变,我们只需要暴力 \(N \bmod (n(n - 1))\) 次操作即可。时间复杂度 \(O(n^{2})\) 。
int a[] = {0, 1, 2, 3, 4, 5, 6};
int N; std::cin >> N;
N %= 30;
for (int i = 0; i < N; i++) {
std::swap(a[i % 5 + 1], a[i % 5 + 2]);
}
for (int i = 1; i <= 6; i++) {
std::cout << a[i];
}
std::cout << "\n";
考虑更特殊的性质,此处由于 \(f\) 为循环移位,于是 \(f^{x}\) 可以被 \(O(1)\) 确定,\(O(n)\) 实现。
最后要剩下需要暴力的次数 \(< s\) ,是 \(T(s) = T(n) = O(n)\) 级别。
设 \(R = N \bmod (n(n - 1)) < n(n - 1)\) ,在当前条件下,有 \(R \sim N\) 。
实际上会进行 \(P = \lfloor \frac{R}{n - 1} \rfloor < n\) 次循环左移,可以 \(O(n)\) 直接处理。这个置换是:
\(0 \ base\) 下的实现:
int b[n] = {0}; memcpy(b, a, sizeof b);
for (int i = 0; i < n; i++) {
a[i] = b[P % n];
P++;
}
再进行 \(Q = R \bmod (n - 1) < n = O(n)\) 次暴力。
\(0 \ base\) 下的实现为:
for (int i = 0; i < Q; i++) {
std::swap(a[i], a[i + 1]);
}
\(1 \ base\) 下的总实现为:
const int n = 6;
int a[n + 1] = {0, 1, 2, 3, 4, 5, 6};
int N; std::cin >> N;
N %= n * (n - 1);
int P = N / (n - 1), Q = N % (n - 1);
int b[n + 1] = {0}; memcpy(b, a, sizeof b);
P++;
for (int i = 1; i <= n; i++) {
a[i] = b[(P - 1) % n + 1];
P++;
}
for (int i = 1; i <= Q; i++) {
std::swap(a[i], a[i + 1]);
}
for (int i = 1; i <= n; i++) {
std::cout << a[i];
}
std::cout << "\n";
D
diffculty 1861
题意
在一根无限长的整数数轴上,每个坐标放置着一个箱子。
\(-100\) 处放置了 \(R\) 个红球,\(0\) 处放置了 \(G\) 个绿球,\(100\) 处放置了 \(B\) 个蓝球。
每次操作可以将一个球移动到相邻箱子,每个箱子中不能存在同色球。
询问最少经过多少次操作可以使数轴上的所有箱子存在球的数量 \(\leq 1\) 。
题解
这是个极其经典的网络流和 DP 模型。
解法一:
裸的无脑最小费用最大流 \(MinCostMaxFlow\) ,时间复杂度 O(流量上限 * spfa 时间),最坏是 \(O(\sum Cost \times V^{2})\) 。
令 \(N = max(100 + R + G + B)\) 。
不难发现实际有用的一个非常宽松的上界是 \([-N, N]\) ,这个区域已经能放下所有球。球移动到这个范围之外只会浪费操作数,没有意义。
建立源点连向 \(-100, 0, 100\) 位置的三个点,边的 cap/cost 分别为 \((R, 0), (G, 0), (B, 0)\) 。三个点集为 \(V_1\) 。
建立 \([-N, N]\) 这 \(2N\) 个点连向汇点,边的 cap/cost 为 \((1, 0)\) 。这 \(2N\) 个点点集为 \(V_2\) 。
对 \(V_1\) 和 \(V_2\) 的点连边,边的 cap/cost 为 \((1, dist)\) 。费用为数轴上两点的距离。
于是就是裸的最小费用最大流。可以发现网络流其实非常无赖。
关于网络流,可以不会。
但是要有板子,最好知道大概怎么流的,知道一些关键点比如:流量平衡、残量网络,并且会 \(addedge\) 。
网络流 24 题要总结出一份板子出来,方便抄。
这个题只用板子一贴,然后 \(addedge\) 就行。
解法二:
令 \(N = max(100 + R + G + B)\) 。
不难发现实际有用的一个非常宽松的上界是 \([-N, N]\) ,这个区域已经能放下所有球。球移动到这个范围之外只会浪费操作数,没有意义。
由于每个位置有 \(4\) 种放置可能,无球、红球、绿球、蓝球。我们可以使用 dp 。
我们从 \(-N\) 到 \(N\) 顺序遍历,枚举当前位置的放置可能。
使用 dp[考虑到第几个位置][还剩下多少红球][还剩下多少绿球][还剩下多少蓝球] ,存储最小操作次数。
初始定义良序 dp[-N - 1][R][G][B] = 0 。(当然也可以定义所有 dp[i][R][G][B] = 0 ,或者直接转移出这些状态)
设 \(k = 0, 1, 2, 3,\) 分别对应空放、放红球、放绿球、放蓝球。
有 dp 公式:
时空复杂度 \(O((R + G + B) R G B)\) 。
当 \(max(R, G, B) \leq 30\) 在空间上可以接受,当 \(max(R, G, B) \leq 100\) 在时间上可以接受。
memset(dp, 127, sizeof dp);
int z = N + 1;
int pos[] = {0, z - 100, z, z + 100};
dp[0][R][G][B] = 0;
for (int i = 1; i <= 2 * N + 1; i++) {
for (int k = 0; k <= 3; k++) {
for (int r = 0; r <= R; r++) {
for (int g = 0; g <= G; g++) {
for (int b = 0; b <= B; b++) {
if (k == 0) {
chkmin(dp[i][r][g][b], dp[i - 1][r][g][b]);
}
else if (k == 1 && r + 1 <= R && dp[i - 1][r + 1][g][b] < inf) {
chkmin(dp[i][r][g][b], dp[i - 1][r + 1][g][b] + abs(pos[k] - i));
}
else if (k == 2 && g + 1 <= G && dp[i - 1][r][g + 1][b] < inf) {
chkmin(dp[i][r][g][b], dp[i - 1][r][g + 1][b] + abs(pos[k] - i));
}
else if (k == 3 && b + 1 <= B && dp[i - 1][r][g][b + 1] < inf) {
chkmin(dp[i][r][g][b], dp[i - 1][r][g][b + 1] + abs(pos[k] - i));
}
}
}
}
}
}
std::cout << dp[2 * N + 1][0][0][0] << "\n";
}
AC 37 个点,RE 48 个点。
因为第 \(i\) 维只由第 \(i - 1\) 维转移,空间可以优化到 \(O(RGB)\) 。
于是当 \(max(R, G, B) \leq 100\) 在时空上都可以接受。
const int inf = 1 << 30;
void chkmin(int &a, int b) { a = std::min(a, b); }
int dp[101][101][101], vdp[101][101][101];
void solve() {
int R, G, B; std::cin >> R >> G >> B;
assert(R <= 100 && G <= 100 && B <= 100);
const int N = 400;
memset(vdp, 127, sizeof vdp);
memset(dp, 127, sizeof dp);
int z = N + 1;
int pos[] = {0, z - 100, z, z + 100};
for (int i = 1; i <= 2 * N + 1; i++) {
vdp[R][G][B] = 0;
for (int k = 0; k <= 3; k++) {
for (int r = 0; r <= R; r++) {
for (int g = 0; g <= G; g++) {
for (int b = 0; b <= B; b++) {
if (k == 0) {
chkmin(dp[r][g][b], vdp[r][g][b]);
}
else if (k == 1 && r + 1 <= R && vdp[r + 1][g][b] < inf) {
chkmin(dp[r][g][b], vdp[r + 1][g][b] + abs(pos[k] - i));
}
else if (k == 2 && g + 1 <= G && vdp[r][g + 1][b] < inf) {
chkmin(dp[r][g][b], vdp[r][g + 1][b] + abs(pos[k] - i));
}
else if (k == 3 && b + 1 <= B && dp[r][g][b + 1] < inf) {
chkmin(dp[r][g][b], vdp[r][g][b + 1] + abs(pos[k] - i));
}
}
}
}
}
memcpy(vdp, dp, sizeof vdp);
memset(dp, 127, sizeof dp);
}
std::cout << vdp[0][0][0] << "\n";
}
AC 57 个点,RE 28 个点。
似乎已经没有比较显然的 dp 时空优化方法了,怎么办呢?
我们需要继续观察。
我们可以发现数轴上的颜色分配一定是 红、绿、蓝 固定三段的。
证明很简单,比如一个红球在绿球右边,那么把这两个球交换位置的结果会更优。
如果位置已经固定,那么数轴上每个位置只有两种状态,放球、不放球。
现在问题已经于颜色无关,所以不需要维护三种颜色球剩余的数量,只需要维护总共的球剩余的数量。
同时可以直接计算出当前位置放置的哪种颜色的球,即可以直接计算转移的贡献。
计算 \(consume = R + G + B - rem\) ,若 \(consume \leq R\) 则是红球,再否则 \(consume \leq R + G\) 则是绿球,再否则是蓝球。
可以直接使用 if else 做到这一步。
一个更简洁、更快的定位目标点所在区间的写法:
定义左开右闭的区间数组 \(t = [-1, R, R + G, R + G + B]\) , lower_bound 左开右闭的区间。依据是 \(consume \leq t[i] \Rightarrow t[i] \geq consume\) 。
能查询到 \(consume\) 在 (-1, R], (R, R + G], (R + G, R + G + B] 哪个区间中。分别对应 \(1, 2, 3\) 。
有: dp[考虑到第几个位置][还剩多少球]
设 \(k = 0, 1\) 分别为不放球、放球。
良序初始化 dp[0][0] = 1 。枚举每个位置的状态。
有公式
时间复杂度 \(O((R + G + B)^{2})\) 。
由于第 \(i\) 维只由第 \(i - 1\) 维转移,可以通过滚动数组将空间复杂度优化到 \(O(R + G + B)\) 。
正解如下。
view
const int inf = 1 << 30;
void chkmin(int &a, int b) { a = std::min(a, b); }
void solve() {
int R, G, B; std::cin >> R >> G >> B;
const int N = 2000;
std::vector<int> dp(R + G + B + 1, inf);
std::vector<int> vdp(R + G + B + 1, inf);
int z = N + 1;
int pos[] = {0, z - 100, z, z + 100};
vdp[R + G + B] = 0;
for (int i = 1; i <= 2 * N + 1; i++) {
for (int k = 0; k <= 1; k++) {
for (int j = 0; j <= R + G + B; j++) {
if (k == 0) {
chkmin(dp[j], vdp[j]);
}
else if (k == 1 && j + 1 <= R + G + B) {
int t[4] = {-1, R, R + G, R + G + B};
int p = std::lower_bound(t + 1, t + 4, R + G + B - j) - t;
assert(1 <= p && p <= 3);
chkmin(dp[j], vdp[j + 1] + abs(pos[p] - i));
}
}
}
vdp = dp;
dp.assign(R + G + B + 1, inf);
}
std::cout << vdp[0] << "\n";
}
浙公网安备 33010602011771号