省選聯考 2024

PassName發表於2024-03-16

省選聯考 2024

前言

有的題沒必要一定要推到滿分才可以,比較陰間的寫個八九十的分就很不錯了,特別陰間的寫個暴力就算了,沒必要一定要全學懂是不是/fad

[省選聯考 2024] 季風

傳送門

講題目轉化為在 \((0,0)\),求最小 \(m\) 使 \(|x-\sum\limits_{i=0}^{m-1}x_{i\mod n}|+|y-\sum\limits_{i=0}^{m-1}y_{i\mod n}|\le k\times m\)

\(m=n\times z+i(0≤i<n)\)\(x_i\) 的字首和 \(X_i\)\(y_i\) 的字首和 \(Y_i\)

原式轉化為 \(|x-X_n\times z-X_i|+|y-Y_n\times z-Y_i|\le k\times(n\times z+i)\)

解絕對值不等式,更新答案即可。

void calc(int k, int b) 
{
	if (k == 0 && b < 0) l = inf + 1;
	else if (k > 0) r = min(r, (int)floor(1.0 * b / k));
	else l = max(l, (int)ceil(1.0 * b / k));
}

signed main() 
{
	int T;
	cin >> T;
	while (T--) 
	{
		cin >> n >> k >> x >> y;
		for (rint i = 1; i <= n; i++)
		{
			cin >> X[i] >> Y[i];
			X[i] += X[i - 1];
			Y[i] += Y[i - 1];			
		}
		if (!x && !y) 
		{
			puts("0");
			continue;
		}
		int ans = inf;
		for (rint i = 1; i <= n; i++) 
		{
			l = 0, r = inf;
			calc(X[n] + Y[n] - n * k, x + y - X[i] - Y[i] + i * k);
			calc(X[n] - Y[n] - n * k, x - y - X[i] + Y[i] + i * k);
			calc(Y[n] - X[n] - n * k, y - x - Y[i] + X[i] + i * k);
			calc(-X[n] - Y[n] - n * k, -x - y + X[i] + Y[i] + i * k);
			if (l <= r) ans = min(ans, n * l + i);
		}
		cout << (ans == inf ? -1 : ans) << endl;
	}
	return 0;
}

[省選聯考 2024] 魔法手杖

傳送門

PS:窩太菜了,調了半天還是 96pts,演算法應該沒有假,應該是程式碼實現哪裡出了問題,懶得調了。

題目大概就是說給你 \(n\)\([0,2^k)\) 中的整數 \(a_1,a_2,\dots,a_n\),第 \(i\) 個數對應代價 \(b_i\)

需要選取一個 \(U=\{1,2,\dots,n\}\) 的子集 \(S\),滿足 \(\sum_{i\in S} b_i\le m\),以及一個 \([0,2^k)\) 中的整數 \(x\),最大化 \(\min\{\min_{i\in S}\{a_i+x\},\min_{i\in U\setminus S}\{a_i\oplus x\}\}\)

最大化min,異或,\(2^k\) 對於這些東西要敏感,直接考慮二分和 dp 並不可做,考慮 01Trie 是否有操作空間

好像能玩,吃碗泡麵開始把玩

從高往低逐位找 \(x\) 以及最終的答案

設當前走到了樹上的某個結點 \(u\),已定答案 \(k\) 且只有高位有值,\(k\) 是從根走到 \(u\) 的路徑異或上 \(x\) 對應的二進位制數。

判斷 \(k\) 單個位數,\(k\) 的當前位是否能為 \(1\),能的話最好。討論 \(k\) 有兩個兒子的情形,一個兒子是類似的。若希望 \(k\) 的當前位為 \(1\),須將 \(u\) 一個兒子子樹內的所有數都劃分進加法部分(該操作簡記為劃分),加法部分對答案的貢獻也 \(\ge k\mid 2^K\),其中 \(2^K\) 為當前位的權值。

