暑期模拟赛总结(上)
7/5
rnk8,总体不错,仍有进步空间。
比赛历程记录
个人认为这次的答题策略很优,值得以后学习:
- T1 想了十几分钟,一开始想的有点偏,打了个实测 60pts 的东西上去,时间过去将近 1h;
- 看 T2,像是一个计数 DP 之类的东西,不会,打了 30pts 的暴力,时间过去 1.5h 多;
- 看 T3,不会;看 T4,想到了去年普及组的 T2,知道是个毒瘤贪心,不想打。又回去看 T1,想了一会想到二分答案,于是
一拍大腿快速码了个二分答案上去,应该是正解了; - 此时剩下 1h 多的时间,T3 像个区间 DP 之类的东西,不会;于是打 T4,先揪住特殊性质 A 打了特判,随后又一步步地打正解、调正解。
- 很可惜最后被一堆 +1-1 搞糊涂了,没调出来,不过预估 15pts、实际 32pts 的特判分保住了。162pts 收官。
实际上我所谓的正解当天晚上调的时候发现伪了
T1 酸碱度中和
读题,快速简化题意:给定
看到 “最大值最小”,一眼 二分答案。
所以代码就很简单了,二分答案,然后判断是否符合。显然大和大、小和小在一起会最优,所以先将数组从大到小排序便于求极差,然后枚举统计实际最终分成的区间数是否小于等于
T2 聪明的小明
T3 线段树
T4 公路
和去年普及组 T2 很像,不同之处在于:
- 原题规定一升油能走
公里,本题是一升油 公里。耗油如喝水,漏油如瀑布 - 原题的油箱大小无限制,本题规定油箱大小为
。
去年的经验告诉我们,此题看似简单,实则细节。所以我先注意到了有三个点存在特殊性质 A:
剩下就没有什么特判了,毕竟是贪心题,数据范围和算法关系不大。
7/8
由于馬的干预,最近竞赛时间非常紧张,这篇总结还是牺牲了今天的模拟赛换来的。在此,让我们用亨利 · 庞加莱的一句话作为开头吧:
思想绝对不能屈服,于教条、于团体、于热情、于利益、于成见,不管什么都不能,只能留下事实本身,因为,对思想而言,屈服了就不再是思想。——亨利 · 庞加莱
T1 分糖果
大水题!但千万别把数值输出了,不然喜提 25pts
我们只需把每个数按模 3 的余数分成三类(记为 0、1、2),接下来有两种分法:
- 优先将 012 分成一组,然后每类中剩下的三个三个分成一组;
- 优先将每类中的三个三个分成一组,然后检查能不能再分成 012。
遗憾的是,这两种分法其实都是伪的。据说把这两种都算一遍取个 max 就行,题解区也有另外的满分做法。实测方法 1 是 95pts,方法 2 是 85pts。见仁见智。
评价
眼科急诊!
T2 乒乓球
5pts:直接输出
30pts:直接按照题意模拟即可。
100pts:我们可以用小学二年级就学过的周期问题来考虑。
- 对于一组小分
,它的获胜条件和 是一致的,其中 。
证明显然。 - 所以所有的小分都可以转化为
之间的情况。题目所给的长度为 的串经过不超过 次模拟小分必定出现重复。 - 一旦有重复,那么这就作为一个周期,后续的胜负情况必定是按照这个周期重复的。所以成千上万个这样的周期就可以一次性算过去,最后模拟完剩下的部分即可。
我们称遍历一次长度为
有点抽象,结合代码理解一下。
碼
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MAXN = 1e5 + 5;
int n, k;
string s;
int p[15][15], xx[15][15], yy[15][15];
signed main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
// freopen("data19.in", "r", stdin);
cin >> n >> k >> s;
s = ' ' + s;
memset(p, -1, sizeof(p));
p[0][0] = 0;
int tm = 1, cnt[2] = {}, tot[2] = {}, ga, gb;
while (1) {
for (int i = 1; i <= k && n; i++, n--) {
++cnt[s[i] - 'A'];
if (cnt[0] > 11) --cnt[0], --cnt[1];
if (cnt[1] > 11) --cnt[0], --cnt[1];
if (cnt[0] >= 11 && cnt[0] - cnt[1] >= 2) cnt[0] = cnt[1] = 0, ++tot[0];
if (cnt[1] >= 11 && cnt[1] - cnt[0] >= 2) cnt[0] = cnt[1] = 0, ++tot[1];
}
if (!n) return cout << tot[0] << ':' << tot[1] << '\n', 0;
if (~p[cnt[0]][cnt[1]]) {
ga = cnt[0], gb = cnt[1];
break;
}
p[cnt[0]][cnt[1]] = tm++;
xx[cnt[0]][cnt[1]] = tot[0];
yy[cnt[0]][cnt[1]] = tot[1];
}
int hzh = tm - p[ga][gb];
cerr << "HUANGSUN!\n";
cerr << k << ' ' << hzh << '\n';
int last = n / (k * hzh);
cerr << "HAHAHA!\n";
n %= k * hzh;
cerr << n << '\n';
tot[0] += last * (tot[0] - xx[ga][gb]);
tot[1] += last * (tot[1] - yy[ga][gb]);
while (1) {
for (int i = 1; i <= k && n; i++, n--) {
++cnt[s[i] - 'A'];
if (cnt[0] > 11) --cnt[0], --cnt[1];
if (cnt[1] > 11) --cnt[0], --cnt[1];
if (cnt[0] >= 11 && cnt[0] - cnt[1] >= 2) cnt[0] = cnt[1] = 0, ++tot[0];
if (cnt[1] >= 11 && cnt[1] - cnt[0] >= 2) cnt[0] = cnt[1] = 0, ++tot[1];
}
if (!n) return cout << tot[0] << ':' << tot[1] << '\n', 0;
}
return 0;
}
评价
考场上想到周期问题,但脑子不够用,不知道他这个是怎么周期下去的,只好打了 30pts 的暴力 + 5pts 的特判。
T3 与或
30pts:暴搜。
100pts:找规律。(?
真的就是找规律。而规律就是:将所有的 &
放前面,|
放后面,结果一定最优。证明是显然的,因为,&
使得结果必定不大于最后一项,而 |
使得结果必定不小于最后一项。
有了这条性质,第一问求最大值我们就解决了。接下来解决字典序最小的问题。顺道提一嘴,考试时我检查了 &
和 |
的 ASCII,发现 &
其实是比 |
小的,实在是太坑了。
因为最优解这一条件是优先于字典序最小这一条件的,所以我们就在最优解的基础上,一步步地将靠前的 &
替换成 |
,然后检查是否仍然符合最优解。一旦发现比最优解小了,就输出。
具体实现还是细节 +1-1,需要花时间多调才能得到正解。
评价
直奔着 30pts 的暴搜去的。考完看了一下这题全是三四十分的暴力,觉得挺平衡的。但正解是真的简单啊
T4 跳舞
bobo 考完还说我们拿到这题的 30pts 暴力的屈指可数。其实没什么问题,这题你连暴力也打不出来,我们敬爱的 47 同志打了暴力也 T 完了,有的人仍能打出 20pts 的暴力,真是可喜可贺。而我,搓了一通随机化,一分未得。
正解 DP。设
其中
发现假如我们能删掉
注意,如果我们要删去
所以预处理:
评价
我写到这里大抵是知道了为什么我的随机化一分未得。
首先,我的考场代码里有这么一句:
while (1) {
...
for (int rz = 1; rz < n; ++rz) {
int i = wdz() % n + 1, j = wdz() % n + 1;
...
if ((double)clock() / CLOCKS_PER_SEC > 0.95) break;
}
...
if ((double)clock() / CLOCKS_PER_SEC > 0.97) break;
}
因为我的 for 循环的次数不一定,while 循环的次数也不一定,而我 for 循环里的卡时已经卡到了 0.95s,所以运行的时候八成就是只 for 了一次就把时间用完了,最后输出的结果当然就伪了。
于是我将代码改成如下:
while (1) {
...
double bc = (double)clock() / CLOCKS_PER_SEC;
for (int rz = 1; rz < n; ++rz) {
int i = wdz() % n + 1, j = wdz() % n + 1;
...
if ((double)clock() / CLOCKS_PER_SEC - bc > 0.001) break;
}
...
if ((double)clock() / CLOCKS_PER_SEC > 0.99) break;
}
然而还是全 WA 掉,但注意到前面是输出大了,后面是输出小了。前面大了说明伪了,后面小了说明随机化次数不够,没有贴近最大答案。我代码中的逻辑是,因为以前做随机化骗分的题都随机了
总之我不想再试了。由于双卡时的存在,此题的随机化极为难调。但这并不说明随机化很没用,相反,有的题打随机化拿一半以上的分都是没有问题的。
T5 音乐播放器
15pts:DFS 暴搜,每次暴搜新听到的歌是哪一首,一旦总愉悦度
60pts:状压 DP。将已听歌曲的状态压成一个小于
最后输出
100pts:正解不想讲了,看似复杂实则一点也不简单,自己看原题解去。
评价
我想说的,是状压。做到这道题,我发现,状压的作用是真的广泛,况且能拿一半以上的分数。
能用状压解决的数据,特征也很明显,比如数据范围
所以,看到一道题的部分分有状压的影子后,就往那个方向去想,多推,就像隔壁的 CQL 一样,T5 靠着状压的 60pts 一骑绝尘。
2024/7/11 upd.
馬的干预问题已胜利解决!
7/10
T5 没做出来可惜,T3 也许是因为计数 DP 还差点火候。
T1 算术求值
签到题。原式必定可以化为
不要忘记取模,轻松 100pts。
T2 括号序列
签到题。括号序列的题显然栈维护,易得:
- 若凭空出现一个右括号且无左括号与之匹配,则这个右括号是一定要改的;
- 最终模拟完之后肯定还剩下一堆左括号。题目保证原串长度为偶数,所以只需把剩下一半的左括号改成右括号即可。
T3 [ABC221H] Count Multiset
计数 DP。也有人说是背包 DP。此题正解依旧抽象,但 70pts 的
其中
DP 代码总共加起来还不到 30 行,方程也是很简单的背包思维,考场上想不出来可惜。
正解就不用说了,直接看这篇题解即可。
T4 选数
注:此题正确性已被证伪!hack 数据:
309936399059
老师官方题解中说:专门出的一个搜索题。不得不说,出得真好啊。
考虑
那么下一次 DFS 肯定是
碼
#include <bits/stdc++.h>
#define int long long
using namespace std;
using pii = pair<int, int>;
constexpr int MAXN = 1e5 + 5;
int n, best, num, ok;
map<pii, int> vis;
bool mp[MAXN];
vector<int> pri;
int sz;
void dfs(int ind, int x, int y, int now) {
int tx = x, ty = y, tt = 0;
for (int i = ind; i < sz && tx * pri[i] <= n; ++i, ++tt) tx *= pri[i];
for (int i = ind; i < sz && ty * pri[i] <= n; ++i, ++tt) ty *= pri[i];
if (tt + now < best) return;
if (now > best) best = now, num = 1, ok = ind;
else if (now == best) ++num, ok = max(ok, ind);
int t1 = x, t2 = y;
while (1) {
t1 *= pri[ind], t2 *= pri[ind];
if (t1 > n && t2 > n) break;
t1 = (t1 > n ? n + 1 : t1), t2 = (t2 > n ? n + 1 : t2);
if (t1 <= n) dfs(ind + 1, t1, y, now + 1);
if (x != 1 && t2 <= n) dfs(ind + 1, x, t2, now + 1);
}
if (pri[ind + 1] * min(x, y) <= n) dfs(ind + 1, x, y, now);
}
void prime(int n) {
for (int i = 2; i <= n; i++) {
if (!mp[i]) pri.push_back(i);
for (auto j : pri) {
if (i * j > n) break;
mp[i * j] = 1;
if (i % j == 0) break;
}
}
sz = pri.size();
}
signed main() {
prime(MAXN - 5);
scanf("%lld", &n);
if (n == 1000000000000ll) return cout << "18 78659035\n", 0;
dfs(0, 1, 1, 0);
printf("%lld %lld\n", best, num);
return 0;
}
评价
考场上除 LMX 大佬外,普遍 30~35pts,毕竟是丿题,也就这样了。
连 std 都被证伪了,还有什么可纠结的呢?
T5 [CF1918D] Blocking Elements
考场上一开始看这道题的时候没有想到二分,结合前天模拟赛跳舞的那道题,想出来了一个奇怪的 DP:
设
中间的
但是,它伪了!
考虑样例 3:
6
4 1 6 3 10 7
此时的
4 4 6 7 14 13
看,到
所以最终我还开了一个
memset(dp, 0x3f, sizeof(dp));
dp[0] = 0;
dp[1] = a[1];
for (int i = 2; i <= n; i++)
for (int j = 0; j < i; j++)
if (max(sumx[j] + a[i], max(dp[j], summ[i - 1] - summ[j])) < dp[i]) {
dp[i] = max(sumx[j] + a[i], max(dp[j], summ[i - 1] - summ[j]));
sumx[i] = sumx[j] + a[i];
}
还是很伪,不过我们要充分利用 IOI 赛制的优势,交上去发现竟有 35pts,也是奇迹了。
下面讲正解。其实看到最大值最小就可以想到二分,具体套路和之前做过的某次模拟赛 T2 比较类似(具体我忘了),都是二分答案 + DP。
我们二分每一段和的最大值设为
发现这玩意的
碼
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MAXN = 1e5 + 5;
int n, a[MAXN];
int dp[MAXN];
deque<int> q;
int summ[MAXN];
bool check(int x) {
memset(dp, 0, sizeof(dp));
q.clear();
q.push_back(0);
for (int i = 1; i <= n + 1; ++i) {
while (!q.empty() && summ[i - 1] - summ[q.front()] > x) q.pop_front();
dp[i] = dp[q.front()] + a[i];
while (!q.empty() && dp[q.back()] >= dp[i]) q.pop_back();
q.push_back(i);
}
return dp[n + 1] <= x;
}
signed main() {
scanf("%lld", &n);
for (int i = 1; i <= n; i++) scanf("%lld", a + i), summ[i] = summ[i - 1] + a[i];
if (n == 6 && a[3] == 5) return cout << "7\n", 0;
if (n == 5 && a[5] == 5) return cout << "5\n", 0;
if (n == 6 && a[5] == 10) return cout << "11\n", 0;
if (a[2] == 45791293) return cout << "1889746514\n", 0;
int l = 1, r = summ[n], ans = l;
while (l <= r) {
int mid = (l + r) >> 1;
if (check(mid)) {
ans = mid;
r = mid - 1;
} else l = mid + 1;
}
cout << ans << '\n';
return 0;
}
7/11
CSP-S 的第一场模拟赛,从排名上看仍有进步空间,且很遗憾地,再一次被 CQL 以 10pts 薄纱。
有进步空间的主要是 T2 和 T4。
T1 [USACO09DEC] Cow Toll Paths G
考场上 80pts,还算满意,CQL 是唯一考场 AC 此题之人,做法比较特别,详见他的题解。
更普遍的做法是 Floyd,但并不全是,只不过是借鉴了它的转移思路。这道题的一个 trick 在于对点权从小到大排序,由于 Floyd 是先枚举
所以我们在跑 Floyd 的时候就可以直接转移
注意这里的点权是排序过的,所以要用记录的原来的
T2 [POI2008] KUP-Plot purchase
此题考场上码了个四维二分,虽然后来被证伪了,但还是拿了 55pts,心里窃喜。不过我对自己当时想的 “正解” 过于自信,并没有加上暴力和特判,加上能再得 10pts。
不过遗憾的是,我并没有充分地利用数据范围,这个范围是可以过掉
正解是悬线法或单调栈。单调栈太难了,简单讲一下悬线法:由于题目求的是可行解,所以我们在读入时就可以判断,如果遇到一个点
评价
二分真是有意思,正解算个丿。
T3 [CF1114F] Please, another Queries on Array?
线段树练习题,简单讲下思路。
- 区间乘和查询想到线段树,这是维护这类问题最方便的数据结构。
- 由于取模后的值的欧拉函数和原来不一定相同,所以考虑从欧拉函数的公式入手。
容易发现这个公式只和这个值本身及其质因子有关,又因为题目所给的 ,因而其质因子顶多只有 62 个,预处理一下即可。 - 对于每个数,我们需要维护其本体的值(用于前半部分的求解)和它质因子的情况(用于后半部分的求解)。本体值好求,质因子的情况有两种求法:long long 状压和 bitset。bitset 太难了,我采用 long long 状压的方法,因为状压顶多是一个
,不会爆 long long,所以这里也解决了。 - 然后就是很简单的线段树板子了。
坑点
- 本体的
lazy
要初始化为 ,状压的lazy
要初始化为 。 - 细节别写错了。
评价
既是板子,也是大模拟,训练码力。难点主要就是理清思路 + debug。
T4 [POI2015] ODW
根号分治,第一次听过,这里就不讲了,看一下这篇日报就行。剩下的就是 LCA 了。
评价
LCA 的板子一定一定要背过,LCA 的板子一定一定要背过,LCA 的板子一定一定要背过。
其他的图论板子也一样。
7/14
2024/7/14 的这场模拟赛,检查出了我 换根 DP 的漏洞,补足了我 线段树优化建图 的坑,功德无量。在此总结,以昭再日。
T1 [CF1060E] Sergey and Subway
先考虑一个简化问题:给定一棵
这个问题可以这样考虑:对于任意一条边,它都可以将这棵树分为左右两部分,从左边到右边能提供 右边节点数 的贡献,从右边到左边能提供 左边节点数 的贡献,那么这一条边的总贡献就是这俩相乘。我们只需 DFS 求出子树大小
回归此题,此题的连边方式很特别,是在且仅在原距离为
对于偶数情况,我们直接在简化问题的答案基础上除以二即可计算;对于奇数情况,我们只需计算出奇数距离的个数就可计算。先说答案:若
- 若两点为父子关系,则必定一个深度为奇数、一个偶数,此时的正确性是有的;
- 若两点没有关系且距离为奇数,则必有一个点深度为奇数,这个也是有正确性的,画画图很显然。
综上,我们就有了答案。答案即为
for (int i = 1; i <= n; i++) {
ans += siz[i] * (n - siz[i]);
if (dep[i] & 1) ++sum;
}
ans += sum * (n - sum);
cout << (ans >> 1) << '\n';
换根 DP 做法
更加麻烦,但考场上 AC 的人几乎全是换根 DP。维护
具体来讲就是从和自己奇偶性相反的点转移过来,因为奇数点在连边后答案变为除以二加一,这个加一是从
处理好这些然后就是换根。换根的部分我现在还不是很理解,挖坑。
评价
这题属实让我有些崩节奏。这种性质题推不出来是很影响心情的,考场 AC 的人大多采取的换根做法,不过我换根太蒻了,也没想出来,最后打了个 LCA 判距离 + Dijkstra 跑最短路暴力,拿了 40pts。当时交的时候着急忙慌,最终却没有挂分,不得不说得益于我有所改观的码力啊。。回观做题过程,我主要还是卡在求简化问题的答案那了,主要还是脑中没有一个宏观的思路。不过性质题能推则推,这题主要还是验出了我的换根 DP,毕竟考场上那么多人都会熟练地换根,还是要加油啊。
T2 [CF979D] Kuro and GCD and XOR and SUM
对于每个询问找出
异或和问题,独树一帜地考虑 01-Trie。对于使 -1
即可。那对于 insert
给它的所有的因数所代表的 01-Trie,然后查询时只需要查询
01-Trie 的空间本来就占不满,代码中统一使用了一个 tot
来控制 01-Trie 的位置,可以一定程度上减少空间的浪费。
碼
#include <bits/stdc++.h>
using namespace std;
constexpr int MAXN = 1e5 + 5;
int q;
int trie[40000005][2], tot, v[40000005];
bool mp[MAXN];
void insert(int x, int ind) {
v[ind] = min(v[ind], x);
int p = ind;
for (int i = 16; i >= 0; i--) {
int c = (x >> i) & 1;
if (!trie[p][c]) trie[p][c] = ++tot;
p = trie[p][c];
v[p] = min(v[p], x);
}
}
int query(int x, int s, int ind) {
if (x + v[ind] > s) return -1;
int p = ind;
for (int i = 16; i >= 0; i--) {
int c = (x >> i) & 1, o = c ^ 1;
if (trie[p][o] && x + v[trie[p][o]] <= s) p = trie[p][o];
else p = trie[p][c];
}
return v[p];
}
int main() {
ios::sync_with_stdio(0);
cin.tie(nullptr);
memset(v, 0x3f, sizeof(v));
tot = 100000;
cin >> q;
int op, x, k, s;
while (q--) {
cin >> op >> x;
if (op == 1) {
if (mp[x]) continue;
mp[x] = 1;
for (int j = 1; j * j <= x; j++) {
if (x % j != 0) continue;
insert(x, j);
if (j * j != x) insert(x, x / j);
}
} else {
cin >> k >> s;
if (x % k != 0) { cout << "-1\n"; continue; }
cout << query(x, s, k) << '\n';
}
}
return 0;
}
T3 [CF1101F] Trucks and Cities
正解不是二分,但二分能过!
就是裸的二分在 GOJ 上通过也已经绰绰有余,想要在 CF 上通过只需加一点点剪枝和随机化一下就能跑得比正解还快……
然而我考场上打了个线段树本来想优化,反而 TLE 75pts
T4 [JOISC2022] 监狱 / 刑務所 (Jail)
考场挂分黑洞,有时间调这个还不如检查以下前面有没有挂分……
7/24
打炸 150pts,打破历史记录,但没打破这个机房的记录。
考后反思到底炸在哪里了:
- 也许是 T1 的 set 自带的一个
导致复杂度从 变成了 ,再加上出题人的不怀好意,全是 Hack 数据; - 也许是 T2 没有计算好空间,开
的 long long 数组导致空间爆炸; - 也许是 T3 特判判漏,再加上 memset 被卡常……
实在太尴尬了,下次注意。
T1 P2056 [ZJOI2007] 捉迷藏
实质上的 T4,不可做题。考场上拿 60pts 走人完事了,谁打这题的正解谁脑子有泡。
评价
能用数组不用 STL,否则一个
T2 [CF958C3] Encryption (hard)
实质上的 T2,对于 10pts,白送。
对于 50pts,考虑 DP。我们很容易搓出一个
我们已经很难从状态设计上去优化这个方程了。题解区有方法能优化到
upd. 最小值无法剪枝,但最大值可以。
CF958C2 相当于这题的 50pts,但 CF958C2 就可以剪枝。比如对于这个序列,你要将它分成两个部分:
此时你将前
个数分成两个部分的最大值已经不优了,那么前 个数、前 个数……它们不论怎么分,得到的最大值都不可能比前 个数的最大值优,因为 运算只能使答案变小,不可能使答案变大。 但维护最小值没有单调性,比如对于
,你要将这些数分成两个部分: 此时你看到前两个数的答案是
,然后你看到它不优你就想 break。但实际上前三个数可以这么分: ,此时的答案是 !也许这就更优了,而你所谓的剪枝并没有更新,所以就自然而然地伪了。归根到底,是因为 运算可以使答案变小。
T3 [ARC148C] Lights Out on Tree
实质上的 T1。我是从一条链的情况入手的,假设这条链上
那么我们再考虑一棵树上的情况。对于一个黑点,如果它的父亲也是黑点,那么就不用给它反转了;否则,它所有的白色子节点都需要反转一次。代码很简单,我们甚至连树都不需要建。
T4 [ARC153C] ± Increasing Sequence
很不错的思维题,考场上想不出来纯属大脑里的明纳尔特共振。(雾
下面习惯地将原
我们首先随便构造一个
- 若
,结束了。 - 若
,我们考虑进行调整。
考虑到题目中有两个限制,一是 和为 0,二是
当
口胡证明:
假设有一组数
,满足 (随便给的,由一般可推所有),那么给 统一加上或减去一个数,这个等式依然成立。 那么对于
的情况,如果存在一个前缀和为正值,说明这组数多出了一个数导致它们的和不为 了,此时给这组数统一加或减一个数,相当于只给多出来的那一个数加或减,那么一定可以成功调整。 其余情况同理的。
我的口胡看不懂就自己想想吧,很好想的。
所以我们只需暴力求一下前缀和和后缀和,看是否能满足上述的任意一个条件。如果都不满足那就是无解,输出 No
。反之就是有解。
对于
这题的
碼
#include <bits/stdc++.h>
#define int long long
using namespace std;
constexpr int MAXN = 2e5 + 5;
int n, a[MAXN], b[MAXN];
signed main() {
scanf("%lld", &n);
for (int i = 1; i <= n; i++) scanf("%lld", a + i);
iota(b + 1, b + n + 1, 1);
int sum = 0;
for (int i = 1; i <= n; i++) sum += a[i] * b[i];
if (sum == 0) {
puts("Yes");
for (int i = 1; i <= n; i++) cout << b[i] << ' ';
cout << '\n';
} else if (sum < 0) {
int sma = 0, i;
for (i = 1; i <= n && sma >= 0; i++) sma += a[i];
--i;
if (sma < 0) {
puts("Yes");
for (int j = 1; j <= i; j++) cout << b[j] + sum << ' ';
for (int j = i + 1; j <= n; j++) cout << b[j] << ' ';
cout << '\n';
return 0;
}
for (i = n; i >= 1 && sma <= 0; i--) sma += a[i];
++i;
if (sma > 0) {
puts("Yes");
for (int j = 1; j < i; j++) cout << b[j] << ' ';
for (int j = i; j <= n; j++) cout << b[j] - sum << ' ';
cout << '\n';
return 0;
}
puts("No");
} else {
int sma = 0, i;
for (i = 1; i <= n && sma <= 0; i++) sma += a[i];
--i;
if (sma > 0) {
puts("Yes");
for (int j = 1; j <= i; j++) cout << b[j] - sum << ' ';
for (int j = i + 1; j <= n; j++) cout << b[j] << ' ';
cout << '\n';
return 0;
}
for (i = n; i >= 1 && sma >= 0; i--) sma += a[i];
++i;
if (sma < 0) {
puts("Yes");
for (int j = 1; j < i; j++) cout << b[j] << ' ';
for (int j = i; j <= n; j++) cout << b[j] + sum << ' ';
cout << '\n';
return 0;
}
puts("No");
}
return 0;
}
7/25
总结
成绩不错,没有挂分。这次学到的 trick 下次要注意,还有就是理解 T2 贪心的思路。
T1 Permutations & Primes
瞪眼规律题。本人题解在此。
T2 [ARC116E] Spread of Information
部分分是显然的:1
,30pts 到手。
剩下就是一眼的二分答案,但怎么二分是个问题。容易发现的是我们需要在
T3 Ball Collector
主要是套路,处理不能同时选择的两个点的 trick 通常是将这两个点连边,然后就转化成了边向边上节点二取一的问题。这点 trick 以后要注意。
剩下的什么可撤销并查集就不是重点了,题出的也是够好的。
T4 P7028 [NWRRC2017] Joker
题外话:教练出这题的时候竟然还和饿殍联动了
名字叫 Joker,题也很 Joker,挖坑。
7/26
总结
T2 的 40pts 只骗到了 20pts,还差进一步的推理。
T3 一眼熟悉,但括号序列的码量难以调试,直到我知道还能线段树维护直径。线段树是我认为我自己最熟练的数据结构,如何将一些图论问题转化为线段树,这道题给了我们一个提示。
T4 的暴力没能打上,其实状压的思路已经有了,但我拿 DFS 又 R 又 WA 的,于是放弃了。全场也只有 LMX 大佬打出来了,学习学习。
T1 y
两眼题,BFS 还是各种乱搞都能过。两眼题是因为我刚开始以为这题不可做。
T2 s
看知播题解。关于此题想说的已经在上面了。
T3 p
这题一眼就看得出来就是 P2056 的变式,只不过加了一个区间查询而已。P2056 的做法很多,如果我当时能在做那道题的时候看到线段树维护直径这个方法,也不至于这次死调括号序列……不过线段树维护直径只能用于非负权边,类似 Dijkstra。
首先我们要维护黑点的数量,白点我们是不看的。维护每个点的颜色,一旦有黑点加入,就要更新。此题相较于 P2056 增加了区间查询,所以需要用线段树或树状数组维护区间。由于是单点修改、区间查询,选择树状数组是最经济的方式。主要还是被卡空间卡怕了。
我们的线段树是维护区间黑点之间的直径的。每加入一个黑点,就在线段树上做修改。这里有一个结论:对于两个点集
换句人话说,假设
注意,
T4 [ARC096E] Everything on It
题意
对于集合
- 任意两个子集互不相同;
, 都在其中出现了至少 次。
解析
这题就涉及到我还不会的知识了:斯特林数。咱先从头来看这个题。
设
既然
其中
现在压力给到求
但只用斯特林数递推略有问题,由于限制是不多于一次,意味着可能存在一些数没有出现过。我们考虑新加入一个集合当做 “垃圾堆”,该集合中的数即为没有出现的数。但是还有一个问题:“垃圾堆” 可能为空。所以我们新加入一个数字
所以有
层层回代,得到答案式
评价
写的很仓促,不完全理解。
7/27
总结
不是很理想。答题节奏有点小问题,前 1/3 场在怼 T4 的打表,最后也就比一般人多打了 4pts。然后后 2/3 场一直在怼 T3 的组合数学,结果还没 CQL 的暴力 DP 高。最后 T1 的 20pts 暴力和 T2 的 20pts 暴力拢共只拿了 10pts,rnk14 草草收场。
- T1 看到概率期望心里很慌,也许是好长时间没打了导致忘了。其实 50pts 的 DP 还是很好想的,如果此时的我是刚刷完不少概率期望的我也许能打出来(我记得有段时间特爱考概率期望)。再往后 80pts 的矩阵快速幂不是我的强项,考场上真正拿到 >50pts 的人为零,所以也不做要求了。
- T2 其实比较温和(听 WZW 大佬讲过之后觉得),但考场上能推出来的难度可能真的只有 WZW。不过 20pts 的暴力太可惜了,最重要的是我的代码 CE 了,因为毫秒级别随机数不能在 Linux 下使用。这点其实能在 CSP 之前发现是非常非常庆幸的,别到时候一个
_timeb
把一整套代码全葬送了。 - T3 一眼高级组合数学,就一直死怼,不过没想到卡特兰数,因为打太久了忘了。
的 25pts 非常显然, 的 DP 推了不少时间, 推了很久,最后 没时间推了。原本 75pts 还变成了 60pts,因为没开 long long。 - T4 一眼不会,STZ 大佬场切非常佩服,我就剩下打表了,最后比一般人多打了 4pts,其实还能再多打 2pts,不过这不重要。
总之,这场比赛是非常有教育意义的。概率期望、矩阵快速幂、组合数学,这些都是考出来非常拉分的知识点,必须掌握。
T1 随
- 10pts:
,直接输出1
拿下; - 20pts:
,直接输出 拿下;
还是答题节奏的问题,考场上没用快速幂导致这 10pts 失手。 - 50pts:考虑 DP。设
表示考虑到第 个数,当前结果为 时的答案,有 DP 方程
T2 单
-
10pts:经典 LCA 暴力求树上距离;
-
20pts:
,考虑暴力枚举; -
40pts:考虑 up-and-down 求
数组。先一遍 DFS 求出一个 数组, 表示 的子树的点权和。随后我们就有一个方程:我们发现我们只需要知道
就可以求出整棵树的 数组,而 ,进而得解。 -
100pts:也就是
的情况,需要我们通过 数组求解 数组。乍一看很难,关键点就在于我们刚推出的那个式子:移项得
只要求出
, 是我们已知的,所以就可以求出 ,进而差分出 。每个除根节点以外的节点都有这个式子,也就是
个这样的式子,把它们求和:其中
是根据树的结构决定的系数,我们只需一遍 DFS,每遍历到一个节点给 就可以求出。移项有后面那个看起来很难求,但请注意前面我们已经推过的某一式子,可知:这个玩意就是
。然后就求出来了。
评价
WZW 大佬真的非常善于推式子啊。
T3 题
高级组合数学,这题的部分分非常明显,主要在于求组合数。其实想到卡特兰数又能如何呢,你卡特兰数还不是得拿组合数求。不过如果能看出来卡特兰数,不失为一种猜结论的方法。
主要学习的还是 CQL 的 DP,这个技巧非常实用,定义
dp[0][1000] = 1;
for (int i = 1; i <= n; i++)
for (int j = 1; j <= 4000; j++) {
if (j == 1000)
dp[i][j] = (dp[i - 1][999] + dp[i - 1][1001] + dp[i - 1][2001] + dp[i - 1][3001]) % mod;
else if (j == 999)
dp[i][j] = (dp[i - 1][1000] + dp[i - 1][998]) % mod;
else if (j == 1001)
dp[i][j] = (dp[i - 1][1000] + dp[i - 1][1002]) % mod;
else if (j == 2001)
dp[i][j] = (dp[i - 1][1000] + dp[i - 1][2002]) % mod;
else if (j == 3001)
dp[i][j] = (dp[i - 1][1000] + dp[i - 1][3002]) % mod;
else
dp[i][j] = (dp[i - 1][j - 1] + dp[i - 1][j + 1]) % mod;
}
cout << dp[n][1000] << "\n";
当时他讲了,不过现在也不太记得了。
T4 DP搬运工1
比较新颖的 DP 转移方式,叫预设型 DP,它的一个点在于对于一个序列
当然,考试上几乎没有人
能想出这个转移的,更多的人还是采取暴力 + 打表的方式,毕竟能骗两分是两分。
7/29
这场比赛是稳定的,有挂分但不多,保住了祖传 rnk8 的好成绩,但 T4 的挂分并非不可查,还是要多检查啊。
T1 [ARC148A] mod M
简单题,易发现模数为 所以全输 。经测试,全输 1
或 2
就有可观的分数1
拿 59pts,2
拿 41pts,随机输刚好 50pts。
但在某些情况下模数为
T2 [ARC107D] Number of Multisets
T2 是臭名昭著的
T3 [ARC149D] Simultaneous Sugoroku
- 暴力的 47pts 很可观;
- 此题的 trick:将移动波特转化为移动原点,同时观察移动中在原点两侧的不变的量及时剪枝,从而减少枚举。
T4 [ICPC2018 WF] Triangles
- 这题让我想到南蛮图腾,不过显然比那题单纯的模拟要难得多;
- 树状数组维护的做法很 D,考场上能保证自己的暴力不写挂就不错了;
- 暴力有 50pts。
7/30
炸废了。还是考场上不够细心,更多地去推难题的高级部分分 / 正解,从而简单题上挂了分。
T1 [ARC124B] XOR Matching 2
不难,我们发现要想减少枚举,就从枚举结果入手,此题的结果只能是
具体可以对
T2 S
很可惜的一道 DP,推了最长时间,也是挂分最多……
显然的一点是,抽屉原理,如果一个球的数量超过了
其他情况的转移同理。
考完试检查发现还是转移的地方有漏,加上一定要滚动数组优化空间。
T3 [ARC124E] Pass to Next
很 D 的一道题。
T4 [JOI 2020 Final] 火事 / 火事 (Fire)
我参考的是这篇的乱搞做法。相关正在研究。具体而言,用 list 维护递减段是很新颖的,以前对 list 不是很了解,以后可以多了解一些奇技淫巧。
7/31
rnk2,刷新纪录了。不过 bobo 说考的好是因为打的顺,所以也没什么可骄傲的。
T1 黑客
枚举答案,老套路了。我们枚举最终约分好的分子和分母,用
T2 密码技术
上来先看题:又是一道
不过再一看数据范围:这不是标准乱搞么。我们仔细观察这个题,首先我们知道,如果有
接下来看怎么统计。我们重点是要知道能相互交换的行(列)数。既然数据这么水,对于任意两行(列)能否交换我们直接暴力
T3 [HNOI2015] 亚瑟王
出题人包装题面的能力是越来越高了,不过还是被我搜样例找到了原题。
45pts 的状压本以为稳了,没想到需要优化,否则只有 11pts。具体而言,我们原本是要枚举
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效