AtCoder Beginner Contest 378
省流版
- A. 判断奇偶性即可
- B. 根据余数计算偏移天数即可
- C. 用
map
记录每个数出现的位置即可 - D. 枚举起点,枚举每步的方向,朴素搜索即可
- E. 考虑前缀和的两数相减代替区间和的情况,减为负数则加回正数,用树状数组维护减为负数的情况数
- F. 枚举点,作为连边的俩个点的
lca
,考虑维护路径点度数为\(33..32\)的数量,组合即可
A - Pairing (abc378 A)
题目大意
给定\(4\)个数。
问做的操作数,每次选两个相同的数,然后丢弃。
解题思路
统计每个数的出现次数\(cnt_i\),答案就是 \(\sum \lfloor \frac{cnt_i}{2} \rfloor\)
神奇的代码
#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 = 4;
array<int, 4> cnt{};
while (n--) {
int a;
cin >> a;
cnt[a - 1]++;
}
int ans = 0;
for (auto& i : cnt) {
ans += i / 2;
}
cout << ans << '\n';
return 0;
}
B - Garbage Collection (abc378 B)
题目大意
\(n\)种垃圾,第 \(i\)种垃圾会在天数 \(d\)收取,其中 \(d\)满足 \(d \% p_i = r_i\)。
回答 \(q\)个询问,每个询问问在第 \(d_i\)天丢的第\(t_i\)种垃圾,会在第几天被收取。如果当天丢且当天可收取,则会被收取。
解题思路
假设\(j = t_i\),先算\(r = d_i \% p_j\),如果 \(r \leq r_j\),那么很显然多过\(r_j - r\)天就会被收取。否则要过一个循环,即\(p_j - r + r_j\)天才会被收取。
神奇的代码
#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;
cin >> n;
vector<array<int, 2>> a(n);
for (auto& x : a)
cin >> x[0] >> x[1];
int Q;
cin >> Q;
while (Q--) {
int t, d;
cin >> t >> d;
--t;
auto [q, r] = a[t];
int ans = (r - d % q + q) % q;
cout << d + ans << '\n';
}
return 0;
}
C - Repeating (abc378 C)
题目大意
给定一个数组\(a\),构造相同长度的数组 \(b\),满足 \(b_i\)是 \(a_i\)上一次出现的位置,或者 \(-1\)。
解题思路
直接用map
记录每个元素\(a_i\)上次出现的位置,然后输出\(map[a_i]\)即可
神奇的代码
#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;
cin >> n;
map<int, int> pos;
for (int i = 0; i < n; i++) {
int x;
cin >> x;
int ans = pos.count(x) ? pos[x] + 1 : -1;
cout << ans << " \n"[i == n - 1];
pos[x] = i;
}
return 0;
}
D - Count Simple Paths (abc378 D)
题目大意
给定一张二维平面,有障碍物。
问方案数,从任意点出发,上下左右走,可以走\(k\)步,不经过障碍物,且每个点只访问一次。
解题思路
由于平面\(10 \times 10\), \(k \leq 11\),直接花\(O(hw)\)枚举点,然后花\((4^k)\)遍历所有方案。 其时间复杂度为\(O(hw4^k)\),约为 \(1e8\),可过。
神奇的代码
#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 h, w, k;
cin >> h >> w >> k;
vector<string> s(h);
for (auto& x : s)
cin >> x;
int ans = 0;
array<int, 4> dx = {0, 1, 0, -1};
array<int, 4> dy = {1, 0, -1, 0};
auto ok = [&](int x, int y) -> bool {
return 0 <= x && x < h && 0 <= y && y < w && s[x][y] != '#';
};
vector<vector<int>> visit(h, vector<int>(w, 0));
auto dfs = [&](auto dfs, int x, int y, int cnt) -> void {
if (cnt == k) {
++ans;
return;
}
visit[x][y] = 1;
for (int i = 0; i < 4; ++i) {
int nx = x + dx[i];
int ny = y + dy[i];
if (ok(nx, ny) && !visit[nx][ny]) {
dfs(dfs, nx, ny, cnt + 1);
}
}
visit[x][y] = 0;
};
for (int i = 0; i < h; ++i) {
for (int j = 0; j < w; ++j) {
if (s[i][j] == '#')
continue;
dfs(dfs, i, j, 0);
}
}
cout << ans << '\n';
return 0;
}
E - Mod Sigma Problem (abc378 E)
题目大意
给定数组\(a\),和模数 \(m\)。求 \(\sum_{1 \leq l \leq r \leq n} ((\sum_{l \leq i \leq r} a_i )\% m)\)
解题思路
预处理前缀和\(sum[i] = (\sum_{j \leq i} a_i )\% m\),则区间和 \([l,r]\)可表示为 \(sum[r] - sum[l - 1]\)。
我们枚举\(r\),然后求所有的 \(l \leq r\),其区间和的和时多少。
由于取模的缘故,其结果但可能为负数,此时要\(+ m\),但有多少个\(l\)需要加呢?自然就是\(sum[l - 1] > sum[r]\)的那些 \(l\)。
由于 \(sum[i] \leq m\)只有\(1e5\),可以开一个计数的桶 \(tree[i]\)表示数字 \(i\)出现的次数,那么上述的 \(l\)的数量就是 \(\sum_{i > sum[r]} tree[i]\)。假设其数量为\(k\),那么当前 \(r\)对答案的贡献即为 \((\sum_{l \leq r} sum[r] - sum[l - 1]) + km = r \times sum[r] - \sum_{l \leq r} sum_[l - 1] + km\)。中间一项就是前缀和的前缀,而 \(k\)就是\(\sum_{i > sum[r]} tree[i]\)。
关于\(k\)的求法,涉及到区间求和和单点修改,因此可以用权值树状数组或权值线段树维护这个桶即可。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
// starting from 0
template <typename T> class fenwick {
public:
vector<T> fenw;
int n;
fenwick(int _n) : n(_n) { fenw.resize(n); }
void modify(int x, T v) {
while (x < n) {
fenw[x] += v;
x |= (x + 1);
}
}
T get(int x) {
T v{};
while (x >= 0) {
v += fenw[x];
x = (x & (x + 1)) - 1;
}
return v;
}
};
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
int presum = 0;
LL ppresum = 0;
fenwick<int> cnt(m);
LL ans = 0;
cnt.modify(0, 1);
for (int i = 0; i < n; ++i) {
int a;
cin >> a;
a %= m;
presum = (presum + a) % m;
int cc = i + 1 - cnt.get(presum);
ans += 1ll * (i + 1) * presum - ppresum + 1ll * m * cc;
cnt.modify(presum, 1);
ppresum += presum;
}
cout << ans << '\n';
return 0;
}
下述想的比较复杂度,同样是枚举\(r\),然后看所有\([l,r-1] \to [l,r]\)区间和的变化。分两类,一类是直接\([l,r-1] + a_r = [l, r]\) ,另一类是\([l,r-1] + a_r - m = [l,r]\) 。
因为区间和的范围同样在\([0,m-1]\),所以用权值线段树维护 \(cnt_i\)表示区间和\([l,r]=i\)的数量,当新增 \(a_r\)时,线段树里的数据都是\([l..r-1]\)的区间和个数,考虑计算贡献,即\(cnt_{0..m - a_i}\)属于第一类, \(cnt_{m - a_i..m-1}\)属于第二类。
分别计算贡献后,考虑\(cnt_i\)怎么变化,即怎么变成\([l..r]\)的区间和个数。由于所有数增加了\(a_r\),因此\(cnt_i\)会进行一个整体偏移 ,即\(cnt_{i+a_r} = cnt_i\),但直接这么做是 \(O(n)\)的,不能这么做。但考虑到是整体偏移,我们可以记录此时表示 \(cnt_0\)的位置,即原来在\([l,r-1]\)时,\(cnt_0\) 表示区间和为\(0\)的个数,在增加 \(a_r\)后, \(cnt_{m - a_r}\)就表示区间和为 \(0\)的个数。即我们自定义\(cnt_0\)的位置,这样就是整体偏移了。
神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int N = 2e5 + 8;
class segment {
#define lson (root << 1)
#define rson (root << 1 | 1)
public:
LL cnt[N << 2];
LL sum[N << 2];
LL lazy[N << 2];
int n;
void pushup(int root) {
cnt[root] = cnt[lson] + cnt[rson];
sum[root] = sum[lson] + sum[rson];
}
void build(int root, int l, int r) {
if (l == r) {
cnt[root] = 0;
sum[root] = 0;
return;
}
int mid = (l + r) >> 1;
build(lson, l, mid);
build(rson, mid + 1, r);
pushup(root);
}
void pushdown(int root, int l, int mid, int r) {
if (lazy[root]) {
sum[lson] += lazy[root] * cnt[lson];
sum[rson] += lazy[root] * cnt[rson];
lazy[lson] += lazy[root];
lazy[rson] += lazy[root];
lazy[root] = 0;
}
}
void update(int root, int l, int r, int L, int R, LL val) {
if (L > R)
return;
if (L <= l && r <= R) {
sum[root] += val * cnt[root];
lazy[root] += val;
return;
}
int mid = (l + r) >> 1;
pushdown(root, l, mid, r);
if (L <= mid)
update(lson, l, mid, L, R, val);
if (R > mid)
update(rson, mid + 1, r, L, R, val);
pushup(root);
}
void insert(int root, int l, int r, int pos, LL val) {
if (l == r) {
cnt[root] += 1;
sum[root] += val;
return;
}
int mid = (l + r) >> 1;
pushdown(root, l, mid, r);
if (pos <= mid)
insert(lson, l, mid, pos, val);
else
insert(rson, mid + 1, r, pos, val);
pushup(root);
}
pair<int, LL> query(int root, int l, int r, int L, int R) {
if (L <= l && r <= R) {
return {cnt[root], sum[root]};
}
int mid = (l + r) >> 1;
pushdown(root, l, mid, r);
pair<int, LL> ans = {0, 0};
if (L <= mid) {
auto tmp = query(lson, l, mid, L, R);
ans.first += tmp.first;
ans.second += tmp.second;
}
if (R > mid) {
auto tmp = query(rson, mid + 1, r, L, R);
ans.first += tmp.first;
ans.second += tmp.second;
}
return ans;
}
pair<int, LL> query_from(int root, int l, int r, int L, int R) {
if (L > R)
return {0, 0};
L = (L % n + n) % n + 1;
R = (R % n + n) % n + 1;
debug(L, R);
if (L <= R)
return query(root, l, r, L, R);
pair<int, LL> ans = {0, 0};
auto tmp = query(root, l, r, L, r);
ans.first += tmp.first;
ans.second += tmp.second;
tmp = query(root, l, r, 1, R);
ans.first += tmp.first;
ans.second += tmp.second;
return ans;
}
void update_from(int root, int l, int r, int L, int R, LL val) {
if (L > R)
return;
L = (L % n + n) % n + 1;
R = (R % n + n) % n + 1;
if (L <= R)
update(root, l, r, L, R, val);
else {
update(root, l, r, L, r, val);
update(root, l, r, 1, R, val);
}
}
} sg;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, m;
cin >> n >> m;
vector<int> a(n);
for (auto& x : a) {
cin >> x;
x %= m;
}
sg.build(1, 1, m);
sg.n = m;
int l = 0;
LL ans = 0;
for (int i = 0; i < n; i++) {
int r = l + m - a[i];
auto [cnt, sum] = sg.query_from(1, 1, m, l, r - 1);
ans += 1ll * cnt * a[i] + sum;
auto [cnt2, sum2] = sg.query_from(1, 1, m, r, l + m - 1);
ans += 1ll * cnt2 * (a[i] - m) + sum2;
sg.update_from(1, 1, m, l, r - 1, a[i]);
sg.update_from(1, 1, m, r, l + m - 1, a[i] - m);
l = r % m;
sg.insert(1, 1, m, (l + a[i]) % m + 1, a[i]);
ans += a[i];
}
cout << ans << '\n';
return 0;
}
F - Add One Edge 2 (abc378 F)
题目大意
给定一棵树,求加一条边的方案数,使得没有重边,且环上的所有点的度数为\(3\)。
解题思路
加一条边\(u \to v\),首先这两个点的度数为 \(2\),然后假设 \(u \to v\)路径上的所有点的度数为 \(3\)。
假设 \(u,v\)的最近公共祖先是 \(lca\),即 \(u \to lca\), \(v \to lca\)的所有点的度数为 \(3\)。
注意到这是一个向父亲方向的,要求路径上所有点为 \(3\)的信息,可以通过预处理 \(up[i]\)表示从 \(i\)往父亲走,其点度为 \(3\)的最浅深度之类的信息。然后我们只需枚举 \(u,v\),看 \(up[u],up[v]\)与 \(lca\)的深度关系,即可知道加的这条边 \(u \to v\)是否符合要求。
但上述时间复杂度为 \(O(n^2)\),我们考虑枚举 \(lca\),然后看其子树有多少对符合条件的 \(u,v\)。
从 \(lca\)的角度,我们需要什么信息?即从该 \(lca\)往儿子方向走,其一路点度数为 \(3\),最后一个点度数为 \(2\),这样的路径条数。不同子树之间的这类点就可以连边(当然 \(lca\)的度数也要是 \(3\))。
注意重边的情况,即 \(lca\)度数为 \(2\),其一个儿子的度数也为 \(2\)。
上述过程可能就是树形\(dp\)(?\(dp[i]\)表示 \(i\)子树内,一路往儿子方向,其点度数为 \(3\),最后一个点度数为 \(2\)的路径条数,然后合并不同子树时计算匹配的点对。
神奇的代码
#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;
cin >> n;
vector<vector<int>> edge(n);
vector<int> du(n);
for (int i = 0; i < n - 1; i++) {
int u, v;
cin >> u >> v;
u--;
v--;
edge[u].push_back(v);
edge[v].push_back(u);
du[u]++;
du[v]++;
}
LL ans = 0;
auto dfs = [&](auto dfs, int u, int fa) -> int {
int ret = 0;
for (int v : edge[u]) {
if (v == fa)
continue;
int nxt = dfs(dfs, v, u);
if (du[u] == 2)
ans += nxt;
else if (du[u] == 3)
ans += 1ll * nxt * ret;
ret += nxt;
}
if (du[u] == 2)
return 1;
else if (du[u] == 3)
return ret;
else
return 0;
};
dfs(dfs, 0, 0);
int extra = 0;
for (int u = 0; u < n; u++) {
for (auto v : edge[u]) {
if (du[u] == 2 && du[v] == 2)
extra++;
}
}
ans -= extra / 2;
cout << ans << '\n';
return 0;
}
G - Everlasting LIDS (abc378 G)
题目大意
给定\(a,b,m\),求 \(1 \sim ab\)的全排列数量,满足以下条件:
- 最长上升子序列长度为\(a\)
- 最长下降子序列长度为\(b\)
- 存在 \(n\)使得在末尾增加一个数 \(n+0.5\),其上述两个长度不改变。
输出数量对 \(m\)取模。
解题思路
<++>
神奇的代码