Good Bye 2024

省流版
  • A. 考虑存在相邻两个数组成三角形即可
  • B. 仅考虑唯一取值的元素是否占满了当前元素的所有取值
  • C. 分阶段考虑贡献,每阶段长度减半,贡献是中点值*区间数量+总偏移量和,维护总偏移量
  • D. 最大值取于俩数组从小到大排序。对于操作,等价于修改有序数组的最右边的数,维护答案
  • E. 两种必胜情况,一种是q在叶子上,p不在叶子上,另一种是q的邻居能一步到叶子,考虑p的取值,统计非叶子非好点的数量即可

A - Tender Carpenter (cf2053 A)

题目大意

给定一个长度为 n 的数组,问是否存在多种切分方法,使得切分出的若干个连续子数组的每一个数组,任取三个数(可以是同一个),它们都能组成一个三角形。

解题思路

一个最朴素的切分方法就是每个元素为一组,这样显然是满足条件的。

考虑还有没有其他的切分方法。朴素的想法是考虑两个相邻的元素,如果两个较小值大于较大值,那么这两个元素可以组成一个三角形,那么这两个元素就可以切分到一组中。

这样就得到另一个切分方法,因此就存在多种切分方法了。否则就不存在了。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
vector<int> a(n);
for (auto& i : a)
cin >> i;
bool ok = false;
for (int i = 0; i < n - 1; ++i) {
int minn = min(a[i], a[i + 1]);
int maxx = max(a[i], a[i + 1]);
ok |= (minn + minn > maxx);
}
if (ok)
cout << "YES" << '\n';
else
cout << "NO" << '\n';
}
return 0;
}


B - Outstanding Impressionist (cf2053 B)

题目大意

有一 n 个元素的数组,但你只记得第 i 个元素的值的范围是 [li,ri]

现在对于每个元素,是否存在一种情况,它的值是独一无二的,即其他元素的值都不等于它。

解题思路

考虑当前元素 ai,它的范围是 [li,ri],如果它不是独一无二的,那么 [li,ri] 中的每个数在其他位置都一定出现了。

而其他位置的元素的范围是 [lj,rj],如果 lj==rj,那么该位置只能取一个值,否则它可以避免和 ai取同样的值。

因此,我们只关注那些只能取一个值(lj==rj)的元素,并记录一下lj这个值出现了,记 forbid[lj]=1

然后对于 ai,如果 [li,ri] 中的每个数都在之前出现过(j=liriforbidj==rili+1),那么 ai 就不是独一无二的。用前缀和即可O(1)判断上述条件。

注意要消除 ai 本身的影响,即若 li==ri,那么要去除它对 forbid[li] 的贡献。

这里就需要记录两个数组:cntforbid,前者记录每个值出现的次数,后者记录当前值是否出现,即 forbid[i]=(cnt[i]>0),前缀和维护 forbid 数组。注意去除贡献时不需要修改前缀和数组。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
vector<int> forbid(2 * n + 1);
vector<int> cnt(2 * n + 1);
vector<array<int, 2>> a(n);
auto add = [&](int x) {
if (cnt[x] == 0)
forbid[x] = 1;
cnt[x]++;
};
auto remove = [&](int x) {
cnt[x]--;
if (cnt[x] == 0)
forbid[x] = 0;
};
for (auto& [l, r] : a) {
cin >> l >> r;
if (l == r) {
add(l);
}
}
vector<int> presum(2 * n + 1);
partial_sum(forbid.begin(), forbid.end(), presum.begin());
auto get_sum = [&](int l, int r) {
return presum[r] - (l == 0 ? 0 : presum[l - 1]);
};
string s(n, '0');
for (int i = 0; i < n; ++i) {
auto [l, r] = a[i];
if (l == r) {
remove(l);
}
auto sum = get_sum(l, r);
if (sum != (r - l + 1) || forbid[l] == 0)
s[i] = '1';
if (l == r)
add(l);
}
cout << s << '\n';
}
return 0;
}


C - Bewitching Stargazer (cf2053 C)

题目大意

一个长度为 n 的全排列,1,2,3,,n,给定 k,对区间 [1,n] 进行以下操作。

考虑当前区间[l,r],长度len=rl+1

  • len<k,停止。
  • len 为奇数,幸运值增加m=l+r2,考虑区间 [l,m1][m+1,r]
  • len 为偶数,考虑区间 [l,m][m+1,r]

问最终的幸运值是多少。初始时幸运值为 0

解题思路

  • 第一阶段,会考虑一个区间:[1,n]
  • 第二阶段,会考虑两个区间:[1,n2][n2+1,n]
  • 第三阶段,会考虑四个区间:[1,n4][n4+1,n2][n2+1,3n4][3n4+1,n]
  • 第四阶段,会考虑八个区间:[1,n8][n8+1,n4][n4+1,3n8][3n8+1,n2][n2+1,5n8][5n8+1,3n4][3n4+1,7n8][7n8+1,n]
  • ......(上述可能会因为奇数的原因有所偏差)
  • 区间长度 <k,停止。