列舉要劃分哪個兒子並判斷是否可行,需要兒子子樹內所有數對應的 \(b_i\) 之和不超過剩餘可用的代價,其次加法部分的數的最小值 \(minn\) 需要滿足 \(minn+(x\mid (2^d-1))\ge k\mid 2^d\)\(x\) 的值任意,將未確定的低位全部置為了 \(1\)。如果發現劃分某一個兒子可以使得答案的當前位為 \(1\),就對應的更新 \(k\)\(x\),朝另一個兒子遞迴。

若確定了 \(k\) 的當前位不能為 \(1\),仍考慮是否要劃分一棵子樹並令異或部分的當前位為 \(1\),此時異或部分不會對最終的答案產生限制,只考慮加法,最優解是將剩下低位全是 \(1\),所以用算出的數更新全域性答案即可。不劃分子樹的情況,列舉 \(x\) 的取值,此時當前位異或為 \(1\) 的子樹不會對答案產生限制,朝異或為 \(0\) 的子樹遞迴即可。

複雜度 \(O (nk)\)

int n, m, k;
int b[N], ch[M][2], tot;
int a[N], minn[M];
int res;
int s[M];

int min(int a, int b) 
{
	return a < b ? a : b;
}
int max(int a, int b) 
{
	return a > b ? a : b;
}

