A - Subsegment Reverse (abc356 A)
題目大意
給定一個 \(1,2,3,...,n\)的排列\(a\),給定兩個數 \(l,r\),左右顛倒\(a[l..r]\)。輸出。
解題思路
按照題意模擬即可。
神奇的程式碼
#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, a, b;
cin >> n >> a >> b;
--a;
vector<int> ans(n);
iota(ans.begin(), ans.end(), 1);
reverse(ans.begin() + a, ans.begin() + b);
for (auto x : ans)
cout << x << ' ';
cout << '\n';
return 0;
}
B - Nutrients (abc356 B)
題目大意
給定一天\(n\)種營養的攝入目標量。
給定\(m\)種食物的\(n\)種營養的含量。
問是否所有營養都達到目標攝入了。
解題思路
按照題意,對每種營養的攝入總量求和,與目標比較即可。
神奇的程式碼
#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<int> a(m);
for (auto& x : a)
cin >> x;
while (n--) {
for (auto& x : a) {
int s;
cin >> s;
x -= s;
}
}
bool ok = true;
for (auto x : a) {
ok &= x <= 0;
}
if (ok) {
cout << "Yes" << '\n';
} else {
cout << "No" << '\n';
}
return 0;
}
C - Keys (abc356 C)
題目大意
\(n\)把鑰匙,有些真的,有些假的。一個門,可以拿一些鑰匙開啟它,若其中有 \(k\)個真的鑰匙,則門開啟。
給定了 \(m\)條記錄,表示用了哪些鑰匙,門是否開啟。
對於這\(n\)把鑰匙的真假,共 \(2^n\)種情況,問有多少種情況,不違反上述的記錄。
解題思路
\(n\)只有 \(15\),直接 \(O(2^n)\)列舉所有情況,然後列舉每個記錄,檢測其鑰匙能否開啟門,與結果是否相符。
神奇的程式碼
#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, k;
cin >> n >> m >> k;
vector<pair<vector<int>, int>> a(m);
for (auto& [v, c] : a) {
int x;
cin >> x;
v.resize(x);
for (auto& x : v) {
cin >> x;
--x;
}
string s;
cin >> s;
c = s[0] == 'o';
}
int ans = 0;
int up = (1 << n);
for (int i = 0; i < up; ++i) {
bool ok = true;
for (auto& [v, c] : a) {
int cnt = 0;
for (auto x : v) {
if (i & (1 << x)) {
++cnt;
}
}
if ((cnt >= k) ^ c) {
ok = false;
break;
}
}
ans += ok;
}
cout << ans << '\n';
return 0;
}
D - Masked Popcount (abc356 D)
題目大意
給定\(n,m\),求 \(\sum_{k=0}^{n}popcount(k\&m)\)
\(popcount(x)\)表示 \(x\)二進位制下 \(1\)的個數。
解題思路
\(n\)高達 \(10^{18}\),直接列舉會超時。
考慮貢獻轉換。
考慮到答案來自於二進位制下\(1\)的個數。 由於\(\&\)運算的特性,這些實際都是來自於\(m\)的每一個\(1\)。 我們需要考慮\(m\)二進位制下每一個 \(1\)對答案的貢獻,即有多少個 \(k\),使得 \(k\&m\)後該位是 \(1\)。
假設\(m\)的二進位制表示為 \(1...10...0\),考慮第 \(i\)位上的\(1\),思考有多少個 \(k\),使得 \(k\&m\)的第\(i\)位是 \(1\)。
考慮如何計算 \(k\)的數量,首先, \(k\)的第 \(i\)位一定是 \(1\),然後就剩下低位
和高位
的情況數。低位就是低於\(i\)位的那些位數的取值,高於 \(i\)位的就是高於 \(i\)位的位數取值。這個計數問題其實和數位\(dp\)差不多。
由於\(k\)有最大值 \(n\)的限制,低位
的情況數會依賴於高位
,為方便表述,設高位是\(up\),當前位是 \(middle\),低位是 \(down\),比如 \(n=110101\),當前第 \(i=2\)位(從 \(0\)開始),則 \(up=110, middle=1, down=01\)。然後情況數其實就分兩種:
- 如果
高位
取值和\(n\)的高位
不一致,則低位
取值沒有限制,因此低位
的情況數是\(2^i\) ,而高位
的情況數是\(up\),若此時 \(m\)的第 \(i\)位是 \(1\),則其貢獻(出現的次數)為 \(2^i \times up\)。 - 如果
高位
取值和\(n\)的高位
一致,則當前位
和低位
的都會收到\(n\)的限制。如果此時 \(middle=0\),則說明該位不能取 \(1\),則 \(m\)的第 \(i\)位沒有貢獻。否則 \(middle=1\),低位
的情況數就有 \(down+1\)種情況,而高位
的 情況數只有\(1\)種,若此時 \(m\)的第 \(i\)位是 \(1\),則其貢獻(出現的次數)為 \(down + 1\)。
綜上,考慮第\(i\)位,其中 \(n\)的高位是 \(up\),當前位是 \(middle\),低位是 \(down\),若 \(m\)的第 \(i\)位是 \(1\),則其對答案的貢獻(滿足 \(k\&m\)的第 \(i\)位是 \(1\)的 \(k\)的個數)為 \(2^i \times up + middle \times (down + 1)\)。
神奇的程式碼
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int mo = 998244353;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
LL n, m;
cin >> n >> m;
LL ans = 0;
LL up = n >> 1, middle = n & 1, down = 0, cnt = 1;
for (int i = 0; i < 60; ++i) {
if ((m >> i) & 1) {
ans += 1ll * up * cnt % mo + middle * (down + 1);
ans %= mo;
}
down = down | (middle << i);
middle = up & 1;
up >>= 1;
cnt <<= 1;
}
cout << ans << '\n';
return 0;
}
E - Max/Min (abc356 E)
題目大意
給定一個陣列\(a\),求 \(\sum_{i=1}^{n}\sum_{j=i+1}^{n} \lfloor \frac{\max(a_i, a_j)}{\min(a_i, a_j)} \rfloor\)。
解題思路
作除法始終是最大值除以最小值,因此可以先對\(a\)進行排序再計算,這不會影響答案。
對 \(a\)從小到大排序後,考慮列舉 \(j\),然後計算 \(i < j\)情況對答案的貢獻,即列舉最大值。
一個比較明顯的觀察是,可能會有若干個 \(a_i\),使得 \(\lfloor \frac{a_j}{a_i} \rfloor\)是同樣的值。那我們可以把這些值合併地來算,即值
\(\times\)個數
。
事實上可以透過數論分塊來求解,\(\lfloor \frac{a_j}{a_i} \rfloor\)的值的可能數量和因子個數同一個數量級,即\(O(\sqrt{a_i})\)個,而造成該取整結果的\(a_i\)的取值就是數論分塊裡的兩個邊界 \(l,r\),可以在 \(a\)中二分得到對應位置,因而得到個數
。而這複雜度是\(O(n\log n\sqrt{a_i})\),會超時。
數論分塊複雜度是根號級別,在這裡用不了,只能另尋它路。
這次考慮列舉 \(i\),然後計算 \(i < j\)情況對答案的貢獻,即列舉最小值。
同樣考慮貢獻轉換,原本的想法是求\(\lfloor \frac{a_j}{a_i} \rfloor = val\)的\(a_j\)數量\(cnt\),然後累計\(val \times cnt\)。
我們將其\(val\)看成\(1+1+1...\),然後重組一下,變成求 \(\lfloor \frac{a_j}{a_i} \rfloor \geq val\)的\(a_j\)數量\(cnt\)。
即原本是求\(\lfloor \frac{a_j}{a_i} \rfloor = 1,2,3\)的\(a_j\)數量,現在求\(\lfloor \frac{a_j}{a_i} \rfloor \geq 1,2,3\)的\(a_j\)數量。
這裡的貢獻轉換就是將\(\lfloor \frac{a_j}{a_i} \rfloor = 3\)對答案有\(3\)的貢獻,拆成了 \(1+1+1\),即\(\lfloor \frac{a_j}{a_i} \rfloor \geq 1 + \lfloor \frac{a_j}{a_i} \rfloor \geq 2 + \lfloor \frac{a_j}{a_i} \rfloor \geq 3\),然後合併所有的\(\lfloor \frac{a_j}{a_i} \rfloor \geq 1\)(或\(2,3\)),求其個數。
現在我們的視角轉換成求 \(\lfloor \frac{a_j}{a_i} \rfloor \geq v\)的\(a_j\)數量,即列舉\(v\),求\(a_j \geq va_i\)的\(j\)的數量。很明顯透過在\(a\)陣列二分\(va_i\),就能求得\(j\)的數量了。
由於\(max_a = 10^6\),那麼這裡列舉的\(v\)的數量就是\(O(\sum_{i=1}^{n} \frac{max_a}{a_i})\)。每次列舉都有一個二分的\(O(\log n)\),因此總的時間複雜度是\(O(\sum_{i=1}^{n} \frac{max_a}{a_i} \log n)\)。
如果所有的\(a_i\)都很小,比如都是 \(a_i=1\),那時間複雜度就是 \(O(nmax_a \log n)\),又炸了!
但看這個式子,非常像對數求和,如果\(a_i\)唯一,那複雜度就是\(O(\sum_{i=1}^{max_a} \frac{max_a}{i} \log n) = O(max_a \log max_a \log n)\),是可過的。
因此,如果\(a_i\)有重複的,那麼我們就合併重複的數,並記錄\(cnt_k\)表示 \(a_i=k\)的數量,然後考慮怎麼修正一下答案的計算。
合併相同的數,並從小到大排序,然後列舉當前的 \(a_i\),再列舉 \(v\),求得第一個\(a_j > va_i\)的 \(a_j\),那麼此時\(\lfloor \frac{a_j}{a_i} \rfloor \geq v\)的數量就是\(cnt_{a_i} \times \sum_{k \geq j} cnt_{a_k}\)。而後者\(\sum_{k=j}^{n} cnt_{a_j}\)就是一個關於\(cnt\)陣列的字尾和,預處理一下就能\(O(1)\)得到。
然後對於相同數之間的貢獻,則是\(\sum_{i} \frac{cnt_{a_i} \times (cnt_{a_i} - 1)}{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<int> r(n);
for (auto& x : r)
cin >> x;
map<int, int> s;
for (auto& i : r)
s[i]++;
vector<int> a;
vector<int> sum;
for (auto& [x, y] : s) {
a.push_back(x);
sum.push_back(y);
}
vector<int> suf(sum.size());
partial_sum(sum.rbegin(), sum.rend(), suf.rbegin(), plus<int>());
LL ans = 0;
n = a.size();
for (int i = 0; i < n; ++i) {
int x = a[i];
int pos = i + 1;
ans += 1ll * sum[i] * (sum[i] - 1) / 2;
while (pos < n) {
pos = lower_bound(a.begin() + pos, a.end(), x) - a.begin();
if (pos < n)
ans += 1ll * sum[i] * suf[pos];
x += a[i];
}
}
cout << ans << '\n';
return 0;
}
F - Distance Component Size Query (abc356 F)
題目大意
<++>
解題思路
<++>
神奇的程式碼
G - Freestyle (abc356 G)
題目大意
<++>
解題思路
<++>
神奇的程式碼