省流版
- A. 計數判斷即可
- B. 計數統計即可
- C. 模擬即可
- D. 注意到字串是左右大小寫映象,從長度大往小依次考慮實際所處的位置,維護映象次數集合
- E. 並查集維護連通性,並嘗試與左右倆格子合併即可
- F. 博弈\(dp\),狀態數只有 \(5e5\),直接記憶化搜尋即可
- G. 列舉打亂起始位置,全排列分成三部分,考慮逆序對來源的\(6\)個部分,注意到打亂部分的逆序對期望數量為定值,其餘 \(5\)部分用權值樹狀陣列或權值線段樹維護即可
A - 123233 (abc380 A)
題目大意
給了\(6\)個數字,問是否
- \(1\)的數字出現 \(1\)次
- \(2\)的數字出現 \(2\)次
- \(3\)的數字出現 \(3\)次
解題思路
統計每個數字出現的次數即可。
神奇的程式碼
#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;
bool ok = true;
for (int i = 1; i <= 3; ++i)
ok &= count(s.begin(), s.end(), i + '0') == i;
if (ok)
cout << "Yes" << '\n';
else
cout << "No" << '\n';
return 0;
}
B - Hurdle Parsing (abc380 B)
題目大意
給定一個包含-|
的字串。統計每兩個|
之間有多少個-
。
解題思路
找到連續的兩個|
的下標,然後其差即為答案。Python
可以一行。
神奇的程式碼
print(' '.join([str(len(s)) for s in input()[1:-1].split('|')]))
C - Move Segment (abc380 C)
題目大意
給定一個01
子串。
連續的1
視為一個塊。
現將第\(k\)個塊移動到第 \(k-1\)個塊前面。
解題思路
按照題意,找到第\(k\)個塊的下標,然後複製即可。下述是Python
程式碼,split('0')
可以輕易找到第\(k\)個塊,然後移動即可。
神奇的程式碼
n, k = map(int, input().split())
k -= 1
s = input().split('0')
one = [pos for pos, i in enumerate(s) if i]
kk = s[one[k]]
s.pop(one[k])
s[one[k - 1]] += kk + '0'
print('0'.join(s))
D - Strange Mirroring (abc380 D)
題目大意
給定一個字串\(s\),重複下述操作 無數次:
- 將 \(s\)的字母大小寫反轉成 \(t\),加到 \(s\)後面
給定 \(q\)個詢問,每個詢問問第 \(k\)個字元是什麼。
解題思路
每進行一次操作,字串\(s\)的長度會翻倍。
容易發現其字串形如\(s_0\overline{s_0}\overline{s_0\overline{s_0}}\overline{s_0\overline{s_0}\overline{s_0\overline{s_0}}}\overline{s_0\overline{s_0}\overline{s_0\overline{s_0}}\overline{s_0\overline{s_0}\overline{s_0\overline{s_0}}}}\)
我們從大往小的看,左右兩邊長度一樣,區別就是反轉。我們找到第一個\(s_i\),使得\(k\)在右半邊,那我們就記錄一次反轉操作,然後遞迴的考慮右邊橫線下方的字串,直到 \(k < |s|\),我們就得到對應的字元和反轉的次數,就得知最後的字元是多少了。
即先找到第一個 \(s_i = s_{i-1}t_{i-1}\),其中 \(k\)位於 \(t_{i-1}\)裡,這裡\(s_{i-1}\)和 \(t_{i-1}\)只是大小寫反了。此時就需要反轉一次了,然後令\(k = k - |s_{i-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);
string s;
int q;
cin >> s >> q;
vector<LL> round{int(s.size())};
auto dfs = [&](auto dfs, LL k, int sign) -> char {
if (k < s.size()) {
if (sign) {
if (isupper(s[k])) {
return tolower(s[k]);
} else {
return toupper(s[k]);
}
} else {
return s[k];
}
}
auto pos = upper_bound(round.begin(), round.end(), k);
LL len = *pos / 2;
return dfs(dfs, k - len, sign ^ 1);
};
while (q--) {
LL k;
cin >> k;
--k;
while (round.back() <= k) {
round.push_back(round.back() * 2);
}
cout << dfs(dfs, k, 0) << ' ';
}
cout << '\n';
return 0;
}
E - 1D Bucket Tool (abc380 E)
題目大意
從左到右\(n\) 個格子,第 \(1\) 個格子顏色為\(i\) 。
維護 \(q\) 個查詢,分兩種:
1 x c
:將第\(x\)個格子連同周圍與其同色的格子塗成顏色\(c\)2 c
:問顏色\(c\)的格子數
解題思路
如果一個格子顏色與周圍相同,則它們就會連通,成為一個整體,我們可以用並查集維護這個連通性。
對於操作\(1\),從並查集就很方便的修改一個整體的顏色,進而維護每種顏色的格子數。但修改顏色後,需要看看這個整體與左右兩個格子的顏色是否相同,能否合併成新的整體。
為了能找到這個整體的左右相鄰格子,並查集需要維護該整體的最左和最右的格子,然後根據顏色決定是否合併。
神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
class dsu {
public:
vector<int> p;
vector<int> l;
vector<int> r;
vector<int> sz;
vector<int> col;
int n;
dsu(int _n) : n(_n) {
p.resize(n);
col.resize(n);
sz.resize(n);
l.resize(n);
r.resize(n);
iota(l.begin(), l.end(), 0);
iota(r.begin(), r.end(), 0);
iota(p.begin(), p.end(), 0);
iota(col.begin(), col.end(), 0);
fill(sz.begin(), sz.end(), 1);
}
inline int get(int x) { return (x == p[x] ? x : (p[x] = get(p[x]))); }
inline bool unite(int x, int y) {
x = get(x);
y = get(y);
if (x != y) {
p[x] = y;
sz[y] += sz[x];
l[y] = min(l[y], l[x]);
r[y] = max(r[y], r[x]);
return true;
}
return false;
}
};
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, q;
cin >> n >> q;
dsu d(n);
vector<int> cnt(n, 1);
while (q--) {
int op;
cin >> op;
if (op == 1) {
int x, c;
cin >> x >> c;
x--;
--c;
int fx = d.get(x);
cnt[d.col[fx]] -= d.sz[fx];
d.col[fx] = c;
cnt[d.col[fx]] += d.sz[fx];
for (auto& nei : {d.l[fx] - 1, d.r[fx] + 1}) {
if (nei >= 0 && nei < n) {
int fnei = d.get(nei);
if (fnei != fx && d.col[fnei] == c) {
d.unite(fx, fnei);
}
}
}
} else {
int c;
cin >> c;
--c;
cout << cnt[c] << '\n';
}
}
return 0;
}
F - Exchange Game (abc380 F)
題目大意
高橋和青木手裡各有\(n,m\)張牌,桌子上有 \(l\)張牌。牌上寫了數字。
高橋和青木輪流操作,直到無法操作的人輸。
操作為,將手裡的一張牌 \(a\)放到桌子上,如果桌上有數字小於 \(a\)的牌,他可以拿一張到自己手上,當然也可以選擇不拿。
高橋先手,問最優策略下誰贏。
解題思路
博弈\(dp\),從卡牌在誰手中的角度思考狀態數,只有 \(O(3^{n+m+l}) = O(3^12) = 5e5\),因此直接搜尋即可。
即設 \(dp[i][j]=1/0\)表示當前 \(i\)先手,局面是 \(j\)(即一個描述\(n+m+l\)張牌在誰手中的狀態,可以是一個陣列,\(0/1/2\)分別表示在高橋、青木或者桌子上,也可以是一個三進位制的壓縮狀態),此時先手必贏/必輸。
轉移則列舉當前手的策略,即先花\(O(n+m+l)\)列舉將手裡的哪張牌放到桌子上,再花 \(O(n+m+l)\)列舉 拿桌子上的哪張牌,然後更新局面狀態,轉移到下一個局面即可。
而當前局是先手必贏/必輸,取決於下一個局面(注意下一個局面的先手是當前局面的後手)。因為是最優策略,因此下一個局面如果有(當前局面的後手)必輸局面,那先手必贏,否則如果下一個局面全是(當前局面的後手)必贏局面,則先手必輸。
因此求解是一個遞迴的過程。總的時間複雜度是\(O((n+m+l)^2 3^{n+m+l})\)
vector狀態1700ms
#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, l;
cin >> n >> m >> l;
vector<int> c(n + m + l);
for (auto& x : c)
cin >> x;
array<map<vector<int>, int>, 2> dp;
vector<int> st(n + m + l);
fill(st.begin(), st.begin() + n, 0);
fill(st.begin() + n, st.begin() + n + m, 1);
fill(st.begin() + n + m, st.end(), 2);
auto dfs = [&](auto dfs, vector<int>& st, int p) -> bool {
if (count(st.begin(), st.end(), p) == 0) {
return dp[p][st] = 0;
}
if (dp[p].count(st))
return dp[p][st];
bool ok = true;
for (int i = 0; i < n + m + l; i++) {
if (st[i] == p) {
st[i] = 2;
for (int j = 0; j < n + m + l; j++) {
if (st[j] == 2 && c[j] < c[i]) {
st[j] = p;
ok &= dfs(dfs, st, p ^ 1);
st[j] = 2;
}
}
ok &= dfs(dfs, st, p ^ 1);
st[i] = p;
}
}
return dp[p][st] = (ok ^ 1);
};
if (dfs(dfs, st, 0))
cout << "Takahashi" << '\n';
else
cout << "Aoki" << '\n';
return 0;
}
三進位制壓縮狀態200ms
#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, l;
cin >> n >> m >> l;
vector<int> c(n + m + l);
for (auto& x : c)
cin >> x;
vector<int> base(n + m + l, 1);
for (int i = 1; i < n + m + l; ++i) {
base[i] = base[i - 1] * 3;
}
array<vector<int>, 2> dp{vector<int>(base.back() * 3, -1),
vector<int>(base.back() * 3, -1)};
auto get_bit = [&](int x, int y) { return x / base[y] % 3; };
auto tr = [&](int& cur, int pos, int v) {
cur -= get_bit(cur, pos) * base[pos];
cur += v * base[pos];
};
auto final = [&](int st, int p) {
for (int i = 0; i < n + m + l; i++) {
if (get_bit(st, i) == p)
return false;
}
return true;
};
auto dfs = [&](auto dfs, int st, int p) -> bool {
if (final(st, p)) {
return dp[p][st] = 0;
}
if (dp[p][st] != -1)
return dp[p][st];
bool ok = true;
for (int i = 0; i < n + m + l; i++) {
if (get_bit(st, i) == p) {
tr(st, i, 2);
for (int j = 0; j < n + m + l; j++) {
if (get_bit(st, j) == 2 && c[j] < c[i]) {
tr(st, j, p);
ok &= dfs(dfs, st, p ^ 1);
tr(st, j, 2);
}
}
ok &= dfs(dfs, st, p ^ 1);
tr(st, i, p);
}
}
return dp[p][st] = (ok ^ 1);
};
int st = 0;
for (int i = 0; i < n + m + l; i++) {
tr(st, i, (i >= n) + (i >= n + m));
}
if (dfs(dfs, st, 0))
cout << "Takahashi" << '\n';
else
cout << "Aoki" << '\n';
return 0;
}
G - Another Shuffle Window (abc380 G)
題目大意
給定一個關於\(n\)的全排列\(p\)和一個數字 \(k\)。進行如下操作一次。
- 第一步,從\([1, n - k + 1]\)隨機選一個數\(i\)
- 第二部,將 \(p_{i, i + k - 1}\)隨機打亂
問進行操作後的逆序對的期望值。
解題思路
期望的求法就是該情況的逆序對數
$\times $發生該情況的機率
,對所有情況求和。因此我們先考慮所有情況怎麼來的。
首先第一步,列舉\(i\),它數量只有 \(O(n)\),可以列舉。
列舉了 \(i\)後,排列 \(p\)就分成了三部分: \(l,m,r\),其中 \(m\)就是將會隨機打亂的\(k\)個數,然後\(l,r\)就是左右兩部分的數。
再然後就是第二部,將中間部分\(m\)打亂,但這情況數有\(O(k!)\),顯然不能列舉,我們考慮能否綜合第二步的所有情況來求。
考慮逆序對\((i,j)\)的來源,根據\(i,j\)的值,有這幾部分:
- \(ll\)部分
- \(lr\)部分
- \(lm\)部分
- \(rr\)部分
- \(mr\)部分
- \(mm\)部分
除了最後一個,前\(5\)個的逆序對的數量不會隨著第二步的操作而改變。當第一個的\(i\)變化時,我們可以實時維護前 \(5\)部分的逆序對數量的變化。
而第六部分的 \(mm\),考慮裡面的任意兩個數 \((i,j)\),綜合所有的隨機打亂情況 ,誰前誰後其實各佔一半,也就是說,任意一對數只有一半的機率會產生逆序對,而\(k\)個數一共有 \(\frac{k(k-1)}{2}\)對數,每對形成逆序對的機率都是 \(\frac{1}{2}\),因此第六部分的期望逆序對數量始終是 \(\frac{k(k-1}}{2} \frac{1}{2} = \frac{k(k-1)}{4}\)。
而前 \(5\)部分的維護,即 \(m\)最左邊少一個數, \(l\)最右邊多一個數, \(m\) 最右邊多一個數,\(r\)最左邊少一個數,依次計算這些數所產生的逆序對數量,然後更新即可。
而產生逆序對的數量,即對於數\(i\) ,我們想知道大於\(i\)或小於 \(i\)的數量,可以透過維護 \(l,m,r\)部分的桶(即\(cnt_{i}\)表示數字\(i\)出現的次數 ),加以樹狀陣列或線段樹的單點修改和區間求和,從而在 \(O(\log n)\)的時間得到。即權值線段樹或權值樹狀陣列。
總的時間複雜度為\(O(n \log n)\)。
神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
// starting from 1
template <typename T> class fenwick {
public:
vector<T> fenw;
int n;
int tot;
fenwick(int _n) : n(_n) {
fenw.resize(n);
tot = 0;
}
inline int lowbit(int x) { return x & -x; }
void modify(int x, T v) {
tot += v;
for (int i = x; i < n; i += lowbit(i)) {
fenw[i] += v;
}
}
T get(int x) {
T v{};
for (int i = x; i > 0; i -= lowbit(i)) {
v += fenw[i];
}
return v;
}
};
const LL mo = 998244353;
long long qpower(long long a, long long b) {
long long qwq = 1;
while (b) {
if (b & 1)
qwq = qwq * a % mo;
a = a * a % mo;
b >>= 1;
}
return qwq;
}
long long inv(long long x) { return qpower(x, mo - 2); }
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n, k;
cin >> n >> k;
vector<int> p(n);
fenwick<int> l(n + 1), mid(n + 1), r(n + 1);
for (auto& x : p) {
cin >> x;
}
LL llc = 0, lmc = 0, mrc = 0, rrc = 0, lrc = 0;
LL mmc = 1ll * k * (k - 1) % mo * inv(4) % mo;
const int large = 1;
const int small = 0;
auto get_cnt = [&](fenwick<int>& f, int p, int large) {
if (!large)
return f.get(p - 1);
else
return f.tot - f.get(p);
};
auto update_l = [&](int num, int v) { // the right of l
l.modify(num, v);
llc += get_cnt(l, num, large) * v;
lmc += get_cnt(mid, num, small) * v;
lrc += get_cnt(r, num, small) * v;
};
auto update_m = [&](int num, int v) {
mid.modify(num, v);
lmc += get_cnt(l, num, large) * v;
mrc += get_cnt(r, num, small) * v;
};
auto update_r = [&](int num, int v) { // the left of r
r.modify(num, v);
lrc += get_cnt(l, num, large) * v;
mrc += get_cnt(mid, num, large) * v;
rrc += get_cnt(r, num, small) * v;
};
for (int i = 0; i < k; ++i)
update_m(p[i], 1);
for (int i = n - 1; i >= k; --i)
update_r(p[i], 1);
LL ans = (llc + lmc + mrc + rrc + lrc + mmc) % mo;
for (int i = 0; i < n - k; ++i) {
update_m(p[i], -1);
update_l(p[i], 1);
update_r(p[i + k], -1);
update_m(p[i + k], 1);
ans = (ans + llc + lmc + mrc + rrc + lrc + mmc) % mo;
}
ans = ans * inv(n - k + 1) % mo;
cout << ans << '\n';
return 0;
}