AtCoder Beginner Contest 377
上周六咕咕咕了
省流版
- A. 排序判断即可
- B. 枚举判断即可
- C. 记录覆盖位置去重,总数-覆盖数即可
- D. 枚举右端点,考虑符合条件的左端点数量即可
- E. 考虑排列的\(i \to p_i\)图,考虑操作数与走的边数关系,利用环循环节算偏移量即可
- F. 考虑每个皇后实际覆盖的位置,枚举先前皇后计算覆盖交集去重,累加实际覆盖数即可
- G. 将操作转换成
Tries
树的节点移动,即一条链上的到达最近叶子的距离,取最小值即可
A - Rearranging ABC (abc377 A)
题目大意
给定三个字母,问能否组成ABC
。
解题思路
排个序看是否是ABC
即可。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
string s;
cin >> s;
sort(s.begin(), s.end());
if (s == "ABC")
cout << "Yes" << '\n';
else
cout << "No" << '\n';
return 0;
}
B - Avoid Rook Attack (abc377 B)
题目大意
国际象棋,车,上下左右任意走,
\(8 \times 8\)的棋盘,给定 \(m\)个车的位置。
问有多少位置,不会被车的范围覆盖。
解题思路
一个车就覆盖一行和一列。
因此就set
记录被覆盖了的行和列,最后还有\(x\)行和 \(y\)列没覆盖,答案就是 \(xy\)。
当然直接花 \(O(8^4)\)枚举位置+判断是否被覆盖也可以。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
set<int> r, c;
for (int i = 0; i < 8; ++i) {
string s;
cin >> s;
for (int j = 0; j < 8; ++j)
if (s[j] == '#') {
r.insert(i);
c.insert(j);
}
}
int ans = (8 - r.size()) * (8 - c.size());
cout << ans << '\n';
return 0;
}
C - Avoid Knight Attack (abc377 C)
题目大意
国际象棋,马,八个方向的日
字走法。
\(n \times n\)的棋盘,给定 \(m\)个马的位置。
问有多少位置,不会被马的范围覆盖。
解题思路
因为一个🐎只覆盖八个位置,用set
记录每个每个🐎覆盖的八个位置并去重,假设是去重后有\(x\)位置被🐎覆盖。
答案就是 \(n^2 - x\)。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
set<array<int, 2>> forbid;
array<int, 8> dx = {2, 1, -1, -2, -2, -1, 1, 2};
array<int, 8> dy = {1, 2, 2, 1, -1, -2, -2, -1};
for (int i = 0; i < m; i++) {
int x, y;
cin >> x >> y;
forbid.insert({x, y});
for (int j = 0; j < 8; j++) {
int nx = x + dx[j];
int ny = y + dy[j];
if (nx >= 1 && nx <= n && ny >= 1 && ny <= n) {
forbid.insert({nx, ny});
}
}
}
LL ans = 1ll * n * n - forbid.size();
cout << ans << '\n';
return 0;
}
D - Many Segments 2 (abc377 D)
题目大意
一维数轴,给定若干条线段\([l_i, r_i]\)。
问\([l,r]\)数量,其不完全包含上述中的任意线段。
解题思路
枚举\(r\),考虑有多少个 \(l\)符合要求。
\([l,r]\)不完全包含任何线段,因此只需考虑 \(r_i \leq r\)的线段,然后 \(l > l_i\)即可。
即\(l\)的取值范围即为(\max_{r_i \leq r} l_i, r],个数即为\(r - \max_{r_i \leq r} l_i\),对所有的\(r\)求和即为答案。
对线段的右端点从小到大排序,由于是从小到大枚举的\(r\),因此\(\max_{r_i \leq r} l_i\)可以在枚举的过程 \(O(1)\)维护。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
vector<array<int, 2>> seg(n);
for (auto& s : seg) {
cin >> s[0] >> s[1];
}
sort(seg.begin(), seg.end(),
[](const array<int, 2>& a, const array<int, 2>& b) {
return a[1] < b[1];
});
LL ans = 0;
int maxl = 0;
int cur = 0;
for (int i = 1; i <= m; ++i) {
while (cur < n && seg[cur][1] <= i) {
maxl = max(maxl, seg[cur][0]);
++cur;
}
ans += i - maxl;
}
cout << ans << '\n';
return 0;
}
E - Permute K times 2 (abc377 E)
题目大意
给定一个排列\(p_i\),进行\(k\)次操作。
每次操作,同时将所有的 \(p_i\)替换为 \(p_{p_i}\)。
问进行\(k\)次操作后的排列。
解题思路
排列的变换,可以考虑建图\(i \to p_i\),即有若干个环。
通过手试会发现,进行第一次操作,相当于走\(1\)条边,进行第二次操作,相当于走\(2\)条边, 第三次走\(4\)条...因为本次走的点,其之前也走过了同样的步数。
即进行 \(k\)次,将会走 \(\sum_{i=0}^{k-1} 2^i = 2^{k} - 1\)条边,由于一个环的循环节大小为环的点数,因此其对环大小取后即为进行了 \(k\)次操作后的位置\(a\),其数就是 \(p_a\)。
因此找出每个环,假设环大小\(sz\),用快速幂算出\(2^k - 1 \% sz\) ,即进行 \(k\)次操作后的偏移量,然后求该环上所有点最终的偏移位置即可。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
long long qpower(long long a, long long b, long long mo) {
long long qwq = 1;
while (b) {
if (b & 1)
qwq = qwq * a % mo;
a = a * a % mo;
b >>= 1;
}
return qwq;
}
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
LL k;
cin >> n >> k;
vector<int> p(n);
for (auto& i : p) {
cin >> i;
--i;
}
vector<int> vis(n);
vector<int> ans(n);
for (int i = 0; i < n; i++) {
if (vis[i])
continue;
int j = i;
vector<int> cycle;
while (!vis[j]) {
vis[j] = 1;
cycle.push_back(j);
j = p[j];
}
int len = cycle.size();
for (int i = 0; i < len; i++) {
int step = (qpower(2, k, len) + len - 1) % len;
int j = (i + step) % len;
ans[cycle[i]] = cycle[j];
}
}
for (auto i : ans)
cout << p[i] + 1 << ' ';
return 0;
}
F - Avoid Queen Attack (abc377 F)
题目大意
国际象棋,皇后,横竖两个对角线的任意走。
\(n \times n\)的棋盘,给定 \(m\)个皇后的位置。
问有多少位置,不会被皇后的范围覆盖。
解题思路
同样考虑所有皇后覆盖了多少位置,然后用总数减去覆盖的位置。
但与🐎不同的是,一个皇后覆盖的数量位置和\(O(n)\)同级,不能像🐎一样记录所有覆盖位置。
考虑每个皇后实际覆盖的位置数,该位置数是去除了先前考虑的皇后的覆盖位置。
首先,可以算出该皇后可以覆盖的数量,即横竖两个对角线的和。然后考虑去除已经算过的覆盖的位置。
由于\(m \leq 1000\),因此可以直接枚举先前考虑的皇后,然后计算皇后重叠的覆盖部分,就能得到该皇后实际覆盖的位置。
所有的皇后的实际覆盖数的和,用总数减去即为答案。时间复杂度为\(O(m^2)\)。
想法比较朴素,实现需要一些细节。
考虑皇后的重叠部份的计算,由于覆盖的部分还会重复覆盖(即皇后\(ab\)重叠的部分,与皇后 \(ac\) 也有重叠),因此计算重复覆盖时还得去重。
如果同行同列或者同对角线, 由于其数量级是\(O(n)\),不能全部记录被覆盖的地方,可以用四个变量表示该行列或对角线是否被覆盖。
然后考虑
- 皇后的
行
与另外的列
和俩对角线
的交集,最多三个点。 - 皇后的
列
与另外的行
和俩对角线
的交集,最多三个点。 - 皇后的
俩对角线
与另外的行
和列
和一对角线
的交集,最多三个点。
遍历先前考虑的所有皇后,得到这些覆盖的点,去重后即为先前已经考虑过的覆盖的点。用该皇后可以覆盖的位置数
减去考虑过的
,即为该皇后实际覆盖的点数。
至于计算交集点,两条斜对角线的计算方式需要解一个二元一次方程组。
代码实现里,除row
外,其余的下标都是y
轴的。注意中心点被重复考虑的情况。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
vector<array<int, 2>> a(m);
for (auto& x : a)
cin >> x[0] >> x[1];
LL tot = 0;
auto calc_diag = [&](int x, int y) {
return min(x, n - y + 1) + min(n - x + 1, y) - 1;
};
auto calc_diag2 = [&](int x, int y) {
return min(x, y) + min(n - x + 1, n - y + 1) - 1;
};
auto intersect = [&](int x1, int y1, int x2, int y2, int& tmp) {
int d1 = x1 + y1, d2 = x2 - y2;
int x = (d1 + d2), y = (d1 - d2);
if ((x & 1) || (y & 1))
return false;
x = x / 2;
y = y / 2;
tmp = y;
return x >= 1 && x <= n && y >= 1 && y <= n;
};
auto intersect2 = [&](int x1, int y1, int x2, int y2, int& tmp) {
int d1 = x1 - y1, d2 = x2 + y2;
int x = (d1 + d2), y = (d2 - d1);
if ((x & 1) || (y & 1))
return false;
x = x / 2;
y = y / 2;
tmp = y;
return x >= 1 && x <= n && y >= 1 && y <= n;
};
auto calc = [&](int x, int y) {
LL cnt = 0;
cnt += n;
cnt += n;
cnt += calc_diag(x, y);
cnt += calc_diag2(x, y);
cnt -= 3;
return cnt;
};
auto in_range = [&](int x) { return x >= 1 && x <= n; };
for (int i = 0; i < m; i++) {
auto [x, y] = a[i];
LL forbid = calc(x, y);
bool row = false, col = false, diag = false, diag2 = false;
set<int> forbid_row, forbid_col, forbid_diag, forbid_diag2;
for (int j = 0; j < i; ++j) {
auto [x2, y2] = a[j];
if (x == x2)
row = true;
if (y == y2)
col = true;
if (x + y == x2 + y2)
diag = true;
if (x - y == x2 - y2)
diag2 = true;
forbid_row.insert(y2);
if (in_range(x2 + y2 - x))
forbid_row.insert(x2 + y2 - x);
if (in_range(y2 - x2 + x))
forbid_row.insert(y2 - x2 + x);
forbid_col.insert(x2);
if (in_range(x2 + y2 - y))
forbid_col.insert(x2 + y2 - y);
if (in_range(x2 - y2 + y))
forbid_col.insert(x2 - y2 + y);
int tmp;
if (in_range(x + y - x2))
forbid_diag.insert(x + y - x2);
if (in_range(x + y - y2))
forbid_diag.insert(y2);
if (intersect(x, y, x2, y2, tmp))
forbid_diag.insert(tmp);
if (in_range(y - x + x2))
forbid_diag2.insert(y - x + x2);
if (in_range(x - y + y2))
forbid_diag2.insert(y2);
if (intersect2(x, y, x2, y2, tmp))
forbid_diag2.insert(tmp);
}
int center = 0;
if (row) {
forbid -= n;
center++;
} else {
forbid -= forbid_row.size();
center += forbid_row.count(y);
}
if (col) {
forbid -= n;
center++;
} else {
forbid -= forbid_col.size();
center += forbid_col.count(x);
}
if (diag) {
forbid -= calc_diag(x, y);
center++;
} else {
forbid -= forbid_diag.size();
center += forbid_diag.count(y);
}
if (diag2) {
forbid -= calc_diag2(x, y);
center++;
} else {
forbid -= forbid_diag2.size();
center += forbid_diag2.count(y);
}
tot += forbid;
tot += max(center - 1, 0);
}
LL ans = 1ll * n * n - tot;
cout << ans << '\n';
return 0;
}
G - Edit to Match (abc377 G)
题目大意
给定\(n\)个字符串,对于每个字符串 \(s_i\),回答以下问题。
进行最少次数操作,使得 \(s_i\)为空,或者存在\(j < i\),使得\(s_i = s_j\)。
操作分两种:
- 删去\(s_i\)末尾的字符。
- \(s_i\)末尾添加任意字符。
解题思路
对尾部操作,对这些字符串建立一颗Trie
树,操作一相当于节点往父亲走,操作二相当于往儿子方向走。然后以最小的步数到达叶子或者根。
考虑预处理数组\(mindeep_i\),表示从点 \(i\)出发,到达叶子的最小步数(其实就是子树的最小叶子深度-当前点深度),然后对该字符串所对应的 Tries
树的所有节点的\(mindeep_i\)+删去末尾字符到达该点的操作数
取个最小值即可。
插入字符串时需要更新 \(mindeep_i\),需要更新的也刚好就是插入时经过的所有节点。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int inf = 1e9 + 7;
const int SZ = 26;
template <typename T, typename K> struct Trie {
struct node {
bool is_terminal = false;
int min_deep = inf;
array<int, SZ> children{};
};
int cast(K val) {
int ret = val - 'a';
assert(ret < SZ and ret >= 0);
return ret;
}
vector<node> tree;
Trie(K val) { tree.push_back(node()); }
int insert(const T& sequence) {
int cur = 0;
int ans = sequence.size();
int cost = sequence.size();
for (int i = 0; i < (int)sequence.size(); i++) {
K value = sequence[i];
if (tree[cur].children[cast(value)] == 0) {
tree[cur].children[cast(value)] = (int)tree.size();
tree.push_back(node());
}
cur = tree[cur].children[cast(value)];
--cost;
ans = min(ans, cost + tree[cur].min_deep);
tree[cur].min_deep = min(tree[cur].min_deep, cost);
}
tree[cur].is_terminal = true;
return ans;
}
};
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n;
cin >> n;
Trie<string, char> trie('a');
for (int i = 0; i < n; i++) {
string s;
cin >> s;
int ans = trie.insert(s);
cout << ans << '\n';
}
return 0;
}