VP 记录 与一些解题笔记
树论
P5588 小猪佩奇爬树
分类讨论很不错的一道思维题
- 对于一条父子关系链的情况,不妨设最深最浅结点分别为 u,v. 我第一发漏思考了 v 除了父亲子树的结点,其非这条链所在的子树结点也能选,这个时候比较难处理,在题解区看到一个很牛的处理方法:记录某一种颜色在搜完子树计数数量有没有变化,就知道这个子树有没有这个颜色的结点以及有多少个了,我先入为主的以为要开 1e6 个 1e6 大小的bitset来存储,这绝对也会爆炸啊,没想到直接计数看有没有变化就完了,以前没遇到过这个方法,记录一下。
点击查看代码
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e6 + 10;
int n;
int w[maxn];
int siz[maxn];
int anc[maxn][23];
int dth[maxn];
int f[maxn];
int cnt[maxn], cnt_sum[maxn];// 用于处理两点成链的情况 cnt_sum 存储一个点的含有相同颜色的子树的大小之和
vector<int> col[maxn];
vector<int> mp[maxn];
void dfs(int u, int fa){
siz[u]++;
dth[u] = dth[fa] + 1;
f[u] = fa;
cnt[w[u]]++;
int precnt = cnt[w[u]];
for(auto v : mp[u]){
if(fa == v) continue;
dfs(v, u);
siz[u] += siz[v];
anc[v][0] = u;
if(cnt[w[u]] > precnt){
precnt = cnt[w[u]];
cnt_sum[u] += siz[v];
}
}
}
void pre(){
for(int j = 1; j <= 20; j++)
for(int i = 1; i <= n; i++)
anc[i][j] = anc[anc[i][j-1]][j-1];
}
int LCA(int x, int y){
if(dth[x] < dth[y]) swap(x, y);
for(int i = 18; i >= 0; i--)
if(dth[anc[x][i]] >= dth[y]) x = anc[x][i];
if(x == y) return y;
for(int i = 18; i >= 0; i--)
if(anc[x][i] != anc[y][i])
x = anc[x][i], y = anc[y][i];
return anc[x][0];
}
int main(){
//freopen("1.in", "r", stdin);
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n;
for(int i = 1; i <= n; i++){
cin >> w[i];
col[w[i]].push_back(i);
}
for(int i = 1; i < n; i++){
int u, v;
cin >> u >> v;
mp[u].push_back(v);
mp[v].push_back(u);
}
dfs(1, 0);
pre();
for(int i = 1; i <= n; i++){
ll ans = 1;
if(!col[i].size()) cout << ans * (n - 1) * n / 2 << endl; // 没有结点,任选2个
else if(col[i].size() == 1){
/* 只有一个结点,答案为三者相加:
1. 在他父亲子树和儿子各选一个
2. 选他和其他一个结点
3. 他的子树之间选两个子树各选一个*/
ll tmp = 0, sum = siz[col[i][0]] - 1; // 这里处理3.
for(auto v : mp[col[i][0]]){
if(v == f[col[i][0]]) continue;
sum -= siz[v];
tmp += 1ll * siz[v] * sum;
}
cout << ans * (siz[col[i][0]] - 1) * (n - siz[col[i][0]]) + n - 1 + tmp << endl;
}
else if(col[i].size() == 2){
// 有两个的情况,先看是不是父子
int u = col[i][0], v = col[i][1];
if(dth[u] < dth[v]) swap(u, v);
int lca = LCA(u, v);
if(lca == v){ // 是父子,根据文末方法特殊处理
//cout << ans * siz[u] * (n - siz[v] + 1) << endl;
cout << ans * (n - cnt_sum[v]) * siz[u] << endl;
}
else {
cout << ans * siz[u] * siz[v] << endl;
}
}
else {
// 找深度最深的两个点,不是父子关系的时候就不会是一条链
int maxu = 0;
for(auto u : col[i])
if(dth[u] > dth[maxu]) maxu = u;
int maxv = 0;
for(auto v : col[i])
if(v != maxu && LCA(maxu, v) != v && dth[v] > dth[maxv]) maxv = v;
if(!maxv){ // 在同一条链上,根据文末方法特殊处理
int mind = maxn, minv = 0;
for(auto v : col[i]){
if(dth[v] < mind){
mind = dth[v];
minv = v;
}
}
cout << ans * siz[maxu] * (n - cnt_sum[maxv] + 1) << endl;
}
else {
int flag = 1;
int lcauv = LCA(maxu, maxv);
for(auto x : col[i]){
if(x == maxu || x == maxv) continue;
if(dth[x] < dth[lcauv]) flag = 0;
if(LCA(x, maxu) != x && LCA(x, maxv) != x) flag = 0;
if(!flag) break;
}
if(!flag) cout << 0 << endl;
else cout << ans * siz[maxu] * siz[maxv] << endl;
}
}
}
return 0;
}
/*
对于一条父子关系链的情况,不妨设最深最浅结点分别为 u,v. 我第一发漏思考了 v 除了父亲子树的结点,其非这条链所在的子树结点也能选。
这个时候比较难处理,在题解区看到一个很牛的处理方法:
记录某一种颜色在搜完子树计数数量有没有变化,就知道这个子树有没有这个颜色的结点以及有多少个了,
我先入为主的以为要开 1e6 个 1e6 大小的bitset来存储,这绝对也会爆炸啊,没想到直接计数看有没有变化就完了,以前没遇到过这个方法,记录一下。
*/
杂项
leetcode 3381 长度可被 K 整除的子数组的最大元素和

