8.5~8.16 做题笔记
记录了做到的一些不错的题,也有模板和知识点。
CF1548C
题意
\(q\) 个询问,每次给定 \(x\),求 \(\sum\limits_{i=1}^N\dbinom{3i}{x}\)。\(1\leq N\leq 10^6\)。
做法一
设 \(f_{x,k}=\sum\limits_{i=0}^{N-1}\dbinom{3i+k}{x}\),则有
- \(f_{x,0}+f_{x,1}+f_{x,2}=\dbinom{3N}{x+1}\)
- \(f_{x,1}=f_{x,0}+f_{x-1,0}\)
- \(f_{x,2}=f_{x,1}+f_{x-1,1}\)
递推就能做了。
用到了上指标求和公式(好像叫什么 Hockey Stick Identity(?):
做法二
设
把 \(x-3\) 替换成 \(x\) 就得到递推式
比方法一的简洁些。
斐波那契数列通项公式
其中 \(\alpha=\dfrac{1+\sqrt{5}}{2}, \beta=\dfrac{1-\sqrt{5}}{2}\)。
可以扩域做,即把 \(a+b\sqrt{5},(a,b\in\mathbb{Q})\) 看作广义的“复数”进行运算。
Codeforces Round #737 (Div. 2)
赛时比较菜,就切了 ABC,D 就是线段树优化 dp,当时有了思路,但没时间写了。E 还是挺不错的,但据说交互库水了点。
长链剖分
树上 k 级祖先
各种做法花里胡哨,这里说下长剖的做法( \(O(n\log n)-O(1)\) )。
预处理:
-
对树进行长链剖分,记录每个点所在链的顶点和深度
-
树上倍增求出每个点的 \(2^n\) 级祖先
-
对于每条链,如果其长度为 \(len\),那么在顶点处记录顶点向上的 \(len\) 个祖先和向下的 \(len\) 个链上的儿子
-
对 \(i\in[1, n]\) 求出在二进制下的最高位 \(h_i\)
对于每次询问 \(x\) 的 \(k\) 级祖先:
- 利用倍增数组先将 \(x\) 跳到 \(x\) 的 \(2^{h_k}\) 级祖先,设剩下还有 \(k^{\prime}\) 级,显然 \(k^{\prime} < 2^{h_k}\),因此此时 \(x\) 所在的长链长度一定 \(\ge 2^{h_k} > k^{\prime}\)。
- 由于长链长度 \(> k^{\prime}\),因此可以先将 \(x\) 跳到 \(x\) 所在链的顶点,若之后剩下的级数为正,则利用向上的数组求出答案,否则利用向下的数组求出答案。
(摘自https://www.luogu.com.cn/blog/xht37/solution-p5903)
HOT-Hotels
经典问题,求树上两两距离相等的三元组个数。
设 \(f_{x,j}\) 表示 \(x\) 的子树中到 \(x\) 距离为 \(j\) 的节点数,转移比较简单:
再设状态 \(g_{x,j}\) 表示在 \(x\) 的子树中选两个点,它们到其 lca 的距离为 \(d\) 且 lca 到 \(u\) 的距离为 \(d-j\) 的方案数。
转移:
这里的 \(f_x^{\prime}\) 表示 \(f_x\) 被 \(y\) 之前的儿子更新过的值。
答案的统计则要分两种情况,第一种情况,三个点都在同一个子树内,大概长这样:(图片有空再补)
对答案的贡献为
第二种情况,三个点有两个在子树内,另一个为祖先节点,对答案的贡献就是 \(g_{x,0}\)。
至此我们就有了 \(O(n^2)\) 的解法。
考虑长链剖分。记 \(x\) 的重儿子为 \(son_x\),我们发现 \(x\) 的信息可以直接从 \(son_x\) 继承来,即 \(f_{x,j}=f_{son_x,j-1},g_{x,j}=g_{son_x,j+1}\),这个可以用指针 \(O(1)\) 实现。对于 \(x\) 的轻儿子 \(y\),我们在 \([0,dep_y]\) 中枚举 \(j\) 转移即可。这样整个过程的复杂度就是所有长链的长度和,时空复杂度均为 \(O(n)\)。
CCPC-Final 2019 G
题意
给定一棵 \(n\) 个节点树,\(1\) 号节点有一个标记,两个人轮流移动这个标记到另一个节点,每次移动的距离必须严格比上一次对手移动的距离长。求有多少个包含 \(1\) 节点的联通子图,使得这个游戏后手必胜。
\(1\leq n\leq 2\times 10^5\)。
做法
博弈就是纸老虎,不难看出,其实就是求有多少个子图使得 \(1\) 节点是直径中点。
先设计这个恶心的 dp。
\(f_{x,j}\) 表示以 \(x\) 为根且最大深度为 \(j\) 的子图方案数。
转移:
其中 \(y\in son(x)\),\(f^{\prime}\) 表示处理到当前儿子之前 \(f\) 的值,这是一个滚动的过程,下文的 \(g^{\prime}\) 同理。
考虑在 \(1\) 节点周围统计答案,再设计一个更恶心的 dp。
-
\(g_{j,0}\) 表示以 \(1\) 为根且最大深度小于 \(j\) 的子图方案;
-
\(g_{j,1}\) 表示以 \(1\) 为根且仅有一个儿子中的最大深度为 \(j\) 的子图方案;
-
\(g_{j,2}\) 表示以 \(1\) 为根且有两个或两个以上儿子中的最大深度为 \(j\) 的子图方案。
转移:
观察这个转移,我们发现所有转移都和深度有关,于是想到用长剖优化。处理当前节点 \(x\) 时,继承 \(x\) 重儿子的信息,再枚举每个轻儿子所在长链,更新 \(x\) 的信息。式子最后面的那个西格玛,枚举 \(j\) 的时候维护一个前缀和就好了。
不过还有一个问题,根据转移,处理 \(x\) 时所有的 \(f_x\) 都会被轻儿子更新,但我们又不能枚举到 \(dep_x\)(否则复杂度有变成 \(n^2\) 了),不过再仔细观察,我们发现当 \(j>dep_y\) 时,后面的那个西格玛就是一个定值,这就相当于给 \(dep_y<j\leq dep_x\) 的 \(f_{x,j}\) 都乘上一个定值,所以打个懒标记就好了,用到的时候再下传懒标记,这样就保证了总的复杂度 \(O(n)\)。
\(g\) 的转移也就同理了,打懒标记即可。
luogu P1273
\(O(n^2)\) 的树上背包
首先把所有节点按后续遍历重新标号
设 \(f_{i,j}\) 为前 \(i\) 个点,背包容量为 \(j\) 的最大价值。设 \(siz_i\) 为以 \(i\) 为跟的子树大小。那么 \(f_{i,j}\) 就会从 \(f_{i-1,j-1}\) 和 \(f_{i-siz_i,j}\) 转移过来。
loj 也有这个模板题 https://loj.ac/p/160
换根 dp
CF708C Centroids
题意
给定一颗树,你可以删去一条边,再加入一条边,保证改动后还是一棵树。
求每个点是否可以通过改动成为这颗树的重心。
要判断 \(x\) 是否能成为重心,我们就是要在 \(x\) 的大于 \(n/2\) 的子树中(\(x\) 为根节点)摘下来不超过 \(n/2\) 的最大的子树,接在 \(x\) 上,再判断这个子树剩下部分是否不大于 \(n/2\)。
我们需要 dp 求出一个 \(f_x\) 表示在 \(x\) 的子树中不超过 \(n/2\) 的最大子树。
转移大概就是
不过,具体求 \(f\) 的时候,我们不仅要求出最大值,还要求出一个次大值,这样当我们从 \(x\) 换根到 \(y\) 的时候,如果 \(x\) 的最优决策在 \(y\) 的子树中,我们就要用次大值来更新 \(y\) 。
这个同时记录最大值和次大值的方法应该是换根 dp 的一种常见思路,下面两道题都是如此。(感觉这几道换根写起来细节都挺多的)
P3647 这个题还是不错的,但可能换根做多了就比较套路了,我做这题调了一整个下午
P4292 [WC2010]重建计划
看到这个式子一定是分数规划,二分之后就是求树上包含边数在 \(L\) 和 \(R\) 之间的权值和最大的路径。
一个很简单的 dp 就是 \(f_{x,j}\) 表示以 \(x\) 为根子树中以 \(x\) 为一个端点的长度为 \(j\) 的路径的最大权值,然后就有
这个转移长剖就完了,不过统计答案时还有一个边数在 \(L\) 和 \(R\) 之间的限制,这是一个区间查询,所以想到用线段树维护长链。dp 过程中,把 \(x\) 的轻儿子信息转移到 \(x\) 时顺便统计经过 \(x\) 的路径对答案的贡献,然后这题就做完了。复杂度 \(O(n\log^2n)\)。
这个题看似复杂,但每一步都非常清晰,不可能做不出来。
矩阵加速 dp
P2579 [ZJOI2005] 沼泽鳄鱼
注意到 \(2,3,4\) 的最小公倍数为 \(12\),计算 \(12\) 个转移矩阵,乘起来之后求一个 \(\left\lfloor \frac{k}{12}\right\rfloor\) 次幂,在乘上前 \(k\bmod 12\) 个矩阵就是总的转移矩阵
P3216 [HNOI2011] 数学作业
状态转移方程:
其中 \(k=\left\lfloor\lg i\right\rfloor +1\)。
矩阵加速:
P2886 [USACO07NOV] Cow Relays G
广义矩阵乘法 \(C_{i,j}=\min\{A_{i,k}+B_{k,j}\}\) (\(\max\) 也一样)依然满足结合率可以做快速幂,本题就是矩阵加速 floyd。
P3502 [POI2010] CHO-Hamsters
一个串接到另一个串后面多出来长 \(l\) 的一截,看成连了一条长 \(l\) 的边,这个过程用到 KMP。
转化成图论模型后就与上题类似了。
P4095 [HEOI2013]Eden 的新背包问题
奇妙的 cdq 分治+多重背包
不选 \(i\) 物品的答案的贡献是由 \(1\sim i-1\) 和 \(i+1\sim n\) 的物品产生的,所以就可以 cdq 求两边对 \(i\) 的贡献,具体地,
- 先把 \((mid,r]\) 的物品加入背包
- 递归 \([l,mid]\)
- 还原(去掉 \((mid,r]\) 的物品)
- 把 \([l,mid]\) 的物品加入背包
- 递归 \((mid,r]\)
复杂度就是 \(O(nV\log n)\)。\(V\) 是背包容量。
附:单调队列优化多重背包代码
void add(int w,int v,int s){ //加入重 w,价值为 v 的物品,数量为 s
for(int i=0;i<w;i++){
deque<pair<int,int> > q;
q.push_back(make_pair(0,f[i]));
for(int j=1;j*w+i<=m;j++){
while(!q.empty()&&q.back().val<=f[j*w+i]-j*v) q.pop_back();
q.push_back(make_pair(j,f[j*w+i]-j*v));
while(!q.empty()&&q.front().id<j-s) q.pop_front();
f[j*w+i]=max(f[j*w+i],q.front().val+j*v);
}
}
}
单调队列优化 dp & 斜率优化 dp
CF1559 D2. Mocha and Diana (Hard Version)
题意
给你两个森林,每次你可以选择两个点 \(u,v\) 在两个森林的 \(u,v\) 节点之间都加一条边,并且要保证两个森林加完边还是森林。你需要加尽量多的边并输出方案。
解题思路
官方题解不说了,也是一个挺不错的做法,用到 set 启发式合并,复杂度 \(O(n\log^2n)\)。
在题解下面看到一个做法
先枚举 \(2\) 到 \(n\) 的节点,能和 \(1\) 连边的就都连上,这之后所有的节点都至少在一个森林里与 \(1\) 联通。
然后我们把所有在第一个森林里和 \(1\) 联通的点压入栈 \(s_1\),在第二个森林里和 \(1\) 联通的点压入栈 \(s_2\)。
然后不断进行一下操作:
- 如果 \(s_1\) 的栈顶在两个森林里都和 \(1\) 联通,把它弹出;
- 如果 \(s_2\) 的栈顶在两个森林里都和 \(1\) 联通,把它弹出;
- 否则,把 \(s1\) 和 \(s2\) 的栈顶之间连边。
总复杂度几乎是 \(O(n)\) 的,只有并查集的复杂度。
按这个算法连出的边显然都是合法的,而连完之后一定有一个森林中所有点都与 \(1\) 连通(只有一个连通块),这样也保证了连出的边是最多的。
CF1559E. Mocha and Stars
题意
求有多少长度为 \(n\) 的序列 \(a\),满足
- \(a_i\in [l_i,r_i]\cap \mathbb{Z}\);
- \(\sum\limits_{i=1}^na_i\leq m\);
- \(\gcd(a_1,a_2,\dots,a_n)=1\)。
\(2\leq n\leq50\),\(1\leq m\leq 10^5\),\(1\leq l_i\leq r_i\leq m\)。
解题思路
赛时看群里大家都说 E 很 trivial,然而我愣是一点都不会。/qd
莫反之后就是求
考虑每个 \(\mu(d)\) 的贡献,把 \(a_i\) 都除以 \(d\)。那就是,对于每个 \(d\),求有多少个序列满足 \(a_i\in\left[\left\lceil l_i/d\right\rceil ,\left\lfloor r_i/d\right\rfloor\right]\cap\mathbb{Z}\) 且 \(\sum a_i\leq m/d\)。
这个直接就可以 dp 了,dp 过程用前缀和优化,单次复杂度 \(O(nm/d)\),总复杂度就是 \(O(nm\log m)\)。
P4027 [NOI2007] 货币兑换
令 \(f_i\) 为第 \(i\) 天可以得到的最多钱数,然后列个方程解出第 \(j\) 天花钱买到的两种金券数 \(x_i=\dfrac{f_jr_j}{a_jr_j+b_j},y_i=\dfrac{f_j}{a_jr_j+b_j}\)。
状态转移方程就来了:\(f_i=\max\{a_ix_j+b_iy_j\}\)。
这个东西如果斜率优化,只能动态维护一个凸包。我记得寒假集训有一个这种斜优题,因为当时不会 cdq 也不会李超线段树,就硬写 Splay,从中午调到半夜没调出来,教练还问我为啥一道题也没补。。。此后我对 Splay 维护凸包就产生了心里阴影。
当然用李超线段树就好多了
变个形:\(f_i=b_i\times \max\{x_j\times\dfrac{a_i}{b_i}+y_j\}\)
把 \(x_j,y_j\) 看成斜率和截距,李超线段树即可,记得离散化。
我这个代码和题解高度相似(
https://www.luogu.com.cn/record/56036585
P4655 [CEOI2017]Building Bridges
也是裸的斜优,cdq 或李超线段树都行。
luogu P4719 【模板】"动态 DP"&动态树分治
单点修改的树上最大权独立集
经典的没有上司的舞会解法,设 \(f_{x, 0}\) 表示不选择节点 \(x\) 时,以 \(x\) 为根的子树的最大权独立集;\(f_{x, 1}\) 表示选择节点 \(x\) 时,以 \(x\) 为根的子树的最大权独立集。
然后
当然过不了带修的,因为每次修改一整条链上的点的 \(f\) 值都发生了变化,最坏是 \(O(mn)\) 的,然而据说过了这道题。
动态 dp
再考虑设一个 \(g_{x,0}\) 表示 \(x\) 的所有轻儿子中都可选可不选的最大独立集,\(g_{x,1}\) 表示所有轻儿子都不选的最大独立集。dp 就变成了
这样以后,我们就可以把转移写成矩乘的形式了!
这里的矩阵乘法是关于 \(\max\) 和加法运算的广义“矩阵乘法”。
考虑在重链上维护 \(g\) 值(线段树维护矩阵乘积即可),因为一条重链的底端一定是叶子节点,而叶子节点就储存了初始的值,我们要查询一个点的 \(f\) 值,只需要查询一下这个点到其所在重链底端的矩阵的乘积就可以了。
再考虑修改操作,修改一个点的权值时,该点到根的路径经过 \(O(\log n)\) 条重链,每条重链的顶端都是其父节点的轻儿子,只有这些位置的 \(g\) 值会发生改变,再线段树上单点修改即可,复杂度 \(O(\log^2n)\)。
这样这道题就做完了,时间复杂度 \(O(n\log^2 n)\)(不过还有一个 \(2^3\) 的常数)。
P5024 [NOIP2018 提高组] 保卫王国
如果会动态 dp 的话,这题就和模板一模一样了。

浙公网安备 33010602011771号