2024西安铁一中集训DAY6 ---- 模拟赛(随机化 + tire树 + dp + 组合数学)

概述

省流:最后1个小时过掉T3,写了T4暴力。
得分: 100 + 100 + 100 + 65 = 365
rk1

题解

A. 谁开了小号(随机化)

题面:
在这里插入图片描述

分析:
用随机化过的。

只有四场比赛,考虑对于每个账号,存储状态 m a s k i mask_i maski 表示 i i i 参加了那些比赛。那么 m a s k i ∈ [ 0 , 15 ] mask_i \in [0, 15] maski[0,15]。若干账号可以合并为一个的条件是任意两个账号的 m a s k mask mask 起来为 0 0 0

但是我们不知道按照怎样的优先级去合并是最优的。比如 ( 1001 ) 2 (1001)_2 (1001)2 ( 1000 ) 2 (1000)_2 (1000)2 都可以与 ( 0110 ) 2 (0110)_2 (0110)2 合并,但是如果还有一个账号的 m a s k mask mask ( 0111 ) 2 (0111)_2 (0111)2,那么显然 ( 0110 ) 2 (0110)_2 (0110)2 ( 1001 ) 2 (1001)_2 (1001)2 合并更好。这个顺序不好求出(实际上是我不会 )。那么我们可以 随机化。即随机一个优先合并的顺序。

我们将 [ 0 , 15 ] [0, 15] [0,15] 随机排列,考虑拿当前状态为 i i i 的和前面状态为 j j j 的元素合并,如果能合并,就让 c n t i ∣ j cnt_{i | j} cntij 加上 m i n ( c n t i , c n t j ) min(cnt_i, cnt_j) min(cnti,cntj) i ∣ j i|j ij 一定大于 i i i,因此如果能接着合并会在后面考虑到。

由于只有 16 16 16 个数,因此多次随机能将最优顺序搞出来。答案很容易正确。

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int n, m, mask[N], c[N], cnt[20], id[20], res, tmp[20], w;
int main() {
	scanf("%d", &n);
	for(int i = 1; i <= 4; i ++ ) {
		scanf("%d", &m);
		for(int j = 1; j <= m; j ++ ) {
			scanf("%d", &c[j]);
			mask[c[j]] |= (1 << (i - 1));
		}
	}
	for(int i = 1; i <= n; i ++ ) {
		cnt[mask[i]] ++;
	}
	int w = 0;
	if(cnt[0]) {
		res = cnt[0] - 1;
		cnt[0] = 1;
		w = res;
	}
	for(int i = 0; i < (1 << 4); i ++ ) id[i] = i;
	while(clock() < CLOCKS_PER_SEC * 0.99) {
		random_shuffle(id, id + 16);
		for(int i = 0; i < (1 << 4); i ++ ) tmp[i] = cnt[i];
		int temp = w;
		for(int i = 0; i < 16; i ++ ) {
			for(int j = 0; j < 16; j ++ ) {
				int val = id[j];
				if(((i & val) == 0) && (i != val) && (tmp[i] > 0 && tmp[val] > 0)) {
					int mn = min(tmp[i], tmp[val]);
					temp += mn;
					tmp[i] -= mn; tmp[val] -= mn;
					tmp[i | val] += mn;
				}
			}
		}
		res = max(res, temp);
	}
	cout << res << endl;
	return 0;
}

B. 终端命令(trie树)

在这里插入图片描述

分析:

s 0 ∼ n − 1 s_{0 \sim n-1} s0n1功能串 S S S目标串。考虑如果当前手上有一个字符串,那么 它一定是目标串或至少一个功能串的前缀。如果不是的话将永远不能变成目标串。

将所有功能串和目标串插入 t r i e trie trie 树中。对于每个节点 u u u,把它和它出边连的点 v v v 连一条长度为 1 1 1有向边,这表示添加字符操作。对于 t a p tap tap 操作,可以在插入功能串时对路径上的点打上标记。因为功能串是有序给出的,因此一个点可以到达第一次标记它的功能串的结尾点。对于功能串结尾点,直接连下一个功能串结尾点即可。

然后跑 d i j k s t r a dijkstra dijkstra 或者 0 / 1 b f s 0/1bfs 0/1bfs

时间复杂度 O ( ∑ ∣ s i ∣ × l o g ( ∑ ∣ s i ∣ ) ) O(\sum|s_i| \times log(\sum|s_i|)) O(si×log(si))

CODE:

