A - ストーブ (Stove)
有 \(n\) 個客人將要來訪,第 \(i\) 個客人的來訪時間為 \([a_i, a_i + 1]\),保證 \(\forall i \in [1, n), a_i < a_{i + 1}\)。
在每個客人來訪時,你都需要保證暖爐是亮著的(初始時暖爐為熄滅狀態)。你可以在任意時刻熄滅暖爐,但每次點亮都需要消耗一根火柴,且你只有 \(k\) 根火柴,求最小的暖爐點亮時間。
\(n \le 10^5, a_i \le 10^9\)。
首先使用一根火柴點亮初始時刻的暖爐,接下來你有 \(k - 1\) 次熄滅暖爐的機會。
考慮在第 \(i\) 個人到第 \(i + 1\) 個人的時間間隙中熄滅暖爐的收益為 \(a_{i + 1} - a_i - 1\),將這個收益排序並取前 \(k\) 大即可。時間複雜度 \(O(n \log n)\)。
Code
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e5 + 5;
int n, k;
int a[N], b[N];
int main () {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> k;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
for (int i = 1; i < n; ++i) {
b[i] = a[i + 1] - a[i] - 1;
}
int ans = a[n] - a[1] + 1;
sort(b + 1, b + n, greater<int>());
for (int i = 1; i < k; ++i) {
ans -= b[i];
}
cout << ans << '\n';
return 0;
}
B - 美術展 (Art Exhibition)
有 \(n\) 件美術品,每件美術品有尺寸 \(A_i\) 與價值 \(B_i\),現在要選擇若干美術品作為展覽,設選擇的美術品集合為 \(S\),最大化 \(\sum\limits_{i \in S} B_i - (\max\limits_{i \in S} A_i - \min\limits_{i \in S} A_i)\)、
\(1 \le n \le 5 \times 10^5, 1 \le A_i \le 10^{15}, 1 \le B_i \le 10^9\)。
首先將所有美術品按照 \(A_i\) 排序,由於所有美術品價值為正,多選一定更優,所以選擇的美術品是一段區間。
對 \(B\) 做字首和。設選擇的區間為 \([l, r]\),則價值為 \(B_r - B_{l - 1} - A_r + A_l\)。考慮設一個左端點 \(l\) 的權值為 \(A_l - B_{l - 1}\),設一個右端點 \(r\) 的權值為 \(B_r - A_r\),線性掃一遍即可。時間複雜度 \(O(n \log n)\)。
Code
#include <iostream>
#include <algorithm>
using namespace std;
using LL = long long;
using Pii = pair<LL, LL>;
const int N = 5e5 + 5;
int n;
Pii a[N];
int main () {
cin.tie(0)->sync_with_stdio(0);
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> a[i].first >> a[i].second;
}
sort(a + 1, a + n + 1);
for (int i = 1; i <= n; ++i) {
a[i].second += a[i - 1].second;
}
LL mxl = 0, ans = 0;
for (int i = 1; i <= n; ++i) {
mxl = max(mxl, a[i].first - a[i - 1].second);
ans = max(ans, mxl + a[i].second - a[i].first);
}
cout << ans << '\n';
return 0;
}
C - 団子職人 (Dango Maker)
有一個 \(n \times m\) 的網格,每個糰子被放在一個格子裡,每個糰子的顏色是
R
,G
, 'W' 中的一種。
現在你可以從上到下,或從左到右選一個 \(1 \times 3\) 的矩形,並將其中的糰子按順序做成一個糰子串,要求三個糰子的顏色必須依次是R
,G
,W
。
求最大的可以製作的糰子數。
\(1 \le n, m \le 3 \times 10^3\)。
對於所有糰子,考慮在 G
處計算貢獻,那麼兩個糰子如果衝突,則它們一定在同一條從左上到右下的對角線上。
所以我們對於每個對角線獨立地 \(\text{dp}\)。考慮當前某個對角線的一個格子,設 \(f_0\) 為該糰子不做糰子串的方案數,\(f_1\) 表示該糰子橫著串的方案數,\(f_2\) 表示該糰子豎著串的方案數。則 \(f_1\) 和 \(f_2\) 之間不能相互轉移。
時間複雜度 \(O(nm)\)。
Code
#include <iostream>
using namespace std;
const int N = 3e3 + 5;
int n, m, f[3], g[3];
char s[N][N];
int main () {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m;
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
cin >> s[i][j];
}
}
int ans = 0;
for (int sum = 2; sum <= n + m; ++sum) {
f[0] = 0, f[1] = f[2] = -N;
for (int i = max(1, sum - m), j = sum - i; i <= n && j; ++i, --j) {
copy(f, f + 3, g);
fill(f, f + 3, -N);
f[0] = max(max(g[0], g[1]), g[2]);
if (s[i][j] == 'G' && s[i - 1][j] == 'R' && s[i + 1][j] == 'W') {
f[1] = max(g[0], g[1]) + 1;
}
if (s[i][j] == 'G' && s[i][j - 1] == 'R' && s[i][j + 1] == 'W') {
f[2] = max(g[0], g[2]) + 1;
}
}
ans += max(max(f[0], f[1]), f[2]);
}
cout << ans << '\n';
return 0;
}
D - 定期券 (Commuter Pass)
給定一張 \(n\) 個結點 \(m\) 條邊的無向圖,邊有邊權。
現在你可以選擇一條 \(s\) 到 \(t\) 的最短路,並將這條路徑上的邊的權值全部設為 \(1\),求操作後 \(u\) 到 \(v\) 最短路的最小值。
\(1 \le n \le 10^5, 1 \le m \le 2 \times 10^5\)。
首先考慮如何刻畫“\(s\) 到 \(t\) 的最短路”,我們從 \(s\) 開始跑一遍 \(\text{Dijkstra}\),求出 \(d_i\) 表示 \(s\) 到 \(i\) 的最短路,對於原圖上的無向邊拆成兩條有向邊,如果一條有向邊 \((u, v)\) 不滿足 \(d_v = d_u + w(u, v)\),則將其從圖中刪去。那麼我們將得到一張 \(\text{DAG}\),此時在新圖上每一條 \(s\) 到 \(t\) 的路徑都是最短路。
然後我們發現答案要麼不經過修改權值的邊,此時就是 \(dis(u, v)\),要麼形如 \(u \rightarrow a \rightarrow b \rightarrow v\),其中 \(a\) 和 \(b\) 在 \(s\) 到 \(t\) 的一條最短路上。則此時答案為 \(dis(u, a) + dis(v, b)\) 或 \(dis(u, b) + dis(v, a)\)。
考慮在 \(\text{DAG}\) 上 \(\text{dp}\),設 \(r_i\) 表示 \(i\) 是否能到 \(t\),\(f_{i, 0}\) 表示 \(i\) 能到達的,且滿足 \(r_x = 1\) 的點中最小的 \(dis(v, x)\),\(f_{i, 1}\) 類似,直接 \(\text{dp}\) 即可。對於所有 \(i\),\(dis(s, i), dis(t, i)\) 可以 \(\text{Dijkstra}\) 預處理。
時間複雜度 \(O(n \log n)\)。
Code
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
using LL = long long;
using Pii = pair<LL, LL>;
const int N = 1e5 + 5;
const LL Inf = 1e18;
int n, m, gs, gt, s, t, deg[N];
vector<Pii> e[N];
vector<int> g[N];
LL dis[3][N], f[N][2];
bool r[N];
void Dijkstra (int st, LL *dis) {
priority_queue<Pii, vector<Pii>, greater<Pii>> q;
q.push({0, st});
fill(dis + 1, dis + n + 1, Inf);
while (!q.empty()) {
Pii tp = q.top();
q.pop();
LL d = tp.first;
int u = tp.second;
if (d < dis[u]) {
dis[u] = d;
for (auto i : e[u]) {
int v = i.first, w = i.second;
q.push({d + w, v});
}
}
}
}
int main () {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> m >> gs >> gt >> s >> t;
for (int i = 1, u, v, w; i <= m; ++i) {
cin >> u >> v >> w;
e[u].push_back({v, w});
e[v].push_back({u, w});
}
Dijkstra(s, dis[0]), Dijkstra(t, dis[1]), Dijkstra(gs, dis[2]);
for (int i = 1; i <= n; ++i) {
for (auto j : e[i]) {
if (dis[2][j.first] == dis[2][i] + j.second) {
g[j.first].push_back(i), ++deg[i];
}
}
}
queue<int> q;
for (int i = 1; i <= n; ++i) {
if (!deg[i]) {
q.push(i);
}
}
fill(f[1], f[n] + 2, Inf);
LL ans = dis[0][t];
for (; !q.empty(); q.pop()) {
int u = q.front();
if (u == gt) {
r[u] = 1;
}
if (r[u]) {
f[u][0] = min(f[u][0], dis[1][u]);
f[u][1] = min(f[u][1], dis[0][u]);
ans = min(ans, min(dis[0][u] + f[u][0], dis[1][u] + f[u][1]));
}
for (auto v : g[u]) {
if (r[u]) {
f[v][0] = min(f[v][0], f[u][0]);
f[v][1] = min(f[v][1], f[u][1]);
r[v] = 1;
}
if (!--deg[v]) {
q.push(v);
}
}
}
cout << ans << '\n';
return 0;
}
E - 毒蛇の脫走 (Snake Escaping)
給定 \(n\),對於一個長度為 \(n\) 的二進位制數 \(x\),定義其權值為 \(a_x\)。接下來進行 \(q\) 次詢問,每次詢問格式如下:
- 給定一個長度為 \(n\) 字串 \(s\),\(s\) 中的每個字元是 \(\texttt{0, 1, ?}\) 三者之一,對於所有將
?
填寫成0
或1
的方案組成的數字,求它們的權值和,此處允許前導 \(\texttt{0}\)。\(1 \le n \le 20, 1 \le q \le 10^6\)。
記 \(s_0, s_1, s_2\) 分別為 \(\texttt{0, 1, ?}\) 在 \(s\) 中的出現位置,\(cnt_0 = |s_0|, cnt_1 = |s_1|, cnt_2 = |s_2|\) 分別為這三種字元在原串中的出現次數。
考慮類似根號分治,因為 \(cnt_0 + cnt_1 + cnt_2 \le 20\),所以 \(\min(cnt_0, cnt_1, cnt_2) \le 6\)。我們 \(\forall i \in [0, 2]\),對於 \(cnt_i \le 6\) 的情況分別處理。
對於 \(cnt_2 \le 6\)
由於 \(\texttt{?}\) 不超過 \(6\) 個,所以我們列舉每個 \(\texttt{?}\) 填什麼,對於所有可能的方案求和即可。
時間複雜度 \(O(2^{cnt_2})\)。
對於 \(cnt_0 \le 6\)
考慮只有 \(\texttt{1}\) 和 \(\texttt{?}\) 時怎麼做,我們將 \(\text{?}\) 看成 \(\texttt{0}\),那麼所有方案的權值和相當於高維字尾和。具體地,設 \(f_s = \sum\limits_{t \supseteq s} a_t\),則一個轉化後的集合 \(s\) 權值和為 \(f_s\)。
現在加入 \(\texttt{0}\),由於 \(\texttt{0}\) 的個數很少,所以我們對 \(\texttt{0}\) 容斥,它可以看作 \(\texttt{?}\) 的方案數減去 \(\texttt{1}\) 的方案數,所以我們列舉每個 \(\texttt{0}\) 是變成 \(\texttt{1}\) 還是 \(\texttt{?}\),得到答案為
時間複雜度 \(O(2^{cnt_0})\)
對於 \(cnt_1 \le 6\)
類似地考慮只有 \(\texttt{1}\) 和 \(\texttt{?}\) 的情況,我們將 \(\texttt{?}\) 看成 \(\texttt{1}\),設 \(g_s = \sum\limits_{t \subseteq s} a_t\),則一個轉化後的集合 \(s\) 權值和為 \(g_{A - s}\),其中 \(A\) 為全集。
還是類似地將 \(\texttt{1}\) 容斥為 \(\texttt{?}\) 的方案減去 \(\texttt{0}\) 的方案,列舉每個 \(\texttt{1}\) 變 \(\texttt{0}\) 還是變 \(\texttt{?}\),可得答案為
時間複雜度 \(O(2^{cnt_1})\)。
最後 \(f, g\) 分別相當於高維後、字首和,直接上 \(\text{And-FWT}\) 和 \(\text{Or-FWT}\) 即可。
總時間複雜度 \(O(2^nn + q 2^{\lfloor \frac{n}{3} \rfloor})\)。
Code
#include <iostream>
using namespace std;
const int N = 20, S = (1 << N);
int n, q, all;
int a[S], f[S], g[S];
void FWT_And (int *a) {
for (int k = 1; k <= all; k <<= 1) {
for (int i = 0; i <= all; i += (k << 1)) {
for (int j = 0; j < k; ++j) {
a[i + j] += a[i + j + k];
}
}
}
}
void FWT_Or (int *a) {
for (int k = 1; k <= all; k <<= 1) {
for (int i = 0; i <= all; i += (k << 1)) {
for (int j = 0; j < k; ++j) {
a[i + j + k] += a[i + j];
}
}
}
}
int main () {
cin.tie(0)->sync_with_stdio(0);
cin >> n >> q, all = (1 << n) - 1;
for (int s = 0; s <= all; ++s) {
char c;
cin >> c;
a[s] = f[s] = g[s] = c - '0';
}
FWT_And(f), FWT_Or(g);
for (string str; q--; ) {
cin >> str;
int s[3] = {0, 0, 0}, cnt[3];
for (int i = 0; i < n; ++i) {
if (str[i] == '0')
s[0] |= (1 << (n - i - 1));
else if (str[i] == '1')
s[1] |= (1 << (n - i - 1));
else if (str[i] == '?')
s[2] |= (1 << (n - i - 1));
}
for (int i = 0; i < 3; ++i) {
cnt[i] = __builtin_popcount(s[i]);
}
if (cnt[2] <= 6) {
int ans = 0;
for (int t = s[2]; ; t = (t - 1) & s[2]) {
ans += a[t | s[1]];
if (!t) break;
}
cout << ans << '\n';
}
else if (cnt[0] <= 6) {
int ans = 0;
for (int t = s[0]; ; t = (t - 1) & s[0]) {
ans += f[t | s[1]] * (__builtin_popcount(t) & 1 ? -1 : 1);
if (!t) break;
}
cout << ans << '\n';
}
else if (cnt[1] <= 6) {
int ans = 0;
for (int t = s[1]; ; t = (t - 1) & s[1]) {
ans += g[(t | s[0]) ^ all] * (__builtin_popcount(t) & 1 ? -1 : 1);
if (!t) break;
}
cout << ans << '\n';
}
}
return 0;
}