2024西安铁一中集训DAY2 ---- 模拟赛(最小生成树 + AC自动机 + 模拟 + rmq)

文章目录

  • 比赛成绩
  • 题解
    • [A. 江桥的生成树(MST)](#A. 江桥的生成树(MST))
    • [B. 江桥的神秘密码(AC自动机,ST表)](#B. 江桥的神秘密码(AC自动机,ST表))
    • [C. 江桥的字符距离](#C. 江桥的字符距离)
    • [D. 江桥的防御力测试(rmq,乱搞)](#D. 江桥的防御力测试(rmq,乱搞))

比赛成绩

估测:60 + 100 + 100 + 0 = 260

实得:5 + 100 + 0 + 0 = 105

rk11
挂大分的一场

题解

A. 江桥的生成树(MST)


题目

分析:

我们考虑 不同颜色的点最终一定是联通的 。因此我们可以对于每一种颜色可以先拿出来一个点,跑一遍 K r u s k a l Kruskal Kruskal 算法,然后剩下的点可以选择一个代价最小的点连起来。这样一定是最优的。需要注意的是由于 图很稠密 ,因此 K r u s k a l Kruskal Kruskal 算法中对边排序会导致 T L E TLE TLE,因此需要用 P r i m Prim Prim 算法。

P r i m Prim Prim 算法思想:考虑往生成树集合中加点,每次选 距离生成树最近的点 加入生成树中,并用它更新其它点到生成树的距离。

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

CODE:

cpp 复制代码
#include<bits/stdc++.h>
#define pb push_back
using namespace std;
typedef long long LL;
typedef pair< int, int > PII;
const int M = 1e6 + 10;
const int N = 5005;
int n, a[N], tot;
LL p, q, w, h;
LL W[N][N], Min_val[N], res, dis[N];
int bin[N], Min_id[N];
bool vis[N];
void prim() {
	memset(dis, 0x3f, sizeof dis);
	vis[1] = 1;
	for(int i = 1; i <= n; i ++ ) {
		if(!vis[i]) dis[i] = min(dis[i], W[1][i]);
	}
	for(int i = 2; i <= n; i ++ ) {
		int id = -1, Min = 1e18;
		for(int j = 1; j <= n; j ++ ) {
			if(!vis[j]) {
				if(dis[j] < Min) Min = dis[j], id = j;
			}
		}
		res += Min;
		vis[id] = 1;
		for(int j = 1; j <= n; j ++ ) {
			if(!vis[j]) dis[j] = min(dis[j], W[id][j]);
		}
	}
}
int main() {
	memset(Min_val, 0x3f, sizeof Min_val);
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) {
	    scanf("%d", &a[i]); 
	}
	scanf("%lld%lld%lld%lld", &p, &q, &w, &h);
	LL tmp = 0;
	for(int i = 1; i <= n * n; i ++ ) {
		tmp = (p * tmp % h + q) % h;
		int x, y;
		if(i % n == 0) x = (i / n), y = n;
		else x = (i / n + 1), y = i % n;
		if(y >= x) {
			W[x][y] = W[y][x] = tmp;
		}
	}
	for(int i = 1; i <= n; i ++ ) {
		for(int j = 1; j <= n; j ++ ) {
			if(W[i][j] < Min_val[i]) Min_id[i] = j, Min_val[i] = W[i][j]; 
		}
	}
	for(int i = 1; i <= n; i ++ ) a[i] --;
	for(int i = 1; i <= n; i ++ ) {
		if(a[i] > 0) 
			res += (1LL * a[i] * Min_val[i]);
	}
	prim();
	cout << res << endl;
	return 0;
}

B. 江桥的神秘密码(AC自动机,ST表)

分析:

套路题。首先我们可以枚举一个右端点,然后求出一个最靠前的左端点更新答案。不难发现固定右端点后长度约短越容易满足限制,具有单调性,因此可以二分。考虑怎样检验:一个区间不包含特殊字符串等价于区间内任意一个位置为结尾都不能在区间里形成特殊字符串 。因此我们处理出每个位置为结尾,距离他最近的左端点,满足左端点到它的区间是一个特殊字符,这个可以在构建 A C AC AC 自动机时处理。那么检验就是区间中所有位置对应的左端点都小于区间左端点。这个可以利用 S T ST ST 表 O ( 1 ) O(1) O(1) 求出区间最小值。

时间复杂度 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)

CODE:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int T, n, m, tot, tr[N][26], fail[N], e[N], L[N], Max[21][N];
char t[N], str[N];
void Insert(char *s){//插入 
	int p = 0, len = strlen(s + 1);
	for(int i = 1; i <= len; i++){
		int c = (int)(s[i] - 'a');
		if(!tr[p][c]) tr[p][c] = ++tot;
		p = tr[p][c];
	}
	e[p] = min(e[p], len);
}
queue< int > q;
void build(){
	int p = 0;
	for(int i = 0; i < 26; i++){
		if(tr[p][i]) q.push(tr[p][i]);//将儿子加入队列中 
	}
	while(!q.empty()){
		int u = q.front(); q.pop();
		for(int i = 0; i < 26; i++){
			if(tr[u][i]) {
		    	fail[tr[u][i]] = tr[fail[u]][i], q.push(tr[u][i]);
				e[tr[u][i]] = min(e[tr[u][i]], e[fail[tr[u][i]]]);	
			}
			else tr[u][i] = tr[fail[u]][i];
		}
	}
}
int ask(int l, int r) {
	int k = log2(r - l + 1);
	return max(Max[k][l], Max[k][r - (1 << k) + 1]);
}
int query(int x) {
	int l = 1, r = x, mid, res = -1;
	while(l <= r) {
		mid = (l + r >> 1);
		if(ask(mid, x) >= mid) l = mid + 1;
		else res = (x - mid + 1), r = mid - 1;
	}
	return res;
}
void solve() {
	tot = 0;
	scanf("%d%d", &n, &m);
	scanf("%s", str + 1);
	int len = 0;
	for(int i = 1; i <= m; i ++ ) {
		scanf("%s", t + 1);
		len += strlen(t + 1);
		Insert(t);
	}
	build();
	int p = 0;
	for(int i = 1; i <= n; i ++ ) {
		p = tr[p][str[i] - 'a'];
		if(e[p] != 0x3f3f3f3f) {
			L[i] = (i - e[p] + 1);
		}
		else L[i] = -1;
	}
	for(int i = 1; i <= n; i ++ ) Max[0][i] = L[i];
	for(int i = 1; (1 << i) <= n; i ++ ) {
		for(int j = 1; j + (1 << i) - 1 <= n; j ++ ) {
			Max[i][j] = max(Max[i - 1][j], Max[i - 1][j + (1 << (i - 1))]);
		}
	}
	int res = 0;
	for(int i = 1; i <= n; i ++ ) {
		res = max(res, query(i));
	}
	printf("%d\n", res);
	for(int i = 0; i <= len; i ++ ) {
		for(int j = 0; j < 26; j ++ ) {
			tr[i][j] = 0;
		}
		e[i] = 0x3f3f3f3f;
        fail[i] = 0;
	}
}
int main() {
	scanf("%d", &T);
	memset(e, 0x3f, sizeof e);
	while(T -- ) {
		solve();
	}
	return 0;
}

C. 江桥的字符距离

考场上写的正解漏了一行,100挂成蛋了。

分析:

将满足任意两个不同位置的数之间的距离不小于 d d d 的数 x x x 称作特殊数字。从高位到低位逐位考虑 :我们对每种数字记一个状态 e a s y i easy_i easyi 表示 i i i 这种数字在当前状态想要成为特殊数字的难易程度。不可能即为无穷大,已经满足了就是 0 0 0。开一个 s e t set set 按照 e a s y easy easy 值从小到大排序。设当前填到了第 p p p 位, e a s y easy easy 值最小的数字位 x x x。那么对于不是 x x x 的数字 y y y,可以通过这一位填 y y y 后 x x x 是否仍能成立来判断能否填 y y y。对于 x x x 可以通过这一位填 x x x 后 e a s y easy easy 值第二小的数 z z z 是否仍能成立来判断能否填 x x x。由于我们需要让字典序最小,所以只需要关心不是 x x x 的数字中最小的数 w w w 和 x x x 能否填就可以了。填过之后 更新所填数的 e a s y easy easy 值 。刻画 e a s y easy easy 值可以通过将这个数从最后一个位置往前填,每次跳 d d d,最后落在的位置来刻画。那么这个位置 越靠后 e a s y easy easy 越小。 说以来有些抽象,具体细节参见代码。

CODE:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
typedef long long LL;
int T, n, d, a[N], cnt[N], lst[N], ans[N], now[N];
struct node {
	int x, pos;
	friend bool operator < (node a, node b) {
		return ((a.pos > b.pos) || (a.pos == b.pos && a.x < b.x));
	}
};
set< node > s;
set< int > num;
bool bok[N];
bool check(int pos, int x) { // 在当前状态下插入x 是否可行 
	auto it = s.begin();
	if(x != (it->x)) {
		if(pos + 1 <= (it->pos)) return 1;
		return 0;
	}
	else {
		int tmp = (it->x);
		if(pos >= lst[tmp] + d && pos <= (it->pos)) return 1;
		else {
			it ++;
			if(it == s.end()) return 0;
			else {
				if(pos + 1 <= (it->pos)) return 1;
				else return 0;
			}
		}
	}
}
int calc(int lst, int cnt) {
	if(cnt == 0) return n + 1;
	else {
		LL p = (1LL * n) - 1LL * (cnt - 1) * d;
		if(p < 1LL * (lst + d)) return -1;
		else return p;
	}
}
void ins(int pos, int x) { // 在 pos 位置插入 x    只会修改x 
	if(pos - lst[x] < d) {
		s.erase(s.find((node) {x, now[x]}));
		lst[x] = pos;
		now[x] = -1;
		cnt[x] --;
		if(!cnt[x]) num.erase(x);
		else s.insert((node) {x, now[x]});
	}
	else {
		s.erase(s.find((node) {x, now[x]}));
		lst[x] = pos;
		cnt[x] --;
		now[x] = calc(lst[x], cnt[x]);
		if(!cnt[x]) num.erase(x), s.insert((node) {x, now[x]});
		else s.insert((node) {x, now[x]});
	}
}
void solve() {
	scanf("%d%d", &n, &d);
	while(!s.empty()) s.erase(s.begin());
	while(!num.empty()) num.erase(num.begin());
	for(int i = 1; i <= n; i ++ ) lst[i] = -d + 1;
	for(int i = 1; i <= n; i ++ ) {
		scanf("%d", &a[i]);
		cnt[a[i]] ++;
		num.insert(a[i]);
	}
	bool f = 0;
	for(int i = 1; i <= n; i ++ ) {
		if(cnt[a[i]] == 1) f = 1; 
	}
	if(f) {
		sort(a + 1, a + n + 1);
		for(int i = 1; i <= n; i ++ ) {
			printf("%d ", a[i]);
		}
		puts("");
	}
	else {	
	    bool flag = 1;
		for(int i = 1; i <= n; i ++ ) {
			if(!bok[a[i]]) {
				s.insert((node) {a[i], calc(lst[a[i]], cnt[a[i]])});
				now[a[i]] = calc(lst[a[i]], cnt[a[i]]);
				bok[a[i]] = 1;
			}
		}
		for(int i = 1; i <= n; i ++ ) {
			auto it = num.begin();
			auto itt = s.begin();
			if((*it) != (itt->x)) { 
				if(check(i, *it)) {
					ans[i] = (*it);
					ins(i, *it);
				}
				else if(check(i, (itt->x))) {
					ans[i] = (itt->x);
					ins(i, itt->x);
				}
				else {
					flag = 0;
					break;
				}
			}
			else {
				if(check(i, *it)) {
					ans[i] = (*it);
					ins(i, *it);
				}
				else {
					it ++;
					if(it != num.end()) {
						if(check(i, *it)) {
							ans[i] = (*it);
							ins(i, *it);
						}
						else {
							flag = 0;
							break;
						}
					}
					else {
						flag = 0;
						break;
				    }
				}
			}
		}
		if(!flag) puts("-1");
		else {
			for(int i = 1; i <= n; i ++ ) {
				printf("%d ", ans[i]);
			}
			puts("");
		}
	}
	for(int i = 1; i <= n; i ++ ) cnt[a[i]] = 0, bok[a[i]] = 0;
}
int main() {
	scanf("%d", &T);
	while(T -- ) {
		solve();
	}
	return 0;
}

D. 江桥的防御力测试(rmq,乱搞)

分析:

首先不难看出:如果我们枚举攻击力 x x x,那么枚举上限是 m a x i = 1 n a i max_{i = 1}^{n}a_i maxi=1nai。因为再往上枚举也都是一次破甲,情况就完全一样了。然后我们考虑,对于一个枚举的 x x x,一个护甲为 a i a_i ai 的人靠护甲每滴血能抗 ⌈ a i x ⌉ \left \lceil \frac{a_i}{x} \right \rceil ⌈xai⌉ 次。对于这个次数相同的人而言,血量越大能抗住的回合数约多。这启示我们枚举 x x x,然后按照 ⌈ a i x ⌉ \left \lceil \frac{a_i}{x} \right \rceil ⌈xai⌉ 的值将人进行划分成若干段,每一段内的人一滴血在 x x x 的攻击力下能抗的回合数相同。对于每一段,我们只需要快速求出这一段内 血量最大血量次大 的人的血量和编号来更新答案即可。这个东西可以使用 S T ST ST 表预处理后求。时间复杂度为 O ( ∑ i = 1 m a x i = 1 n a i m a x i = 1 n a i i = m a x a i × l o g 2 m a x a i ) O(\sum_{i = 1}^{max_{i = 1}^{n}a_i}\frac{max_{i = 1}^{n}a_i}{i} = maxa_i \times log_2 maxa_i) O(∑i=1maxi=1naiimaxi=1nai=maxai×log2maxai)

也可以记录 s u f 1 i suf1_i suf1i 和 s u f 2 i suf2_i suf2i 表示护甲值大于等于 i i i 的最大生命值和次大生命值。然后每次跳段时调用大于等于这段最小护甲值的 s u f suf suf 数组来更新答案。这样做的正确性在于 一个人如果生命值和护甲值都小于另一个人,那么这个人一定不能坚持到最后。因此不用考虑更新它的答案的情况。

cpp 复制代码
#include<bits/stdc++.h> // 枚举 x, 发现[a_i / x] 相等的人只有生命值最大的有贡献,使用rmq就可以在O(Tnlogn)的复杂度统计 
using namespace std; // 但是发现可以统计后缀生命最大值,因为护甲值更大的人一定会在后面算到,此时用较小的物抗算贡献不会影响最终答案 
typedef long long LL;
const int N = 2e5 + 10;
int T, n, h[N], a[N];
LL ans[N];
int suf1[N], suf2[N], id1[N], id2[N];
bool bok[N];
int calc(int x, int y) {
	if(x % y == 0) return x / y;
	else return x / y + 1;
}
void solve() {
	int maxa = 0;
	scanf("%d", &n);
	for(int i = 1; i <= n; i ++ ) 
		scanf("%d", &h[i]);
	for(int i = 1; i <= n; i ++ ) {
	    scanf("%d", &a[i]);
	    maxa = max(maxa, a[i]);
	} 
	memset(suf1, 0, sizeof suf1);
	memset(suf2, 0, sizeof suf2);
	memset(id1, 0, sizeof id1);
	memset(id2, 0, sizeof id2);
	memset(ans, 0, sizeof ans);
	for(int i = 1; i <= n; i ++ ) {
		if(h[i] > suf1[a[i]]) {
			id2[a[i]] = id1[a[i]];
			suf2[a[i]] = suf1[a[i]];
			suf1[a[i]] = h[i];
			id1[a[i]] = i;
		}
		else if(h[i] > suf2[a[i]]) {
			id2[a[i]] = i;
			suf2[a[i]] = h[i];
		}
	}
	for(int i = maxa; i >= 1; i -- ) {
		if(suf1[i + 1] > suf1[i]) {
			id2[i] = id1[i];
			suf2[i] = suf1[i];
			id1[i] = id1[i + 1];
			suf1[i] = suf1[i + 1];
		}
		else if(suf1[i + 1] > suf2[i]){
			id2[i] = id1[i + 1];
			suf2[i] = suf1[i + 1];
		}
		if(suf2[i + 1] > suf2[i]) {
			id2[i] = id2[i + 1];
			suf2[i] = suf2[i + 1];
		}
	}
	for(int o = 1; o <= maxa; o ++ ) {
		int d1 = 0, d2 = 0;
		LL c1 = 0, c2 = 0;
		for(int j = 1; (j - 1) * o + 1 <= maxa; j ++ ) { // 枚举抗的次数 
			int st = (j - 1) * o + 1; // 起点 
			if(id1[st] != 0 && !bok[id1[st]]) {
				if(1LL * calc(a[id1[st]], o) * h[id1[st]] > c1) {
					bok[d2] = 0;
					c2 = c1; d2 = d1;
					c1 = 1LL * calc(a[id1[st]], o) * h[id1[st]];
					d1 = id1[st]; bok[d1] = 1;
				}
				else if(1LL * calc(a[id1[st]], o) * h[id1[st]] > c2) {
					bok[d2] = 0;
					d2 = id1[st]; c2 = 1LL * calc(a[id1[st]], o) * h[id1[st]];
					bok[d2] = 1;
				}
			}
			if(id2[st] != 0 && !bok[id2[st]]) {
				if(1LL * calc(a[id2[st]], o) * h[id2[st]] > c1) {
					bok[d2] = 0;
					c2 = c1; d2 = d1;
					c1 = 1LL * calc(a[id2[st]], o) * h[id2[st]];
					d1 = id2[st]; bok[d1] = 1;
				}
				else if(1LL * calc(a[id2[st]], o) * h[id2[st]] > c2) {
					bok[d2] = 0;
					d2 = id2[st]; c2 = 1LL * calc(a[id2[st]], o) * h[id2[st]];
					bok[d2] = 1;
				}				
			}
		}
		if(d1 != 0) {
			bok[d1] = bok[d2] = 0;
			ans[d1] = max(ans[d1], c1 - c2);
		}
	}
	for(int i = 1; i <= n; i ++ ) {
		printf("%lld ", ans[i]);
	}
	puts("");
}
int main() {
	scanf("%d", &T);
	while(T -- ) {
		solve();
	}
	return 0;
}
相关推荐
刚学HTML1 小时前
leetcode 05 回文字符串
算法·leetcode
AC使者2 小时前
#B1630. 数字走向4
算法
冠位观测者2 小时前
【Leetcode 每日一题】2545. 根据第 K 场考试的分数排序
数据结构·算法·leetcode
古希腊掌管学习的神3 小时前
[搜广推]王树森推荐系统笔记——曝光过滤 & Bloom Filter
算法·推荐算法
qystca3 小时前
洛谷 P1706 全排列问题 C语言
算法
浊酒南街3 小时前
决策树(理论知识1)
算法·决策树·机器学习
就爱学编程3 小时前
重生之我在异世界学编程之C语言小项目:通讯录
c语言·开发语言·数据结构·算法
学术头条3 小时前
清华、智谱团队:探索 RLHF 的 scaling laws
人工智能·深度学习·算法·机器学习·语言模型·计算语言学
Schwertlilien3 小时前
图像处理-Ch4-频率域处理
算法
IT猿手4 小时前
最新高性能多目标优化算法:多目标麋鹿优化算法(MOEHO)求解TP1-TP10及工程应用---盘式制动器设计,提供完整MATLAB代码
开发语言·深度学习·算法·机器学习·matlab·多目标算法