trick or 坑点记录(技巧性思维)
-
概览:用递归方便处理括号类问题。
在解决括号序列的展开(内部,比如
(ABA)之类的)问题时可以考虑用dfs来做,好写又不容易错。注意可以用手写栈预处理出相邻对应括号之间的关系。 -
概览:括号序列合法的另一个等价条件是,对于所有前缀,左括号不比右括号少,并且最后出现次数相同。
-
概览:括号类问题中左括号减右括号数非负。
由 2 我们可以想到,在设计 dp 状态时,可以记录左括号减右括号数这一个值,因为它是非负的。这样就方便转移了。
-
概览:DP 初始化赋为极大值与极小值。
在 dp 时,若要进行 \(\max\) 操作,最好把 dp 数组赋值为 \(- \infty\)。因为赋值成 \(0\) 或 \(-1\),那么可能存在一些情况,不合法但因为某些 dp 值是 \(0\) 或 \(-1\)(影响很小)解出错误的答案。
-
概览:路径信息拆成两条链的信息,在树论中大量使用。
在解决一些关于树上简单路径的题时,可以考虑两个点 \(x\),\(y\),然后考虑维护 \(x\) 到 \(\text{LCA(x,y)}\) 和 \(\text{LCA(x,y)}\) 到 \(y\) 两条链的信息。这种方法在 GESP 中很适用。
-
在跑负环的时候,尤其是在求最优比率环的时候要注意,有的时候为了提高程序效率,不必等到
cnt[y](入队次数)\(> n\) 时才return 1;,可以提前返回true。这有点像卡时技巧。 -
概括:前后缀合并解决问题。
如果在 dp 中遇到一些比较容易用前缀和后缀拼起来的答案但很难直接一次 dp 搞定的问题,不妨分两次 dp,一次 dp 做前缀,另一次 dp 做后缀,这样思路会清晰很多。
-
概括:
bitset优化。 -
概括:正难则反。
- 图论删边倒序做变成加边,用并查集做。
-
概括:多个数取 \(\max\)。
在取多个数 \(\max\),可以这样操作:
max({a,b,c})。 -
概括:DP 大致维度的确定。
dp 的状态设计可以根据数据范围来确定开几维。
-
概括:\(\text{mex}\) 与值域无关。
一个长度为 \(n\) 的序列 \(a\) 的 \(\text{mex}\) 值在 \([0,n+1]\) 之间,而与 \(a_i\) 的值域 \(V\) 无关。
-
概括:\(\log\) 的性质。
\(\log\) 可以将乘法换成加法,在差分约束系统有显著效果。例题:P4926 倍杀测量者。
-
概括:\(\min\) 的处理。通常用单调栈或者笛卡尔树求出每个数作为最小值的区间。
-
概括:\(\text{med}\)(中位数)的处理。对于一个想成为中位数的 \(k\),则 \(\ge k\) 的数至少占序列的一半。将 \(< k\) 的数记作 \(-1\),将 \(\ge k\) 的数记作 \(1\),那么中位数至少为 \(k\) 的条件就是这些标记的和 \(\ge 0\)。
-
概括:在可以差分的情况下,\([l,r]\) 可以拆成 \([1,r] - [l-1,r]\)(\(1\) 为左端点,视情况而定)。
-
概括:一个小小的容斥思路。恰好为 \(L\) 的方案数可以拆成 \(\le L\) 的方案数减去 \(\le L-1\) 的方案数。
-
概括:方格格子化点处理。遇到一些方格状物的图形而又不好进行格与格之间的动态规划时,可以考虑把一个格映射成一个点,具体可见 P4667。
-
概括:奇数位异或 \(1\)。在一些题目(考虑奇偶性,但是正着做难做)中,可以考虑使用奇数位异或 \(1\) 的想法,有时可以使解题难度降低,例题 礼物。
-
冒泡排序性质(适于计数):记 \(b_i = \sum_{j<i} [a_j > a_i]\)。则冒泡排序的轮数为 \(\max_{i=1}^{n} b_i\)。
考虑如何证明这件事情。
- 至多有一个 \(j\),使得 \(a_j\) 在本轮中被扔到 \(a_i\) 后面。设有两个位置为 \(j_1,j_2\),那么若 \(a_{j_1} > a_{j_2}\),那么 \(j_1\) 可以一直移动,\(j_2\) 因为 \(a_{j_1} > a_{j_2}\) 而被固定住了。否则 \(a_{j_1} \le a_{j_2}\),那么 \(j_1\) 无论如何都换不到 \(j_2\) 右边。证毕。(可以画图辅助理解)
- 至少有一个 \(j\),使得 \(a_j\) 在本轮中被扔到 \(a_i\) 后面。考虑取前缀最大值,一定 \(> a_i\),那么比它小的 \(j\) 都会失效。
- 综上,每轮,对每一个 \(i\) 来说,都会有恰好一个 \(j\),使得 \(a_j\) 被冒泡至 \(i\) 右边。
不难发现此时的轮数就是 \(\max b_i\)。
题型运用:P14352 排序。
-
原图加上若干条边的 MST (加入的边都有用)一定不会用到原图的非 MST 边。(总觉得这条 trick 非常显然)运用:CSP-S 2025 T2。证明:Kruskal。
-
wyp 学长:观察最终答案形态。
-
序列局部排序问题的常见解决方法是二分答案转 01 序列处理。
-
概览:\(\min (p,q) + |p-q| = \max (p,q)\)。
-
概览:边权转点权。
在树上的边(记为 \(u \rightarrow v\),其中 \(u\) 是 \(v\) 的父亲)的边权可以挂到点权上处理。例题:P4315 月下“毛景树”。 -
概览:如果问题没有突破口,可以考虑枚举。例如 NOIP 2025 T1。
-
经典结论:
对于函数
\[f(n)=\oplus_{i=1}^ni \]我们有
\[f(n) = \left\{\begin{aligned} n \space (n \equiv 0 \pmod 4) \\ 1 \space (n \equiv 1 \pmod 4) \\n+1 \space (n \equiv 2 \pmod 4) \\0 \space (n \equiv 3\pmod 4) \end{aligned}\right. \] -
求出一组解再进行调整法。
应用:P3513。
-
概览:单谷函数的充分条件。
函数 \(f(x)\) 的导函数 \(f'(x)\) 如果单调不减 \(\implies\) \(f(x)\) 为单谷函数,这里默认一次函数是单谷函数。HHC 大神:一次函数在一段区间内的最小值可以看作是“谷”。
-
次大值等价于去掉最大值后的最大值。例子:U681932 second。
-
构造题中好用的结论:在网格中确定行、列最大值考虑从大到小填数。
-
来自 li_cat:关于 2-SAT 的提醒:dfs 序的反序是拓扑序,仅针对 dfs 的完成序,dfs 的进入序与拓扑序并没有很好的关联。
-
概括:用更少的信息简化 DP 状态。DP 若有多个信息需要维护,观察是否能通过更少的信息表达所有信息。这样可优化空间复杂度。
例子:
- P16687。
-
DP 中的变换维度,当一个维度有后效性时考察其它维度的无后效性。
例子:
- P16686。
-
图论建模中的 \(|i-j|\) 形如这样的绝对值想到拆点之间的距离。
-
\(a \le b \le c\) 或者形如 \(b\) 在中间的又只能枚举一个,优先考虑枚举 \(b\)。
-
\(1\le a \le b \le c \le n\) 的计数。考虑映射 \(a'=a\),\(b'=b+1\),\(c'=c+2\),则 \(1\le a' < b' < c' \le n+2\),答案即为 \(\binom{n+2}{3}\),推广至 \(k\) 个中间元素,答案为 \(\binom{n+k-1}{k}\)。
-
如果这样用倍增写 LCA:
int lca(int x,int y){ if(d[x]<d[y])swap(x,y); for(int i=20;i>=0;--i){ if(d[anc[x][i]]>=d[y])x=anc[x][i];//注意这里,若 dep[root]=dep[0]=0,会导致求一个任意的点和根节点的 LCA 时跳过去。 } if(x==y)return x; for(int i=20;i>=0;--i){ if(anc[x][i]!=anc[y][i]){ x=anc[x][i]; y=anc[y][i]; } } return anc[x][0]; } -
求 LCA 新做法:\(dfs\) 序。核心思路:先分类讨论,再归一化处理,将求 \(u\),\(v\) 的 LCA 转化为求 \([dfn_u + 1,dfn_v]\) 深度最小的节点的父亲。用 ST 表来实现。实测跑得比倍增快的多。
注意这里为了节省空间,我们甚至不需要记录一个点的父亲,直接
st[0][dfn[x]]=fa;解决。好文推荐:this。
-
概括:树的直径性质。
好文推荐:this。
多条树的直径一定交于一点,且这个点是直径的中点。证明考虑反证法。
- 假设两条树的直径不相交,那么如图所示

所以树的直径是相交的。 - 再来证明树的直径交于中点。分两步说明。


- 假设两条树的直径不相交,那么如图所示
-
(施工中)基环树思路。
- 破环成树,做树形 DP。例题:P2607 [ZJOI2008] 骑士。
-
(施工中)AC 自动机题的思路。
- 以自动机上的 \(\text{trie}\) 边作为 DP 转移的阶段。例:\(dp[i][j]\) 表示串长为 \(i\),当前在点 \(j\) 的 xxx。
-
一棵树进行 dfs 时,对遍历到到的点打 \(1\) 标记,回溯过的点再打 \(-1\) 标记,这样标记为 \(1\) 的点,组成根节点到当前点的一条链(有效路径)。例题:P2414 [NOI2011] 阿狸的打字机。
-
快读注意事项:
::::warning[code]
inline int read(){ int x=0; int f=1; char c=getchar(); while(c<'0' || c>'9'){ if(c=='-')f=-1; c=getchar(); } while(c>='0' && /* 这里是&& 不是 || */ c<='9'){ x=(x<<1)+(x<<3)+(c^48); c=getchar(); } return x*f; }::::
-
线段树分治中,我采取的是这种写法:(P5787)
::::info[code]
void dfs(int x,int l,int r,bool flag){ int lst=top; for(auto g:tr[x]){ int u=g.u,v=g.v; if(find(u)==find(v))flag=0; merge(u,v+n); merge(v,u+n); } if(l==r){ if(flag)ans[l]=1; else ans[l]=0; while(top>lst){ auto p=st[top--]; fa[p.v]=p.v; sz[p.u]=p.sz; } return ; } int mid=(l+r)>>1; dfs(ls(x),l,mid,flag); dfs(rs(x),mid+1,r,flag); while(top>lst){ auto p=st[top--]; fa[p.v]=p.v; sz[p.u]=p.sz; } }::::
高亮处我对叶子节点进行了撤销操作,事实上,这里如果忘记写了,虽然被父节点通过
lst撤销。还是会被 Hack! 因为它是先序遍历。 -
重链剖分不能写假!
如下面代码片段所示:
for (int y : e[x]) { if (y != wc[x]) {//这句话一定要写,否则链长为1,lca 时间退化为暴力求解 dfs2(y, y); } } -
关于最短路图两个结论:
- 一条最短路径 \(S \to T\) 中的一条子路径 \(s \to t\) 也是一条最短路,若否,可以找到一条更短的 \(s \to t\) 的路径替换,\(S \to T\) 的最短路径变小,矛盾。
- 最短路图上不存在环。若存在环 \(\color{blue} u_1 \to u_2 \cdots \to u_t \to u_1\)。那么 \(dis[u_2] = dis[u_1]+w(u_1,u_2) > dis[u_1]\),以此类推,\(dis[u_t] > dis[u_1]\),\(dis[u_1] > dis[u_t]\),故矛盾。
-
- 均匀随机标号树(Prüfer 模型):期望高度 \(O(\sqrt n)\)。
- 均匀随机二叉搜索树 / 递归随机树:期望高度 \(O(\log n)\),儿子数 \(O(\log n)\)。
-
加法恒等式 \(a+b = a \oplus b + 2(a \mathbin{\&} b)\)。这是因为异或是不进位加法,而
&操作保留了进位,但是要进行左移,因为进位产生的效果在下一位,故而乘 \(2\)。 -
两个有序数组(假定从小到大)\(a\),\(b\),长度均为 \(n\),问题是找一组匹配 \((a_i,b_j)\),使得 \(\sum |a_i - b_j|\) 最小(求和符号里面的东西可以是只跟 \(|a_i - b_j|\) 有关的其它式子)。
我们猜测直接 \(a_i\) 和 \(b_i\) 配对是最优的。下面使用邻项交换法给出证明:
Case 1:

Case 2:

Case 3:

Case 4:

-
多测时若使用 Tarjan 求强连通分量、ecc 或 vcc,要注意清空 dfn 和 low。low 的清空不是必要的,但若 dfn 未清空,会导致判断
!dfn[y]时会出现错误。 -
区间 \(0/1\) 反转状态想到:
- 线段树
- 异或数组 \(d_i = a_i \oplus a_{i-1}\)。
-
写倍增时,如果用到类似下面的代码片段:
for(int i=18;i>=0;--i){
if((p>>i)&1){
x=nxt[i][x][sx];
sx=(sx+(1<<i))%k;
}
}
一定要小心,这里是固定了一个二进制最高位,若 \(p\) 比较大。而 ST 表(或类倍增过程)的第一行循环写 (1<<j)<=n,\(n\) 的量级比 \(p\) 的低,就会访问出界,建议把两者进行统一。
-
生成树森林满足 \(V-E = C\),其中 \(C\) 为联通块数。因为一棵树满足 \(V-E = 1\),\(V-E = \sum_{i=1}^C V_i - E_i = C\)。
-
一棵树的导出子图是生成树森林。
-
几种 STL 容器进行
swap的时间复杂度:- vector:\(O(1)\)(交换内部指针、容量、大小)
- deque:\(O(1)\)
- list / forward_list:\(O(1)\)
- map/set:\(O(1)\)(仅交换根指针)
- array:\(O(n)\)(必须逐元素交换,固定大小无法只换指针)
-
在无向图中写
dfs(有环)时注意:vis[x]=1; for(auto G:e[x]){ int y=G.v,id=G.id; if(id==pre)continue; if(!vis[y]){ // do something here }else if(dep[y]<dep[x]){ //这个 dep[y]<dep[x] 一定要判! } }原因:造一个环是
1 2,2 3,3 4,4 1。从 \(1\) 开始dfs,搜索到 \(4\),此时所有点都被打上了vis标记,那么访问 \(1\) 的另一个连着的点 \(4\) 时,由于vis[4]=1,会走下面的分支。因此要判深度关系。 -
两个圈做边集的异或,会形成若干个圈。这是由点的度数异或后仍为偶数决定的。
-
判定两个集合 \(S = S'\) 方法:
- 随机化异或。
- 若知道 \(S \subseteq S'\) 或 \(S \supseteq S'\),只需判定 \(|S| = |S'|\)。

浙公网安备 33010602011771号