1108比賽總結
T1 太水不講。
T2:
何老闆有 \(n\) 個紅球和 \(m\) 個綠球。他想要把這個 \(n+m\) 球排成一行。
這一行中,設 \(R_i,G_i\) 分別表示 \([1,i]\) 位置中紅球和綠球的數量。何老闆要求,對於任意位置 \(i\),必須滿足 \(R_i\le G_i+k\)。
何老闆想知道,總共有多少種滿足上述要求的排球方案,答案可能很大, \(\operatorname{mod} 10^9+7\) 後再輸出。
\(1\le n,m\le 10^6\)
這題賽時猜結論暴力 dp 驗證,由於沒判邊界掛了 10 分。
都看出來是卡特蘭了還是不會,因為忘了卡特蘭數了。
我們可以把紅球看成右括號,把綠球看成左括號,問題變為卡特蘭數經典模型。
一個右括號看成向上走一步,左括號看成向右走一步,初始在 \((0,0)\),如果不考慮 \(R_i\le G_i+k\) 的限制,那麼答案就是花 \(n+m\) 步走到 \((n,m)\) 方案數 \(\tbinom{n+m}{n}\)。
加上這個限制後,路徑就不能穿過 \(y=x+k\) 這條直線(但可以碰到)。
我們觀察每一條不合法路徑,發現將它們第一次穿過這條直線前的路徑全部反轉(向右改成向上,向上改成向右)後它們都會經過點 \((n-k-1,m+k+1)\)。所以不合法路徑數位 \(\tbinom{n+m}{m+k+1}\),答案為 \(\tbinom{n+m}{n}-\tbinom{n+m}{m+k+1}\)
#include <cstdio> #include <cstring> #define int long long inline int max(const int x, const int y) {return x > y ? x : y;} inline int min(const int x, const int y) {return x < y ? x : y;} const int mod = 1e9 + 7; int fact[2000005], inv[2000005], n, m, k; int qpow(int a, int b) { int ret = 1LL; while (b) { if (b & 1) ret = ret * a % mod; a = a * a % mod, b >>= 1; } return ret; } inline int C(int n, int m) { return n < m ? 0 : fact[n] * inv[m] % mod * inv[n - m] % mod; } signed main() { fact[0] = 1, inv[0] = 1; for (int i = 1; i <= 2000000; ++ i) fact[i] = fact[i - 1] * i % mod; inv[2000000] = qpow(fact[2000000], mod - 2); for (int i = 1999999; i; -- i) inv[i] = inv[i + 1] * (i + 1) % mod; scanf("%lld%lld%lld", &n, &m, &k); if (n > m + k) return putchar('0'), 0; if (n == 0) return putchar('1'), 0; if (m == 0) return putchar(n <= k ? '1' : '0'), 0; printf("%lld", (C(n + m, n) - C(n + m, m + k + 1) + mod) % mod); }
T3:
這題資料範圍是真的離譜,不應該是 \(n\le 10^6\) 嗎。
首先列舉區間是沒救的,考慮列舉右端點快速找最優左端點也沒啥出路,考慮列舉次大值算貢獻。
首先,如果區間 \([a,b]\in [c,d]\) 且區間 \([a,b]\) 次大值與 \([c,d]\) 次大值一樣,則 \([c,d]\) 顯然優於 \([a,b]\)。
設 \(l_i\) 為 \(i\) 左邊第二個大於 \(a_i\) 的數,\(r_i\) 為 \(i\) 右邊第二個大於 \(r_i\) 的數。
則以 \(i\) 為次大值的區間的最大評分就是 \(i\) 與區間 \([l_i+1,r_i-1]\) 中任意數異或的最大值。
然後套個可持久化 01-trie 就沒了。
注意整個序列最大值不能與任何數異或。
我用的兩個單調棧求 \(l_i,r_i\),但程式碼中有三個棧,第三個棧只是箇中轉站。
#include <cstdio>
inline int max(const int x, const int y) {return x > y ? x : y;}
struct Stack {
int a[50005], n;
inline void push(const int x) {a[++ n] = x;}
inline void pop() {-- n;}
inline int size() {return n;}
inline int top() {return a[n];}
} s1, s2, s3;
int a[50005], l[50005], r[50005], root[50005], ch[4000005][2], cnt[4000005], tot, n, ans;
void insert(int &u, int v, int x, int d) {
if (!u) u = ++ tot;
if (d == -1) {cnt[u] = 1; return;}
bool k = x & 1 << d;
insert(ch[u][k], ch[v][k], x, d - 1), ch[u][!k] = ch[v][!k];
cnt[u] = cnt[ch[u][0]] + cnt[ch[u][1]];
}
int query(int u, int v, int x, int d) {
if (d == -1) return 0;
bool f = x & 1 << d;
if (cnt[ch[v][!f]] > cnt[ch[u][!f]]) return query(ch[u][!f], ch[v][!f], x, d - 1) + (1 << d);
else return query(ch[u][f], ch[v][f], x, d - 1);
}
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; ++ i) scanf("%d", a + i), insert(root[i], root[i - 1], a[i], 29);
for (int i = 1; i <= n; ++ i) {
while (s2.size() && a[s2.top()] < a[i]) r[s2.top()] = i, s2.pop();
while (s1.size() && a[s1.top()] < a[i]) s3.push(s1.top()), s1.pop();
while (s3.size()) s2.push(s3.top()), s3.pop();
s1.push(i);
}
while (s1.size()) r[s1.top()] = n + 1, s1.pop();
while (s2.size()) r[s2.top()] = n + 1, s2.pop();
for (int i = n; i; -- i) {
while (s2.size() && a[s2.top()] < a[i]) l[s2.top()] = i, s2.pop();
while (s1.size() && a[s1.top()] < a[i]) s3.push(s1.top()), s1.pop();
while (s3.size()) s2.push(s3.top()), s3.pop();
s1.push(i);
}
for (int i = 1; i <= n; ++ i)
if (l[i] || r[i] != n + 1) ans = max(ans, query(root[l[i]], root[r[i] - 1], a[i], 29));
printf("%d", ans);
return 0;
}
T4:
何老闆是糖果銷售員。
某市有 \(n\) 所幼兒園,通過 \(m\) 條雙向道路連線起來,幼兒園編號 \(1\) 到 \(n\)。任意幼兒園之間都存在可以相互到達路線。
何老闆想要去所有幼兒園銷售糖果。幼兒園的小朋友都喜歡糖果,每個幼兒園的小朋友對何老闆都提出了糖果要求。
其中 \(i\) 號幼兒園要求,何老闆必須攜帶至少 \(a_i\) 顆糖,才允許經過該幼兒園。
\(i\) 號幼兒園想要購買 \(b_i\) 顆糖果,每次經過該幼兒園,何老闆可以選擇賣給幼兒園 \(b_i\) 顆糖,也可以選擇不賣。
何老闆可以以任何幼兒園作為起點,他要在每所幼兒園都銷售一次糖果,
問,開始時,他最少需要攜帶多少顆糖(在任何時刻,何老闆攜帶的糖果數都是不能為負的)
思維題,像我這種思維僵化型選手這輩子都不可能切的。
這種題初看無從下手,沒有可以直接套用的演算法,還是要仔細分析性質才能找到突破口。
性質1:若點 \(u\) 被經過多次,我們一定會在最後一次給點 \(u\) 賣糖。
性質2:令 \(c_u=\max(a_i-b_i,0)\),則如果當前糖數能進入點 \(u\) 並賣糖,則在點 \(u\) 賣糖至少會剩下 \(c_u\) 顆。
性質3:對於之間直接有邊相連的點 \(u,v,c_u\ge c_v\),先去 \(c_u\) 划算。這條性質不能理解畫一下圖就理解了。
然後,我們按照 \(c_u\) 建一顆樹出來,類似重構樹的過程,\(c_u\) 大的當父親,\(c_u\) 小的當兒子。
但是根據性質3,不是所有邊都要定個向(從 \(c\) 大的指向 \(c\) 小的)然後保留嗎?
仔細想想,我們推答案時是從下推到上,這就意味著我們原圖上的所有約束在這棵樹上都保留了。刪去這條樹上的任何一條邊,都會導致圖不連通,約束變弱,答案變小。
然後開始 dp。
\(dp_u\) 表示給 \(u\) 為根子樹的所有點賣糖最小代價,\(sum_u\) 為\(u\) 為根子樹的所有點 \(b\) 值之和。
若 \(u\) 是葉子,\(dp_u=\max(a_u,b_u)\)
否則,列舉 \(u\) 兒子 \(v\),\(dp_u=sum_u-sum_v+\max(c_u,dp_v)\)。
#include <cstdio>
#include <vector>
#include <algorithm>
#define int long long
inline int min(const int x, const int y) {return x < y ? x : y;}
inline int max(const int x, const int y) {return x > y ? x : y;}
struct Node {
int val, id;
inline bool operator < (const Node a) const {return val < a.val;}
} pnt[100005];
int n, m, a[100005], b[100005], c[100005], fa[100005], sum[100005], dp[100005];
std::vector<int> G[100005], son[100005];
bool mark[100005];
int find(int x) {return fa[x] == x ? x : fa[x] = find(fa[x]);}
void dfs(int u) {
sum[u] = b[u];
if (son[u].empty()) {dp[u] = max(a[u], b[u]); return;}
for (int v : son[u]) dfs(v), sum[u] += sum[v];
dp[u] = 1e18;
for (int v : son[u]) dp[u] = min(dp[u], sum[u] - sum[v] + max(c[u], dp[v]));
}
signed main() {
scanf("%lld%lld", &n, &m);
for (int i = 1; i <= n; ++ i)
scanf("%lld%lld", a + i, b + i), pnt[i].val = c[i] = max(a[i] - b[i], 0), pnt[i].id = fa[i] = i;
for (int i = 1, u, v; i <= m; ++ i)
scanf("%lld%lld", &u, &v), G[u].push_back(v), G[v].push_back(u);
std::sort(pnt + 1, pnt + n + 1);
for (int i = 1; i <= n; ++ i) {
mark[pnt[i].id] = true;
for (int j : G[pnt[i].id])
if (mark[j] && find(pnt[i].id) != find(j))
son[pnt[i].id].push_back(fa[j]), fa[fa[j]] = fa[pnt[i].id];
}
dfs(pnt[n].id);
printf("%lld", dp[pnt[n].id]);
return 0;
}