1. 程式人生 > 其它 >1108比賽總結

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