AtCoder Beginner Contest 013
A
Problem
给一个大写字母,询问其是从 \(A\) 起的第几个数(\(A\) 是第 \(1\) 个)
Solutions
char c; std::cin >> c;
std::cout << int( c - 'A' + 1) << "\n";
B
Problem
给两个数字 \(a, b\ (1 \leq a, b \leq 9)\) 。
操作有两种:
- \(a = (a + 1) \bmod 10\)
- \(a = (a - 1) \bmod 10\)
询问 \(a\) 到 \(b\) 的最少操作。
Solutions
显然是两个方向的最短路径的最小值。
\(min((b - a + 10) \bmod 10, (a - b + 10) \bmod 10)\) 。
C
Problem
一开始你的饱腹度是 \(H\) ,你要度过 \(N\) 天。每天你可以选择三种操作之一:
- 支出 \(A\) 元,饱腹度增加 \(B\) 。
- 支出 \(C\) 元,饱腹度增加 \(D\) 。
- 不支出,饱腹度减少 \(E\) 。
询问在 \(N\) 天内不饿死(饱腹度 \(\leq 0\) )的情况下,最少支出是多少。因为你有一个神奇的胃,饱腹度没有上限。
\(1 \leq N \leq 5 \times 10^{5}, 1 \leq A,B,D,E \leq 10^{6}, 1 \leq H \leq 10^{9}\) 。
Solutions
由于饱腹度没有上限,所以操作可以不在乎顺序,只在乎结果。或者可以贪心地先把饱腹度提到最高。
设三种操作次数为 \(X, Y, Z\) 。不难想到 \(O(N^{3})\) 枚举三操作。检查不饿死的最小支出。
由于 \(X + Y + Z = N\) ,于是可以 \(O(N^{2})\) 只枚举 \(X, Y\) ,\(Z = N - X - Y \geq 0\) 。检查不饿死的最小支出。
注意 \(XB + YD - (N - X - Y)E + H > 0 \quad s.t. \ X,Y \in \mathbb{Z} \wedge X,Y \geq 0 \wedge X + Y <= N\) 。只存在 \(X, Y\) 两个变量,由于是一次函数容易变量分离,然后分配一个到式子两侧:
于是只需要 \(O(N)\) 枚举 \(Y\) ,此次只需要找到最小的 \(X\) ,然后检查是否所有约束被满足。
view
i64 N, H, A, B, C, D, E;
std::cin >> N >> H >> A >> B >> C >> D >> E;
auto divceil = [&] (i64 a, i64 b) -> i64 {
if (a % b == 0) return a / b;
else if ((a ^ b) >= 0) return a / b + 1;
else return a / b;
};
const i64 INF = 1LL << 62;
i64 ans = INF;
for (int Y = 0; Y <= N; Y++) {
i64 X = std::max(0LL, divceil(1 + N * E - H - Y * (D + E), B + E));
if (X >= 0 && Y >= 0 && X + Y <= N) {
ans = std::min(ans, X * A + Y * C);
}
}
std::cout << ans << "\n";
D
Problem
emm,就是给一块板。
有 \(n\) 条竖线。
然后 \(m\) 条短横线 \(a_1, a_2, \cdots, a_m (1 \leq a_i \leq n - 1)\) 。按从上往下的顺序,\(a_i\) 表示在第 \(a_i\) 和第 \(a_{i + 1}\) 条竖线之间连一条横线。
从板的最上端,任选一条竖线开始往下走,每遇到一条就要沿着这条横线走到另一条竖线上,直到走到板的最下端。
如图是从第 \(4\) 条竖线向下走,最终会到达第 \(3\) 条竖线。