inline int read() 
{
	int x = 0, f = 1;
	char ch = getchar();
	while (ch < '0' || ch > '9') 
	{
		if (ch == '-')
			f = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9')
		x = x * 10 + ch - '0', ch = getchar();
	return x * f;
}

void print(int n) 
{
	if (n < 0) 
	{
		putchar('-');
		n *= -1;
	}
	if (n > 9) print(n / 10);
	putchar(n % 10 + '0');
}

void insert(int x, int t) 
{
	int p = 0;
	for (rint i = k - 1; i >= 0; i--) 
	{
		if (!ch[p][x >> i & 1]) 
		{
			ch[p][x >> i & 1] = ++tot;
			minn[tot] = inf;
			s[tot] = 0;
		}
		p = ch[p][x >> i & 1];
		minn[p] = min(minn[p], x);
		s[p] += t;
	}
}

//mr -> min
void solve(int p, int d, int mr, int w, int x, int ans) 
{
	if (!d) 
	{
		res = max(res, min(mr + x, ans));
		return ;
	}
	int tag = 1;
	tag <<= d - 1;
	if (!ch[p][0] || !ch[p][1]) 
	{
		if (ch[p][0]) solve(ch[p][0], d - 1, mr, w, x | tag, ans | tag);
		if (ch[p][1]) 
		{
			if (x + mr > ans) solve(ch[p][1], d - 1, mr, w, x, ans | tag);
			else 
			{
				res = max(res, x + mr + tag - 1);
				solve(ch[p][1], d - 1, mr, w, x | tag, ans);
			}
		}
		return ;
	}
	bool flag = 0;
	if (w + s[ch[p][0]] <= m) 
	{
		if (min(mr, minn[ch[p][0]]) + x > ans) solve(ch[p][1], d - 1, min(mr, minn[ch[p][0]]), w + s[ch[p][0]], x, ans | tag), flag = 1;
		else res = max(res, min(mr, minn[ch[p][0]]) + x + tag - 1);
	}
	if (w + s[ch[p][1]] <= m) 
	{
		if (min(mr, minn[ch[p][1]]) + x + tag > ans) solve(ch[p][0], d - 1, min(mr, minn[ch[p][1]]), w + s[ch[p][1]], x | tag, ans | tag), flag = 1;
		else res = max(res, min(mr, minn[ch[p][1]]) + x + tag + tag - 1);
	}
	if (!flag) 
	{
		solve(ch[p][0], d - 1, mr, w, x, ans);
		solve(ch[p][1], d - 1, mr, w, x | tag, ans);
	}
}

signed main() 
{
	int useless, T;
	long times = 0;
	useless = read();
	T = read();
	while (T--) 
	{
		times++;
		for (rint i = 0; i <= tot; i++) ch[i][0] = ch[i][1] = 0;
		res = 0;
		tot = 0;
		n = read();
		m = read();
		k = read();
		int min_ = inf;
		int sum = 0;
		for (rint i = 1; i <= n; i++) 
		{
			a[i] = read();
			min_ = min(min_, a[i]);
		}
		for (rint i = 1; i <= n; i++) 
		{
			b[i] = read();
			sum += b[i];
		}
		if (sum <= m) 
		{
			res = min_ + (1 << k) - 1;
		}
		for (rint i = 1; i <= n; i++) insert(a[i], b[i]);
		solve(0, k, inf, 0, 0, 0);
		print(res);
		cout << endl;
	}
	return 0;
}

[省選聯考 2024] 迷宮守衛

題目傳送門

PS:之所以直接跳到了 Day 2 T1 是因為 Day 1 T3 暴力都不會打。

題意要求滿二叉樹上最大化最小葉子點權字典序

顯然不是 01Trie,考慮 二分和 dp 是否有操作空間

能玩,而且可做性比 Day 1 T2 高一些,剩下的泡麵吃兩口開始把玩

對比 Day 1 T2,思路是找當前為能否設定為 1,同樣都是最大化最小什麼的東西,考慮同樣的思路,轉化為判斷能否使第一位是某個數的判定性問題

把大於等於某個數 \(x\) 的數視為 \(1\),視為 \(0\),判斷是否能在魔力值夠的前提下使最小字典序序列的第一位為 \(1\)

\(g_i\) 為使以 \(i\) 為根的子樹的最小字典序序列的第一位為 \(1\) 的最小代價,顯然有轉移 \(g_i=g_{i\times2}+\min(g_{i\times2+1},w_i)\)

對第一位進行操作,二分這個 \(x\),在最小字典序序列的第一位可以為 \(1\) 的前提下,\(x\) 取到最大值時,最小字典序序列的第一位一定是 \(x\)

同一個子樹的最小字典序序列中的數,在最終的最小字典序序列中也一定在一起 。從 \(q_k=x\) 的結點 \(k\) 出發,一路向父親走,走到的結點的子樹所對應的最小字典序序列,在最終的最小字典序序列中排在最前面。每個走到的節點 \(u\) 的父親的另一個兒子 \(j_u\) 的子樹所對應的最小字典序序列,在最終的最小字典序序列中按照走到的順序依次從前向後排。按照這個順序,對於所有上述的 \(j\) 的子樹,依次推倒重來,即進行上面的過程

對一些子樹推倒重來時可能改變左右子樹最小字典序序列的大小關係,需要考慮這些情況,保證 Bob 第一個到達的葉結點一定是 \(k\),以及不浪費魔力值。如果一個走到的節點 \(i\) 是一個右兒子,那麼它的子樹對應的最小字典序序列一定小於 \(j_i\) 的,因為消耗更多魔力值改變方案只會變得更大,所以可以將 \(j_i\) 的方案直接推倒重來;如果 \(i\) 是一個左兒子,那麼當 \(g_{j_i}≤w_{fa_i}\)時,也可以直接將 \(j_i\) 的方案直接推倒重來;否則,如果目前方案剩餘的魔力值足以從啟用父親的石像守衛改為使 \(j_i\) 的子樹的最小字典序序列的第一位視為 \(1\),那麼就不啟用父親的石像守衛,不然仍然需要啟用;最後將 \(j_i\) 的方案推倒重來。

int check(int x, int k) 
{
	if ((x << 1) >= n)
	{
		f[x] = (a[x << 1] < k ? inf : (a[(x << 1) + 1] < k ? a[x] : 0));
	}
	else 
	{
		check(x << 1, k);
		check((x << 1) + 1, k);
		f[x] = min(inf, f[x << 1] + min(a[x], f[(x << 1) + 1]));
	}
	return f[x];
}

void dfs(int x) 
{
	int l = 1;
	int r = n;
	while (l < r) 
	{
		int mid = (l + r + 1) >> 1;
		if (check(x, mid) <= w) l = mid;
		else r = mid - 1;
	}
	check(x, l);
	l = rev[l]; w -= f[x];
	ans[++cnt] = a[l]; ans[++cnt] = a[l ^ 1];
	int i = l >> 1;
	while (i != x) 
	{
		if (i & 1) w += f[i ^ 1];
		else if (f[i ^ 1] <= a[i >> 1]) w += f[i ^ 1];
		else if (w >= f[i ^ 1] - a[i >> 1]) w += a[i >> 1];
		dfs(i ^ 1);
		i >>= 1;
	}
}

signed main() 
{
	int t;
	cin >> t;
	while (t--) 
	{
		cin >> m >> w;
		n = 1 << m;
		cnt = 0;
		for (rint i = 1; i < 2 * n; i++) cin >> a[i];
		for (rint i = n; i < 2 * n; i++) rev[a[i]] = i;
		dfs(1);
		for (rint i = 1; i <= n; i++) cout << ans[i] << " ";
	    cout << endl;
	}
	return 0;
}

[省選聯考 2024] 重塑時光

傳送門

對於 85pts 部分的 dp 還是比較好弄得,但是剩下最佳化到可以卡進去的程式碼我是不會寫的,看別人的題解自己寫還算不出來,寫到 85pts 算了。

\(f_S\) 表示集合為 \(S\) 的點放在一段,內部排序有多少的方案數。

\(h_{i,S}\) 表示集合為 \(S\) 的點分成 \(i\) 段,使得這 \(i\) 段中兩兩沒有邊。dp 時列舉超集轉移即可。

\(g_{i,S}\) 表示現在加了 \(i\) 個非空段,且它們由集合 \(S\) 裡的點組成且合法的方案數。這 \(i\) 個非空段構成一個 DAG 的時候是合法的,所以我們考慮每次假如 \(j\) 個入度為 \(0\) 的非空段,讓它們與前面的段連邊。但我們發現這樣會重複。於是考慮更改轉移意義為加入 \(j\) 個入度為 \(0\) 的非空段,使得入度為 \(0\) 的至少為 \(j\) 個,容斥一下,得出轉移

\(g_{i,S}=\sum_{j=1,T} (-1)^{j+1}\times g_{i-j,S-T}\times h_{j,T}\)

複雜度 \(O(3^nn^2)\)

int qpow(int a, int b = mod - 2, int p = mod) 
{
	int res = 1;
	while (b)
	{
	    if (b & 1) res = res * a % p;	
	    a = a * a % p;
		b >>= 1;
	}
	return res;
}

bool v(int s, int t) 
{
	return adj[s] & t;
}

signed main() 
{
	int n, m, k;
	cin >> n >> m >> k;

	for (rint i = 1; i <= m; i++) 
	{
		int u, v;
		cin >> u >> v;
		u--;
		v--;
		for (rint s = 0; s < (1 << n); s++)
			if (s & (1 << u))
				adj[s] |= 1 << v;
	}

	for (rint i = 0; i < 32; i++)
		for (rint j = c[i][0] = 1; j <= i; j++) 
			c[i][j] = (c[i - 1][j - 1] + c[i - 1][j]) % mod;
		

    fac[0] = ifac[0] = 1;
	for (rint i = 1; i < 32; i++) ifac[i] = qpow(fac[i] = fac[i - 1] * i % mod);

	h[0] = 1;

	for (rint s = 1; s < (1 << n); s++) 
	  for (rint i = 0; i < n; i++)
		if ((s & (1 << i)) && !v(1 << i, s ^ (1 << i))) 
		  h[s] = (h[s] + h[s ^ (1 << i)]) % mod;

	g[0][0] = 1;

	for (rint i = 1; i <= n; i++)
	  for (rint s = 1; s < (1 << n); s++) 
		for (rint t = s; t; t = (t - 1) & s)
		  if (!v(t, s ^ t) && !v(s ^ t, t)) 
			g[i][s] = (g[i][s] + g[i - 1][s ^ t] * h[t]) % mod;	

	for (rint i = 1; i <= n; i++)
		for (rint s = 1; s < (1 << n); s++) 
		{
			g[i][s] = g[i][s] * ifac[i] % mod;
			if (~i & 1) g[i][s] = mod - g[i][s];
		}

	f[0][0] = 1;

	for (rint i = 1; i <= n; i++)
	  for (rint s = 1; s < (1 << n); s++) 
		for (rint t = s; t; t = (t - 1) & s)
		  if (!v(s ^ t, t)) 
			for (rint j = i; j; j--)
			  f[i][s] = (f[i][s] + f[i - j][s ^ t] * g[j][t]) % mod;

	int ans = 0;

	for (rint i = 1; i <= min(k + 1, n); i++) 
		ans = (ans + f[i][(1 << n) - 1] * fac[i] % mod * c[k + 1][i]) % mod;

	cout << ans * fac[k] % mod * ifac[n + k] % mod << endl;
	
	return 0;
}

[省選聯考 2024] 最長待機

傳送門

視一條長度為 \(y\) 的全是 \(1\) 的鏈為 \(x^y\)

結論:

  1. \(x^y = x \times (nx^{y-1})\)\(n\) 為常數)
  2. \(x^y+x^{y-1}=(x+1) \times x^{y-1}=x^y\)
  3. \(x^{y-1}+x^y>x^y\),因為左式的 \(x^{y-1}\) 和右式的 \(x^y\) 同時輸入,所以可以保證左式的 \(x^y\) 比右式大,不能忽略。

