Bootstrap

07/04/24 融合热身赛赛后总结&题解

一、总体情况

  • 这次考试一共有五道题
  • 考试总共有3个小时,我花了一个多小时AC了 A A A B B B C C C 三道题,而 D D D E E E 最后实在没想出来

二、题解

A题 被2的n次方整除

题目描述

给定一个正整数数组 a 1 , a 2 . . . a n a_{1}, a_{2}...a_{n} a1,a2...an

使数组中所有数字的乘积(即 a 1 ∗ a 2 ∗ . . . ∗ a n a_{1} * a_{2} * ... * a_{n} a1a2...an)能被 2𝑛 整除。

你可以执行以下操作任意次数:

选择任意索引 i i i ( 1 ≤ i ≤ n ) (1 ≤ i ≤ n) (1in) 并将值 𝑎𝑖 替换为 a i = a i ∗ i a_i = a_i * i ai=aii
不能重复对单个索引应用操作。换句话说,所有选择的 i i i 的值必须不同。

找到使数组中所有元素的乘积能被 2 n 2^n 2n 整除所需执行的最小操作次数。请注意,并非总是存在这样一组操作。

思路分析

这道题就是让我们尽量通过最小的操作次数,让所有数的乘积能被 2 n 2^{n} 2n 整除,换句话说,就是要让序列中所有的数一共尽可能有 n n n 个因数 2 2 2。知道了这点后,我们可以先把题目所给数组中的因数 2 2 2 的个数统计出来,再把每个 i i i 中的因数 2 2 2 的个数统计出来,再把每个 i i i 中的因数 2 2 2 的个数从大到小排序后,枚举即可。

代码
#include <iostream>
#include <vector>
#include <algorithm>

#define int long long

using namespace std;

const int N = 2e5 + 5;

int t;
int n;
int a[N];
vector<int> tmp;

bool cmp(int a, int b) {
	return a > b;
}

void init() {
	fill(a, a + N, 0);
	tmp.clear();
}

int count(int x) {
	int res = 0;
	while (x > 0) {
		if (x % 2 == 0)
			res++;
		else
			break;
		x /= 2;
	}
	return res;
}

main() {
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> t;
	while (t-- > 0) {
		init();
		cin >> n;
		int k = n;		//让序列里面尽量出现k个2
		int cnt = 0;	//记录因数2出现的数量,若>=k,则输出答案
		int ans = 0;	//操作次数(答案)
		for (int i = 1; i <= n; i++) {
			cin >> a[i];
			cnt += count(a[i]);
			if (count(i) != 0) {
				tmp.push_back(count(i));
			}
		}
		sort(tmp.begin(), tmp.end(), cmp);
		int flag = false;
		for (int i = 0; i < tmp.size(); i++) {
			if (cnt >= k) {
				cout << ans << '\n';
				flag = true;
				break;
			}
			cnt += tmp[i];
			ans++;
		}
		if (!flag)
			if (cnt >= k)
				cout << ans << '\n';
			else
				cout << -1 << '\n';
	}
	return 0;
}

B题 资料分发

题目描述

有一些电脑,一部分电脑有双向数据线连接。如果一个电脑得到数据,它可以传送到的电脑都可以得到数据。现在,你有这个数据,问你至少将其输入几台电脑,才能使所有电脑得到数据。

思路分析

看到这道题后,很容易就想到了用并查集来维护这些电脑间的连通性关系,最后统计并查集的fa数组中一共有几个不同的值,就是答案。

代码
#include <iostream>
#include <set>

using namespace std;

const int N = 1e5 + 5;

int n, m;
int fa[N];
set<int> st;

int find(int u)
{
	return fa[u] == u ? u : fa[u] = find(fa[u]);
}

int main()
{
	ios::sync_with_stdio(false);
	for (int i = 0; i < N; i++)
		fa[i] = i;
	cin >> n >> m;
	int u, v;
	for (int i = 1; i <= m; i++) {
		cin >> u >> v;
		fa[find(u)] = find(v);
	}
	int ans = 0;
	for (int i = 1; i <= n; i++)
		if (st.count(find(i)) == 0) {
			ans++;
			st.insert(find(i));
		}
	cout << ans << '\n';
	return 0;
}

C题 统计和

题目描述

给定一个长度为 n ( n ≤ 1 0 5 ) n(n ≤ 10^{5}) n(n105),初始值都为 0 0 0 的序列, x ( x ≤ 1 0 5 ) x(x ≤ 10^{5}) x(x105) 次的修改某些位置上的数字,每次加上一个数,然后提出 y ( y ≤ 1 0 5 ) y(y ≤ 10^{5}) y(y105) 个问题,求每段区间的和。