现在的新问题是。
给出 \(n, m\) 和 \(a\) 数组。将 \(D\) 块一样的板自上而下拼接。询问从第 \(1\) 块板上端的第 \(i\ (1 \leq i \leq n)\) 条直线开始,走到第 \(D\) 块板下端会到哪条直线。
Solutions
不妨先考虑 \(D = 1\) 时应该怎么办。\(1, 2, \cdots, n\) 一定对应 \(to[1], to[2], \cdots, to[3]\) 。
假设我们得到了这个 \(to\) 数组,那么 \(i\) 最终会到达 \(\underbrace{to[ \cdots to[to[i]]]}_{D'times}\) 。
不妨设多个 \(to\) 对 \(i\) 的复合为:
显然对于每个 \(i\) ,可以 \(\log D\) 倍增处理出答案。
总的时间复杂度是 \(O(n \log D)\) 。
问题是 \(to[i]\) 数组怎么求?暴力模拟的话是 \(O(n m)\) 。
不妨先设 \(to^{1}_i = i\) 。首先这是一个排列。
不难发现 \(to_x\) 是可以递推出来的。
to 的递推方法一:
考虑 \(to_x\) 是到当前为止,\(x\) 能到达 \(to_x\) 。
于是从前往后递推。
先出现 \(a_1\) ,会导致 \(to_j = a_{1}\) 和 \(to_k = a_{2}\) 的 \(j, k\) 交换位置。\(swap(to_j, to_k)\) 。
再出现 \(a_2\) ,会导致 \(to_j = a_{2}\) 和 \(to_k = a_{3}\) 的 \(j, k\) 交换位置。\(swap(to_j, to_k)\) 。
……
一直到 \(a_m\) 。
如何找到上述的 \(k, k\) ?可以构造映射 \(rec\) 使 \(to[rec[a_i]] = a_i\) 。
j, k = rec[a[i]], rec[a[i + 1]]
swap(to[j], to[k])
rec[a[i]], rec[a[i + 1]] = j, k
这样就解决了。
to 的递推方法二:
考虑 \(to_x\) 为,\(x\) 最终可以到达 \(to_x\) 。
于是从后往前递推。
枚举 \(i = m \to 1\) ,\(swap(to_{a_i}, to_{a_{i + 1}})\) 就做完了。
于是在最终可以通过两种方法之一以 \(O(m)\) 得到 \(to\) 数组,通过 \(O(\log D)\) 得到 \(to^{D}_i\) 。总时间复杂度为 \(O(n \log D)\) 。
此问题的另一个解法
不妨依旧递推出 \(to\) 数组,使用更简便的递推方法二。
显然 \(\forall i \to to_i\) 是一个置换。为了方便表示不妨定义 \(p = to\) 。
设置换
定义 \(v = [1, 2, \cdots, n]\) ,那么最终只需要求解新排列
暴力的单次置换乘法时间复杂度 \(O(n)\) ,可以通过快速幂 \(O(\log D)\) 计算 \(f^{D}\) 。这时候已经可以 \(O(m + n \log D)\) 解决问题。
实际上一个置换 \(f\) 可以 \(O(n)\) 分解为若干个循环置换 \(g_i\)
循环置换的幂次 \(g_{i}^{D} = g_{i}^{D - 1} \circ g\) 的结果是 \(g\) 的上部分向量循环左移位 \(D \bmod |g_i|\) 次,可以 \(O(|g_i|)\) 做到。于是 \(\sum g_i^{D}\) 的时间复杂度为 \(O(\sum |g_i|) = O(n)\) 。
这时候 \(O(n)\) 计算置换 \(\forall g_i^{D}\) 的乘积,得到 \(f^{D}\) 。
比如:
设 \(a_i = i\)
很直观地,\(f^{D}\) 中 \(a_i\) 的下一位被\(g_1^{D} \circ g_2^{D} \circ \cdots \circ g_k^{D}\) 中 \(a_i\) 的下一位计算出。
第一种解法不依赖于置换的性质,倍增时间复杂度 \(O(m + n \log D)\) 。
第二种解法依赖置换,快速幂时间复杂度 \(O(m + n \log D)\) 。然后分解成循环置换可以 \(O(m + n)\) 实现。
倍增是比较显然的,不想写实现了。
\(O(m + n \log D)\) 的置换快速幂实现:
view
std::vector<int> operator * (std::vector<int> const &p, std::vector<int> const &q) {
int n = p.size() - 1;
assert(n == (int)q.size() - 1);
std::vector<int> res(n + 1);
for (int i = 1; i <= n; i++) res[i] = q[p[i]];
return res;
}
std::vector<int> ksm(std::vector<int> p, i64 pw) { // 可能常数会比 pow 好?复杂度乘 log pw
int n = p.size() - 1;
std::vector<int> res(n + 1); std::iota(res.begin(), res.end(), 0);
for (;pw;pw>>=1,p=p*p)if(pw&1)res=res*p; return res;
}
void solve() {
int N, M, D; std::cin >> N >> M >> D;
std::vector<int> a(M + 1);
for (int i = 1; i <= M; i++) std::cin >> a[i];
std::vector<int> t(N + 1);
std::iota(t.begin(), t.end(), 0);
for (int i = M; i >= 1; --i) std::swap(t[a[i]], t[a[i] + 1]);
std::vector<int> q(N + 1); std::iota(q.begin(), q.end(), 0);
std::vector<int> ans;
ans = ksm(t, D) * q;
for (int i = 1; i <= N; i++) std::cout << ans[i] << "\n";
}
\(O(m + n)\) 的置换分解实现:
view
struct permutation {
// 只处理下标的排列置换!!!
// 1 base 常用于置换
// 1, 2, ..., n -> p_1, p_2, ..., p_n
std::vector<int> p; int n;
std::vector<std::vector<int> > cycs; // 循环置换用 0 base 存
permutation () {}
permutation (std::vector<int> p_) {
p = p_;
n = p_.size() - 1;
std::vector<int> vis(n + 1, 0);
for (int i = 1; i <= n; i++) if (!vis[i]) {
int x = i;
vis[x]++;
std::vector<int> cyc{x};
while (!vis[p[x]]) {
x = p[x];
vis[x]++;
cyc.push_back(x);
}
cycs.push_back(cyc);
}
}
std::vector<int> pow(i64 pw) {
// 1, 2, ..., n -> p_1, p_2, ..., p_n
assert(pw > 0); // 考虑是否存在幺元
std::vector<int> q(n + 1);
for (auto cyc : cycs) {
int m = cyc.size();
int g = pw % m;
// c_0, c_2, ..., c_{m - 1} -> c_g, c_{g + 1}, ..., c_{g - 1}
for (int i = 0; i < m; i++) {
q[cyc[i]] = cyc[(i + g) % m];
}
}
return q;
}
std::vector<std::vector<int> > getcycs() {
return cycs;
}
};
std::vector<int> operator * (std::vector<int> const &p, std::vector<int> const &q) {
int n = p.size() - 1;
assert(n == (int)q.size() - 1);
std::vector<int> res(n + 1);
for (int i = 1; i <= n; i++) res[i] = q[p[i]];
return res;
}
void solve() {
int N, M, D; std::cin >> N >> M >> D;
std::vector<int> a(M + 1);
for (int i = 1; i <= M; i++) std::cin >> a[i];
std::vector<int> t(N + 1);
std::iota(t.begin(), t.end(), 0);
for (int i = M; i >= 1; --i) std::swap(t[a[i]], t[a[i] + 1]);
permutation p(t);
std::vector<int> q(N + 1); std::iota(q.begin(), q.end(), 0);
std::vector<int> ans;
ans = p.pow(D) * q;
for (int i = 1; i <= N; i++) std::cout << ans[i] << "\n";
}
浙公网安备 33010602011771号