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;
}
posted @ 2021-11-08 20:42  zqs2020  阅读(41)  评论(0编辑  收藏  举报