- 这里有一个非常巧妙的处理方法
此题先根据前缀和下标按mod k分组,同一组里随便选两个点,它们之间的子数组长度一定是 k 的倍数。
于是对于同组内,问题转化为 \(\max (pre[j] - pre[i]) \quad \text{其中 } j > i\)
这是一个经典套路:
在从左到右遍历j的时候,维护目前看到的最小的pre[i],然后试一试pre[j] - min_pre。
也就是“最大差值 = 当前值 - 之前最小值”。
CF
[Codeforces Round 1053 (Div. 2)](Codeforces Round 1053 (Div. 2))
- C
这个题,洛谷上 \(\texttt{Toorean创建时间:2025-09-26 11:26}\) 和 \(\texttt{Engulf创建时间:2025-09-26 18:58}\) 的题解讲的非常好。
我们对\(2n\)个数分配正负性,设\(s_i\)表示\([1,i]\)的负号比正号分配多的次数,满足\(k\geq\)\(s_i\geq0\),且\(s_2n=0\)。
对于\(k=n\),显然只要使右边\(n\)个为正,左边\(n\)个为负即可最大化,这也是全局答案的最大值。
对于\(k=n-1\),尝试调整一对正负号,使调整的影响最小。假设我们调换的是\(a_l\)与\(a_r\)的正负性 ( \(a_i\)保存的是值的绝对值),那么对答案的影响为\(2\cdot(a_l-a_r)\)。也就是说,我们需要最小化\(a_l-a_r\) 可以独立处理。由于\(a_i\)有序,故取\(l=n\),\(r=n+1\)即可。
同样的,我们不断地根据上式处理,即可达到每个\(k=i\)的最大值。由于\(s_i\leq i\),因此我们只能在左边连续的\(k\)个负号处做调整,也就是\(k=i+1\)时的\([l,r]\)左右端外,也就是在\(l-1,r+1\)处交换符号。 - D
赛场上拟合出要在每一列根据相应规则分配一个黑格子,且在不同行有填充个数约束
当时没做出来的核心原因在于没有逆着来写,从n到1考虑每一行的可能分配方式,乘法原理,每一行的种数显然是 \(\begin{pmatrix}c_i\\a_i\end{pmatrix}\),其中a给定,c就是每一行的分配
注意预处理模意义下阶乘以及其逆元
点击查看代码
void pre(){
fac[0] = 1;
for(int i = 1; i <= maxn - 5; i++){
fac[i] = fac[i - 1] * i % mod;
}
inv[maxn - 5] = pow(fac[maxn - 5], mod - 2, mod); // 快速幂,这里利用费马小定理
for(int i = maxn - 6; i >= 0; i--)
inv[i] = inv[i + 1] * (i + 1) % mod;
}
ll cxy(int x, int y){
if (y < 0 || y > x) return 0;
return fac[x] * inv[y] * inv[x - y] % mod;
}
Codeforces Round 1042 (Div. 3)
和同学喝完酒回来打的,头晕脑胀大失败
一小时才过了ABC
-
D 哼哧构造直径再统计距离直径深度大于1的叶子数量,死活算出样例四是五次,忽略了一个关键点是根不一定选直径两端,而应该选深度一叶子最多的点,赛时一小时一无所获,补题的十分钟AC了。
-
E 想到贪心分类讨论能异或原来的a[i+1],或者能异或异或过的a[i+1],显然从后往前贪,后面的构造不出b[i+1]前面的也不用管了。
于是构造了一个c数组保存前面异或完的值,但是WA了,后面看了题解1分钟AC了,因为C数组实际有效存储值就是b数组(服了猪脑子
启发性思考还是不够啊
Codeforces Round 1037 (Div. 3)
-
vptime:Aug/05/2025 22:02UTC+8
![image]()
-
D 题启发思考了一下直接贪 real 值,粗略证明感觉很对
补题
-
E 赛后看了看题解,发现构造虚拟数组原来只需要 p[i] 和 s[i] 的最小公倍数,然后再带入验证集就行了。
之前一直想的都是通过单一数组的前缀构造后一个新数再与另一个数组做比较验证,这是很错的。 -
F 想到了预处理某点按颜色周围边权和得到一个数组CNT,纠结半小时修改时得把子节点的含有父节点新旧颜色的CNT都修改,这很不可做。
但是看完题解才发觉这个预处理重复统计了一个点的父亲,实际只需要预处理一个点儿子点的按颜色边权和,并且这样子预处理还能完美规避掉要更新儿子CNT的问题,因为儿子的CNT只包含孙子,不包含其父亲。所以修改操作只需要修改该点父亲一个点的CNT即可
收获:加深理解边绑定到儿子的操作
Codeforces Round 991 (Div. 3)
- vp time:Apr/08/2025 19:35UTC+8
- 00:03 AC A
- 00:22 AC B 分类讨论,模拟发现奇位的值只能传递给奇位,偶位同理。然后看奇偶位的和能不能分别整除并得到相同值即可。
- 00:43 - 00:45 WA+AC C 首先有取模过程可以推断出一个数能被9整除,那么各位数字和也应该是9的倍数,平方操作只能2->4, 3->9,增加2/6,于是先扫一遍统计各位和和2,3的个数,然后由同余可知,堆各位和,2的个数,3的个数全对9取模,然后暴力枚举 O(10*10) 看有没有可能构造出9的倍数。
- 01:12 AC D 直接贪心。我怎么写了这么久,不应该啊。
- 01:29 AC E 很明显的DP,注意边界处理:
memset(f, 0x3f, sizeof(f));
string a, b, c;
cin >> a >> b >> c;
a = " " + a;
b = " " + b;
c = " " + c;
f[0][0] = 0;
for(int i=1; i<a.size(); i++)
f[i][0] = f[i-1][0] + (a[i] != c[i]) ;
for(int i=1; i< b.size(); i++)
f[0][i] = f[0][i-1] + (b[i] != c[i]);
for(int i=1; i< a.size(); i++)
for(int j=1; j< b.size(); j++)
f[i][j] = min({f[i][j], f[i-1][j] + (a[i] != c[i+j]), f[i][j-1] + (b[j] != c[i+j])});
cout << f[a.size()-1][b.size()-1] << endl;
CF1006 Div3
- vp time:Apr/08/2025 13:52UTC+8
\(0:16\) A题AC,宝宝巴士。
\(0:28\) B题AC,贪心,组合数学拆贡献
\(1:41\) C题AC,中模拟,贪心过了就行,修了n久没特判 N==1 情况,补上才过
D题多测, \(\sum n \le 4*10^6\) 很迷惑。
考虑到翻转操作对逆序对个数的贡献,容易知道,对于一个区间翻转只影响这个区间内的逆序对个数,假设翻转操作时把 \(a_l\) 移动到 \(a_r\) 后面,增量为区间内大于 \(a_l\) 的数的个数,减量为区间内小于 \(a_l\) 的数的个数;
于是想出一个 \(O(t*n^2)\) 解法,枚举左指针,遍历右指针维护当前区间大于小于 \(a_l\) 的数的个数,算一下最劣情况,感觉坠机。
但是其实就是正解,作者说渐进复杂度 \(O(n^2)\),??????
upd: 题目给的是 \(\sum n^2 \le 4*10^6\),服了。
好几次都因为题目读的不仔细坠机,以后写题要把关键数据都列一遍。
Codeforces Round 886 (Div. 4)
- vp time: Apr/08/2025 09:25UTC+8
- 00:32 AC ABC 三题宝宝巴士,不说了
- 00:48 AC D 先排序,然后正难则反,求一个相邻数差值绝对值不大于 k 的最长子序列len,然后 ans = n-len
- 01:06 AC E 直接拆贡献, \(c = \sum_{i=1}^{n} (s_i+2w)^2\),反过来推出w,于是得到一个二元一次方程,过程中会出现爆longlong的情况,那就把分母放进根号里,先除再乘,这里容易算出不会出现小数。
- 01:24 AC F 先分类讨论,一跳大于n的肯定不行,我们直接暴力枚举每一个青蛙能跳到哪,计数,扫一遍取最大值。算一下最劣情况,显然坠机,那我们优化一下,用map记录同一步长的青蛙有几只,最劣情况下要跑 \(\sum_{i=1}^{n} n/a_i\),算一下发现 \(\sum_{i=1}^{n} 1/a_i\)是log级的,在n取2E5时约12,可以过。
- 01:37 AC G 有了第一场排位赛的类似经验,对于每一个点,我们直接开四条直线存储一个点的贡献:一个点的八个方向在四条直线上,可以被x, y, x+y, x-y统计,于是开四个数组统计各直线上有多少点,然后组合数学 \(C_n^2\)
Codeforces Round 905 (Div. 3)
-
vp时间:Apr/07/2025 20:45UTC+8
-
00:07 AC A
-
00:38 - 00:53 WA*3+AC C 分类讨论,显然在 k = 2, 3, 5 的时候,先把所有数对 k 取模,而后取 \(min(k-a_i)\)。
对于k==4比较复杂,不仅取 \(min(k-a_i)\),还要考虑有因子 2 的情况,有两个2,不用操作;有一个2,只操作1次(已经有一个2,那么只用构造另一个2因子即可,其他数对2取模只可能是1,所以只操作1次);一个都没,那就看有没有3,有就只操作一次,没有就操作两次(因为对4取模结果在此时只可能是1,取两个1即可)。 -
01:29 - 01:45 WA*2+AC D 贪心想,如何判断是否存在不相交段:取r值最小左段和l值最小右段比较相交性即可。但是我写成取l值最小左段坠机了,还好样例强救回来了。
那么开两个堆,一个维护r最小段,一个维护l最大段,用map维护一个段是否还在集合内,查询的时候发现堆top不存在了就循环pop换下一个。 -
01:53 AC B 才注意到可以重排列串。对一个串如果可以排列为回文串,将其所有字符数量进行统计,最多只允许存在一个奇数个数字符种类,即奇数个数字符种类为0/1时可以重排列为回文串。
补题
- E 改了我45min,吐大血,脑子也不想转,搁那里肌肉惯性乱写,全部删掉重新开始写了3次,没过这种状态实在要不得。
吃了罐八宝粥,刷了一个小时视频,缓过来了,14min秒了。 - F 神经病阅读理解,实在是没读懂,扔AI会了。给定一个序列,要给这个序列的合法子数组计数,合法子数组定义为不存在下标集合与该子数组不同的任意子序列与之相同。
对于任意一个子数组,我们考虑其是否合法:我们先看左右端点值,给定右端点,如果左端点的左边存在一个与左值相同的值,那么该子数组不合法:存在一个下标集合不同的子序列与子数组值一样,右端点同理。之后的问题就很容易解决了。 - G1 贪心
- G2 发现对于m变大的情况,b的趋势是用更大的数来填补这个坑,贪心地想删除操作数是否存在单调/突跃/可二分性,于是拿G1代码暴力跑一下发现是这样的,结果因为r没有开到m+1维护m取什么值都和原来一样调了半小时
CF1003 Div4
-
vp时间:Apr/07/2025 14:35UTC+8
-
00:08 AC A
-
00:12 AC B
-
00:40 AC C1 发现对于 \(a_i\) 有且仅有两种情况,于是维护一个前一个数的最小单调值,进行递推DP,如果两种情况都无法维护,即不可能。
-
01:03 AC C2 贪心,将B排序。设前一个数保持前面序列单调不减得到的最小结果为 tmp,对于 \(a_i\),若存在一个 \(b_i\) 使得 \(a_i\) 能够改变为单调状态,将式子变形可知我们要找的就是:
int idx = lower_bound(b+1, b+1+m, tmp+a[i]) - b;
若不存在,则无法通过操作满足条件,若不操作也无法满足条件,显然坠机。
-
01:46 AC D 直接拆分贡献,发现对于一个子序列,他的贡献是 \(\sum_{i=1}^{n} (n-i) * a[i] + sum*len_{rightOthers}\),但是想复杂了,显然现在要贪心排个序,实际只用比较前半部分(因为所有序列长度相同),但是都写了花太多时间了。
-
02:05 WA E 谁长用谁构造长 k 的纯血序列,少的那个用来消耗长的,长的多的直接丢出来。结果写挂了。
赛后发现输出 01 与后面的 1 输出有影响,如果中间没有 0,会拉长纯 1 子序列的长度,实际次序应该改为先输出多余的,在输出 01 串,再输出纯血构造串,保证少了某一个部分也不会拉长构造串。以 n > m为例:
for(int i=1; i<=m-n+k; i++) printf("1");
for(int i=1; i<=n-k; i++) printf("01");
for(int i=1; i<=k; i++) printf("0");
补题:
-
F 思考约20min,列举状态发现满足严格多数序列条件只有两种:一种包含相邻同一个数值,一种在一条长度为3的序列中有两个相同数值。
不知道怎么清空一个vector< int > e[maxn];的树,后来发现只要清空含有新树所拥有的点的vector即可,这与n同阶。
于是写一个DFS,对每个点都用其值搜一次,只搜索三层,到第三层看是否满足两种条件, 期望复杂度O(3n)。
然后交了,然后 TLE 了,发现复杂度算错了,如果是菊花图会大坠机。
然后发现对一个点跑完其所有出边统计数值,如果有不等于原点的值有两个或者有一个点与原点值相同,那么该值可以被构造,复杂度与边数同阶。
交了,A了,总用时50余min,太慢。 -
G 以i是不是质数分类讨论拆贡献,维护的有一点复杂,约40+min交了一发TLE了,然后改了n久一直TLE,按照复杂度来算O(240N)不会TLE啊,然后看了眼卡的测试点,加了个重复数字特判,还是T,然后仔细思考发现判断一个数是不是半质数要再跑一次其另一个因子是不是质数,可能造成复杂度再加一个大常数,改用二分查找,各种奇奇怪怪写错好多次,约1h10minac了。
-
F 我鱼鱼,所以先不写了.


浙公网安备 33010602011771号