Hebut 天梯赛选拔题解
A
模拟题,开一个变量 \(cur\),表明当前签到一天所得的分数,开一个变量 \(cnt\),表明已经连续有 \(cnt\) 天每天签到得 \(cur\) 分。
当 \(cur = cnt\) 时,按照规则说明 \(cur\) 该增加了,遂令 \(cur \leftarrow cur + 1, cnt \leftarrow 0\)。同理,如果出现断签,则 \(cur, cnt\) 均要清零。
Code
#include <iostream>
#include <string>
using namespace std;
int n, ans = 0, cur = 1, cnt = 0;
string s;
int main()
{
cin >> n >> s;
for (int i = 0; i < s.size(); i++)
{
if (cur == cnt)
{
cnt = 0;
cur++;
}
cnt++;
if (s[i] == '0')
{
cur = 1;
cnt = 0;
}
else
ans += cur;
}
cout << ans << '\n';
return 0;
}
B
考虑国王走 \(n\) 步最多可以走到哪里,不难发现国王每次都可以使其所在位置的横纵坐标变化 1。这样的话国王走 \(n\) 步最终可以走到的边界位置为 \((-n, n), (n, n), (n, -n), (-n, -n)\) 四点围成的正方形,所以答案就是这个正方形的面积,为 \((2n + 1)^2\)。
注意答案可能会爆 int,注意要使用 long long。
Code
#include <cstdio>
long long n;
int main()
{
scanf("%lld", &n);
printf("%lld\n", (2 * n + 1) * (2 * n + 1));
return 0;
}
C
由于每一次涂的颜色都是连续的一段,每种颜色最多只会使用一次,且每种颜色的左右端点不会被覆盖。那么我们只需预处理出每种颜色最后出现的位置,从第一个格子开始,根据第一个格子的颜色,跳到该颜色最后出现的位置,这一段最底下就应该全是该颜色。之后我们往后跳一格,再根据所在格子的颜色确定下个颜色段,重复这个过程即可。
Code
#include <algorithm>
#include <cstdio>
#include <cstring>
int n, pos[30];
char s[100010];
int main()
{
scanf("%d%s", &n, s + 1);
for (int i = 1; i <= n; ++i)
pos[s[i] - 'a'] = std::max(pos[s[i] - 'a'], i);
int p = 1;
while (p <= n)
{
for (int i = p; i <= pos[s[p] - 'a']; ++i)
putchar(s[p]);
p = pos[s[p] - 'a'] + 1;
}
return 0;
}
D
下文中的翻转是指 A 的一次翻转。
不难发现状态只有 \(2^9\) 种,并且如果我们确定了要翻哪些格子,则我们翻转格子的顺序和最终我们翻出的结果没有关系。而根据贪心的原则我们不会将同一个位置翻转两次,所以我们最优翻转策略也只有 \(2^9\) 种。我们又发现如果我们翻转特定的一块,都不能通过翻转其他块达到只翻转这特定的一块所造成的效果,所以我们可以证明对于局面集合到最优翻转策略的映射关系为双射关系。
所以我们只需考虑二进制枚举 \(2^9\) 种策略,分别预处理出它们所对应的局面。然后如果不考虑输出所占的时间复杂度,我们就可以 \(O(1)\) 回答所有问题了。
Code
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cstring>
#include <cmath>
#include <string>
using namespace std;
typedef long long ll;
int a[9] = {
0x00b, // 000001011
0x017, // 000010111
0x026, // 000100110
0x059, // 001011001
0x0ba, // 010111010
0x134, // 100110100
0x0c8, // 011001000
0x1d0, // 111010000
0x1a0 // 110100000
};
int ans2[555][15];
int cnt[555];
void output(int x, int y)
{
for (int i = 0; i <= 8; ++i)
{
if ((1 << i) & y)
{
ans2[x][++cnt[x]] = i + 1;
}
}
}
void output2(int x)
{
cout << cnt[x] << endl;
for (int i = 1; i <= cnt[x]; ++i)
{
cout << ans2[x][i] << endl;
}
}
int b[10];
int main()
{
int T;
T = 512;
while (T--)
{
int m;
m = T;
int ans = 0;
for (int i = 0; i < (1 << 9); ++i)
{
ans = 0;
for (int j = 0; j <= 8; ++j)
{
if ((1 << j) & i)
{
ans ^= a[j];
}
}
if (ans == m)
{
output(m, i);
}
}
}
cin >> T;
while (T--)
{
int n;
cin >> n;
int m = 0;
memset(b, 0, sizeof(b));
for (int i = 1; i <= n; ++i)
{
int x;
cin >> x;
b[x] ^= 1;
}
for (int i = 1; i <= 9; ++i)
{
if (b[i])
m += (1 << (i - 1));
}
output2(m);
}
return 0;
}
E
题目中需要求有考察点的连通分支个数。我们只需考虑枚举所有考察点,如果其所在连通分支没有被设立考察点,就对其所在连通分支进行标记,并让考察点数量加 1。最后输出答案即可。
除此之外还有并查集的写法,在此不再赘述。
Code
#include <cstdio>
#include <vector>
int a[1003];
bool vis[100003];
std::vector<int> edge[100003];
void dfs(int u)
{
vis[u] = 1;
for (int v : edge[u])
if (!vis[v])
dfs(v);
}
int main()
{
int n, m, k;
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < k; i++)
scanf("%d", a + i);
for (int i = 0, x, y; i < m; i++)
{
scanf("%d%d", &x, &y);
edge[x].push_back(y);
edge[y].push_back(x);
}
int ans = 0;
for (int i = 0; i < k; i++)
if (!vis[a[i]])
dfs(a[i]), ans++;
printf("%d", ans);
return 0;
}
F
我们可以假想一个有电的 0 号村庄,建发电站可以看成建一条边跟 0 号村庄相连,然后直接求解最小生成树即可。
Code
#include <algorithm>
#include <cstdio>
#include <cstring>
const int maxm = 3e5 + 10;
int n, m, par[2010];
struct edge
{
int u, v, w;
edge() {}
edge(int u, int v, int w) : u(u), v(v), w(w) {}
}e[maxm];
int find(int x)
{
return par[x] == x ? x : par[x] = find(par[x]);
}
inline bool unite(int x, int y)
{
x = find(x), y = find(y);
if (x == y)
return false;
par[x] = y;
return true;
}
int main()
{
scanf("%d%d", &n, &m);
int cnt = 0;
for (int i = 1, c; i <= n; ++i)
{
par[i] = i;
scanf("%d", &c);
e[++cnt] = edge(0, i, c);
}
for (int i = 1, u, v, w; i <= m; ++i)
{
scanf("%d%d%d", &u, &v, &w);
e[++cnt] = edge(u, v, w);
}
std::sort(e + 1, e + cnt + 1, [&](edge x, edge y){return x.w < y.w;});
int ans = 0, j = 0;
for (int i = 1; i <= cnt; ++i)
{
if (unite(e[i].u, e[i].v))
{
ans += e[i].w;
++j;
if (j == n)
break;
}
}
printf("%d\n", ans);
return 0;
}
G
由于期望直接求显然无法求,所以我们根据期望的线性性,不妨考虑求出每个被删去字母对答案的贡献,而每个字母可以对答案产生贡献的概率为 \(\dfrac{25}{26}\),我们计算期望只需让最大总贡献乘以 \(\dfrac{25}{26}\) 即可。
现在只需考虑如何求出每个字母的贡献即可,考虑到一个单词对答案的贡献为:
我们可以对其做一个简单的变换,将式子化为:
所以一个字母 \(w_{i, j}\) 对答案的贡献为:\((\left\vert w_i \right\vert - j + 1) \times j\)
由于我们要选择 \(m\) 个单词,则我们需要从所有字母中选择贡献为前 \(m\) 大的字母即可。形式化地,我们可以将问题转化为:给定 \(n\) 个形如 \(f_i(x) = (a_i - x)x\) 的二次函数,求这些二次函数前 \(m\) 大的函数值之和。其中 \(a_i\) 即为本题中的 \(\left\vert w_i \right\vert + 1\)。
由于二次函数是从 \(x = \dfrac{a_i}{2}\) 处取得最值,所以我们可以考虑首先将这 \(n\) 个二次函数的最大值放进大根堆中,然后从堆中依次取出 \(m\) 个值。但我们需要考虑一个二次函数的极值小于其他二次函数的非极值的情况,所以我们每次从堆中取出一个函数值 \(f_i(x)\) 后,由于二次函数由最值处函数值向两边递减,我们只需要按照具体情况再将 \(f_i(x - 1)\) 或者 \(f_i(x + 1)\) 放入堆中,以便考虑周全。
该算法的时间复杂度为 \(O(m \log n)\)。为了卡掉将所有函数值求出并排序的做法,本题将单词长度之和开到了 \(2 \times 10^9\)。
Code
#include <algorithm>
#include <cstdio>
#include <cassert>
#include <cstring>
#include <queue>
typedef long long ll;
const int p = 998244353;
const int inv26 = 729486258;
const int maxn = 2e5 + 10;
int n, m, w[maxn];
struct node
{
int x, a;
ll val;
node() {}
node(int x, int a, ll val) : x(x), a(a), val(val) {}
bool operator < (const node& rhs) const
{
return val < rhs.val;
}
};
std::priority_queue<node> q;
inline ll calc(ll x, ll a)
{
return x * (a - x);
}
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1, w; i <= n; ++i)
{
scanf("%d", &w);
++w;
if (w & 1)
q.push(node(w / 2, w, calc(w / 2, w)));
q.push(node(w / 2, w, calc(w / 2, w)));
}
int ans = 0;
for (int i = 1, x, a; i <= m; ++i)
{
x = q.top().x, a = q.top().a;
ans = (ans + q.top().val % p) % p;
q.pop();
if ((a & 1) == 0 && x == a / 2)
q.push(node(x - 1, a, calc(x - 1, a)));
q.push(node(x - 1, a, calc(x - 1, a)));
}
ans = 25ll * inv26 % p * ans % p;
printf("%d\n", ans);
return 0;
}
H
由于选择的配料的美味度只要能重新排列成等比数列就可以,所以本题和配料出现的顺序无关。为了方便我们设 \(cnt_x\) 表明 \(x\) 这个美味度在配料中出现了多少次。我们设所有配料中最大的美味度为 \(A\)。
发现题目中比较难统计的一点是公比为分数的情况,所以我们要考虑为什么会出现公比为分数的情况。不难发现,若将公比的最简分数形式设为 \(\dfrac{p}{q}\),则为了保证等比数列的三项均为整数,其首项 \(i\) 必然要满足 \(q^2 \mid i\)。所以我们需要求出对于 \([1, A]\) 中每个数求出其因子中的所有完全平方数。为了使这部分时间复杂度尽可能低,我们反过来考虑一个完全平方数 \(q^2\) 其可以成为哪些数的因子。不难发现这部分的时间复杂度为 \(O(\sum_{q=1}^{\sqrt{A}} \dfrac{A}{q^2}) = O(A)\)。
然后我们枚举等比数列的首项,然后枚举 \(i\) 因子中的一个完全平方数作为公比的分母,再枚举一个数作为公比的分子。配合 \(cnt\) 数组,我们就可以算出该等比数列对答案的贡献。为了避免算重,我们需要保证我们枚举的公比大于 1,且是最简分数形式。
对于公比为 1 的等比数列,我们再将 \(\sum_{i=1}^{A} \binom{cnt_i}{3}\) 计入答案即可。
本算法的时间复杂度我们可以粗略计算一下,考虑到以上枚举还是可以看成先确定完全平方数 \(q^2\),再确定首项是 \(q^2\) 的几倍,再确定公比的分子。按照这个顺序枚举,可以得出最终的所需的时间代价:
显然本算法的时间复杂度不超过 \(O(A \log A)\),且该算法常数较小,在不卡常的情况下,最慢的测试点也保持在 700 ms 以下。这个算法是验题人给出的。出题人由于过菜,只给出了一个 \(O(A \sqrt{A})\) 的算法。
Code
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e6 + 5;
int cnt[N], p[N], n, m, r;
vector<int> f[N];
int gcd(int a, int b)
{
return b == 0 ? a : gcd(b, a % b);
}
int main()
{
long long ans = 0;
ios::sync_with_stdio(false);
cin.tie(0), cout.tie(0);
for (int i = 1; i < N; i++)
cnt[i] = 0;
cin >> n;
while (n--)
{
cin >> m;
cnt[m]++;
}
for (int i = 1; i * i < N; i++)
{
for (int j = 1; j * i * i < N; j++)
f[j * i * i].push_back(i);
}
for (int i = 1; i < N; i++)
{
if (cnt[i] == 0)
continue;
for (unsigned int j = 0; j < f[i].size(); j++)
{
for (int k = f[i][j] + 1;; k++)
{
if (f[i][j] > 1 && gcd(k, f[i][j]) != 1)
continue;
m = i / f[i][j] * k;
r = m / f[i][j] * k;
if (r > 1e6)
break;
ans += 1ll * cnt[i] * cnt[m] * cnt[r];
}
}
}
for (int i = 1; i < N; i++)
ans += 1ll * cnt[i] * (cnt[i] - 1) * (cnt[i] - 2) / 6;
cout << ans;
return 0;
}