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;
}