Tarjan专题
Tarjan 算法原理
如下图所示。左边是最初的有向图,右边是经过 tarjan 算法后的图,其中实线有向边为树边;虚线有向边为回边;左图存在而右图不存在的边为弃边;每个结点旁边的数字为 dfn 序号。
树边、回边和弃边更精确的定义:
- 树边:dfs 过程中,从 \(u\) 到达 \(v\),\(v\) 未被分配 dfn 序号。
- 回边:dfs 过程中,从 \(u\) 到达 \(v\),\(v\) 已分配 dfn 序号,但 \(v\) 还未被分配到某个强连通分量中。(\(dfn_{u} > dfn_{v}\))
- 弃边:dfs 过程中,从 \(u\) 到达 \(v\),\(v\) 已分配 dfn 序号,且 \(v\) 已分配到某个强连通分量中。(\(dfn_{u} < dfn_{v}\))
所有结点和所有树边形成一棵 dfs 树;其中每个点引出的回边都会指向某个 dfn 序更小的结点;弃边没有用。
每个结点遍历完所有出边之后,比较该结点的 dfn 与 low 值是否相等。若相等则需要结算强连通分量。
要烂熟 low 数组的定义!!!
关于 tarjan 算法的疑难杂症:回边情况下 low 值的更新语句。两种不同的写法,会导致 \(low[u]\) 的定义不同。
两种写法都正确,都可以正确地处理出所有强连通分量。但第一种写法的 low 数组定义更明确,即使第二种写法看起来更简便一些,还是推荐使用第一种写法,对理解整个 tarjan 算法的帮助更大,唯一的缺点就是多敲了几下代码。
SCC缩点
// SCC缩点
vector<int> G[N];
int dfn[N],tim;
int low[N]; //low[u]: u及u的子树内的所有点,最多经过一条回边,能够到达的dfn序号最小的点(最上方的点)
int stk[N],top; //栈维护已经遍历过但还未被结算倒某个强连通分量内的结点
int scc[N],cnt_scc; //scc[u]==x表示结点u在第x个强连通分量中, cnt_scc表示当前强连通分量的个数
int siz[N]; //siz[i]表示第i个强连通分量内点的个数
void Tarjan_scc(int u)
{
dfn[u] = low[u] = ++tim;
stk[++top] = u;
for(auto v : G[u]){
if(!dfn[v]){ // (u, v) 是树边
Tarjan_scc(v);
low[u] = min(low[u], low[v]); // 注意上面还有一句递归,此时 low[v] 已计算完。low 数组维护的是子树内往上走可到达 dfn 序号最小的点,因此要和儿子 v 的 low 取 min。
}
else if(!scc[v]){ // (u, v) 是回边
low[u] = min(low[u], dfn[v]); // 与上面的区别在于不触发递归,直接利用回边指向的点的 dfn 序号更新当前结点的 low 值。
}
// else{} // (u, v) 是弃边
}
if(dfn[u] == low[u]){ // 扎起了口袋,开始结算强连通分量
cnt_scc++;
int x; //表示栈顶元素,每次弹出的点均属于同一个强连通分量
do{
x = stk[top--];
scc[x] = cnt_scc;
siz[cnt_scc] ++;
} while(x != u);
}
}
// 调用
for(int i = 1; i <= n; i ++){
if(!dfn[i]) Tarjan_scc(i);
}
P1073
SCC缩点结合 spfa 求最长路
P2272
半连通图定义:对于图中的任意两点 \(u, v\),\(u\)可达\(v\) 或 \(v\)可达\(u\)。
关于半连通图有一个重要的性质:缩点后一定是一条链。
因此计算最大半连通子图的大小,等价于在缩点后的图中求最长链,直接 \(dp\) 即可。
定义 \(dp_{u}\):以 \(u\) 为终点的最长链长度;\(cnt_{u}\):以 \(u\) 为终点的最长链数量。状态转移见 code。
注意缩点后的重边会对 最长链数量的计算 和 拓扑排序 产生影响,因此需要对缩点后的重边去重。
P2515
SCC缩点结合树上背包
与 树上带依赖的01背包 这个问题相比,只是多了依赖关系可能形成环的可能性。显然我们可以将整个环的所有物品看作是一个物品,于是 \(scc\) 缩点之后的图仍然是若干棵树,问题便与上述等价。
P3275
SCC缩点结合差分约束
Q:一共 \(n\) 个数,每个数至少 \(\geq 1\)。要求这 \(n\) 个数满足以下 \(5\) 种类型的共 \(m\) 条约束:
- \(1\space u\space v\):\(u = v\)
- \(2\space u\space v\):\(u < v\)
- \(3\space u\space v\):\(u \ge v\)
- \(4\space u\space v\):\(u > v\)
- \(5\space u\space v\):\(u \le v\)
求所有数总和的最小值,无解输出 -1。
考虑图论建模,利用差分约束的经典建图方式:设 \(u \rightarrow v\),边权为 \(w\),表示 \(v - u \ge w\)。
于是以上 \(5\) 种约束均可体现为在图中的有向边,形成 \(n\) 个点 \(m\) 条边的有向图。
显然,对于图中的任何一个环,环中的所有边必须全为 \(0\),否则无解。
剩下的所有环,包含的所有边的边权均为 \(0\),等价于该环内所有数均相同。于是缩点后,强连通分量中的所有点等价于一个点,只需要记录它的大小。对于缩点形成的 DAG,将所有入度为 \(0\) 的点置为 \(1\),然后拓扑排序 dp 即可。具体实现见 code。
P4819
SCC缩点结合高难度思维分析
Q:\(n\) 个人,其中有 \(1\) 个人是杀手,他们谁是杀手的概率是相等的。给出若干 \(x \rightarrow y\),表示第 \(x\) 个人知晓第 \(y\) 个人的身份,且知晓身份具有传递性(\(a\) 知晓 \(b\) 的身份,\(b\) 知晓 \(c\) 的身份,则 \(a\) 知晓 \(c\) 的身份)。现在可以盘问这些人中的若干个,可以知道 被盘问者 及 他知晓的所有人 的身份,但需要保证不能盘问到杀手。现在你知道每个人知晓其他人身份的情况(即已知所有的有向边),求查出杀手是谁且保证不盘问到杀手的最大概率。
若盘问了 \(x\) 个人,则概率为:
即排除这 \(x\) 个人中存在杀手的概率。
那么最大化概率,等价于最小化被盘问者的数量。
对于所有知晓关系构成的有向图中的某个强连通分量,显然只要盘问其中的任意一个人,那么整个强连通分量内的所有人的身份都会知晓。于是我们对原图进行缩点后,对于形成的 DAG,只需要盘问每个入度为 \(0\) 的结点中的某一个人,就可以知晓所有人的身份。那么答案理应是缩点后入度为 \(0\) 的点的数量,然而,这并不是我们想象得这么简单。
存在一种比较刁钻的情况:若某个强连通分量内只有 \(1\) 个人,且该点孤立(下方无出边),那么在盘问完其他所有强连通分量并知晓其他 \(n-1\) 个人的身份后,这个人就无需再继续盘问了。
所以,我们需要额外考虑缩点后的 DAG 中是否存在强连通分量大小为 \(1\) 的孤点。
还有一种刁钻的情况:若某个强连通分量 \(u\) 内只有 \(1\) 个人,下方存在出边,但是所有出边对应的强连通分量的身份可以被某一其他源头知晓,那么这个强连通分量 \(u\) 内的人也无需盘问。
至此,我们只需要检查是否存在这两种刁钻的情况中的某一种,便可确定是否可以少盘问一个人,进而得到最终的答案。
还有一点需要注意:我们都清楚,SCC缩点后的图中可能会存在重边。对于本题,是需要去除重边的,以避免对每个点的入度判断产生影响,便于我们对第二种刁钻情况的分析:只需要确定该点对应所有出边的点的入度是否均 \(>1\) 即可。
具体实现见 code。
割边,边双连通分量 EDCC
应用于无向连通图。所有割边及边双缩点后的点形成一棵树。
tarjan 算法中判定割边的核心:若一条树边的上方点为 \(u\),下方点为 \(v\),发现 \(low[v] > dfn[u]\),则该边为割边。证明可以直接利用 “扎口袋” 原理,得出 \(v\) 没有任何一条回边可以触及到 \(u\) 所在的连通块(最远只能触及到 \(u\) 的下方),因此 \((u, v)\) 成为了连接 \(u,v\) 所在连通块的唯一一条边,是割边。
tarjan 算法与 SCC缩点 一致,low 数组的定义不变,边的类型仍分为树边,回边和弃边三种。唯一不同的地方在于有向图变成了无向图,我们仍可以看作是有向图。
注意三种边中回边与弃边的定义有略微改变。
回边与弃边一定对应在一条无向边中(可以看作回边与弃边配对),即:从下往上看是回边的无向边,从上往下看是弃边。
树边所在无向边对应的另一条有向边一定会被跳过搜索,因此树边不与任何其他种类的边配对。
注意无向图的 tarjan 算法 dfs 每个结点时,一定要避免继续搜索父节点!!!
//边双连通分量 EDCC
//DCC缩点后形成的无向图是一棵树,SCC缩点形成DAG
//注:不能处理带重边和自环的图,需要改成链式前向星
vector<int> G[N];
int dfn[N], low[N], tim;
int stk[N],top;
int cnt_bri,cnt_edcc;
int edcc[N]; //dcc[u]表示点u在第dcc[u]个边双连通分量上
struct Bridge{
int u,v;
}bri[M];
void Tarjan_edcc(int u, int father){
dfn[u] = low[u] = ++tim;
stk[++top] = u;
for(auto v : G[u]){
if(!dfn[v]){ // (u, v) 为树边
Tarjan_edcc(v, u);
low[u] = min(low[u], low[v]);
if(low[v] > dfn[u]){ // 割边
bri[++cnt_bri] = {u, v};
}
}
else if(v != father){ // 回边和弃边的情况统一处理(弃边相当于不执行,因为一定有 dfn[v] > dfn[u] >= low[u]),但一定要保证避免回到父节点
low[u] = min(low[u], dfn[v]);
}
}
if(dfn[u] == low[u]){
++cnt_edcc;
int x;
do{
x = stk[top--];
edcc[x] = cnt_edcc;
} while(x != u);
}
}
当无向图中存在重边时,必须用链式前向星来建图:
// 含重边,自环版本 -> 需要将每个重边用不同的编号区分,因此使用链式前向星来存图
int h[N], e[M<<1], ne[M<<1], idx = 1;
int dfn[N], low[N], tim;
int stk[N],top;
int cnt_bri,cnt_edcc;
int edcc[N]; //edcc[u]表示点u在第dcc[u]个边双连通分量上
struct Bridge{
int u,v;
}bri[M]; //割边
void addedge(int a,int b){
e[++idx] = b; // 注意,边的下标要从2开始,便于利用异或操作判断重边
ne[idx] = h[a];
h[a] = idx;
}
void Tarjan_edcc(int u, int in_edge){
dfn[u] = low[u] = ++tim;
stk[++top] = u;
for(int i = h[u]; i ; i = ne[i]){
int v = e[i];
if(!dfn[v]){
Tarjan_edcc(v, i);
low[u] = min(low[u], low[v]);
if(low[v] > dfn[u]){
bri[++cnt_bri] = {u, v};
}
}
else if(i != (in_edge ^ 1)){
low[u] = min(low[u], dfn[v]);
}
}
if(dfn[u] == low[u]){
++cnt_edcc;
int x;
do{
x = stk[top--];
edcc[x] = cnt_edcc;
}while(x != u);
}
}
割边分隔两个边双连通分量。
P7687
Q 省略。
显然关键边必须是割边,否则删除这条边不会影响原图的连通性,进而不会改变每个结点获得两种服务的状态。
考虑某一条割边,它是关键边,当且仅当对于其连接的两个连通块,每个连通块中提供某种服务的点数量必须 \(>0\)。
边双缩点后 dfs 割边树,分别统计每条割边连接的两个连通块中服务点的数量即可。
CF118E
Q:给定 \(n\) 个点 \(m\) 条边的无向连通图,需要为每条无向边指定一个方向,使得形成的有向图是一个强连通图(任意两点之间相互可达),求出任意一种合法方案。
显然,若原图存在割边则一定无解。
构造方案需要在 tarjan 求割边的过程中做:从有向图的 tarjan 算法可知,只需要保留树边和回边。因此在无向图上跑 tarjan 时记录所有树边和回边即可。
hdu2460
EDCC 缩点结合并查集
Q: 给定 \(n\) 个点 \(m\) 条边的无向连通图,处理 \(q\) 条操作,每次操作添加一条边 \((u, v)\),计算每次加边后整张图中割边的数量。
考虑割边树,有一条很显然的性质:边双连通分量数量 = 割边数量+ 1。因此我们只需要维护边双连通分量的个数。
考虑新加边 \((x, y)\) 对应的 \(x, y\) 两点是否在同一个 EDCC 内:
- 若 \(x, y\) 在原图中属于同一个 EDCC,从割边树的视角看,整个图的割边树不会改变,EDCC 数量也不变。
- 否则,\(x, y\) 在原图中不属于同一个 EDCC,这次加边会导致割边树上产生环,环中的所有 EDCC 会合并成一个 EDCC。
合并两个 EDCC 可以通过并查集合并。对于每次询问,暴力向上跳 \(x, y\),只要发现 \(x\) 与 \(fa[x]\) 不属于同一个 EDCC 就合并。整体复杂度均摊下来是 \(O(割边数量)\)。
P2860
Q:给点 \(n\) 个点 \(m\) 条边的无向连通图,求至少添加多少条边,使得整张图是一个 EDCC。
答案为 \(\lceil\frac{割边树中度数为1的结点数量}{2}\rceil\)。
CF652E
Q:给定 \(n\) 个点 \(m\) 条边的无向连通图,每条边上要么有商品,要么无商品。给定 \(s,t\),问是否存在一条从 \(s\) 到 \(t\) 的路径,使得路径上每条边不重复,且存在商品。
先给出做法:求出割边树,考虑 \(s\) 所在边双结点到 \(t\) 所在边双结点的路径上的所有 割边 和 边双结点,只要存在商品就可行。
证明需要利用到一个新名词算法 —— 耳分解。
结论:一个无向图可以耳分解,当且仅当整个无向图是一个边双连通分量。于是,对于每个边双,我们需要利用耳分解证明出:
- 对于边双内的任意一条边 \(e\),以及任意两个点 \(u, v\),总是存在一条从 \(u\) 到 \(v\) 的、包含 \(e\) 的路径,使得该路径可经过重复的点,不经过重复的边。
证明:考虑耳分解的逆过程 —— 最初有一个简单环,往简单环中不断添加新耳朵。上述结论在简单环中显然成立,现在在图中添加一个新耳朵(挑选原图中的两个点作为端点并添加若干个新点,形成一条新路径),那么对于新图,原来的部分与新添加的部分必然存在至少两个点相交(这两个点也可以是同一个点),我们一定可以利用这两个点,使得新图中无论挑选哪条边,挑选的两个点无论在原图中还是在新添加的耳朵中,上述结论仍成立(具体就不证明了,不会描述qwq..)。那么每次加新耳朵的过程就是不断递推的过程,而由于边双一定可以耳分解,因此耳分解的逆过程一定可以形成任何一个边双。于是对于任意边双,上述结论一定成立。证毕。
P5234
边双缩点并不是这道题的重点,这里只记录一个 trick:检测树中的某个边集中的所有边是否完全在某条路径上,可以将边集内的边权置为 \(1\),不在边集内的边权置为 \(0\),然后求这棵树的直径是否等于所有边的总权值。
P7924
边双缩点结合树上点差分。
Q:给定 \(n\) 个点 \(m\) 条边的无向连通图,每个点有点权。一共 \(q\) 条操作,每次给定两个点 \(x,y\),计算 \(x\) 到 \(y\) 的全部可能的路径经过所有点的并集的权值之和。注意只需要求对于 \(q\) 条操作答案的总和,不需要分别求。
EDCC 缩点形成割边树后,每个询问的答案显然为两个点所在边双结点的路径权值之和。那么每条操作等价于经过了某条路径的所有边双结点一次。那么对于多次操作,显然可以利用点差分优化树上路径加。最终恢复差分得到每个边双结点的经过次数,统计答案即可。注意本题 \(n,m\) 特别大,因此尽量不要使用倍增求 \(LCA\),会多带个 \(\log\)。可以通过欧拉序预处理 \(lca\) 进而将求 \(lca\) 操作优化到 \(O(1)\)。
CF555E
Q:给定 \(n\) 个点 \(m\) 条边的无向图(不一定连通),给定 \(q\) 条要求:
- \(x\space y\):从 \(x\) 出发,可以到达 \(y\)
必须将每条无向边改成有向边,是否存在方案可以满足全部要求。
若存在 \(x,y\) 不连通的情况,直接判断为 No。
对所有连通块进行边双缩点后,在某一边双内,即使为边双内的每条边定向,仍然可以保证任意两点之间互相可达。因为由边双可耳分解的性质,我们可以把每个环与每个耳朵中的边全部钦定为一个方向,这样任意两个环之间就能够互相可达,进而任意两个点互相可达。
考虑在同一连通块,但不在同一边双内的两个点。对于割边树,两个点所在边双结点的路径上的所有边必须定向。于是,对于每条要求,等价于为割边树上的某条路径的所有边定向,我们只需要考虑是否存在某条边两个不同的方向同时存在操作钦定。可以利用树上边差分实现这一过程,具体实现见代码。
注意本题可能存在重边,跑边双时需要用链式前向星来建图!!!
P8867
边双缩点结合树形 dp。
Q:给定 \(n\) 个点 \(m\) 条边的无向连通图,对某个点集与边集构成的总集合计数(点集非空,边集可空;只要点集或边集不同,两个集合就不同),要求:任选不在边集内的一条边删除,不会影响点集内任意两点的连通性。
显然边双内的边是否删除不会影响任意两点的连通性,因此这些边是否加入到边集中并不重要,我们只需要考虑割边是否在边集中。于是问题貌似变成了一个树上计数 dp。我们只需要在割边树上选点集与边集。
需要发掘到一个很关键的性质:任意一个合法的点集与边集,在割边树上一定构成一个连通子图(这里指所有点必须由边集中的边构成一整个连通块,边可以多出来)。证明略。
借助这个性质,我们有了一个巧妙的状态定义:
状态定义
\(dp_{u}\):考虑子树 \(u\),至少选择一个点,对于任意被选择的点,其所在的边双结点到 \(u\) 的所有割边必须被选,满足全部要求的点集与边集的总集合的方案数。
为什么要这样定义状态呢?我们发现,对于任意一个连通子图,一定对应一个包含该整个连通子图的极小子树(该子树的根结点也一定在连通子图内),而这样的极小子树所选择的点集与边集是恰好对应了我们所定义的 dp 状态(相当于从结点 \(u\) 出发向子树 \(u\) 内部延伸构成的一个连通块,显然每个点到 \(u\) 的所有割边必选)。那么,对所有这样的连通子图计数,就等价于求所有子树 \(u\) 的 dp 值,而子树外的点一定都不选;子树外的割边可以自由选择(!!!需要特别考虑 \(u\) 上方那条边,必须不选,因为选择这条边的情况会在计算 \(dp_{fa[u]}\) 时再计算一次,需要容斥掉);而非割边无论如何都是可以自由选择的。即:
注意割边树的根结点 \(1\) 上方无边,因此要特殊考虑。
那么剩下的问题就是如何得到 \(dp_{u}\)。
状态转移
初始时整个子树内只有点 \(u\),其中任意两点一定在同一边双中,因此初始化:
\(siz[u]\) 为 边双结点 \(u\) 内点的数量。
经典套路,考虑从已计算完的部分子树 \(u\) 合并新的子树 \(v\)。
从以下几个角度思考转移式:
- 子树 \(u\) 内选点,子树 \(v\) 内不选点:此时 \((u, v)\) 可自由选择,子树 \(v\) 内的割边也可自由选择。
- 子树 \(u\) 内不选点,子树 \(v\) 内选点:子树 \(u\) 内的割边可自由选择;为了保证 \(v\) 中所有被选的点到 \(u\) 上的割边都选,\((u, v)\) 必选。
- 子树 \(u,v\) 内均选点:显然 \((u, v)\) 必选。
割点,点双连通分量 VDCC
// 割点:对于一个无向图,若删掉某个点及该点引出的所有边,图中的连通块数量增加,则该点为割点
// 图中有重边和自环不会影响割点的判定
int n,m;
vector<int> G[N];
int dfn[N],tim;//时间戳数组
int low[N];//low[u]表示从结点u出发,可以通过返祖边和横叉边回溯到的最小dfn
bool cutpoint[N]; //判断u是否为割点
void Tarjan_CutPoint(int u, bool isroot){ // isroot表示点u是否为搜索树的根,对应连通块第一个遍历到的点
dfn[u] = low[u] = ++tim;
int child = 0;
for(auto v : G[u]){
if(!dfn[v]){ // 树边
child ++; // 树边儿子增加
Tarjan_CutPoint(v, false);
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]){
// 不是根时只需要存在一个儿子的low[v]>=dfn[u]
// 若为根节点,则至少需要在树边上存在至少2个儿子
if(!isroot || child >= 2){
cutpoint[u] = true;
}
}
}
else{ // 回边或弃边
low[u] = min(low[u], dfn[v]);
}
}
}
loj10103
Q:给定一个无向图,求删除某个点后,整张图中连通块的最大数量。
枚举删除哪个点,计算新增连通块的数量即可。
跑一遍 tarjan 割点。对于每个连通块的根结点,若删除它,则新增连通块数为与其相连接的树边数量;若不是所在连通块的根结点,则删除它,新增连通块数为所有满足 \(low[v] \ge dfn[u]\)的邻结点 \(v\) 个数 + 1(因为还有上方的一个连通块)
P5058
Q:给定无向图,并给定两个点 \(a,b\)。初始时 \(a,b\) 连通,找到删除某个点能让 \(a,b\) 不再连通的最小编号结点。
结论:以 \(a\) 为其所在连通块的根结点,跑一次 tarjan。若发现删除某个点 \(u\) 时,该点是割点,且 \(b\) 在 \(u\) 的某个儿子 \(v\) 的子树内,那么删除这个结点就可以使得 \(a,b\) 不再连通。
那么剩下的问题就是如何 在确认某个点是割点的情况下 判断 \(b\) 是否在其某个儿子 \(v\) 子树中:很简单,只需要看 \(dfn_{b} \ge dfn_{v}\) 是否成立即可。因为如果 \(dfn_{b} < dfn_{v}\),则一定是在遍历 \(b\) 后才遍历到 \(v\),显然 \(b\) 不在 \(v\) 的子树内;若未遍历到 \(b\) 时遍历到了 \(v\),且 \(b\) 不在 \(v\) 的子树内,则判断该条件时 \(dfn_{b}\) 为 \(0\)。将 \(u\) 删除,整个子树 \(v\) 都会与根结点 \(a\) 失去联系(\(a\) 在 \(u\) 的上方父亲处的连通块),子树 \(v\) 中的 \(b\) 自然也不会再与 \(a\) 连通。
下面的每一个圈就是一个点双。可见割点划分了点双连通分量,且一个割点可能会在多个点双内(取决于在 dfs 树中的邻结点数量)
//点双连通分量 VDCC
//注意需要处理自环
int n,m;
vector<int> G[N];
int dfn[N], low[N], tim;
int stk[N], top;
vector<int> VDCC[N]; // VDCC[i]存储第i个点双连通分量中的所有点
bool cutpoint[N];// 记录割点
int cnt_vdcc;
void Tarjan_vdcc(int u, bool isroot){
if(G[u].size() == 0){ // 孤立点,也算作是一个点双连通分量,需要特判
VDCC[++cnt_vdcc].push_back(u);
return;
}
dfn[u] = low[u] = ++tim;
stk[++top] = u;
int child = 0;
for(auto v : G[u]){
if(!dfn[v]){
Tarjan_vdcc(v, false);
low[u] = min(low[u], low[v]);
if(low[v] >= dfn[u]){
child++;
if(!isroot || child >= 2){ // 记录割点
cutpoint[u] = true;
}
VDCC[++cnt_vdcc].push_back(u); // 处理新的点双
int x;
do{
x = stk[top--];
VDCC[cnt_vdcc].push_back(x);
}while(x != v);
}
}
else{
low[u] = min(low[u], dfn[v]);
}
}
}
LC LCP54
根据题意,占据某个点,对于整张图中未被占据的点的连通性变化,等价于删除掉已被占据的结点和其所有出边。
为了满足 “未被夺取的据点始终在同一个连通区里” 这一条件,
当存在不止一个点双时,不能从该点双的割点开始占据,否则会导致 “未被夺取的据点始终在同一个连通区里” 这一条件不成立。故每个只含有一个割点的点双需要从某个非割点出发,占据整个点双;而对于存在多个割点的点双,可以在处理完倒数第二个只含有一个割点的点双后,顺带着无代价处理掉该点双和最后一个只含有一个割点的点双。
因此,最优解为:
- 整个图中只存在一个点双时,该点双一定不存在割点。此时从最小权值点出发占据所有据点即可。
- 整个图中存在多个点双时,对于只有一个割点的点双,每次花费 从该点双内除割点外的最小权值点出发 的代价,将整个点双占据(注意割点不占据)。为了让代价最小,可以按该最小权值升序依次处理每个含有唯一割点的点双;等到处理完最小权值倒数第二大的点双时,会从该点双的唯一割点出来,然后顺带着将剩下的含有唯一割点的点双无代价地处理掉(在处理完每一个含有唯一割点的点双后,该割点就不再是割点了;最终只剩下一个含有唯一割点的点双时,可以直接从该割点出发(接着刚处理过的点双走,不花费该割点的权值代价),顺带着被处理掉)。
文字叙述起来有些复杂,具体细节见左神视频。
P3225
Q:给定 \(n\) 个点 \(m\) 条边的连通无向图,每个点均可以被设置或不设置为标记点。求 最少标记点数量 以及 设立最少标记点的方案数,使得任何一个点及其出边删除后,剩下的所有点均与至少某一个标记点连通。
还是分成整张图 只含有一个点双 与 含有多个点双 两种情况讨论:
- 只含有一个点双时,整张图不存在割点。显然最少需要标记 \(2\) 个点。因为若只标记一个点,则删除该标记点时便不符合要求。由于在点双内,删除任意一个点都能保证其他点仍然连通,因此可以标记任意两个不同点,方案数为 \(C_{siz}^{2}\)。
- 含有多个点双时,首先割点被标记是没用的,因为若删除的是该割点,则边双内剩余点便会与外界不连通。因此对于只存在一个割点的点双,需要至少标记一个非割点;而对于存在多个割点的点双,由于只会删除一个点,因此这里的点不需要被标记,因为删除任意一个割点,一定还会剩下至少一个割点,该割点所在点双内部的标记点便与该点双内的点连通。
SP2878
点双连通分量 结合 染色法判断二分图。
由于 \(n \le 10^{3}\),完全图总边数也不是很多,我们可以考虑建出原图关于完全图的反图,也就是只要两个骑士不相互厌恶,就连边,表示他们在圆桌中可以相邻;两点之间无直接连边,则这两个骑士在圆桌上一定不能相邻。也就是说,对于圆桌中任意两个相邻点,在新图中一定存在这两个点的直接连边。
我们有结论:对于 \(\forall\) 两个点 \(a,b\),若 \(a, b\) 不在同一个点双内(也就是存在某个割点 \(X\),删除这个点后 \(a,b\) 不再连通),则这两个点代表的骑士一定无法参与同一场会议。
证明:利用反证法。由于 \(a,b\) 不在同一个点双内,显然 \(a,b\) 之间一定不存在直接连边。假设 \(a,b\) 在同一个圆桌中(可以不相邻),那么在圆桌中,\(a, b\) 之间的 \(2\) 个部分必须均能间接联系这两个点,且这两个部分交集为空。而由上图,显然对于 \(a, b\) 之间的任何一条路径,必须经过 \(X\),因此 \(X\) 无论放在哪一侧,另一侧总无法间接地连接 \(a,b\),进而无法满足 \(a,b\) 同时在圆桌中存在,证毕。
因此,对于任意一种合法方案,在圆桌中的所有骑士,必须属于同一个点双。故我们只需要对每个点双分别检查就可以了。
结论:只要点双内存在奇环,那么该点双内的每个点均可以出现在圆桌中;否则该点双内的任何点都不能出现在圆桌中。
证明:如上图所示。考虑该点双内存在的奇环,其自身一定作为一种合法方案(因为对于任何一个奇环,就是一种合法方案)。而点双内任意两点均连通,因此我们可以指定该奇环中的某两个不同的点 \(u,v\),并考虑点双内奇环外的某个点 \(X\),一定存在 \(u\) 到 \(X\) 的某条路径与 \(v\) 到 \(X\) 的某条路径,共同拼成该奇环的一个耳朵(耳分解)。而对于 \(u,v\) 所在的奇环,其两侧内的点数必然是一奇一偶,因此,不论 \(u,v\) 之间包含 \(X\) 的那条路径长度的奇偶性,一定可以形成一个包含 \(X\) 的奇环。进而我们得到:对于点双内的任何一个点,只要内部存在奇环,那么所有点一定均在某个奇环内出现,进而每个点都存在一种合法方案被包含,证毕。
反之,某个点不出现在任何存在奇环的点双中,是不能出现在圆桌中的,因为一种合法方案形式上对应的就必须是一个奇环。
因此本题的解法就是:对于每个点双判断内部是否存在奇环,若存在,则将该点双内的每个点均标记为可行,答案即为不可行结点的数量。判断奇环可以利用二分图染色法在 \(O(n + m)\) 复杂度内完成。

















浙公网安备 33010602011771号