思路分析

这道题是典型的的单点修改+区间查询,直接使用树状数组即可。

代码
#include <iostream>

#define int long long

using namespace std;

const int N = 1e5 + 5;

int n, m;
int tree[N];

int lowbit(int x)
{
	return x & -x;
}

void modify(int idx, int c)
{
	for (int i = idx; i <= n; i += lowbit(i))
		tree[i] += c;
}

int query(int idx)
{
	int res = 0;
	for (int i = idx; i > 0; i -= lowbit(i))
		res += tree[i];
	return res;
}

main()
{
	ios::sync_with_stdio(false);
	cin >> n >> m;
	char op;
	int a, b;
	for (int i = 1; i <= m; i++) {
		cin >> op >> a >> b;
		if (op == 'x')
			modify(a, b);
		else
			cout << query(b) - query(a - 1) << '\n';
	}
	return 0;
}

D题 水火串

通过了光明狮子的考验,小渡来到了创界山的第二层。没想到第二层的守护神——火焰凤凰正沉迷于字母游戏,不能自拔!为了尽快得到她的帮助,小渡决定出手帮她解决字母游戏的谜题。 字母游戏规则是这样的:
一个字母串里包含有两个相邻的重复子串则称为“水串”,否则为“火串”
例如AA、ABCABC都是"水串",而D、DC、ABDAD、CBABCBA都是“火串"。
输入正整数L和n,输出由前L个字母组成的、字典序第n个的"火串"。

思路分析

这道题保证了字符串的长度最大为 80 80 80,则可以直接使用暴力法 DFS 求解,但在每次 DFS 时要加一个 check 函数判断新的这个串是不是火串,判断方法如下:

设一个要检查的字符串为"ACBABCABC",则检查过程如下:请添加图片描述

因为每次DFS是往后加一个元素,所以要判断的重复子串中的其中一个的右端点一定是最后一个 (图中标*的元素)。然后去枚举另一个子串的右端点 (图中标.的元素),每次就以这两个右端点来暴力判断即可。

另外,这道题的输出限制非常严格,每一行的行尾甚至不能有空格,否则就很容易喜提 PE。

代码
#include <string>
#include <iostream>

#define s now

using namespace std;

int n, l;
int idx = 0;
string now;

bool check()
{
	if (s.size() == 1)
		return true;
	int last = s.size() - 1;
	for (int i = last - 1; i >= (int)(s.size() - 1) / 2; i--)
		if (s[i] == s[last]) {
			string t1 = "";
			string t2 = "";
			for (int j = i, k = last; j >= 0 && k > i; j--, k--) {
				t1 += s[j];
				t2 += s[k];
			}
			if (t1 == t2)
				return false;
		}
	return true;
}

void output()
{
	int t1 = 0;
	int t2 = 0;
	for (auto it = s.begin(); it != s.end(); it++) {
		cout << *it;
		t1++, t2++;
		if (t2 == 64) {
			cout << '\n';
			t2 = 0, t1 = 0;
		} else if (t1 == 4 && it != s.end() - 1) {
			cout << ' ';
			t1 = 0;
		}
	}
	if (t2 == 0)
		cout << s.size() << '\n';
	else
		cout << '\n' << s.size() << '\n';
}

void dfs()
{
	if (idx > n)
		return;
	if (idx == n) {
		output();
		return;
	}
	for (int i = 0; i < l; i++) {
		char tmp = 'A' + i;
		now.push_back(tmp);
		if (check()) {
			idx++;
			dfs();
		}
		now.pop_back();
	}
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	while (cin >> n >> l && n && l) {
		now = "";
		idx = 0;
		dfs();
	}
	return 0;
}

E题 过河

题目描述

在河上有一座独木桥,一只青蛙想沿着独木桥从河的一侧跳到另一侧。在桥上有一些石子,青蛙很讨厌踩在这些石子上。由于桥的长度和青蛙一次跳过的距离都是正整数,我们可以把独木桥上青蛙可能到达的点看成数轴上的一串整点: 0 , 1 , ⋯   , L 0,1,\cdots,L 0,1,,L(其中 L L L 是桥的长度)。坐标为 0 0 0 的点表示桥的起点,坐标为 L L L 的点表示桥的终点。青蛙从桥的起点开始,不停的向终点方向跳跃。一次跳跃的距离是 S S S T T T 之间的任意正整数(包括 S , T S,T S,T)。当青蛙跳到或跳过坐标为 L L L 的点时,就算青蛙已经跳出了独木桥。

题目给出独木桥的长度 L L L,青蛙跳跃的距离范围 S , T S,T S,T,桥上石子的位置。你的任务是确定青蛙要想过河,最少需要踩到的石子数。

