#0x01 拓扑排序
做题记录:
- P1113 板子
- P1347 碰到DAG(有向无环图)一定要考虑一下拓扑 拓扑排序可以用于判断是否存在环,只需要进行一次拓扑然后判断入队个数是否为n即可。拓扑排序可以处理不等式链,如果是一条确定大小关系的不等式链,要保证对于一个节点入队点不超过一个。
- P1685 较为板子的题目
- P3243 贪心的证明说实话没太看懂,但不太想纠结这一题…
- P1983 出现了大小关系,所以考虑拓扑,有一定的思考量,但只要想到怎么建图拓扑就很简单了,注意内存限制,可以判重边省内存
- P1038 板子,题目描述有点小问题(NOIP特色
- P4934 很有思考量的一题,拓扑可以维护两个数的继承性关系,比如a=b,b=c,a一定=c的关系。
- P4017 板子
P4934 礼物
题意中的两个物品不能放在一次的条件是大数的二进制表达的1包含小数的每一位二进制表达的1。比如
5
=
(
101
)
2
5=(101)_2
5=(101)2 和
4
=
(
100
)
2
4=(100)_2
4=(100)2 不能放在一次,但可以和
2
=
(
010
)
2
2=(010)_2
2=(010)2 放在一起。
如果我们已经知道对于每一个物品,所有不能和它放在一个箱子的物品,那么我们可以通过构造DAG,走拓扑排序获得答案。
首先,显然可以通过题目字面意思想到一个
O
(
n
2
)
O(n^2)
O(n2) 建图,求解的暴力解法。
如果我们将题意通过上述转化,我们可以变成
O
(
n
∗
2
p
o
p
c
o
u
n
t
)
O(n*{2^{{popcount}}})
O(n∗2popcount) 的对值域的解法,然后发现复杂度一点没变。此时我们发现 (实际上是翻找了题解后,其实没必要对每一个点都和所有满足条件的比它小的点相连接,只要让大数连接所有比他小
2
k
2^k
2k的即可,显然其余的点都会间接和它相连。
#0x02 欧拉路/欧拉回路
做题记录:
- P1341 板子,注意找欧拉路要判断图的连通性(建议用并查集,代码简单)。欧拉路的定义不重复经过所有的边,而不是经过所有的点。存在两个奇点的图存在欧拉路(且分别为起点终点),不存在奇点的图存在欧拉回路。
- P1333 板子,字符串可以用字典树或哈希处理,注意找欧拉路/回路一定要判断图的连通性,用并查集判断的化小心空图!
- qoj1780 非常有意思的题,初见真不知道该如何建图,其次注意1e5-6就容易爆栈,这题学了一下模拟栈,受益匪浅~,而且第一次知道欧拉回路欧拉路还能这么应用。
#0x03 割点/割边
做题记录:
- P3388 割点板子(注意不一定联通),关于一些割边割点模板的的细节,在下边写
P3388 【模板】割点(割顶)
n
u
m
num
num数组是点的dfs序(其实depth也可以,dfs序可以被证明是正确的,我不知道怎么证)
最好写带判断父节点的模板,这样子方便改割边,割边代码既是条件从大于等于换成大于的代码。
注意如果是未访问的to,我们用它的minn更新自己的minn是没问题的。
而对于已经访问的并且不是父节点的to,我们用它的num更新自己的minn即可。
void dfs(int p,int fa)
{
num[p]=minn[p]=++dfn;
int child=0; //并非子节点,而是独立的子树的数量
for(int i=head[p];i;i=e[i].next)
{
int to=e[i].v;
if(!num[to])
{
++child;
dfs(to,p);
minn[p]=min(minn[p],minn[to]);
if(minn[to]>=num[p]&&p!=st)
ju[p]=1;//此行可能会在求割点反复运行,是为了方便直接改成求割边代码
}
else if(to!=fa)
minn[p]=min(minn[p],num[to]);
//如果是求割点,其实不必须判断父节点,因为我们是用num更新minn的
//判断条件是>=,所以即便跳回父节点对结果无影响,
//但如果是割边一定要判,因为判断条件是>防止走回边
}
if(p==st&&child>=2) ju[p]=1;
}
#0x04 Floyed算法
正常来说会把floyed放在最短路算法里作为一种多源最短路径的算法,但我觉得floyed的算法思想非常有意思。
详见dp博客,floyed作为状压dp的转移方法尤为关键。-2025.11.3补
做题记录:
- P1730 这个题使用floyed状态转移为什么是正确的?,我的理解是floyed本质是dp,上一步的最优状态是之前所有的步最优状态。
- P2419 不是很难,floyed实现的传递闭包和拓扑排序有相似之处
#0x05 最短路
- P4779 D i j k s t r a Dijkstra Dijkstra 模板题,算法的本质是 “BFS+贪心+优先队列优化” ,如果没有优先队列优化的话,算法是 O ( n 2 ) O(n^2) O(n2),而有优先队列优化的话是 O ( ( m + n ) l o g n ) O((m+n)log_n) O((m+n)logn) ,对于稀疏图来说,堆优化的复杂度更优,而密集图直接暴力会更优。
#0x06 树上问题_LCA
LCA算法对比
树链剖分、倍增法、Tarjan法求LCA对比
| 特性 | 树链剖分 (Heavy-Light Decomposition) | 倍增法 (Binary Lifting) | Tarjan法 (离线算法) |
|---|---|---|---|
| 预处理复杂度 | O(n) | O(n log n) | O(n + q) |
| 单次查询复杂度 | O(log n) | O(log n) | O(1) |
| 在线/离线 | 在线 | 在线 | 离线 |
| 额外空间 | O(n) | O(n log n) | O(n + q) |
| 常数大小 | 较大 | 较小 | 很小 |
算法特点总结
| 方面 | 树链剖分 | 倍增法 | Tarjan法 |
|---|---|---|---|
| 核心思想 | 重链分解+线段树 | 二进制跳跃 | DFS+并查集 |
| 优势 | 功能强大,支持路径操作 | 在线算法,实现相对简单 | 批量查询效率极高 |
| 劣势 | 代码复杂,常数较大 | 预处理慢,空间较大 | 必须离线,功能单一 |
倍增法
树上倍增法首先预处理树上倍增,复杂度
O
(
n
l
o
g
n
)
O(nlog_n)
O(nlogn),查询的时候分三跳
一跳相同深度,二跳不同才跳,三跳只跳一步。
代码实现 P3884
#include <bits/stdc++.h>
#define intt long long
#define N 105
#define M 10005
#define debug(x) cout<<#x<<":"<<x<<" ";
using namespace std;
//using namespace __gnu_pbds;
int T,n,m;
int dep[N],f[N][30],num[N],maxn1,maxn2;
struct edge
{
int u,v,next;
}e[M<<1];
int cnt,head[N];
void yadd(int u,int v)
{
++cnt;
e[cnt].u=u;
e[cnt].v=v;
e[cnt].next=head[u];
head[u]=cnt;
}
void ydfs(int p,int fa)
{
f[p][0]=fa;
dep[p]=dep[fa]+1;
maxn1=max(maxn1,dep[p]);
++num[dep[p]];
maxn2=max(maxn2,num[dep[p]]);
for(int i=head[p];i;i=e[i].next)
{
int to=e[i].v;
if(to==fa) continue;
ydfs(to,p);
}
}
void ypre()
{
for(int i=1;(1<<i)<=n;++i)
{
for(int t=1;t<=n;++t)
{
f[t][i]=f[f[t][i-1]][i-1];
}
}
}
int ylca(int u1,int u2)
{
if(dep[u1]<dep[u2])
{
swap(u1,u2);
}
for(int t=22;t>=0;--t)
{
if(dep[f[u1][t]]>=dep[u2]) u1=f[u1][t];
}
if(u1==u2) return u1;
for(int t=22;t>=0;--t)
{
if(f[u1][t]!=f[u2][t])
{
u1=f[u1][t];
u2=f[u2][t];
}
}
return f[u1][0];
}
int main()
{
cin>>n;
for(int t=1;t<=n-1;++t)
{
int u1,u2;
cin>>u1>>u2;
yadd(u1,u2);
yadd(u2,u1);
}
ydfs(1,0);
ypre();
int u11,u22;
cin>>u11>>u22;
cout<<maxn1<<endl<<maxn2<<endl;
cout<<2*dep[u11]-3*dep[ylca(u11,u22)]+dep[u22]<<endl;
return 0;
}
Tarjan法
tarjan的算法核心是通过并查集+DFS实现。优点是,复杂度优秀,查询 O ( n + m ) O(n+m) O(n+m),代码实现简单;缺点是,必须离线查询,不能在线。所以在泛用性上不如前者。
代码实现P3379
#include <bits/stdc++.h>
#define intt long long
#define N 500005
#define M 500005
#define debug(x) cout<<#x<<":"<<x<<" ";
using namespace std;
//using namespace __gnu_pbds;
int T,n,m;
struct edge
{
int u,v;
int next;
}e[N<<1];
int cnt,head[N],fa[N],ans[N],S;
bool valid[N];
vector<pair<int,int> > v[N];
int yfind(int u1)
{
if(fa[u1]==u1) return u1;
return fa[u1]=yfind(fa[u1]);
}
void ymerge(int u1,int u2)
{
if(yfind(u1)==yfind(u2)) return ;
fa[yfind(u1)]=yfind(u2);
}
void yadd(int u,int v)
{
++cnt;
e[cnt].u=u;
e[cnt].v=v;
e[cnt].next=head[u];
head[u]=cnt;
}
void ytarjan(int p)
{
valid[p]=1;
for(int i=head[p];i;i=e[i].next)
{
int to=e[i].v;
if(valid[to]) continue;
ytarjan(to);
ymerge(to,p);
}
for(auto i:v[p])
{
if(valid[i.first])
{
ans[i.second]=yfind(i.first);
}
}
}
int main()
{
cin>>n>>m>>S;
for(int t=1;t<=n;++t) fa[t]=t;
for(int t=1;t<=n-1;++t)
{
int u1,u2;
cin>>u1>>u2;
yadd(u1,u2);
yadd(u2,u1);
}
for(int i=1;i<=m;++i)
{
int u1,u2;
cin>>u1>>u2;
if(u1==u2) ans[i]=u1;
v[u1].push_back(make_pair(u2,i));
v[u2].push_back(make_pair(u1,i));
}
ytarjan(S);
for(int t=1;t<=m;++t)
{
cout<<ans[t]<<endl;
}
return 0;
}
做题记录:
- P3884/P3379 板子
- P3258 需要树上差分或者树链剖分,差分的解法需要求LCA。具体来说,树上差分实现思路是把路径分成左右两部分,再分别对两部分差分操作,可以参考这个网站 https://oi-wiki.org/basic/prefix-sum/
- P3398 本质上是判断两条路径是否有相交的点,用两点的各自LCA与两点的LCA的LCA判断。对于树上LCA问题,常常不需要分类讨论链状(因为链状可以看作角状的特殊情况,lca是一个其中端点的特殊情况),这样子可以减少分类讨论的复杂度。
- P2680 cs卡常题,话又说回来,其实这题的二分思路非常巧妙,首先是题目要求最大值最小,所以可以想到二分答案,但是如何快速check?显然的思路是枚举删去每一条边,然后再判断所有路径非否满足要求,超时。我们先观察要删去的边有哪些性质,首先这条边一定要经过所有的不合法路径(即路径长比二分的mid更长的路径),而且要保证删去这条边可以让最长的不合法路径变的合法(这样子所有不合法路径肯定也会变得合法),所以我们要找的是一条经过所有非法路径的边,并且边权最大的边。找边可以用树上差分+lca实现。
即便如此依然通过不了此题,因为如果递归差分的话会因为深度问题TLE,所以需要用非递归的形式优化,详见另一篇卡常博客。
#0x07 树上问题_树链剖分
由于树的特殊结构,导致很多高级数据结构(比如线段树)在树的问题上不能使用,而树链剖分就是把树上问题转化成正常数列问题。具体可以看https://oi-wiki.org/graph/hld/
如果要学习树链剖分,必须先了解LCA,树上差分,线段树等知识。
做题记录:
- P3384 模板题(代码200行起步
- P3950 多解题,此题如果学了树链剖分就会变得很简单,但是需要解决一个问题就是如何记录边权,解决方法是将边权转化成深度更深的点的点权,(但是这种情况可能会出现,计算边权和的时候多记录了最顶端的点的点权)需要特别注意一下。
- P2146 也是挺模板的题。
#0x08 最小生成树
Kruskal算法
用并查集和贪心算法,即每次贪心的添加不在同一集合的最短边,当添加了n-1条边之后停止。
算法复杂度
O
(
m
l
o
g
m
)
O(mlog_m)
O(mlogm) (对边排序的复杂度)。
#0x09 树上启发式合并
树上启发式合并通常解决的是静态子树查询问题。对于一颗子树而言,我们需要知道它所有子树的信息,但是暴力处理绝对会超时,所以我们可以用二分的思想,只保留节点最多的子树的信息,统计它的答案的时候:
- 先遍历所有的轻儿子(递归遍历)(概念不知道的看#0x07树链剖分)
- 然后不保存轻儿子的集合
- 之后遍历重儿子保存答案,然后在遍历轻儿子(节点p的加入时机因题目而异)
时间复杂度因要维护的数据结构而异,如果要维护的是集合set,要在nlogn基础上多一个log的复杂度,也有可能是map,multiset之类的。
刷题记录:
- P2971 多解问题,此题启发式合并思路显然,只要维护一个深度集合然后枚举lca(即枚举子树)就行;另一种思路是利用树的直径的性质(即在边权非负的条件下,最长的直径一定有一个端点是树上最深的点),代入此题就是,对于每一种颜色(政党)都记录一个最深的深度,然后枚举节点求与他相同颜色的最深节点的距离就行。
https://www.luogu.com.cn/record/244780787(防止自己忘了dsu怎么写
- P9233 要稍微思考一下:维护每种颜色出现的个数 的集合(如果数量为0除外),然后判断最小的数量和最大的数量如果相等就是各种颜色都数量相同。
- P2971 此题带给人的启发是,dsu不一定都维护的set,此题就是维护map
335

被折叠的 条评论
为什么被折叠?