#include<bits/stdc++.h>
#define pb push_back
using namespace std;
const int N = 2e5 + 10;
const int M = 1e6 + 10;
int n, tr[M + N][26], id[M + N], tot, To, ed[N], head[N + M], len;
int ID[N + M], dis[N + M];
char S[N], t[M];
bool bok[N + M]; 
vector< int > E[N + M];
void add(int u, int v) {
    E[u].pb(v);
}
void ins(char *str, int idx) {
	int len = strlen(str + 1);
	int p = 0;
	for(int i = 1; i <= len; i ++ ) {
		if(!tr[p][str[i] - 'a']) {
			tr[p][str[i] - 'a'] = ++ tot;
			add(p, tr[p][str[i] - 'a']);
		}
		if(!id[p] && idx != -1) id[p] = idx; 
		p = tr[p][str[i] - 'a'];
	}
	if(idx != -1) {
		ed[idx] = p;
		bok[p] = 1;
		ID[p] = idx;
	}
	else To = p;
}
struct state {
	int x, w;
	friend bool operator < (state a, state b) {
		return a.w > b.w;
	}
};
bool vis[N + M];
priority_queue< state > q;
void dijkstra(int s) {
	memset(dis, 0x3f, sizeof dis);
	dis[s] = 0; q.push((state) {s, dis[s]});
	while(!q.empty()) {
		state u = q.top(); q.pop();
		int x = u.x;
		if(vis[x]) continue;
		vis[x] = 1;
		for(auto v : E[x]) {
			if(dis[v] > dis[x] + 1) {
				dis[v] = dis[x] + 1;
				q.push((state) {v, dis[v]});
			}
		}
	}
}
int main() {
	scanf("%d", &n);
	scanf("%s", S + 1);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%s", t + 1);
		ins(t, i);
	}
	ins(S, -1);
	for(int i = 0; i <= tot; i ++ ) {
		if(!bok[i]) {
			if(id[i] > 0) add(i, ed[id[i]]);
		}
		else {
			int o = ID[i];
			o ++;
			if(o == n + 1) o = 1;
			add(i, ed[o]);
		}
	}
	dijkstra(0);
	printf("%d\n", dis[To]);
	return 0;
}

C. 又见LIS(dp)

在这里插入图片描述
分析:

这题卡了好长时间,最后在 30 m i n 30min 30min 内写完了。

这里有一个关键性质: Lis数组不会突变。因此对于当前位置而言,设前面 L i s Lis Lis 数组的最大值为 x x x,那么当前可填数的范围就是 [ 1 , x + 1 ] [1, x + 1] [1,x+1]

具体的一种构造 v i v_i vi 方式可以 v i v_i vi L i s i Lis_i Lisi 相等。那么在满足上述条件的基础上任意构造 L i s Lis Lis 数组都是合法的。

这下我们有了一个 d p dp dp 状态: f i , j f_{i, j} fi,j 表示考虑了前 i i i 个位置,最大的 L i s Lis Lis 的值为 j j j所有关心位置 取值的方案数。

分情况讨论转移:

  • a i = 0 a_i = 0 ai=0

d p i , j ← d p i − 1 , j × j dp_{i, j} \gets dp_{i - 1, j} \times j dpi,jdpi1,j×j
d p i , j + 1 ← d p i − 1 , j dp_{i, j + 1} \gets dp_{i - 1, j} dpi,j+1dpi1,j

分别表示 不成为最大值成为最大值 的两种转移。

  • a i > 0 a_i > 0 ai>0

d p i , j ← d p i − 1 , j ( j ≥ a i ) dp_{i, j} \gets dp_{i - 1, j}(j \geq a_i) dpi,jdpi1,j(jai)

这个相当于是 舍去一些不合法的方案

  • a i = − 1 a_i = -1 ai=1

d p i , j + 1 ← d p i − 1 , j dp_{i, j + 1} \gets dp_{i - 1, j} dpi,j+1dpi1,j

这个我认为是最难想的:由于我们不关心这个位置的取值,所以把它按照 a i = 0 a_i = 0 ai=0 的方式转移一定会重复。考虑 将所有上一个状态 唯一对应 转移给一个当前状态。当前层的状态所存储的方案就是不重复的。为了不遗漏,所以我们想让所有合法状态在后面都尽可能被转移,因此我们将 a i a_i ai 设为 j + 1 j + 1 j+1 进行转移。

用有向图表示转移如下:

a i = 0 a_i = 0 ai=0
在这里插入图片描述

a i = − 1 a_i = -1 ai=1
在这里插入图片描述

时间复杂度 O ( n 2 ) O(n^2) O(n2)

CODE:

#include<bits/stdc++.h> // -1 全部接成大的  这样能保证不重复 
using namespace std;
const int N = 5100;
typedef long long LL;
const LL mod = 998244353;
LL dp[N][N]; 
int n, a[N];
int main() {
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) {
		scanf("%d", &a[i]);
	}
	dp[0][0] = 1LL;
	for(int i = 0; i < n; i ++ ) {
		for(int j = 0; j <= i; j ++ ) {
			if(a[i + 1] == 0) {
				if(j != 0) dp[i + 1][j] = (dp[i + 1][j] + dp[i][j] * (1LL * j) % mod) % mod; // 接在里面 
				dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % mod; // 接后面 
			}
			else if(a[i + 1] == -1) {
				dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % mod; // 规定 -1 只接大的 
			}
			else {
				if(j + 1 >= a[i + 1]) {
					if(j + 1 == a[i + 1]) dp[i + 1][j + 1] = (dp[i + 1][j + 1] + dp[i][j]) % mod;
					else dp[i + 1][j] = (dp[i + 1][j] + dp[i][j]) % mod;
				}
			}
		}
	}
	LL res = 0;
	for(int i = 1; i <= n; i ++ ) {
		res = (res + dp[n][i]) % mod;
	}
	cout << res << endl;
	return 0;
}

D. 飞一飞呀(组合数学)

原题连接

在这里插入图片描述
分析:

这题有两种做法:转化坐标系组合意义推导

首先 N 2 N^2 N2 做法是简单的:设 x , y , z x, y, z x,y,z 分别表示在 x x x 轴, y y y 轴, z z z 轴的移动步数。枚举 x x x y y y,那么 z z z 可以求出。对 x , y , z x,y,z xyz 分别组合数求方案然后相乘就好了。

下面是正解:

S o l 1 : Sol_1: Sol1 变换坐标系

这里有一个套路:

首先思考在一个二维平面上,你要走 n n n 步,从 ( 0 , 0 ) (0, 0) (0,0) 走到 ( X , Y ) (X, Y) (X,Y)。每次可以通过四种坐标变换

( + 1 , 0 ) (+1, 0) (+1,0) ( − 1 , 0 ) (-1, 0) (1,0) ( 0 , + 1 ) (0, +1) (0,+1) ( 0 , − 1 ) (0, -1) (0,1)

问方案数。

我们将坐标系变换一下,将 ( x , y ) (x, y) (x,y) 点映射到 ( x + y , x − y ) (x + y, x - y) (x+y,xy) 上。不难发现这样的映射是一 一对应的,即 原来的一个点唯一对应变换后的一个点,变换后的一个点也唯一对应原来的一个点。 那么原来的四种坐标变换 在变换后的坐标系 就对应成了:

( + 1 , + 1 ) (+1, +1) (+1,+1) ( − 1 , − 1 ) (-1, -1) (1,1) ( + 1 , − 1 ) (+1, -1) (+1,1) ( − 1 , + 1 ) (-1, +1) (1,+1)

起点是 ( 0 , 0 ) (0, 0) (0,0),终点是 ( X + Y , X − Y ) (X + Y, X - Y) (X+Y,XY)

我们发现 x x x轴 和 y y y 轴 的变换独立了。因此分别求一个组合数相乘即可。时间复杂度 O ( 1 ) O(1) O(1)

放到这个问题上:我们发现三维中只有 6 6 6 种坐标变换,想让每个轴上的移动独立需要 9 9 9 种变换。因此不能拓展二维的方法。但是我们可以枚举一维,那么另外两维可以用上述方法 O ( 1 ) O(1) O(1) 求出。

时间复杂度 O ( N ) O(N) O(N)

CODE:

#include<bits/stdc++.h>
using namespace std;
const int N = 1e7 + 10;
typedef long long LL;
const LL mod = 998244353;
int n, X, Y, Z;
LL res, fac[N], inv[N];
LL Pow(LL x, LL y) {
	LL res = 1LL, k = x % mod;
	while(y) {
		if(y & 1) res = (res * k) % mod;
		y >>= 1;
		k = (k * k) % mod;
	}
	return res;
}
LL C(int n, int m) {
	return fac[n] * inv[m] % mod * inv[n - m] % mod;
}
LL Cnt(int x) {
	LL step = (x - Z) / 2;
	return C(n, x) * C(x, Z + step) % mod;
}
LL calc(int x) {
	LL p = abs(X - Y), q = abs(X + Y);
	if(x < p || x < q) return 0;
	if(((x - p) & 1) || ((x - q) & 1)) return 0;
	return C(x, p + (x - p) / 2) % mod * C(x, q + (x - q) / 2) % mod;
}
int main() {
	fac[0] = 1LL;
	for(int i = 1; i < N; i ++ ) fac[i] = (fac[i - 1] * (1LL * i)) % mod;
	inv[N - 1] = Pow(fac[N - 1], mod - 2LL) % mod;
	for(int i = N - 2; i >= 0; i -- ) inv[i] = inv[i + 1] * (1LL * i + 1LL) % mod;
	cin >> n >> X >> Y >> Z;
	X = abs(X); Y = abs(Y); Z = abs(Z); 
	for(int i = Z; i <= n; i += 2) {
		res = (res + Cnt(i) * calc(n - i) % mod) % mod;
	}
	cout << res << endl;
	return 0;
}

S o l 2 : Sol_2: Sol2 组合意义

感觉很神奇,不想推了。参考下面文章吧。

文章

  • 16
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值