输入格式

输入共三行,

  • 第一行有 1 1 1 个正整数 L L L,表示独木桥的长度。
  • 第二行有 3 3 3 个正整数 S , T , M S,T,M S,T,M,分别表示青蛙一次跳跃的最小距离,最大距离及桥上石子的个数。
  • 第三行有 M M M 个不同的正整数分别表示这 M M M 个石子在数轴上的位置(数据保证桥的起点和终点处没有石子)。所有相邻的整数之间用一个空格隔开。
输出格式

一个整数,表示青蛙过河最少需要踩到的石子数。

2
数据范围
  • 对于 30 % 30\% 30% 的数据, 1 ≤ L ≤ 1 0 4 1\le L \le 10^4 1L104
  • 对于 100 % 100\% 100% 的数据, 1 ≤ L ≤ 1 0 9 1\le L \le 10^9 1L109 1 ≤ S ≤ T ≤ 10 1\le S\le T\le10 1ST10 1 ≤ M ≤ 100 1\le M\le100 1M100
思路分析

这道题如果只看前 30 % 30\% 30% 的数据的话很容易就可以想到动态规划。设 f [ i ] f[i] f[i] 表示走到 i i i 这个位置时最小的石子数,则可以退出状态转移方程:

  • i i i 的位置没有石子时,则 f [ i ] = m i n ( f [ i − t + 1 ] , f [ i − t + 2 ] . . . f [ i − s + 1 ] ) f[i] = min(f[i - t + 1], f[i - t + 2]...f[i - s + 1]) f[i]=min(f[it+1],f[it+2]...f[is+1])
  • i i i 的位置有石子时,则 f [ i ] = m i n ( f [ i − t + 1 ] , f [ i − t + 1 ] . . . f [ i − s + 1 ] ) + 1 f[i] = min(f[i - t + 1], f[i - t + 1]...f[i - s + 1]) + 1 f[i]=min(f[it+1],f[it+1]...f[is+1])+1

但是,我们发现, L L L 的最大值是 1 0 9 10^9 109,但 S S S, T T T, M M M 都很小,可以考虑该怎么把每两个点间的距离压缩。

设每一步最小距离为 s s s,最大距离为 t t t,则可以证出:任何距离 ≥ ≥ l c m ( s , t ) lcm(s, t) lcm(s,t) 的点都可以走到,证明如下:

请添加图片描述
当对路径做完压缩后,就可以用动态规划求解。

代码
#include <iostream>
#include <map>
#include <algorithm>

#define rank _
#define INF 0x3f3f3f3f

using namespace std;

const int N = 105;

int l;
int s, t, m;
int rock[N];
int d[N];
int rank[N];
int f[10005];
map<int, bool> has_rock;

int gcd(int a, int b)
{
	return b == 0 ? a : gcd(b, a % b);
}

int main()
{
	ios::sync_with_stdio(false);
	cin.tie(0), cout.tie(0);
	cin >> l >> s >> t >> m;
	int min_distance = s * t / gcd(s, t);
	for (int i = 1; i <= m; i++)
		cin >> rock[i];
	sort(rock + 1, rock + m + 1);
	if (s == t) {
		int ans = 0;
		for (int i = 1; i <= m; i++)
			if (rock[i] % s == 0)
				ans++;
		cout << ans << '\n';
		return 0;
	}
	for (int i = 1; i <= m; i++) {
		rank[i] = rank[i - 1] + min(rock[i] - rock[i - 1], min_distance);
		has_rock[rank[i]] = true;
	}
	l = min(l - rank[m], min_distance) + rank[m];
	for (int i = 1; i <= l; i++) {
		int tmp = INF;
		for (int j = s; j <= t; j++)
			if (i - j >= 0)
				tmp = min(tmp, f[i - j]);
		if (has_rock[i])
			f[i] = tmp + 1;
		else
			f[i] = tmp;
	}
	int ans = INF;
	for (int i = l - t + 1; i <= l - s + 1; i++)
		ans = min(ans, f[i]);
	cout << ans << '\n';
	return 0;
}

三、存在的不足

这次考试时简单题很快就全部AC了,而且AC后还有足够的时间,当时那两道难题我就一直在推,但没有思路,更没想怎么写代码。这导致了这次考试纯靠暴力电风扇DFS就能过的 D D D 题,代码都没写。

四、下次考试的改进措施

  • 对于一些很明显暴力就能做的题,先想暴力做法,然后一定要写一遍代码,说不定就过了。但这并不等于说没思路就盲目开写,这么做的前提是这道题有很大希望是暴力都能过的,并且还有充足的时间来写代码。

五、谢谢大家阅读

;