蓝桥杯省赛真题(砍树 整数删除 景区导游 翻转硬币)
蓝桥杯省赛真题(砍树 整数删除 景区导游 翻转硬币)
四道比较难的题(题解是官方提供的)
砍树 (树上差分)
https://www.lanqiao.cn/problems/3517/learning/
解题思路
在这个问题中,我们需要找到一条边,砍掉它之后,所有给出的节点对 \((a_i, b_i)\) 之间都不再连通。换言之,这条边应是所有 \((a_i, b_i)\) 的通路必经之路。
为了找到这样的边,我们需要先确定这些路径(节点对 \((a_i, b_i)\) 之间的通路)的交集。即我们要找到一些节点,这些节点是所有路径的公共部分。
这些节点其实就是所有 \((a_i, b_i)\) 的最近公共祖先(LCA)。为什么呢?因为对于任意两个节点 \(a\) 和 \(b\),它们的路径必然经过它们的最近公共祖先。也就是说,如果我们找到了所有 \((a_i, b_i)\) 的 LCA,那么这些 LCA 就是所有路径的交集。因此,我们需要找到的边就是连接这些 LCA 与它们父节点的边。
举个例子,假设我们有三对节点,\((a_1, b_1)\)、\((a_2, b_2)\)、\((a_3, b_3)\),它们的 LCA 分别是 \(c_1\)、\(c_2\)、\(c_3\)。假设 \(c_1\) 是 \(c_2\) 的祖先,\(c_2\) 是 \(c_3\) 的祖先,那么所有的路径都会经过节点 \(c_2\),所以我们需要找的边就是连接 \(c_2\) 和它的父节点的边。
所以,我们需要做的第一步就是找到所有 \((a_i, b_i)\) 的 LCA。这可以通过一种叫做“倍增”的算法来实现。这种算法可以在 \(O(\log n)\) 的时间内找到任意两个节点的 LCA。
之后我们还需要使用树上差分的思想来处理这个问题,首先对每一对 \((a_i, b_i)\) 的路径进行 \(+1\) 操作,这意味着我们在 \(a_i\) 和 \(b_i\) 之间添加了一条边。接着,我们对它们的最近公共祖先(LCA)进行 \(-2\) 操作,这意味着我们从 LCA 中移除了两条边(因为从根节点到 LCA 的路径被计算了两次,所以需要减去两次)。
这个操作可以使用深度优先搜索(DFS)来实现。首先,对所有的节点对进行 \(+1\) 操作,然后对它们的 LCA 进行 \(-2\) 操作。然后,使用 DFS 从根节点开始,更新每个节点的计数值。
在这些操作之后,如果一个节点的计数值等于 \(m\)(对数的数量),那么这个节点就是所有 \((a_i, b_i)\) 的通路的交点。因为,只有当所有的 \((a_i, b_i)\) 都经过某个节点时,这个节点的计数值才会等于 \(m\)。换句话说,从根节点到这个节点的路径就是我们需要找的边。
具体实现
- 初始化:首先,我们需要初始化每个节点的深度和父节点。我们从树的根节点开始,递归地设置每个节点的深度(即距离根节点的距离)和父节点。
- 计数:接下来,我们需要找到所有 \((a_i, b_i)\) 的路径,并将它们的计数添加到数组中。具体来说,我们将路径上的每个节点的计数加一,但是要将它们的最近公共祖先的计数减二。这样做的目的是,如果一个节点的计数值等于 \(m\),那么从根节点到该节点的路径就是我们需要找的边。
- 深度优先搜索:然后,我们通过深度优先搜索,遍历所有的节点,更新它们的计数值。我们从根节点开始,对每个节点的子节点进行递归操作,更新每个节点的计数值。
- 寻找结果:最后,我们再次遍历所有的节点,找出计数值等于 \(m\) 的节点。如果存在多个这样的节点,我们取其中编号最大的边作为结果。
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
typedef pair<int, int> pii;
const int N = 1e5 + 5, M = 20;
ll n, m, dep[N], cnt[N];
vector<pii> v[N];
int id[N], f[N][M];
void dfs (int u, int fa) {
dep[u] = dep[fa] + 1, f[u][0] = fa;
for (int i = 1; (1 << i) <= dep[u]; i++) f[u][i] = f[f[u][i-1]][i-1];
for (auto vi : v[u]) {
int j = vi.first;
if (j == fa) continue;
dfs (j, u);
id[j] = vi.second;
}
}
int lca (int a, int b) {
if (dep[a] < dep[b]) swap (a, b); //确保a往上跳
for (int k = 15; k >= 0; k--) {
if (dep[f[a][k]] >= dep[b]) { //注意是>=
a = f[a][k];
}
}
if (a == b) return a;
for (int k = 15; k >= 0; k--) {
if (f[a][k] != f[b][k]) {
a = f[a][k], b = f[b][k];
}
}
return f[a][0];
}
void add (int u, int fa) {
//cnt[fa] += cnt[u]; //这里不行
for (auto vi : v[u]) {
int j = vi.first;
if (j == fa) continue;
add (j, u);
cnt[u] += cnt[j];
}
}
int main () {
scanf ("%lld%lld", &n, &m);
for (int i = 1; i < n; i++) {
int a, b;
scanf ("%d%d", &a, &b);
v[a].push_back ({b, i}), v[b].push_back ({a, i});
}
dfs (1, 0); //倍增初始化
for (int i = 0; i < m; i++) {
int a, b;
scanf ("%d%d", &a, &b);
cnt[a]++, cnt[b]++, cnt[lca(a, b)] -= 2;
}
add (1, 0); //求前缀和
int ans = -1;
for (int i = 1; i <= n; i++) {
if (cnt[i] == m) ans = max(ans, id[i]);
}
printf ("%d\n", ans);
}
整数删除 (优先队列)
https://www.lanqiao.cn/problems/3515/learning/
解题思路
删除数组中的任意元素的时间复杂度为 \(O(n)\),本题存在大量删除操作,若使用数组会导致时间复杂度过大,故考虑使用链表进行存储、删除操作。而需要动态的求最小值,显然可以使用堆。每次从堆中取出最小值的下标,然后在链表中删除它。但本题特殊点在于将其删除,并把与它相邻的整数加上被删除的数值,所以会导致还在堆中的元素的权值变化。
我们可以注意到,每次删除操作只会让一些元素变大,而不会让元素变小。也就是,可能会让原本的最小值变成不是最小值。因此我们取出堆中的最小值时,需要将此元素的排序权值和实际的值进行对比,如果实际的值变大了,则当前元素并不一定是最小值,需要重新放回堆中。
具体做法如下:
- 使用双向链表存储数列,并使用数组 \(v_i\) 存储每个节点的值,\(l_i\) 存储节点 \(i\) 的前驱节点,\(r_i\) 存储节点 \(i\) 的后继节点。
- 使用最小堆存储数列中未被删除的节点,堆中的元素是 { 权值,结点下标}。
- 初始化双向链表和最小堆,并将所有节点加入最小堆中。
- 重复执行以下步骤 \(K\) 次:
- 取出最小堆中的堆顶元素(最小值)。
- 比较堆顶元素的权值和实际值是否相等,如果不相等,则说明这个节点的实际值已经发生了变化,当前元素并不一定是最小值,需要重新放回堆中,否则删除该节点,将其相邻节点的值加上被删除节点的值。
- 如果需要重新放回堆中,那么需要将该节点的权值更新为实际值,并重新放回堆中。
- 输出双向链表中剩余的节点的值。
每次删除操作最多会让两个元素的值变化,因此从堆中取出的次数是 \(K\) ,时间复杂度为 \(O((n + k)\log n)\)。
Code
#include <bits/stdc++.h>
#define ll long long
using namespace std;
typedef pair<ll, ll> pii;
const int N = 5e5 + 5;
ll n, k, l[N], r[N], a[N];
int main () {
scanf ("%lld%lld", &n, &k);
priority_queue<pii, vector<pii>, greater<pii>> q;
for (int i = 1; i <= n; i++) {
scanf ("%lld", &a[i]);
l[i] = i - 1, r[i] = i + 1; //双向链表
q.push ({a[i], i});
}
while (k && !q.empty ()) {
auto t = q.top ();
q.pop ();
ll val = t.first, id = t.second;
if (val == a[id]) { //实际值等于当前值,可删除
a[l[id]] += a[id], a[r[id]] += a[id], k--;
r[l[id]] = r[id], l[r[id]] = l[id], a[id] = 0;
}
else q.push ({a[id], id}); //把实际值放进来
}
for (int i = 1; i <= n; i++) if (a[i]) cout << a[i] << ' ';
cout << endl;
}
景区导游
https://www.lanqiao.cn/problems/3516/learning/
解题思路
最近公共祖先 (倍增求LCA)
其中 \(dis_{i, j}=dis_{v_i}+dis_{v_j}-2dis_{lca(v_i, v_j)}\)
Code
这题简单欸,比上一个LCA的简单
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 1e5 + 5, M = 20;
ll n, m, f[N][M], dep[N], v[N], ans;
ll h[N], e[N * 2], ne[N * 2], w[N * 2], idx;
ll del[N], dis[N];
void add (int a, int b, ll c) {
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx++;
}
void dfs (int u, int fa) {
dep[u] = dep[fa] + 1, f[u][0] = fa;
//dis[u] += dis[fa] + w[u];
for (int i = 1; (1 << i) <= dep[u]; i++) f[u][i] = f[f[u][i-1]][i-1];
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
if (j == fa) continue;
dis[j] += dis[u] + w[i];
dfs (j, u);
}
}
int lca (int a, int b) {
if (dep[a] < dep[b]) swap (a, b); //确保a往上跳
for (int k = 19; k >= 0; k--) {
if (dep[f[a][k]] >= dep[b]) { //注意是>=
a = f[a][k];
}
}
if (a == b) return a;
for (int k = 19; k >= 0; k--) {
if (f[a][k] != f[b][k]) {
a = f[a][k], b = f[b][k];
}
}
return f[a][0];
}
ll cal (int a, int b) {
return dis[a] + dis[b] - 2ll * dis[lca (a, b)];
}
int main () {
memset (h, -1, sizeof h);
scanf ("%lld%lld", &n, &m);
for (int i = 1; i < n; i++) {
int a, b;
ll c;
scanf ("%d%d%lld", &a, &b, &c);
add (a, b, c), add (b, a, c);
}
dfs (1, 0);
//for (int i = 1; i <= n; i++) cout << dis[i] << ' ';
//cout << endl;
for (int i = 1; i <= m; i++) scanf ("%lld", &v[i]);
for (int i = 2; i <= m; i++) del[i] = cal (v[i-1], v[i]), ans += del[i];
printf ("%lld ", ans - del[2]);
for (int i = 2; i < m; i++) printf ("%lld ", ans - del[i] - del[i+1] + cal (v[i-1], v[i+1]));
printf ("%lld\n", ans - del[m]);
}
翻转硬币
https://www.lanqiao.cn/problems/3509/learning/
解题思路
这个问题的解决方案建立在数论的基础上,主要使用了 Möbius
反演公式和 Dirichlet
卷积。我们可以先对问题进行一些转化和简化,然后对结果进行求和。
推理证明
-
首先,我们从硬币 \(1\) 开始,将所有对应硬币和它的倍数位置的硬币翻转。这种策略是有效的,因为我们每次操作都会翻转硬币 \(i\) 和它所有的倍数位置的硬币,这意味着我们实际上是在对硬币 \(i\) 的所有因子进行操作。
-
对每个硬币 \(i\),我们定义 \(f(i)\) 为在递推过程中,硬币 \(i\) 是否需要被翻转。我们可以得到 \(f(i) = \sum_{d | i, d \neq i} f(d)\),其中 \(d\) 是 \(i\) 的所有因子。
-
显然,硬币 \(1\) 一定会被翻转,所以我们有 \(f(1) = 1\)。对于其他硬币,因为它们最初都是朝上的,所以我们有 \(\sum_{d | i} f(d) = \varepsilon(i)\)。在
Dirichlet
卷积形式中,这可以写作 \(1 \times f = \varepsilon\)。对此进行Möbius
反演,我们可以得到 \(f = \varepsilon \times \mu = \mu\)。根据此公式,我们知道当 \(f(i) = \mu(i) = -1\) 时,我们要将 \(f(i)\) 转换为 \(1\)。 -
我们要求的实际上是 \(f\) 的前缀和,也就是 \(\sum_{i=1}^{n} \mu^2(i)\),这个和表示的是每个数 \(i\) 是否含有平方因子。我们可以通过枚举所有的平方因子,并对 \(\varepsilon\) 进行一次反演来计算这个和。
具体实现
我们首先对小于 \(n\) 的所有数进行筛选,找出所有的质数,并计算它们的 Möbius
函数值。这可以通过经典的筛法完成。
然后,对于每个完全平方数,我们计算它对答案的贡献,并将这些贡献加起来。具体来说,对于每个完全平方数 \(i\),它对答案的贡献是 \(\mu(i) \cdot \left\lfloor \frac{n}{i^2} \right\rfloor\)。我们可以通过预处理和整除分块的技术来计算这个和。
我们使用了一个哈希表来存储已经计算出的 Möbius
函数值的和,以避免重复计算。同时,我们利用了整除分块的方法来优化计算过程。
时间复杂度分析
这段代码的主要功能是计算给定的整数 \(n\) 下的 Möbius
函数的前缀和。它使用了线性筛法预处理质数和 Möbius
函数的值,并利用了整除分块的思想和哈希表优化了计算过程。
我们先来分析每个部分的时间复杂度:
-
线性筛法:时间复杂度为 \(O(n)\),其中 \(n\) 是筛选的上界。在这段代码中,筛选的上界是 \(\text{lim} = n^{\frac{2}{5}}\),所以时间复杂度为 \(O(n^{\frac{2}{5}})\)。
-
预处理
Möbius
函数的前缀和:时间复杂度为 \(O(n)\),在这段代码中,时间复杂度为 \(O(n^{\frac{2}{5}})\)。 -
计算
Möbius
函数的前缀和:时间复杂度为 \(O(\sqrt{n} \cdot \log{n})\),这是因为在最差情况下,每个数 \(i\) 都需要计算 \(\frac n i\) 次,其中 \(i\) 从 \(1\) 到 \(\sqrt{n}\)。这里使用了哈希表来存储已经计算过的Möbius
函数的前缀和,以避免重复计算。同时,利用了整除分块的思想,对于每个区间 \([l, r]\),都只需要计算一次Möbius
函数的前缀和。 -
计算结果:时间复杂度为 \(O(\sqrt{n})\),这个部分主要是对 \(\sqrt{n}\) 的所有整数进行了一次遍历。
因此,总体来看,这段代码的时间复杂度是 \(O(n^{\frac{2}{5}} + \sqrt{n} \cdot \log{n})\)。在实际运行时,由于代码中使用了预处理和整除分块的优化技术,所以运行速度会比这个理论的时间复杂度更快。
Code
我不会数论啊qaq
(先放个代码明天学)
//c++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
constexpr int N = 1e9 + 10, M = 1.6e7;
int lim;
vector<int> pr;
int vis[M], mu[M], Sm1[M];
void sieve(){
mu[1] = 1;
for(int i = 2; i <= lim; i++){
if(!vis[i]) pr.push_back(i), mu[i] = -1;
for(int j = 0; j < pr.size() && pr[j] * i <= lim; j++){
vis[pr[j] * i] = 1;
if(i % pr[j] == 0){
mu[pr[j] * i] = 0;
break;
}
mu[pr[j] * i] = -mu[i];
}
}
for(int i = 1; i < lim; i++)
Sm1[i] = Sm1[i - 1] + mu[i];
}
unordered_map<int, int> Sm2;
int Sm(int n){
if(n <= lim) return Sm1[n];
auto it = Sm2.find(n);
if(it != Sm2.end()) return it->second;
int res = 1;
for(int l = 2, r; l <= n; l = r + 1){
r = n / (n / l);
res -= (r - l + 1) * Sm(n / l);
}
return Sm2[n] = res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr); cout.tie(nullptr);
ll n; cin >> n;
lim = powl(n, 2.0 / 5) + 10;
sieve();
ll m = sqrtl(n);
ll ans = 0, pre = 0, cur;
for(ll l = 1, r; l <= m; l = r + 1){
r = sqrtl(n / (n / (l * l)));
ans += (Sm(r) - Sm(l - 1)) * (n / (l * l));
}
cout << ans << "\n";
return 0;
}