對詢問的情況分類討論:

  1. \(e_{k}=1\),所以此時的答案和 \(k\) 所在的子樹中,與 \(k\)\(1\) 最多的鏈的 \(1\) 的數量有關。
  2. \(e_{k}=0\),此時我們從 \(k\) 向下遍歷直到遍歷到第一個 \(1\) 或者葉子結點的 \(0\),把它的答案(根據 \(e_k = 1\) 部分的答案)從左到右排序,形成一個序列,它的答案就是不能被忽略的數的和,即那些左邊沒有數字比它大的數的和。

複雜度 \(O(nm)\) 可以直接寫

int bfs(int x, int depth) 
{
	int res = depth;
	for (auto y : e[x]) res = max(res, bfs(y, depth + a[y]));
	return res;
}

void dfs(int x) 
{
	if (a[x]) 
	{
		v.push_back(bfs(x, 1));
		return ;
	} 
	else if (e[x].empty())
	{
		v.push_back(0);
	} 
	for (auto y : e[x]) dfs(y);
}

signed main() 
{
	cin >> n >> m;
	for (rint i = 1; i <= n; i++) 
	{
		int k;
		cin >> a[i] >> k;
		for (rint j = 1; j <= k; j++) 
		{
			int x;
			cin >> x;
			e[i].push_back(x);
		}
	}
	while (m--) 
	{
		cin >> op >> x;
		if (op == 1) 
		{
			a[x] ^= 1;
		}
		if (op == 2)  
		{
			v.clear();
			dfs(x);
			if (v.size() == 1) 
			{
				cout << v[0] + (v[0] == 0) << endl;
			}
			else 
			{
				int last = -1, ans = 0, idx = 0;
				for (auto i : v) 
				{
					if (i >= last) 
					{
						if (i == 0) ans++;
						else ans += i;
						idx++;
						last = i;
					}
				}
				cout << ans + (idx != 1) << endl;
			}
		}
	}
	return 0;
} 

之後用線段樹最佳化不會寫,可以停了。

後記

怎麼說呢,現在 HE 已經是弱省了,除了 Day 1 T1 要穩過以外別的題暴力打滿能拿得分都拿到就好了。現在 CCF 出題方向不再是考陰間的演算法而是在於基礎,這次省選考的演算法其實都是 NOIP 大綱裡的,如果可以把 NOIP 範圍內的演算法做到爐火純青打省選其實一點問題都沒有。

簡言之,在保證自己能吃的泡麵都吃到的基礎上再去考慮能不能加餐。