最终的幸运值就是每个阶段的贡献和。

注意到,无论是哪个阶段,所考虑的区间的长度都是一样的:从 n,变成了 n2,再变成了 n4,再变成了 n8...,这意味着,如果当前阶段的区间长度是奇数,那么它们对幸运值都有贡献,区别只是偏移量不同。

即如果第二阶段的区间长度是奇数,那么第二阶段的两个区间都会对幸运值有贡献,区别只是偏移量不同:第一个区间的贡献是 m,第二个区间的贡献则是 shift+m。而这个偏移量 shift 是来自第一阶段的中点。

从中点值、偏移量的角度考虑每个阶段的贡献部分,则可以分成两个部分:中点值 × 区间数量 + 总偏移量和。

对于当前的第 i 阶段,区间长度是 n,则中点值是 n+12,区间数量是 2i1,考虑总偏移量和怎么求,从上述举例的分析可以得知它可以从上一阶段的总偏移量和中得到。

i1 阶段的总偏移量和是 add[i1],考虑每个偏移量的来源,像上述的第二阶段中的 shift,它作用在区间[n2+1,n]上,到了第三阶段,该区间就分成两个区间,每个区间算贡献时都要带上这个偏移量,因此该偏移量的贡献就会翻倍。由此实际上每个偏移量的贡献都会翻倍,即 add[i]=2×add[i1]

同时还有新的偏移量出现:考虑区间[1,n2],到了第 i 阶段,该区间会被一分为二,第二个区间就新增偏移量 n2,而一共有 2i1 个区间,因此新增的偏移量和就是 2i1×n2

因此,从第 i1 阶段到第 i 阶段,总偏移量和就是 add[i]=2×add[i1]+2i1×n2。注意这里的n是当前阶段的区间长度,不是题目给定的n

由此,当前阶段的贡献:中点值 × 区间数量 + 总偏移量和,即 n+12×2i1+add[i],都能O(1)得到,而阶段数是O(logn)的,因此一次询问的时间复杂度是O(logn)的。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = unsigned long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
auto solve = [&](int n, int k) {
LL ans = 0;
__int128 nn = 1;
__int128 add = 0;
while (n) {
if (n < k)
break;
if (n & 1) {
ans += (n + 1) / 2 * nn + add;
}
add = add * 2 + (n + 1) / 2 * nn;
nn *= 2;
n /= 2;
}
return ans;
};
int t;
cin >> t;
while (t--) {
int n, k;
cin >> n >> k;
LL ans = solve(n, k);
cout << ans << '\n';
}
return 0;
}


D - Refined Product Optimality (cf2053 D)

题目大意

给定两个数组 a,b,长度均为 n

可以打乱数组b,最大化 i=1nmin(ai,bi)

维护 q 次操作,每次操作,将让 一个数组的一个元素加一,然后回答其最大值。

结果对 998244353 取模。

解题思路

首先考虑如何最大化乘积。可以发现,两者都从小到大排序时,乘积最大。

可以这么考虑:我们从所有数的大到小考虑,对于每个数,比如是 ai,如果它对答案有贡献,那么要从 b 中找到一个数 bj,使得 bjai,这样才能使得 min(ai,bj)=ai,如果没有这样的数,它对答案没有贡献,我们就把它放在 a的池子里。接下来考虑的数比如 bi,如果它对答案有贡献,那么要从 a 中找到一个数 aj,使得 ajbi,这样才能使得 min(aj,bi)=bi,而此时 a 的池子里的数一定都比 bi 大,因此我们可以直接取出来与 bi 匹配。

即维护两个数组,一个是 a 的池子,一个是 b 的池子,然后从大到小考虑,如果当前数对答案有贡献,就从另一个数组的池子里找一个数匹配,否则就放到自己的池子里。容易发现这样匹配的结果,就是两个数组从小到大排序的结果。

知道最优情况怎么求了,接下来考虑如何维护操作。

每次操作只会让一个数加一,看着变化不大,容易想到一个朴素的维护方法:加一后,与右边的数比较,如果比右边的数大,就交换,直到不再比右边的数大为止。交换的同时维护答案。但这样的时间复杂度有问题。

考虑数组a1,1,1,1,1,1,2,第一次操作将第一个数加一,则变成1,1,1,1,1,2,2,经历了O(n)次交换,然后再让第一个数加一,变成1,1,1,1,2,2,2,又经历了O(n)次交换,这样的时间复杂度是O(qn),会超时。

怎么办呢?考虑上述朴素做法的问题:当我们第一个数加一时,不断交换位置时,其实答案是不变的:因为交换的两个数是同一个值。而从操作前和操作后的结果对比,其实它等价于让当前数的最右边的数加一,就没了

因此,对于当前操作,假设对数组a操作,以及一个排了序的数组sorta,当前对ai=p加一,它等价于在sorta中找到最右边的sortai=p,然后让sortai加一,同时维护答案(即先除以原来的数,操作后再乘以新的数)。

