AtCoder Beginner Contest 006
A
题意
输入一个数字 \(n\) ,若 \(n\) 能整除于 \(3\) 或者 \(n\) 的数位中有 \(3\) 。输出 YES ,否则 NO 。
题解
判一下数字是否被 \(3\) 整除,再拆位判一下就行。时间复杂度 \(O(n + \log n)\) 。
i64 N; std::cin >> N;
int ok = 0;
ok |= N % 3 == 0;
while (N) {
ok |= (N % 10 == 3);
N /= 10;
}
std::cout << (ok ? "YES" : "NO") << "\n";
有意思的东西 * 1
一个数如果能被 \(9\) 整除,则这个数的所有数位加起来能被 \(9\) 整除。
一个数如果能被 \(3\) 整除,则这个数的所有数位加起来能被 \(3\) 整除。
证明:
则 \(\overline{a_0 a_1 a_2 \cdots a_{n - 1}} = \sum_{i = 0}^{n - 1} a_i \times 10^{i} = \sum_{i = 0}^{n - 1} a_i \times (10^{i} - 1) + \sum_{i = 0}^{n - 1} a_i\) 。
由常见性质 \(10^{i} - 1 = \underbrace{99 \cdots 9}_{i \textbf{个}}\) ,显然 \(\sum_{i = 0}^{n - 1} a_i \times (10^{i} - 1)\) 是 \(3 \wedge 9\) 的倍数。
若 \(\sum_{i = 0}^{n - 1} a_i = 3 t\) ,则 \(\overline{a_0 a_1 a_2 \cdots a_{n - 1}} = 3 K + 3 t = 3(K + t) \equiv 0 (\bmod 3)\) 。
若 \(\sum_{i = 0}^{n - 1} a_i = 9 t\) ,则 \(\overline{a_0 a_1 a_2 \cdots a_{n - 1}} = 9 K + 9 t = 9(K + t) \equiv 0 (\bmod 9)\) 。
\(\square\)
当然一个数 \(X\) 可能同时满足 \(X \equiv 0 (\bmod 3)\) 和 \(X \equiv 0 (\bmod 9)\) 。
有意思的东西 * 2
\(10^{i} - 1 = \underbrace{99 \cdots 9}_{i \textbf{个}}\) 的另一个常见用途是 \(\frac{10^{i} - 1}{9} \times x \ (x = 0, 1, 2, \cdots, 9)\) 以构造低 \(i\) 位的相同数位。
pro 版
题意:
输入一个非负整数 \(n\) ,询问有多少个非负整数 \(m\) 满足 \(m \leq n\) 且 \(3 \mid m\) 或 \(m\) 的数位中存在 \(3\) 。
题解
\(A \cup B = 1 - (\neg A \cap \neg B)\) 。
所有 \(1 \leq m \leq n\) 满足 \(3 \neq m\) 且 \(m\) 的数位不存在 \(3\) 的数,可以使用数位 DP ,设计算函数为 \(calc(n)\) 。
定义 f[考虑到第 pos 位][前 pos - 1 位未满][前 pos - 1 位组成的数] 。
则有:
auto calc = [&] (int x) -> i64 {
std::string s = std::to_string(x);
int m = s.size();
std::vector<std::vector<i64> > f(m, std::vector<i64>(2, -1));
std::function<i64(int, int, i64)> dfs = [&] (int pos, int smaller, i64 num) -> i64 {
if (pos == m) return 1 * (num % 3 != 0);
i64 &res = f[pos][smaller];
if (res != -1) return res;
res = 0;
int up = smaller ? 9 : (s[pos] - '0');
for (int i = 0; i <= up; i++) {
if (i == 3) continue;
res += dfs(pos + 1, smaller, num * 10 + i);
}
return res;
};
dfs(0, 0, 0);
return f[0][0];
};
于是答案为 \(n - calc(n)\) 。
B
题意
定义 \(f[n] = f[n - 1] + f[n - 2] + f[n - 3]\) ,定义 \(f[1] = 0, f[2] = 0, f[3] = 1\) 。
输入一个 \(n\) ,询问 \(f[n]\) 。结果 \(\bmod 1007\) 。
题解
一:\(O(n)\) 做法。
直接递推:
const int MOD = 10007;
void solve() {
int N; std::cin >> N;
std::vector<int> f(N + 1);
f[1] = f[2] = 0; f[3] = 1;
for (int i = 4; i <= N; i++) f[i] = (f[i - 1] + f[i - 2] % MOD + f[i - 3]) % MOD;
std::cout << f[N] << "\n";
}
二:\(O(\log n)\) 做法。
考虑
根据 \(f_{i} = f_{i - 1} + f_{i - 2} + f_{i - 3}\) 可以构造出
则
。
时间复杂度 \(O(w^{3} \log n)\) 。\(w\) 为方阵大小,矩阵乘法的时间复杂度为 \(O(w^{3})\) 。快速幂最多自乘 \(O(\log n)\) 次,\(n\) 为幂次的大小。
回忆一个 \(m \times k\) 的矩阵 \(B\) 左乘一个 \(n \times m\) 的矩阵 \(A\) ,通项公式实际上是
这是矩阵乘法 \(O(n m k)\) 时间复杂度的贡献来源。
view
const int MOD = 10007;
template<typename T, const int MOD = 10007>
struct Matrix {
std::vector<std::vector<T> > a;
int n, m;
Matrix () {}
Matrix (int n, int m) { init(n, m); }
void init (int n, int m) {
this->n = n;
this->m = m;
a.resize(n + 1);
for (auto &u : a) u.resize(m + 1);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= m; j++)
a[i][j] = 0;
}
void set(int i, int j, T v) {
a[i][j] = v;
}
T& get(int i, int j) {
return a[i][j];
}
void setI() {
assert(n == m);
for (int i = 1; i <= n; i++) a[i][i] = 1;
}
Matrix operator * (Matrix<T> &V) {
assert(this->m == V.n);
int n = this->n, m = this->m, k = V.m;
Matrix<T> res(n, k);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= k; j++) {
for (int l = 1; l <= m; l++) {
res.get(i, j) += (this->a[i][l] * V.get(l, j)) % MOD;
res.get(i, j) %= MOD;
}
}
}
return res;
}
};
Matrix<i64> ksm(Matrix<i64> a, i64 pw) {
int n = a.n, m = a.m;
assert(n == m);
Matrix<i64> res(n, n);
res.setI();
for ( ;pw; pw>>=1, a = a * a) if (pw & 1)
res = res * a;
return res;
}
void solve() {
int N; std::cin >> N;
Matrix<i64> f(3, 1);
f.set(1, 1, 0);
f.set(2, 1, 0);
f.set(3, 1, 1);
Matrix<i64> V(3, 3);
V.set(1, 1, 0); V.set(1, 2, 1); V.set(1, 3, 0);
V.set(2, 1, 0); V.set(2, 2, 0); V.set(2, 3, 1);
V.set(3, 1, 1); V.set(3, 2, 1); V.set(3, 3, 1);
V = ksm(V, N - 1);
f = V * f;
// for (int i = 1; i <= f.n; i++) {
// for (int j = 1; j <= f.m; j++) {
// std::cout << f.get(i, j) << " \n"[j == f.m];
// }
// }
std::cout << f.get(1, 1) << "\n";
}
C
题意
大人有两条腿,老人有三条腿,婴儿有四条腿。总共 \(N\) 个人,\(M\) 条腿。
询问任意一个答案 \(a, b, c\) 表示大人、老人、小孩的数量。不存在答案则输出 "-1 -1 -1" 。
题解
可以得到一个线性齐次方程组,并且转成矩阵形式。
如果满秩,即向量全部线性无关,则可以通过 \(O(w^{3})\) 的高斯消元计算答案。
这是我们的增广矩阵
但显然不满秩。
不妨手动消元成上三角矩阵:
得到 \(a + b + c = N, b + 2c = M - 2N\) 。
由 \(b + 2c = M - 2N\) (不妨)可以用 \(O(M)\) 复杂度枚举 \(b\) ,然后得到 \(c\) 。当然 \(c\) 需要是非负整数。
再然后检查是否存在一个非负整数 \(a\) 满足 \(a + b + c = N\) ,即检查 \(N - b - c\) 是否为非负整数。
int N, M; std::cin >> N >> M;
for (int b = 0; b <= M - 2 * N; b++) {
int c = (M - 2 * N) / 2; if (c * 2 + b != M - 2 * N) continue;
int a = N - b - c; if (a < 0) continue;
std::cout << a << " " << b << " " << c << "\n";
return;
}
std::cout << "-1 -1 -1\n";
扣常数?
注意线性方程 \(\sum_{i = 1}^{m} n_i x_i = n_1 x_1 + n_2 x_2 + n_3 x_3 + \cdots + n_m x_m = c\) ,如果已知 \(x_i, c\) 。
有 \(n_k = \frac{c - \sum_{i = 1}^{m} n_i x_i}{x_1} + n_k x_k \leq \frac{c}{x_k}, \ s.t. \ 1 \leq k \leq m\) 。
我们优先枚举 \(x_k\) 更大的数,会得到更优的常数。
int N, M; std::cin >> N >> M;
for (int c = 0; c <= (M - 2 * N) / 2; c++) {
int b = M - 2 * N - c * 2; if (b < 0) continue;
int a = N - b - c; if (a < 0) continue;
std::cout << a << " " << b << " " << c << "\n";
return;
}
std::cout << "-1 -1 -1\n";
问题规模比较大?
- 假设矩阵的向量个数是 \(W\) 。
- 矩阵的自由度(即向量数 - 极大线性无关向量数),如果在给定条件下能够被约束到一个范围,则将它假设是 \(K\) 。
- 假设数值值域是 \(M\) 。
先进行 \(O(W^{3})\) 的高斯消元,然后通过 \(O(M^{K})\) 的复杂度计算出所有特解。
因为秩是可求的,所以自由度的暴力计算也是可以实现的,比如 dfs 。
时间复杂度 \(O(W^{3} + M^{K})\) ,最坏是 \(O(W^{3} + M^{W})\) 。
此题的自由度刚好是 \(1\) 。
D
diffculty 1696 (上古时代一个朴素 DP 都要 1700 吗?大家技能点都是往哪点的?)
题意
给一堆牌 \(N\) 张,从上到下分别是 \(c_1, c_2, c_3, \cdots, c_N\) 。
一次操作可以抽出一张牌然后插入任何地方,询问最少操作数使得牌从上到下升序。
\(1 \leq N \leq 1000\)
\(i \neq j, c_i \neq c_j\)
题解
每张牌都不同,需要按照一个全偏序 \((G, <)\) 排序。
考虑偏序中的相对位置不变,选择极大的序列满足其符合偏序。
将相对位置有误的数,逐个插入(并入)偏序的正确位置即可。
这个极大序列显然是最长上升子序列,设长度为 \(ans\) 。则插入次数是 \(n - ans\) 。
幽默的早期 \(ABC\) ,让我们 \(O(n^{2})\) 的朴素最长上升子序列 DP 可过。
众所周知最长上升子序列存在两种 \(O(N^{2})\) 的 DP 。
- 第一种可以通过二分 \(O(N \log N)\) 。\(N\) 是数组大小。
- 第二种可以通过 BIT/SegTree 优化到 \(O(N \log M)\) 。\(M\) 是值域大小。由于离散化可以保证 \(M \leq N\) ,所以这个算法的实际复杂度为 \(O(N (\log N + \log min(N, M)))\) 。
方法一:
维护 \(dp[i][j]\) 表示前 \(i\) 个数中,长度为 \(j\) 的最长上升子序列中,最小的最大值。
初始 dp 数组为 inf ,良序为 dp[i][0] = -inf s.t \(0 \leq i \leq N\) 。
方程是:
时空都为 \(O(N^{2})\) 的实现如下:
std::vector<std::vector<int> > dp(N + 1, std::vector<int>(N + 1, 1 << 30));
dp[0][0] = -(1 << 30);
for (int i = 1; i <= N; i++) {
dp[i][0] = -(1 << 30);
for (int j = 1; j <= i; j++) {
dp[i][j] = std::min(dp[i][j], dp[i - 1][j]);
if (dp[i - 1][j - 1] <= a[i] && a[i] < dp[i - 1][j]) {
dp[i][j] = std::min(dp[i][j], a[i]);
}
}
}
int mx = std::lower_bound(dp[N].begin(), dp[N].end(), 1 << 30) - dp[N].begin() - 1;
std::cout << N - mx << "\n";
不难发现转移不依赖 \(i\) ,于是可以滚动数组优化空间。
但实际上可以压缩在一维转移:我们注意到每次考虑到第 \(i\) 个数 \(a_i\) ,转移只会发生一次,因为 \(a_i\) 最多只有一个位置可以插入。
于是每次转移只需要 \(O(N)\) 找到位置然后跳出:
std::vector<int> dp(N + 1,1 << 30);
dp[0] = -(1 << 30);
for (int i = 1; i <= N; i++) {
for (int j = 1; j <= i; j++) {
if (dp[j - 1] <= a[i] && a[i] < dp[j]) {
dp[j] = a[i];
break;
}
}
}
int mx = std::lower_bound(dp.begin(), dp.end(), 1 << 30) - dp.begin() - 1;
std::cout << N - mx << "\n";
进一步地,注意到 \(i < j, dp[i] \leq dp[j]\) ,即这个数组是单调的。
当秩序这种插入位置查询操作: \(arr[p - 1] \leq x, x < arr[p]\) ,可以通过在 \(arr\) 中 upper_bound \(x\) 以 \(O(\log N)\) 找到 \(p\) 。
于是空间优化为 \(O(N)\) ,时间优化为 \(O(N \log N)\) :
std::vector<int> dp(N + 1,1 << 30);
dp[0] = -(1 << 30);
for (int i = 1; i <= N; i++) {
auto it = std::upper_bound(dp.begin(), dp.end(), a[i]);
if (it == dp.end()) continue;
int p = it - dp.begin();
dp[p] = a[i];
}
int mx = std::lower_bound(dp.begin(), dp.end(), 1 << 30) - dp.begin() - 1;
std::cout << N - mx << "\n";
方法二:
维护 \(dp[i]\) 为考虑了前 \(i\) 个数的最长上升子序列长度。
初始化 dp 数组为 \(1\) ,良序 dp[0] = 1 。
朴素算法空间复杂度 \(O(N)\) 、时间复杂度 \(O(N^{2})\) 。
std::vector<int> dp(N + 1, 1);
for (int i = 1; i <= N; i++) {
for (int j = 1; j < i; j++) {
if (a[j] < a[i]) {
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
}
int mx = *std::max_element(dp.begin(), dp.end());
std::cout << N - mx << "\n";
显然地 \(j < i, a[j] \leq a[i]\) 的限制条件是一个二维偏序,我们可以通过一些数据结构(配合离散化)维护这个二维偏序的关系。
这里不妨先离散化。
-
第一维 \(x\) 用遍历的历史数据维护,查询历史数据总能保证 \(\forall j, j < i\) ,于是维护了第一个偏序。因为历史数据包括当前版本,这里可以先查询所有历史数据,再插入第一维 \(x = i\) 的点。
-
第二维 \(a[x]\) 用权值 BIT 维护,总能在 BIT 查询 \(\forall j, a[j] \leq a[i]\) ,于是维护了第二个偏序。
-
这就完成了 \(j < i, a[j] \leq a[i]\) 的所有点的查询。又这些点的点集中,需要查询 \(v = max\{dp[j]\}\) 。我们可以在 BIT 上将点权求和改成点权求 max 。然后有转移方程 \(dp[i] = max(dp[i], v + 1)\) 。
-
最后是插入点 \((i, a[i])\) 。对这个点赋上点权 \(dp[i]\) 的操作是 \(modify(a[i], dp[i])\) 。
注意可以通过位置偏移或离散化规避树状数组只能传入正整数的问题。
// ~~ SEG STARST
template<class T>
struct BIT {
std::vector<T> c;
int sz, LGN;
BIT(){}
BIT(int n) { init(n); }
void init(int n) {
sz = n + 1; // sz 是树状数组的域
LGN = 31 - __builtin_clz(sz); // LGN 是权值的值域,注意点权离散化后 LGN 还是 int 级别的
c.resize(sz + 5);
c.assign(sz + 5, 0);
}
T query(int x) {
assert(x <= sz);
T s = 0;
for (int i = x; i; i -= i & -i)
s = std::max(s, c[i]);
return s;
}
void modify(int x, T s) {
assert(x > 0);
for (int i = x; i <= sz; i += i & -i)
c[i] = std::max(c[i], s);
}
};
// ~~ SEG END
void solve() {
int N; std::cin >> N;
std::vector<int> a(N + 1);
std::vector<int> nums;
for (int i = 1; i <= N; i++) {
std::cin >> a[i];
nums.push_back(a[i]);
}
std::sort(nums.begin(), nums.end());
nums.erase( std::unique(nums.begin(), nums.end()), nums.end());
std::vector<int> dp(N + 1, 1);
BIT<int> Fe(N + 5);
for (int i = 1; i <= N; i++) { // ponit is (i, a[i])
int num = std::lower_bound(nums.begin(), nums.end(), a[i]) - nums.begin() + 1;
int mxdpj = Fe.query(num);
dp[i] = std::max(dp[i], mxdpj + 1);
Fe.modify(num, dp[i]);
}
int mx = *std::max_element(dp.begin() + 1, dp.end());
std::cout << N - mx << "\n";
}
浙公网安备 33010602011771号