2024西安铁一中集训DAY2 ---- 模拟赛(最小生成树 + AC自动机 + 模拟 + 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:

#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:

#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:

#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 数组来更新答案。这样做的正确性在于 一个人如果生命值和护甲值都小于另一个人,那么这个人一定不能坚持到最后。因此不用考虑更新它的答案的情况。

#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;
}
  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值