如何找到最右边的sortai=p呢?因为sorta是排好序的,可以用二分查找,upper_bound的前一个就是最右边的p

这样的时间复杂度是O(qlogn)的。

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
const int mo = 998244353;
int qpower(int a, int b) {
int qwq = 1;
while (b) {
if (b & 1)
qwq = 1ll * qwq * a % mo;
a = 1ll * a * a % mo;
b >>= 1;
}
return qwq;
}
int inv(long long x) { return qpower(x, mo - 2); }
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--) {
int n, q;
cin >> n >> q;
array<vector<int>, 2> a{vector<int>(n), vector<int>(n)};
for (auto& x : a)
for (auto& y : x)
cin >> y;
auto sorta = a;
for (auto& x : sorta)
sort(x.begin(), x.end());
LL ans = 1;
auto get = [&](int x) { return min(sorta[0][x], sorta[1][x]); };
for (int i = 0; i < n; ++i) {
ans = ans * get(i) % mo;
}
cout << ans << ' ';
while (q--) {
int o, x;
cin >> o >> x;
--o;
--x;
int val = a[o][x];
int pos = prev(upper_bound(sorta[o].begin(), sorta[o].end(), val)) -
sorta[o].begin();
ans = ans * inv(get(pos)) % mo;
a[o][x] = val + 1;
sorta[o][pos] = val + 1;
ans = ans * get(pos) % mo;
cout << ans << " \n"[q == 0];
}
}
return 0;
}


E - Resourceful Caterpillar Sequence (cf2053 E)

题目大意

给定一棵树,对于一对点(p,q),定义一个毛毛虫序列:pq的路径上的点,其中p是毛毛虫的头,q是毛毛虫的尾。

两人博弈,先手拉着毛毛虫的头,后手拉着毛毛虫的尾,两人轮流执行操作。每次可以选择一个点,然后将毛毛虫的头或尾连同身子往前移动到这个点的子节点上。

如果头到了叶子节点,先手胜利,如果尾到了叶子节点,后手胜利,如果始终无法到达叶子节点,平局。

两人都会采取最优策略,问(p,q)的数量,使得后手必胜。

解题思路

考虑后手必胜的情况,容易想到的一个是:

  • q在叶子节点,p不在叶子节点,那么后手必胜。这个统计一下叶子数量即可得到。

还有一种情况是,先手移动后,q在一个特别的位置,它可以一步移动到叶子节点,这样后手必胜。

我们定义能一步到叶子节点的点为好点,对于q点,考虑其每一个邻居u,如果u好点,那么(u,q)这对点对答案有贡献,考虑贡献怎么求。

由于先手移动一步后,qu,那么p得在u的子节点上,这样先手移动才有qu,考虑p的取值:

  • 不能在叶子上
  • 不能在好点
  • 其他点都可以

因此就是统计一个子树里非叶子非好点的点的数量。一个简单的计数问题,可以用DFS解决。

选定一个点为根后,对于每个点q,考虑其儿子(u,q),还有父亲(fa,q),计算它们的贡献即可。

时间复杂度是O(n)

神奇的代码
#include <bits/stdc++.h>
using namespace std;
using LL = long long;
int main(void) {
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
vector<vector<int>> edge(n);
vector<int> du(n);
for (int i = 0; i < n - 1; ++i) {
int u, v;
cin >> u >> v;
--u, --v;
edge[u].push_back(v);
edge[v].push_back(u);
du[u]++;
du[v]++;
}
if (n == 2) {
cout << 0 << '\n';
continue;
}
int leaves = 0;
vector<int> good(n);
for (int i = 0; i < n; ++i) {
if (du[i] == 1) {
leaves++;
for (int v : edge[i]) {
good[v] = 1 && (du[v] != 1);
}
}
}
int goods = accumulate(good.begin(), good.end(), 0);
LL ans = 1ll * leaves * (n - leaves);
auto dfs = [&](auto& dfs, int u, int fa) -> array<int, 3> {
int sum = good[u];
int sz = 1;
int leave = du[u] == 1;
for (int v : edge[u]) {
if (v == fa)
continue;
auto [nsum, nleave, nsz] = dfs(dfs, v, u);
if (good[v]) {
ans += nsz - nleave - nsum;
}
sum += nsum;
sz += nsz;
leave += nleave;
}
if (u != fa && du[u] != 1) {
if (good[fa]) {
int ano_sum = goods - sum;
int ano_sz = n - sz;
int ano_leave = leaves - leave;
ans += ano_sz - ano_leave - ano_sum;
}
}
return {sum, leave, sz};
};
int root = 0;
while (root < n && du[root] == 1)
root++;
dfs(dfs, root, root);
cout << ans << '\n';
}
return 0;
}


posted @   ~Lanly~  阅读(166)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
点击右上角即可分享
微信分享提示