Week Round 30
T1 是无意义题,就不说了。这次周赛出得最差的题目就是 T1。
T2: ABC282E
题目描述
有 个数 ,你每次可以选出两个数 和 ,获得 分,并选择这两个数中的一个数删掉,求最大得分。
。
题目思路
我们把选出的两个数看成一条边, 就是边权。先对两两个数建边,可以得到一个图。可以知道,通过操作得到的一个图不存在环。即选出的子图是一颗树。
也就是说:对原图求最大生成树,就是答案。
T3: CF1954E
题目描述
给定一个长度为 的序列 ,你还有一个属性 ,定义一次操作为:
-
选择 中一段极长的区间 ,满足 。
在这里,极长的区间定义为 满足条件但 与 不满足条件。
-
,执行 。
定义 为当 时,为使 的最小操作数。
你需要分别求出 的值。
保证 ,。
题目思路
这次周赛出得最好的题目之一就是 T3。
为了简单起见,我们从 开始。
第一道闪电可以向任何怪物发射,因为它总是会扩散到所有怪物身上。我们将继续发射闪电,直到有怪物死亡。当一个或多个怪物死亡时,问题就会分解成几个独立的子问题,因为没有闪电会穿过死亡的怪物。而且我们不管选择什么怪物来发射闪电,分成的子问题都是相同的。这意味着不存在 "最少秒数 "的概念——答案并不取决于发射闪电的怪物的选择。这个结论真的是妙!
那么我们该如何计算这个答案呢?攻击第一个怪物,直到它死亡。这需要 秒。然后我们继续攻击第二个怪物。如果它的生命值比第一个怪物高,我们就需要额外发射 枚闪电来杀死它。否则,它已经死亡。在这两种情况下,第三个怪物会受到多少伤害?假设它的生命值很高。在第一种情况下,它会受到 伤害,因为所有的闪电都会击中它。但在第二种情况下,它也会受到 次伤害。以此类推。这就意味着 个怪物需要被 个闪电击中。
那么 的答案就等于 。
如何计算任意 的答案呢?事实上,两者的差别并不大。只需将每个怪物的健康值从 改为 即可,而前面所述的整个过程将保持不变。因此,任何 的答案都等于 。
这个结论也真的是妙!对于我来说,很难获得 30 分 算法。
继续优化。把 max 拆开,看每一个 的系数,取决于两个条件:
- 如果是 或 ,则系数增加 ;
- 如果是 和 ,则系数减少 。
我们把 怪兽的这个系数称为 。因此,我们需要计算 。注意, 是固定的。
这是什么?数论分块,只不过是向上取整的数论分块,但是我们知道 ,所以依然可以转化为下取整。
我们可以考虑每个 对答案的贡献,比如当前极长 使得 ,那么答案区间 整体就加上 。这个也 tm 很妙!
#include <bits/stdc++.h>
using namespace std;
#define PII pair<int, int>
#define _for(i, a, b) for (int i = (a); i <= (b); i++)
#define _pfor(i, a, b) for (int i = (a); i >= (b); i--)
#define int long long
const int N = 3e5 + 5;
int n, a[N], maxn, ans[N];
signed main() {
cin >> n;
_for(i, 1, n) cin >> a[i], maxn = max(maxn, a[i]);
_for(i, 1, n) {
int cnt = 0;
if (i == 1 || a[i] > a[i - 1]) cnt++;
if (i < n && a[i + 1] > a[i]) cnt--;
int l = 1, r;
while (l <= a[i]) {
int t = (a[i] - 1) / l;
if (t) r = (a[i] - 1) / t;
else r = a[i];
ans[l] += cnt * (t + 1);
ans[r + 1] -= cnt * (t + 1);
l = r + 1;
}
ans[a[i] + 1] += cnt; // warning!
}
_for(i, 1, maxn) ans[i] += ans[i - 1];
_for(i, 1, maxn) cout << ans[i] << ' ';
}
T4: ARC100E
题目描述
给你一个长度为 的序列 ,每个 ,找出最大的 (,)并输出。
表示按位或运算。。
题目思路
求出 的最大值,等于说是求出等于 ,等于 ,一直到 的最大值。但是小于 的在之前的询问中求过,所以我们只需要把 res 放在外面就求出 的最大值。比如:
int res = 0;
for (int i = 1; i < ((1 << n) - 1); i++) {
res = max(res, or=i的答案);
cout << res << endl;
}
两个数或起来等于 ,那么这两个数肯定是 二进制表示的子集。定义 表示 二进制子集中 数组的最大值, 表示 二进制子集中 数组的次大值。那么答案就是 。
预处理 mx 数组和 mn 数组,首先外层循环枚举 ,再枚举 的子集,花费 的时间,足以通过本题,440ms。
signed main() {
cin >> n;
_for(i, 0, (1 << n) - 1) a[i] = read();
_for(i, 0, (1 << n) - 1) {
for (int j = i; j; j = (j - 1) & i) {
if (a[j] > tt[i]) tt2[i] = tt[i], tt[i] = a[j];
else if (a[j] > tt2[i]) tt2[i] = a[j];
}
if (a[0] > tt[i]) tt2[i] = tt[i], tt[i] = a[0];
else if (a[0] > tt2[i]) tt2[i] = a[0];
}
int res = 0;
_for(i, 1, (1 << n) - 1) {
res = max(res, tt[i] + tt2[i]);
wr(res); putchar('\n');
}
}
但是我们可以用一个更高效的方式,类似于 dp(被叫做高维前缀和)。就是 可以由 转移过来,其中 是 的子集。当然,快多了,32 ms。
signed main() {
cin >> n;
_for(i, 0, (1 << n) - 1) a[i] = read(), tt[i] = a[i];
_for(j, 0, n - 1) {
_for(i, 0, (1 << n) - 1) {
if (i >> j & 1) {
// mx[i]由子集mx[i^(1<<j)]转移过来
if (tt[i ^ (1 << j)] > tt[i]) tt2[i] = tt[i], tt[i] = tt[i ^ (1 << j)];
else if (tt[i ^ (1 << j)] > tt2[i]) tt2[i] = tt[i ^ (1 << j)];
}
}
}
int res = 0;
_for(i, 1, (1 << n) - 1) {
res = max(res, tt[i] + tt2[i]);
wr(res); putchar('\n');
}
}
T5: AGC030F
这个题也出的不错。
题目描述
有一个 个数的序列 ,从 到 标号。你要把 这些数填进去,使它形成一个排列。
但是已经有一些位置强制填了特定的数了,输入时会给出。
最后令长度为 的序列 为:令 。
询问所有方案中能得到的不同的 的数量。 。
题目思路
把这 个数两两配对,然后每一对中的较小数会进到 B 里面去。
把已经确定的配对 ,剩下的是一对中有一个确定了的,和两个都没确定的。
把 这些数排成一排,配对的连一条线,那么 中的元素就是每条线左端点的值,但是 中还有顺序需要进行处理。
考虑:如果某一条线的某一端的值,在 A 中是存在的,也就是说这一对的一端已经是确定的,那么在 中的位置也是确定的。
如果这条线两端的值都没有在 中,那么我们先不给这条线赋值,等到最后统计完所有方案后,可以发现这样的线的个数是确定的(就等于 减去一端在 中的数对的数量),假设为 ,把答案乘以 就行了。
那么,也就是说,我们需要统计连线方案数,两个方案不同当且仅当某个位置在其中一个方案中是线的左端点,而在另一个方案中不是,或者某个左端点所在的线的标号不同(如果这条线的一端在 中存在)。
设 表示值 是否在 中存在。则从大到小 dp,设状态 表示考虑了 的值,其中有 个 的右端点还未配对,有 个 的右端点还未配对。
则有转移, 可以转移给:
-
:,表示匹配了一个更大的 的右端点。
-
:,表示自己变成一个右端点。
-
:,表示匹配了一个更大的 的右端点。
-
:,表示匹配了一个更大的 的右端点。注意这里需要乘系数 ,且每一个带来的这条线的标号都不同。(注意理解)
-
:,表示自己变成一个右端点。
时间复杂度 。代码:
#include <bits/stdc++.h>
using namespace std;
#define PII pair<int, int>
#define _for(i, a, b) for (int i = (a); i <= (b); i++)
#define _pfor(i, a, b) for (int i = (a); i >= (b); i--)
#define int long long
const int N = 605, mod = 1e9 + 7;
int n, a[N], vis[N], tot;
int b[N], m, dp[2][N][N];
void add(int &a, int b) {
a += b;
if (a > mod) a -= mod;
}
signed main() {
cin >> n;
_for(i, 1, 2 * n) cin >> a[i];
for (int i = 1; i <= 2 * n; i += 2) {
int cnt = (a[i] == -1) + (a[i + 1] == -1);
if (cnt == 1) {
if (a[i] != -1) vis[a[i]] = 1;
if (a[i + 1] != -1) vis[a[i + 1]] = 1;
}
if (cnt == 2) tot++;
if (cnt == 0) vis[a[i]] = vis[a[i + 1]] = 2;
}
_pfor(i, 2 * n, 1) if (vis[i] <= 1) b[++m] = i;
// cout << m << endl;
// _for(i, 1, m) cout << b[i] << ' '; puts("");
dp[0][0][0] = 1;
_for(i, 1, m) {
memset(dp[i & 1], 0, sizeof dp[i & 1]);
_for(j, 0, n) _for(k, 0, n) {
if (vis[b[i]] == 1) {
add(dp[i & 1][j][k], dp[i - 1 & 1][j + 1][k]);
if (k) add(dp[i & 1][j][k], dp[i - 1 & 1][j][k - 1]);
}
else {
if (j) add(dp[i & 1][j][k], dp[i - 1 & 1][j - 1][k]);
add(dp[i & 1][j][k], dp[i - 1 & 1][j + 1][k]);
add(dp[i & 1][j][k], dp[i - 1 & 1][j][k + 1] * (k + 1) % mod);
}
}
}
int res = dp[m & 1][0][0];
_for(i, 1, tot) res = res * i % mod;
cout << res << endl;
}
T6: 弹飞绵羊
考试的题目是这题的加强版,只不过我觉得那题十分智慧,数据也十分友情,干脆不补了。
用分块做更简单。定义 表示从 开始,跳出 这个块的步数, 表示从 开始,跳出这个块能跳到的下标。
先对这两个数组进行预处理:
_pfor(i, n, 1) {
if (i + a[i] > rx[bel[i]]) {
cnt[i] = 1;
p[i] = i + a[i];
}
else {
cnt[i] = cnt[i + a[i]] + 1; // 递推
p[i] = p[i + a[i]];
}
}
如果是单点修改,那么直接对这个数所在块,仿照上述方法重新更新一遍就行。
while (m--) {
int op, x, y;
cin >> op >> x;
x++;
if (op == 1) {
int res = 0, t = x;
while (t <= n) {
res += cnt[t];
t = p[t];
}
cout << res << endl;
}
else {
cin >> y;
a[x] = y;
_pfor(i, rx[bel[x]], lx[bel[x]]) {
if (i + a[i] > rx[bel[i]]) {
cnt[i] = 1;
p[i] = i + a[i];
}
else {
cnt[i] = cnt[i + a[i]] + 1;
p[i] = p[i + a[i]];
}
}
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】