搜索
剪枝
分组类
把物品分组放在一起。
类型
- 每组必须要“放满” ,如167. 木棒
- 每组不一定要“放满”,如165. 小猫爬山,1118. 分成互质组
dfs方式
- 枚举每一组中放的物品(枚举组合)
- 枚举每个物品放哪一组 or 新开一组
有种感觉,第一类问题要用解法一,第二类都可以??
还是不太清楚的感觉,因为我还没有想清楚小猫爬山这题,为什么做法一几乎是做法二的五倍 30ms and 150ms
迭代加深
是用时间换空间的做法,感觉起来类似BFS?
使用场景
- 有些分支的层数可能极大,并且求出的答案无用且费事
- 最优解的层数最小。
估价函数优化(IDA*)
用一个估价函数f(<= g(真实步数))使,在当前最大深度下无法求解的分支,直接剪掉。
与 A*类似。
A* 与 IDA*的选择
当 状态容易表示 状态指数增长较慢 时用 A*
当 状态表示很大 状态指数增长较快 时用 IDA*
(墨染空大佬的总结)
图论
重在建图!!!
注意
要根据数据范围
和要求
来看
最后选符合的最便利实现方式。
不一定考什么模型就一定用什么。(如:香甜的黄油)
最短路问题
单源点
- 边权均非负
- 朴素版dijkstra(稠密图) (O(n^2))
- 堆版dijkstra(稀疏图) (O(mlogn))
- 有负权边
- bellman_ford(SPE:走k次的最短路、判负环) (O(nm)) (因为负权边要与
INF/2
比较来判定无法到达) - spfa(SPE:判负环)(循环队列可以手写qwq 用的是0 - N-1) (O(km...nm))
- bellman_ford(SPE:走k次的最短路、判负环) (O(nm)) (因为负权边要与
多源点
- flody
可以有负权边(因为负权边要与INF/2
比较来判定无法到达)(不能有负环)
与DP
DP为非拓扑图时转化为最短路问题
多起点或多终点时
考虑用虚拟源点。
虚拟源点就相当于开始时把所有的起点都放入队列。
floyd算法
- 最短路
- 传递闭包
- 找最小环 d[i][j] + g[i][k] + g[k][j]
- 恰好经过k条边的最短路(倍增)
恰好经过k条边的最短路
- bellman_ford - (O(nm)) - 稀疏图
- 类Floyd的DP + 倍增 - (O(n^3logn)) - 稠密图
最小生成树
- prim
- 朴素版 (O(n^2+m)) 稠密图
- 堆版 (O(nlogn + mlogn -> mlogn)) 与dijkstra相同的优化 基本不用
- kruskal
- (O(mlogm)) 稀疏图
可以用prim的一定可以用kruskal。
注意
若有负权边(即加一条边方案不严格变坏),不能无脑套模板
拓展
- 生成树和虚拟源点
- 次小生成树(1. 非严格 2. 严格)
- 先求最小生成树,再枚举删去最小生成树的边求解。(O(mlogm + nm)) (Kruskal)
- 先求最小生成树,再依次枚举非树边,然后将改变加入树中,同时从树中去掉一条边,使最终的图仍是一棵树。(O(m + n^2 + mlogm / mlogn))(朴素/LCA)
若求非严格次小生成树,只需要维护两点间的最大路径
若求严格次小生成树,还需要维护两点间的次大路径
负环
常用做法:spfa
- 统计每个点入队次数,某个点入队n次,说明存在负环(与bellman_ford的判断相同)
- 统计当前每个点的最短路所包含的边数,若所包含边数>=n,说明存在负环(推荐)
都基于抽屉原理
全都为-1
1 -> 2 -> 3 -> 1
法1:n(n-1) + 1 -> (n^2)
法2:n -> (n)
注意
下方,指图中存在负环!!不一定与起点相连
- 存在负环要将所有点入队(不一定全部走得到)相当于建立一个
虚拟源点0
; - dist[i] 任意(把所有点放入)
如果是求从某一点出发的负环,即与某一点连通的负环时,dist还是要memset(dist, 0x3f, sizeof dist)。
不然就需要把所有点放入,再判断负环与起点的连通性,就麻烦了。洛谷负环模板
经验
因为时间复杂度看做(O(nm))
当所有点的入队次数超过2n - 4n ...时,我们就认为图中有很大可能是存在负环的。(超时的话,trick的做法)
from yxc
也可以使用stack栈来操作,经验上效果不错。(同样也可以看洛谷那个模板题,有一个点十分久,这两种做法还是看数据,没有绝对的好坏)
差分约束
用最短路:
Xi <= Xj + Ck
x - 自变量,Ck - 常数
用最长路求解:
Xj <= Xi - Ck
最短路-松弛定理 - 不等式组
i -> j
dist[i] <= dist[j] + w
限制是边对点的限制,点能否都走到是无所谓的!!!
转换后若存在负环,即存在Xi < Xi,即无解
应用
- 求不等式组的可行解
- 源点需要满足的条件:源点需要能够走到所有的边(这样能都满足松弛定理)
- 步骤:
- 把每个不等式转化为边(Xi <= Xj + Ck)-> (Xj -> Xi 长度为Ck)
- 找到一个超级源点,使其可以遍历到所有边
- 存源点求一遍单源最短(长)路
- 存在负(正)环,无解
- 无负(正)环,dist[i]是一个可行解
- 求最大值和最小值,每个变量的最值
- 能求的条件:一定是有某个Xi与常数的关系,不然都只是相对关系
- 结论:
- 若求最小值,则求最长路;若求最大值,则求最短路。
- 转化Xi <= c?
- 建立超级源点0,建立0 -> i,长度为c的边
- 求Xi最大值为例:
通过链式转换后,一定可以得到 Xi <= 0 + c1 + c2 ...,一系列上界,取上界的最小值就是Xi的最小值。(每一个上界是从源点走到Xi的所有方案的权和),即求路径的边长和的最小值
理解
Xi的上界or下界有许多,这些上界or下界就是从虚拟源点出来到达Xi的所有方案的路径总长度,也就是Xi要满足的所有条件,因为全都要满足,所以若求最小值,则求最长路;若求最大值,则求最短路。
注意
一定要去看是否所有的限制条件都被考虑到了。
还有结果是合法的之类的。
最近公共祖先LCA
-
向上标记法 - O(n)
-
倍增 - 二进制拼凑 - 预处理 O(nlogn) 查询 O(logn)
- 定义
- fa[i][j]表示从i节点开始,向上走(2^j)步能走到的节点。(0 <= j <= [logn])
j == 0, f[i][j] = fa[i];
j > 0, fa[i][j] = fa[f[i][j - 1], j - 1]; - depth[i]表示i节点的深度
- 哨兵:若从i节点跳(2^j)步会跳过根节点,那么fa[i][j] = 0, depth[0] = 0;(用于判断跳出去的非法状态)
- fa[i][j]表示从i节点开始,向上走(2^j)步能走到的节点。(0 <= j <= [logn])
- 步骤
- 先将两个点跳到同一层(若已经是同一个点就直接退出) 枚举k 从大到小
- 让两个点同时往上跳,跳到最近公共祖先的下一层(若直接跳到公共祖先的话,不能直接判断它是不是最近的公共祖先) 枚举k 从大到小
- 定义
-
Tarjan - 离线求LCA - O(n + m)
- 定义
- 实质:对向上标记法的优化(运用并查集)
- 在DFS时,将所有点分为三大类
2
-已经遍历过且回溯过的点(一个节点其对应子树被遍历完的意思)1
-正在搜索的分支0
-还未搜索到的点
- 寻找现在遍历到的点a相关的点b,若这个相关点b已经被遍历过,那么lca(a, b)就是b现在所被合并到当前正在搜索分支上的某个节点。
- 遍历并回溯了一个点就可以把这个点合并到父节点
from 算法提高课
- 定义
-
RMQ(不常用)预处理 - O(nlogn) 查询 - O(1)
- 做法
- 先求dfs序(回溯也要记一次)
- 求lca(x, y),在dfs序中找到任意一个x和y,找到x和y之间深度最小的节点编号即最小值,也就转化为区间最小值了。
- 做法
在线 - 问一个答一个
离线 - 全问完了,回答
树上差分
x, y, c
d(x) += c
d(y) += c
d(anc) -= c * 2
- 每个点的值就是以这个点为根的子树里的和
对anc以上,x、y以下也没有变化
有向图的强连通分量 O(n + m)
- 概念
连通分量:u、v可以互相到达
强连通分量(SCC),即极大连通分量。加入任何点就不是连通分量的 连通分量
对于一个搜索树,有向边(x, y)(由第一次DFS遍历到的时间节点来编号等得到的时间戳dfn[])
树枝边:x是y的父节点(特殊的前向边)
前向边:x是y的祖先(所有的包括自己的父辈之一)节点
后向边:y是x的祖先节点(往回搜)
横叉边:除了以上三种情况的边(dfn[y] < dfn[x], y已经被搜过了)
dfn[u]遍历到u的时间戳
low[u]从u开始走,它所能遍历到的最小的时间戳
u是其所在强连通分量的最高点,等价于low[u] == dfn[u] - 判断x是否在SCC中
- 存在后向边指向祖先节点
- 存在一个横叉边,通过横叉边走到祖先节点
in_stk = true 说明还没有回溯。
(这个我不知道怎么描述,通过代码+图的方式可能好说明)
应用
将任意一个有向图通过缩点(将所有的连通分量缩成一个点)的方式转化为有向无环图(DAG、拓扑图)
后用拓扑序递推方式求最短路or最长路
有向图!!!
缩点 重新建图 按拓扑序求解!!!
模板
void tarjan(int u)
{
dfn[u] = low[u] = timestamp ++;
stk[++ top] = u, in_stk[u] = true;
for(int i = h[u]; ~i; i = ne[i])
{
int v = ver[i];
if(!dfn[v]) // 树枝边
{
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(in_stk(v)) // 不是向下的边,横叉边是不需要考虑的,因为是`有向图`,不能够构成一个连通分量
low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u])
{
int v;
++ scc_cnt;
do
{
v = stk[top --];
in_stk[v] = false;
id[v] = scc_cnt;
} while(u != v);
}
}
缩点 -> DAG
for(int i = 1; i <= n; i ++)
for i 所有邻点
if(i 和 v不在一个SCC)
add(id(i), id(v)); // 连通块相连(有时不用真的建图,可能只是入读和初度)
注意
SCC编号递减
的顺序一定是拓扑序
因为不一定联通, 但每个点都应该要在缩点后的集合内有对应的点
for 所有的点
if(!dfn[i]) tarjan(i);
tarjan不是能做所以最长路,且对空间复杂度要求较高。
结论
对一个有向图,把它变为scc需要加max(p, q)条边(p出度为0,q入度为0的点的个数)
无向图的双连通分量
- 概念
对于无向图来说。
双连通分量,即重连通分量。- 边双连通分量
把桥这条边删去,整个图不连通
极大的不包含桥的连通块
性质:
1. 不管在内部删掉那条边,图还是连通的
2. 存在两条没有公共边的路径 - 点双连通分量
割点,把割点删除后这个图不连通
极大的不包含割点的连通块
每个割点至少属于两个连通分量
性质:
1.
2.
两个割点之间的边不一定是桥
桥的两个端点也不一定是割点
(两者没啥关系)
树的边是桥,每个点事边连通分量
所有的边是点连通分量
- 边双连通分量
边双连通分量
无向图中不存在横叉边
维护dfn,low
- 如何找到桥
y不能走到x或x的祖宗
dfn[x] < low[y];
- 如何找到所有的边的双连通分量
- 把所有的桥删掉
- 用栈 dfn[x] == low[x], x走不回x的父节点
缩点之后是一棵树,边都是桥
度数为1的点至少要加一条边(最少的加边方法就是把两个度数为1的点连一条边)
连了之后导致x,y点分别到根节点的路径都变为不是桥。
点双连通分量
- 如何求割点
dfn[x] <= low[y];- 如果x不是根节点,那么x就是割点
- x是根节点,至少有两个子节点割点
- 如何求点双连通分量
if(dfn[x] <= dfn[y])
{
cnt ++;
if(x != root || cnt > 1) cnt[x] = true;
将栈中元素弹出弹到y位置为止,且x也属于该点双连通分量(x == y 显然,x < y 弹出后只剩x和y的边,是一个双连通分量)
}
开始时要特判若是孤立点也是双联通分量。
缩点之后是一棵树,点是v-dcc
每次都会把会使它自己成为割点的点(那条分支的子节点弹完,也就是弹到v)都弹掉(最后把自己u补上去),这样之后它就不是割点,也就是说存在当前栈中的点(到割点 or 根节点的点)构成一个v-dcc
割点向包含它的v-dcc连条边,只是为了说起来方便,实际上就是直接把割点就放在那个v-dcc中了
(好用的样例 from ioj)
模板
不经过in_e这条边,无法回到x点或其他祖宗节点
e-dcc
int id[N], dcc_cnt;
bool is_bridge[M];
int indeg[N];
// 要存from 成对变换
void tarjan(int u, int from) // in_e
{
stk[++ top] = u;
dfn[u] = low[u] = ++ timestamp;
for(int i = h[u]; ~i; i = ne[i])
{
int v = ver[i];
if(!dfn[v])
{
tarjan(v, i);
low[u] = min(low[u], low[v]);
if(dfn[u] < low[v])
is_bridge[i] = is_bridge[i ^ 1] = true;
}
else if(i != (from ^ 1)) // 只要不是往回走的那个边,都可以用来更新(判重边)
low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u]) // 一块
{
++ dcc_cnt;
int v;
do
{
v = stk[top --];
id[v] = dcc_cnt;
} while(u != v);
}
}
for(int i = 1; i <= n; i ++)
if(!dfn[i]) tarjan(i, -1);
不经过x点回到祖宗节点,它就不是割点。
(不是根节点的情况下)
所以在dfs过程中,若回溯到这个点它的子节点仍不能访问到x之前的点,它就是割点。
根节点就是它至少有两个子节点。
是否对根节点要特判
v-dcc
求割点
void tarjan(int u)
{
dfn[u] = low[u] = ++ timestamp;
int cnt = 0; // 删去u点产生的连通块个数
for(int i = h[u]; ~i; i = ne[i])
{
int v = ver[i];
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if(dfn[u] <= low[v]) cnt ++;
}
else low[u] = min(low[u], dfn[v]);
}
if(u != root && cnt) cnt ++;
}
缩点
int root;
int dcc_cnt;
vector<int> dcc[N];
bool cut[];
void tarjan(int u)
{
stk[++ top] = u;
dfn[u] = low[u] = ++ timestamp;
// 是孤立点
if(u == root && ~h[u])
{
++ dcc_cnt;
dcc[dcc_cnt].push_back(u);
return;
}
int cnt = 0;
for(int i = h[u]; ~i; i = ne[i])
{
int v = ver[i];
if(!dfn[v])
{
tarjan(v);
low[u] = min(low[u], low[v]);
if(dfn[u] <= low[v])
{
cnt ++;
if(u != root || cnt > 1) cut[u] = true; // 对根节点的特判
++ dcc_cnt;
int y;
do
{
y = stk[top --];
dcc[dcc_cnt].push_back(y);
} while(v != y);
dcc[dcc_cnt].push_back(u);
}
else low[u] = min(low[u], dfn[v]);
}
}
}
遍历每个dcc[i][j] 可以求每个dcc[i]中的cut数
结论
一个无向图,加cnt + 1 >> 2 个边可以使他成为边双连通分量(cnt为缩点后度数为1的点的个数)
二分图
-
概念
相邻的点是不属于一个集合,特殊的最大流问题
指无向图
匈牙利算法(基于深搜)
匹配:一组边,没有公共点的集合
最大匹配:边数最多的一组匹配
匹配点:在匹配中的点
非匹配点:不在匹配中的点
增广路径:从一个非匹配点开始走,先走非匹配边再走匹配边最后走到一个非匹配点(左非-> 非 -> 匹 -> ... -> 右非) (可以把所有匹配的边去掉变为非匹配的边,增广了)
最小点覆盖,最大独立集,最小路径点覆盖(最小路径重复点覆盖)
最小点覆盖,不止是在二分图,而是在一个图中,从中选出最少的点,使得所有的边有一端可以有一个点
最大独立集,不止是在二分图,选出最多的点,使得选出的点之间(内部)是没有边的。
推导:在二分图中,等价于出掉最少的点,将所有的边都破坏掉,等价于找最小点覆盖,能把所有的边破坏掉。等价于找最大匹配
最大团(与最大独立集互补的),选最多的点,使任意边之间有边
最小路径点覆盖(最小路径覆盖),对一个DAG(有向无环图)来说,用最少的互不相交(点和边都不重复,即点不重复)的路径,将所有点覆盖住
进行拆点(普通的拆点,1 1' 2 2',一个出点,一个入点),路径 -> 匹配,路径终点 -> 左部非匹配点
推导:想让求最小路径点覆盖,就是让路径终点最少,左部非匹配点最少,找最大匹配
最小路径重复点覆盖(和最小路径点覆盖类似,只是没有对重复经过的点的限制)
做法:求传递闭包(建了许多新边) -> G', 在G'求最小路径覆盖。
(最小费用流)最优匹配(给每个边一个权重,达到最大匹配的情况下,最大边权和是多少),KM
(最大流)多重匹配(每个点可以匹配多个点) -
结论(在二分图中)
二分图 == 不存在奇数环 == 染色法不存在矛盾
最大匹配 == 不存在增广路径
(二分图中)最大匹配数 == 最小点覆盖 == 总点数 - 最大独立集 == 总点数 - 最小路径覆盖 -
应用
染色法 - 判定二分图
一般的二分图做法,匈牙利算法 - 求最大匹配数
注意
染色成二分图的染色方案只需保证两个集合内无边,任意一条边的端点在两个集合内。
虽然是无向图问题,但边可以只建有向边
match[],可以看做右边连向(匹配到)左边的边
模板
染色法判二分图
- 注意 可能是非联通的
bool dfs(int u, int c, int limit)
{
color[u] = c;
for(int i = h[u]; ~i; i = ne[i])
{
int v = ver[i];
if(color[v] == c) return false;
else if(!color[v])
if(!dfs(v, 3 - c, limit)) return false;
}
return true;
}
bool check(int mid)
{
memset(color, 0, sizeof color);
for(int i = 1; i <= n; i ++)
if(!color[i])
{
if(!dfs(i, 1, mid)) return ...;
}
}
匈牙利算法
int n1, n2; // n1表示第一个集合中的点数,n2表示第二个集合中的点数
int h[N], e[M], ne[M], idx; // 邻接表存储所有边,匈牙利算法中只会用到从第一个集合指向第二个集合的边,所以这里只用存一个方向的边
int match[N]; // 存储第二个集合中的每个点当前匹配的第一个集合中的点是哪个
bool st[N]; // 表示第二个集合中的每个点是否已经被遍历过
bool find(int x)
{
for (int i = h[x]; i != -1; i = ne[i])
{
int j = e[i];
if (!st[j])
{
st[j] = true;
if (match[j] == 0 || find(match[j]))
{
match[j] = x;
return true;
}
}
}
return false;
}
// 求最大匹配数,依次枚举第一个集合中的每个点能否匹配第二个集合中的点
int res = 0;
for (int i = 1; i <= n1; i ++ )
{
memset(st, false, sizeof st);
if (find(i)) res ++ ;
}
作者:yxc
链接:https://www.acwing.com/blog/content/405/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
欧拉路径和欧拉回路
-
概念
欧拉路径,存在一条S到T的路径,不重不漏地经过每条边一次
欧拉回路,起点和终点是同一个点的欧拉路径 -
结论
- 对于无向图来说(所有边都是连通的,点无所谓)
- 存在欧拉路径的充分必要条件:度数为奇数的点只能有0或2个。
- 存在欧拉路径的充分必要条件:度数为奇数的点只能有0个。
- 对于有向图来说(所有边都是连通的,点无所谓)
- 存在欧拉路径的充分必要条件:要么所有点的出度均等于入度;要么除了两个点之外,其余所有点的出度等于入度,剩余的两个点:一个满足出度比入度多1(起点),另一个满足入度比出度多1(终点)。
- 存在欧拉回路的充分必要条件:所有点的出度均等于入度。
从S开始DFS,一定会在T结束。(度数都为偶数,进来就要出去
- 对于无向图来说(所有边都是连通的,点无所谓)
代码
- 得到欧拉路径的倒序,求最小字典序只需要先找点编号小的
所有u的后继节点,都找过了。
欧拉路径从奇数点开始搜,欧拉回路从任意点开始搜。
用边来判重!!!
时间复杂度 - (O(m^2))
优化用完每一条边就删去,时间复杂度 - (O(n + m))
无向图,成对变换,反边要标记。
由图可知,这个dfs由于进去然后continue;出来的无意义行为增加时间复杂度
具体题目:1184. 欧拉回路
dfs(u)
{
for 邻边
dfs // 扩展
把u加到序列中
}
拓扑排序
- 概念
拓扑图,有向无环图。
为保证字典序,优先队列编号
模板
先将所有入度为0的点入队
while(q.size())
{
t = q[hh ++];
for t的邻边
if(-- ind[v] == 0) 放入;
}
高级数据结构
并查集
- 概念
带权并查集(时间复杂度与k无关)(维护k类),相对思想(相对距离)
扩展域并查集(O(k)),枚举思想(是第一类...,第二类...) - 应用
合并两个集合
查询某个元素的祖宗节点 - 优化
路径压缩 - O(logn)
按秩合并 - O(logn)(深度小的接在大的后面)
路径压缩 + 按秩合并 - O(alpha(n)) - 扩展
- 维护每个集合的大小 -> 绑定在根节点上
- 每个点到根节点的距离 -> 绑定在每个元素上(维护“多类”,例食物链)(一般维护当前节点到根的关系)