杂好题选讲
本人对于杂好题的定义是无计划做到的好题。
Q:为什么 CF 和 AT 的好题不算杂好题?
A:因为在做 CF 和 AT 的题目的时候,本人是有意地去筛自己想要的好题,而这些题有的来自模拟赛,有的来自随机跳题,有的是扒大佬的提交记录扒出来的,极其随缘,所以是杂好题。
Q:你如何判断一个题是不是好题?
A:我做不出来的对我来说就是好题啊。
[ZJOI2006] 物流运输
题目链接:here
考虑按天 DP。设 \(f_{i}\) 表示前 \(i\) 天的答案,答案显然是 \(f_n\),枚举改路线的那一天,有转移:
其中 \(S_{i,j}\) 表示第 \(j\) 天到第 \(i\) 天从 \(1\) 到 \(m\) 的最短路,这个东西预处理或者现跑都行,使用 Dijkstra 的话时间复杂度 \(O(n^2e\log m)\),数据小,稳稳过。code
【UNR #7】那些你不要的
题目链接:here
重申这篇博客的好题标准是我自己做不出来的题,尽管这个题到目前 148 个差评有一个是我点的。
首先注意到每一次删数必然会删掉原序列中排在偶数位的一个数,所以把奇数位置上的数排序然后取第 \(\lfloor\frac{\lceil\frac{n}{2}\rceil}{2}\rfloor\) 个就完了。有点过于扯淡,模拟赛见到这个题也是被喂屎了。code
[CSP-S2019] Emiya 家今天的饭
题目链接:here
注意到对于列的限制,如果有一列不满足,那么不满足的必然只有这一列,因为两个大于 \(\lfloor\frac{K}{2}\rfloor\) 的和必然大于 \(K\),于是考虑容斥,问题转化为每一行最多选一个的个数减去每一行最多选一个、存在一列大于 \(\lfloor\frac{K}{2}\rfloor\) 的个数。前面的可以直接算,我们只用考虑后面的怎么做。于是考虑枚举违规的这一列,然后对于每一列都 DP 算。设违规的是第 \(x\) 列,\(f_{i,j,k}\) 表示前 \(i\) 行中第 \(x\) 行选了 \(j\) 个,其余行选了 \(k\) 个的方案数,考虑当前行的情况,设 \(s_i=\sum_{j=1}^m a_{i,j}\),列出递推式:
硬做时间复杂度 \(O(mn^3)\),过不了,考虑优化,发现无论我们是在转移还是在统计答案的过程中,都并不关心 \(j,k\) 的值,而更关心的是他们的差,于是设 \(f_{i,j}\) 表示考虑到 \(i\) 行,第 \(x\) 列比其他列多出了 \(j\) 个,共多少种情况,于是有转移:
总复杂度降到了 \(O(n^2m)\),可过。
这道题从暴力到部分分需要我们发现列的性质,这提醒我们在以后对 \(k/2\) 之类的形式要学会追问,这个性质保障了什么或者说告诉了我们什么,这需要我们创造性地转化;从部分分到满分则需要对 DP 的过程和目标有充足的理解,但是在场上能写出部分分就不错了。好题。code
[USACO15FEB] Censoring S
看到字符串匹配想到 KMP 对吧,然后手摸一组阳历:
ababccy
abc
发现匹配到第一个 c 上时前面一段匹配成功,然后在前面有一个 ab,然后就匹配成功了,这其实我们可以在 \(\text{kmp}\) 的过程中记下每个位置的 \(j\) 指针,然后有成功匹配就直接跳到没成功匹配的那个地方的 \(j\),并且开一个栈,每次扫到一个字符就把这个字符塞进去,如果当前 \(j=|T|\),就把头 \(j\) 个弹出去。代码直接拷这,因为原来的 KMP 假了,现在这版保真(?)
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
const int N = 1e6 + 5;
char s[N], t[N];
int n, m, pi[N], p[N], st[N], top;
int main()
{
cin.tie(0)->sync_with_stdio(false);
cout.tie(0);
cin >> (s + 1) >> (t + 1);
n = strlen(s + 1), m = strlen(t + 1);
for (int i = 2, j = 0; i <= m; i++)
{
while (j && t[j + 1] != t[i]) j = pi[j];
if (t[j + 1] == t[i]) j++;
pi[i] = j;
}
for (int i = 1, j = 0; i <= n; i++)
{
while (j && s[i] != t[j + 1]) j = pi[j];
if (s[i] == t[j + 1]) j++;
p[i] = j, st[++top] = i;
if (j == m)
{
top -= m;
j = p[st[top]];
}
}
for (int i = 1; i <= top; i++) cout << s[st[i]];
return 0;
}
[yLCPC2024] F. PANDORA PARADOXXX
场上被创死了,场下因为混淆树剖的 fa 和并查集用的 f 调了半天。。。
删边不好做,所以正难则反,不断加边,然后考虑两个连通块的直径怎么合并,那合并之后的连通块的直径只可能是以下几种:
- 原来两部分各自的直径
- 两个直径拼起来
考虑直径怎么拼,新的直径的端点肯定是原来两个连通块的直径的端点中的两个。证明这个只需要证到任意点距离最远的点一定是直径的两个端点之一。
代码直接看我发的题解吧。link
拆分数列
题目链接:here
约定:\(v[i\dots j]\) 表示子串 \(s[i\dots j]\) 所表示的数。
题目中的两个条件考虑如何满足。逻辑上肯定是先满足优先级高的,也就是最后一个数尽量小。设 \(g_i\) 表示考虑到 \(s_i\),使得 \(v[g_i \dots i]\) 最小且满足条件,于是有转移:
这个转移时间复杂度是 \(O(n^3)\),稳稳过。可这还没完,还有另外一个条件。于是考虑设 \(f_i\) 表示考虑到 \(s[i\dots n]\),后缀最小的同时 \(v[i\dots f_i]\) 最大,于是有转移:
并且 \(f_{g_n}=n\)。可这么做是假的,考虑 hack:
1234050
我们会把它分成
1,2,3,40,50
可显然有更有的分拆:
12,34,050
所以只要在计算 \(f\) 的时候让最后一段拥有尽量多的先导零,这样就对了。
[HAOI2012] 高速公路
我们要求的是
其中 \(\text{d}(i,j)\) 表示 \(i\) 到 \(j\) 的距离。发现分子不好算,考虑转化成算贡献,于是记分子为 \(\text{Ans}\),有
于是开线段树,记 \(a_i,a_i\times i,a_i\times i^2\) 的和就行了。
Calculating
题目链接:here
首先 \(\sum_{i=l}^r f(i)=\sum_{i=1}^rf(i)-\sum_{i=1}^{l-1}f(i)\),然后发现 \(f(x)\) 就是 \(x\) 的因数个数,于是:
发现这很难算,考虑算贡献,对于每个 \(i\),他对答案的贡献就是他乘起来小于 \(n\) 的数量,即 \(\lfloor\frac{n}{i}\rfloor\),于是答案就是:
整除分块直接搞就完了。code
[国家集训队] Crash的数字表格 / JZPTAB
然后利用经典算贡献套路把 \(d\) 提出来,枚举 \(i,j\) 改为枚举 \(di,dj\) 来枚举 \(\gcd(i,j)\),不妨设 \(n\leq m\),有
由莫比乌斯反演公式,我们有 \(\sum_{d|n}\mu(d)=\epsilon(n)\),于是原式转化为
然后再利用枚举 \(\gcd\) 的套路,有
记 \(s(x)=\sum_{i=1}^x i\),于是有
把 \(s(\lfloor\frac{n}{dx}\rfloor)s(\lfloor\frac{m}{dx}\rfloor)\) 提前面,就有
然后预处理 \(b\mu(b)\) 的 Dirichlet 前缀和,然后整除分块就能做了。代码自己写去吧。
「LibreOJ β Round #4」求和
考虑利用莫反经典套路,枚举 \(\gcd\) 然后改变枚举顺序。
考虑研究 \(\mu^2*\mu\) 的性质。先打个表:
这是 \(\mu\):
1 -1 -1 0 -1 1 -1 0 0 1 -1 0 -1 1 1 0 -1 0 -1 0 1 1 -1 0 0 1 0 0 -1 -1 -1 0 1 1 1 0 -1 1 1 0 -1 -1 -1 0 0 1 -1 0 0 0 1 0 -1 0 1 0 1 1 -1 0 -1 1 0 0 1 -1 -1 0 1 -1 -1 0 -1 1 0 0 1 -1 -1 0 0 1 -1 0 1 1 1 0 -1 0 1 0 1 1 1 0 -1 0 0 0
这是 \(\mu^2*\mu\):
1 0 0 -1 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 -1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1
调动观察能力发现 \(\mu^2*\mu\) 绝大多数都是零,少数不是零的位置都对应着完全平方数,且值等于 \(\mu(\sqrt{n})\),于是直接把它当结论写写写,然后就过了。然后考虑证明
先证当 \(n\) 是完全平方数的情况。此时 \(\forall d|n \text{ and } d \not = \sqrt n\),不妨设 \(d\leq \frac{n}{d}\),那么 \(\sqrt{n}/d\) 必然在 \(n/d\) 中出现了两次,所以 \(\mu(\frac{n}{d})=0\),所以只有 \(\sqrt n\) 可能对答案有贡献,并且答案就是 \(\mu(\sqrt n)\),这一部分证毕。再证 \(n\) 不是完全平方数的情况。考虑 \(\mu(d)\mu(\frac{n}{d})\),若所有质因数次数都大于二,那 \(d\) 和 \(\frac{n}{d}\) 中定有至少一个存重因子,故答案就是 0;若 \(n\) 存在一个次数为一的质因子,那么这个质因子对答案的贡献必然一正一负,当它在 \(\mu^2\) 位置时就是正的,否则是负的,两个抵消,这一部分证毕,于是全部证毕。
象棋与马
题目链接:here
考虑四个本质不同的操作 \((a,b),(b,a),(-a,b),(-b,a)\),设它们出现的次数分别是 \(x_1,x_2,x_3,x_4\),可知只要能走到 \((1,0)\),由归纳法就可以走到棋盘上所有位置,所以 \(p(a,b)=1\) 等价于
然后把式子化简,得到 \(p(a,b)=1\) 的等价条件是
由 \(a\perp b\) 的条件我们知道第二条条件等价于 \(a+b\equiv 1(\bmod 2)\),于是我们要求的就是
考虑强制令 \(i\equiv 0(\bmod 2)\),那么不妨枚举 \(i/2\),可知约束条件并没有改变,于是答案就变成了
定义 \(f(n,m)=\sum_{i=1}^n\sum_{j=1}^m[i\perp j][2|i][2\not | j]\),并且令
于是我们有
可以递归求解,时间复杂度 \(O(\sqrt n\log n)\),瓶颈在于求 \(\sum_{i=1}^n\mu(i)\) 时开不了 \(O(10^{11})\) 的空间,于是考虑杜教筛,时间复杂度是 \(O(n^{\frac{2}{3}}\log n)\)。code
踩气球
题目链接:here
利用线段树,把每个区间拆成线段树上的节点,在线段树节点上开 vector 记这个节点上存在的区间,然后维护一下区间和就可以过,时间复杂度为 \(O(n\log n)\) 的。
复杂度怎么证?
考虑每个区间会被分成 \(\log n\) 个区间,总共 \(m\log n\) 个区间,更新一个熊孩子区间最多 \(\log n\),次,所以最多更新 \(O(m\log n)\) 次,所以时空复杂度都是 \(O(n\log n)\) 的,所以我们可以把这个小 trick 称作线段树均摊?code
[HNOI2011] 卡农
神仙题!从第一步就想不到,然后每一步都出乎我意料!设 \(f_i\) 表示选出 \(i\) 个片段且符合要求的情况。由于每个音阶的出现次数必须是偶数次,所以如果我们确定了前 \(i-1\) 个片段,那第 \(i\) 个片段也就确定了,所以满足这个条件的方案数就是随便选 \(i-1\) 个片段的方案数,也就是 \(A_{2^{n}-1}^{i-1}\)。然后其中肯定有不合法的,而不合法的情况分两种,一种是第 \(i\) 个片段为空,另一种是存在一个片段与第 \(i\) 个片段相同。对于第一种情况,我们知道这样的情况数就相当于是 \(f_{i-1}\)。对于第二种情况,假如和当前片段重复的是第 \(j\) 个片段,那么把这两个片段去掉就构成了一个规模为 \(i-2\) 的合法音乐,种数就是 \(f_{i-2}\),而 \(j\) 的位置有 \(i-1\) 种取值,而这个片段的取值种数为 \(2^n-1-(i-2)=2^n-i+1\),于是我们就有递推式
然后把顺序也就是 \(m!\) 除掉 \(O(n)\) 就过掉了!真的是神题!code
Black Nodes in Subgraphs
题目中有两个限制条件,分别是点集大小和黑点个数。两个维度,必然滚掉一位,于是考虑把黑点的可行性转化为最优化。设 \(f_{i,j}\) 表示必须走到第 \(i\) 个点,经过 \(j\) 个点最多能到多少个黑点,相对应的定义 \(g_{i,j}\),然后做树上背包。我们的过程是这个样的:
void dp(int u, int fath)
{
siz[u] = 1, f[u][1] = g[u][1] = a[u];
for (int v : G[u])
{
if (v == fath) continue;
dp(v, u);
for (int i = siz[u]; i >= 1; i--)
for (int j = 1; j <= siz[v]; j++)
{
f[u][i + j] = max(f[u][i + j], f[u][i] + f[v][j]);
g[u][i + j] = min(g[u][i + j], g[u][i] + g[v][j]);
}
siz[u] += siz[v];
}
for (int i = 1; i <= siz[u]; i++) mx[i] = max(mx[i], f[u][i]), mn[i] = min(mn[i], g[u][i]);
}
于是有人怀疑这么做时间复杂度应该是 \(O(n^3)\) 或者跑不满的,但肯定不可过,交上去一看发现过了,于是就怀疑是不是常数小导致的。实际上这么做复杂度是 \(O(n^2)\) 的。直接求和不好做,我们算每个点的贡献,发现每个点最多被看 \(n\) 次,总数就是 \(O(n^2)\) 的。注意 siz[u] += siz[v] 这一行一定要在后面,不然就是 \(O(n^3)\) 的暴力了。code
[USACO24OPEN] Identity Theft P
dsu on tree 都不会证复杂度,菜死了。
题目要求的是每个字符串不能是别的串的前缀,于是建立 01-trie,从下往上递归,这个点对答案的贡献就是这个字数内能走到树外面的最小步数,然后 dsu on tree 就行了。dsu on tree 新开一个博客写吧。code
[BJOI2018] 链上二次求和
体现线段树维护区间性质的好题以及展现 CNOI 超大马良的性质。我的做法和这篇一样,简单来说就是拆贡献,懒得写了。code
「StOI-2」独立集
首先考虑能不能直接把链塞到状态里直接做,但是判重太困难了,显然不可做,所以考虑在子树里 dp。设 \(f_{i,0/1}\) 表示点 \(i\) 为根的子树内选不选以 \(u\) 为 \(\operatorname{lca}\) 的路径的情况数,并记 \(g_i=f_{i,0}+f_{i,1}\)。容易列出转移:
记路径 \(L\) 两端点的 \(\operatorname{lca}\) 为 \(\operatorname{lca}(L)\),点 \(i\) 的父亲为 \(\text{fa}_i\),有
发现 \(\text{fa}_j\in L\) 很难维护,于是考虑直接拿 \(\text{fa}_j\) 算,这样就可以链上求和做了。注意到 \(f_{i,0}=\prod_{j\in \text{son}_i}g_j\) 的形式,于是把连乘的东西变成这个东西,但是会多算链上的,于是把链上的点的 \(g\) 除掉,有
然后线段树维护 \(\prod\frac{f_{j,0}}{g_j}\) 就可以了。注意如果存在 \(g_j=0\),那么求出来的 \(g_1\) 是 0,但是答案是 \(1\),所以说答案不会小于一,所以最后的时候 \(g_1\) 与 \(1\) 取个 \(\max\) 就可以了。
代码直接进洛谷专栏看我写的题解吧。
创世纪
从 \(a_i\) 到 \(i\) 建出外向基环树森林,考虑对于单个连通块怎么做。先找出环,对于其中一个点,断掉 \((a_i,i)\) 这条边,这样就成了一颗树,以 \(i\) 为根 dp,然后考虑加上 \((a_i,i)\) 的限制算一遍,也就是以 \(a_i\) 为根做一遍 dp,设 \(f_{i,0/1}\) 表示点 \(i\) 选或不选,有转移
转化一下有
于是答案就是 \(\max(f_{i,0},f_{i,1},f_{a_i,1})\)。code
[HNOI/AHOI2018] 毒瘤
题目链接:here
中译中:求最大独立集。
神仙题,不知道为什么要起这样一个恶心的名字。
一棵树上的做法是显然的。考虑非树边。显然有三种情况:\((0,0),(1,0),(0,1)\),容易发现 \((0,0/1)\) 的情况可以一起考虑,所以对于非树边只用考虑 \(\text{dfn}\) 较小的就行了。观察到非树边个数(记为 \(k\))较小,于是想到状压,对于每一个状态都跑一边 dp 就可以以 \(O(2^k n)\) 的时间复杂度获得 75pts。发现 dp 过程中有许多都是重复计算了的,所以考虑优化 dp 过程。
发现如果想快速处理非树边的问题,相当于处理非树边上的点的问题,也就是一些关键点的问题。根据虚树问题格容易想到对于这些点建规模不超过 44 的虚树,对于虚树外的点,求出 \(g_{i,0/1}\) 表示去掉虚树上的点之后当前点的 dp 值,然后考虑虚树上怎么做。根据虚树的性质,一条边 \(u\rightarrow v\) 对应原树上一条链,并且这条链上如果出现分叉那必然是一个没有关键点的子树,假设这条链为 \(u, k_1, k_2, \dots, k_r, v\),不妨手搓一下。下面式子中 \(a\leftarrow b\) 等价于给 \(a\) 乘上 \(b\)。
\(f_{k_r, 0}\leftarrow f_{v,0}+f_{v,1}, f_{k_r, 1} \leftarrow f_{v, 0};\)
\(f_{k_{r-1},0}\leftarrow f_{k_r,0}+f_{k_r,1}=g_{k_r,0}\cdot (f_{v,0}+f_{v,1})+g_{k_r,1}\cdot f_{v,0}\)
\(f_{k_{r-1},1}\leftarrow f_{k_r,0}=g_{k_r,0}\cdot (f_{v,0}+f_{v,1})\)
发现虚树上 \(f_{u,0/1}\) 虽然不能像树上独立集问题一样拥有简洁的系数,但是系数我们并非不能求。于是预处理 \(f_{v,0/1}\) 对 \(f_{u,0/1}\) 对 \(f_u\) 的贡献系数,然后在虚树上 dp。预处理的时间复杂度为 \(O(n)\),但是对于每种状态 dp 的时间复杂度降低了,于是总时间复杂度为 \(O(n+k2^k)\),可以通过。code 没错你根本看不了但这个人是我小号。
树上游戏
link: here
一题多解,妙!
法一:对每种颜色算贡献。考虑当前一个点 \(u\),记其颜色为 \(c\),考虑 \(u\) 的一个以 \(v\) 为根的子树,考虑 \(v\) 中颜色为 \(c\) 的点,那么 \(u\) 能产生贡献的路径端点必然在这些点以上。这些点可以直接存下来,因为其总数是 \(\Theta(n)\) 的,然后利用树上差分统计答案,对于 \(v\) 子树内 \(x\) 满足 \(c_x=c\),设 \(s=siz_v-\sum_{c_x=c}siz_x\),差分数组 \(d_x\leftarrow d_x-s,d_v\leftarrow d_v+s\),时间复杂度 \(\Theta(n)\)。code 反正你也看不了。
法二:点分治。吐了,不会。
[USACO21DEC] HILO G
模拟赛放了这个题,场上想到了 naive 的 \(O(n\log n)\) 做法,场下发现有 \(O(n)\) 的笛卡尔树做法,直接兴奋了。这个询问有两个部分:序列上的前后关系与值的关系。我们发现前后关系满足小根堆性质,值得关系满足 BST 性质,于是建出笛卡尔树。\(\text{HILO}\) 等价于先向左子树走再向右子树走,值得注意的是如果走到了一个询问点并且前一个操作是 \(\text{HI}\) 那么当前也是 \(\text{HILO}\),于是在笛卡尔树上 dfs 就可以了。code
[NOI2009] 二叉查找树
考虑在笛卡尔树上维护答案。因为权值为实数所以全职互不相同是可以忽略不计的,于是操作相当于我们选择一点,然后将这个点旋转到这棵子树的根。\(dp(l,r,k)\) 表示 \([l,r]\) 区间上建笛卡尔树,根的权值是 \(k\) 的答案,然后枚举根节点 \(p\),决策当前根是否花费额外权值就行了。
[KSN2021] Binary Sea
你需要打表!!!
观察发现如果 \(x\text{ and } y=0\) 那么必然有 \((x-1,y)\) 或 \((x,y-1)\) 是黑格。证明的话不妨设 \(\operatorname{lowbit}(x)<\operatorname{lowbit}(y)\),然后 \(x-1=x-lowbit(x)+(lowbit(x)-1)\),也就是二进制末尾的 \(10000\) 变成了 \(01111\),\(\operatorname{and}\) 起来还是 0.所以说黑点大概就构成了一个树的样子,矩形内的黑点数就是上边和左边的边数。
不妨只考虑上边,答案就是 \(\sum_{i=0}^y[i\operatorname{and}x=0][i\operatorname{and}(x-1)=0]=\sum_{i=0}^y[i\operatorname{and}(x\operatorname{or}(x-1))]\)。这玩意考虑数位 dp。因为约束是 \(\leq y\),所以设 \(dp(i,0/1)\) 表示从低到高考虑到第 \(i\) 位,当前是否超过 \(y\) 且异或是 0 的方案数,转移就做完了。
送给好友的礼物
link:https://www.luogu.com.cn/problem/P7276
因为贡献是 max 所以没啥好性质,考虑 dp 维护 \((u,i,j)\) 分别表示 \(u\) 子树内 M 走了 \(i\) 步 B 走了 \(j\) 步是否可行,转化为最优性问题就是 \(dp(u,i)\) 表示 \(u\) 字数内 \(M\) 走 \(i\) 步的时候 B 最少走 \(dp(u,i)\) 步,树上背包就是 \(O(n^2)\) 的。
[CERC 2020] Storage Problems
朴素的背包是 \(O(n^4)\) 的并且无法优化。考虑容斥,我们只需要求出所有一块的背包,然后每次减去包含了当前黑帮分子的方案数就可以了。
双端队列
link: https://www.luogu.com.cn/problem/P10465
发现对于排序后的序列,每个双端队列对应的是一个连续段,并且这个连续段中的编号是单谷的。然后就做完了。
「KDOI-06-S」签到题
link: https://www.luogu.com.cn/problem/P9747
观察:一个区间可行等价于:
- 设区间或和为 x,那么 x 出现过
- 对于一个等于 x 的数,存在一个子区间不包含这个数并且或和为 x
钦定 a(i) 为 x,考虑求出所对应的区间 (l,r)。显然 (l,r) 的或和要等于 a(i)。
我们可以找出最小的 L(i) 满足 [L(i),i] 的或和是 a(i),最大的 R(i) 满足 [i,R(i)] 的或和是 a(i),这个可以二分预处理。
最大的 p(i) 满足 [p(i),i) 的或和是 a(i),最小的 q(i) 满足 (i,q(i)] 的或和为 a(i),然后左右端点的取值范围就分别是 [L(i),p(i)] 和 [q(i),R(i)],这个也可以二分求。
考虑求答案。扫描 r,如果 r 介于 [q(i),R(i)] 之间,那么左端点为 [L(i),p(i)] 的答案就会是 r,这样的话我们只需要在扫描到 q(i) 的时候给 [L(i),p(i)] 加一,在 R(i) 的时候给 [L(i),p(i)] 减一,只需要找最大的不是 0 的 l 然后和询问区间比较就可以;如果 r 在 R(i) 外,那么左端点为 [L(i),p(i)] 的答案就会是 R(i), 直接维护答案,相当给区间对一个单调递减的一次函数取 max,所以只需要维护左端点的抉择就可以了。

浙公网安备 33010602011771号