省流版
- 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\)取模。
解題思路
<++>
神奇的程式碼