这几道题都是 Tarjan 和其他算法的综合,难度较大,含金量较高,特别是第二题。
我的博客:关于 Tarjan 算法的入门博客。
传送门 - 板子题 B3609 题解
开始——
例题一 受欢迎的牛
传送门 - P2314 受欢迎的牛 G
思路
概述: Tarjan + 缩点 DAG + 出入度
1. Tarjan
在一个强连通分量中,很明显,所有奶牛都是互相喜欢的,所以我们分别求出每一个强连通分量,并记录下他们的 size 。
2. 缩点 DAG
我们把强连通分量看成一个超级点,并在相互之间建边。
(图片附在第 3 点中。)
缩点是什么?
如果你没接触过缩点,没有关系,缩点算法博客 + 很好的阅读体验 。
如何建边?
我们输入时存下每一条边的 u 和 v ,然后现在判断他们是否属于同一强连通分量中,如果不属于,就在他们之间建边。注意,在这之前要先清空结构体 e ,数组 hd 以及变量 cnt 。
这样,缩完点之后它就是一个 DAG ——有向无环图,我们运用的就是它不成环的特质。
但是!!!这道题不用这么麻烦!
我们在思想上把它们重新建边了,但在实现过程中,我们只需要判断两点是否在同一强连通分量里,如果没有,就进行建边之后的操作(具体见第 3 点或代码);如果有,那可以直接忽略。
缩点有什么用?
缩点之后我们可以大大简化这道题了。对于每一个超级点(也就是一个强连通分量),我们有进一步关于出度的操作了。
3. 出度的运用
接下来我们把图看成一个只有 3 个点的 DAG 。
首先放几张做了二十分钟的百万图片 (wtcl)。
ps:关于图片,看不清请打开一个新标签页,放大看。由于本人第一次画图,所以丑了点…图 1 中有一条画错的边,已改正,正确边为从 E 到 G 。
从上图不难看出,有 3 个强连通分量,我们的第一步操作为反向建边,操作之后图为这样。
出度有什么作用?
它就是我们用来找出符合条件的超级点。
因为每一个超级点要被所有的点(也就是牛)所喜欢,那么它必须满足出度为 0 。
假设我们有一条边为 ( u , v ) (u, v) (u,v) ,表示从 u 到 v ,此时 u 的出度为 1 , v 的出度为 0 。
只有一个超级点出度为 0 ,也就是它没有喜欢的奶牛了,才可能是受欢迎的牛。因为此图没有环,所以它的喜欢不会回来,它也就没有受到所有牛的喜欢。
同时,若该图有两个或两个以上的出度为 0 的点,此题答案依旧为 0 。为什么?还是那句话,因为现在的 DAG 不成环。相信不用我再说,这点已经很好理解了。
为什么要反向建边?
若是正常地建边,我们在计算出度的时候会较为麻烦,所以反向建边,这样一来在统计出度的时候就可以变成“统计入度” 了,相对来说简单很多。
最终,我们的图会变成这样。
这题说到这里,相信已经不用我再说什么了。(好累啊,逃)
数据范围也没有什么特别的,正常设就行。
最后,代码附上。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 50005;
int n, m;
int cnt, hd[maxn];
struct node{
int to, nxt;
}e[maxn * 2];
int dfn[maxn], low[maxn];
int top, st[maxn], de[maxn], si[maxn];
int col, co[maxn];
int tmp;
void add (int u, int v)
{
e[++cnt].to = v;
e[cnt].nxt = hd[u];
hd[u] = cnt;
}
void tarjan (int u)
{
dfn[u] = low[u] = ++tmp;
st[++top] = u;
for (int i = hd[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v])
{
tarjan (v);
low[u] = min (low[u], low[v]);
}
else if (!co[v]) low[u] = min (low[u], dfn[v]);
}
if (dfn[u] == low[u])
{
co[u] = ++col;
++si[col];
while (st[top] != u)
{
++si[col];
co[st[top]] = col;
--top;
}
--top;
}
}
int main ()
{
scanf ("%d %d", &n, &m);
for (int i = 1; i <= m; i++)
{
int u, v;
scanf ("%d %d", &u, &v);
add (v, u);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i]) tarjan (i);
}
for (int i = 1; i <= n; i++)
{
for (int j = hd[i]; j; j = e[j].nxt)
{
int v = e[j].to;
if (co[i] != co[v]) de[co[v]]++;
}
}
int ans, u;
ans = u = 0;
for (int i = 1; i <= col; i++)
{
if (!de[i]) ans = si[col], u++;
}
if (u == 1) printf ("%d\n", ans);
else printf ("0\n");
return 0;
}
例题二 最大半联通子图
P2272 [ZJOI2007]最大半连通子图 - 传送门
题目大意
强连通分量我们都知道,这题问的是“半连通分量”——跟强连通分量差不多,半连通子图是指在有向图 G 中,在任意两点 u,v 之间有一条从 u 到 v 的路径,或从 v 到 u 的路径。
这道题要我们求的就是最大半连通子图的节点数以及数量。(是的,别被那么复杂的题面骗了。)
求解最大半连通子图
PRE 概述
这道题难度较大,综合性较强,除此之外也没啥了。
主要运用三个算法:Tarjan、拓扑排序、DP。
STEP 1 Tarjan 缩点
毋庸置疑,每一个强连通分量都是一个半连通子图。所以,我们可以在求出所有强连通分量的个数以及节点数后,缩点,让图变成一个 DAG。( Tarjan 代码操作这里不说了。)
在缩点前我们还有很重要的一步——去重边。
(注意:“重”念作 c h o ˊ n g ch \acute{o} ng choˊng,读错音容易影响理解。)
-
为什么要去重边?
为了后续使用拓扑排序,我们就需要统计入度(啊不用反向建边),那么重边就容易让入度值偏大,导致错误。
-
怎么去重边?
具体我们有两步预操作,实际操作是在缩点的时候一起判:
-
重新遍历每条边,让初始点、终点的点编号变成它所在的强连通分量的编号—— x i ← c o ( x i ) x_i \gets co(x_i) xi←co(xi);
-
给所有边排序(这样才能在缩点时判重边),排序时按初始点的编号大小,相等则按终点的标号大小,小的在前。
代码操作如下:
inline bool cmp (int a, int b) { if (x[a] != x[b]) return x[a] < x[b]; return y[a] < y[b]; } int n[maxn]; inline void remove () { for (int i = 1; i <= m; i++) { nu[i] = i; x[i] = co[x[i]]; y[i] = co[y[i]]; } sort (nu + 1, nu + m + 1, cmp); }
-
然后就是缩点操作了。
记得清空数组变量结构体。
我们缩点前判重,然后边缩点边统计入度。
-
遍历每条边(已排序);
-
若起始点和初始点不相同(不在同一个强连通分量)且不和上一条边相同,就:
-
统计入度—— d e ( y ( n u i ) ) ← d e ( y ( n u i ) ) + 1 de(y(nu_i)) \gets de(y(nu_i))+1 de(y(nui))←de(y(nui))+1( nu 的作用从上面代码可以看出);
-
建边。
-
代码操作如下:
inline void new_map ()
{
cnt = 0;
memset (hd, 0, sizeof hd);
memset (e, 0, sizeof e);
for (int i = 1; i <= m; i++)
{
int op = nu[i];
if ((x[op] != y[op]) and (x[op] != x[nu[i - 1]] or y[op] != y[nu[i - 1]]))
{
de[y[op]]++;
add (x[op], y[op]);
}
}
}
STEP 2 拓扑排序 + DP
对于一幅新图(还是 DAG),我们易得:
-
求最大半连通子图的节点数:求最长链长度。
为什么是链?
因为它不能有分支。证明:不难知道,对于一条为半连通子图的链:
-
有边所连接起来的两点(此时已缩点)的边有且仅有一个方向;
-
整条链的各个边的方向一致(抽象)。
既然已知这两点,那么无论分支与链的连接边的方向如何,它都不能满足与链上所有点之间都有连通路径。
所以,我们要求的最大半连通子图,就是在 DAG 上的一条链。
-
-
求最大半连通子图的个数 —— 最长链个数。
-
拓扑排序初始入队:
我们分别开两个数组,dis 和 e:dis 统计长度,类似(就是) spfa 中的 dis,松弛操作更新它;ec 统计长度相等的链的个数。
我们开一个变量 ans 表示当前最长链链顶的编号。
初始操作和拓扑模板一样,找到入度为 0 的压入栈即可。
代码:
inline void init ()
{
for (int i = 1; i <= col; i++)
{
if (!de[i])
{
ue[++w] = i;
dis[i] = si[i];
ec[i] = 1;
if (dis[ans] < dis[i]) ans = i;
}
}
}
-
拓扑排序及递推 dis:
主要的流程跟拓扑模板没有什么太大出入,重点是在松弛时的操作。
松弛的时候有先后顺序:
- d i s v < d i s u + s i z e v dis_v < dis_u+size_v disv<disu+sizev (临时最长距离被更新,重新统计方案数):用后者松弛前者,那么就 e c v ← 0 ec_v \gets 0 ecv←0 (清空以前以 v 结尾的链的个数);
这时候你肯定会问:为什么 e c v ← 0 ec_v \gets 0 ecv←0 呢,怎么会没有个数呢?
-
第二步,我们就更新了 e c v ec_v ecv。 d i s v = d i s u + s i z e v dis_v=dis_u+size_v disv=disu+sizev 统计方案数即可。
-
若以前已经统计过以 v 结尾且长度和现在从 e 到 v 的链相等,直接在 v 的数量上加上 e 的数量;
-
如果这是目前以 v 为结尾的链最长的一条且以前没有统计过这个长度的链呢?这时候要注意,我们的两步是并行的,所以,此时 v (简写了)的数量就等于以 e 结尾的数量。
总的来说:两步是并行的。
-
最后别忘了每次入度变为 0 的点要压入栈。
代码:
inline void topo ()
{
while (t < w)
{
int u = ue[++t];
for (int i = hd[u]; i; i = e[i].nxt)
{
int v = e[i].to;
de[v]--;
if (dis[v] < dis[u] + si[v])
{
dis[v] = dis[u] + si[v];
ec[v] = 0;
if (dis[ans] < dis[v]) ans = v;
}
if (dis[v] == dis[u] + si[v]) ec[v] = (ec[v] + ec[u]) % mod;
if (!de[v]) ue[++w] = v;
}
}
}
最后的最后,统计一下答案总数即可。
完整代码:
#include<bits/stdc++.h>
using namespace std;
inline int read ()
{
int x = 1, s = 0;
char ch = getchar ();
while (ch < '0' or ch > '9'){if (ch == '-') x = -1; ch = getchar ();}
while (ch >= '0' and ch <= '9'){s = s * 10 + ch - '0'; ch = getchar ();}
return x * s;
}
const int maxn = 1500005;
int n, m, mod;
int cnt, hd[maxn];
struct node{
int nxt, to;
}e[maxn * 2];
int x[maxn], y[maxn];
inline void add (int u, int v)
{
e[++cnt].nxt = hd[u];
e[cnt].to = v;
hd[u] = cnt;
}
int dfn[maxn], low[maxn], si[maxn];
int de[maxn], ue[maxn], st[maxn], co[maxn];
int col, top, tmp;
inline void tarjan (int u)
{
dfn[u] = low[u] = ++tmp;
st[++top] = u;
for (int i = hd[u]; i; i = e[i].nxt)
{
int v = e[i].to;
if (!dfn[v]) {tarjan (v); low[u] = min (low[u], low[v]);}
else if (!co[v]) low[u] = min (low[u], dfn[v]);
}
if (dfn[u] == low[u])
{
co[u] = ++col, si[col]++;
while (st[top] != u)
{
si[col]++;
co[st[top]] = col;
top--;
}
top--;
}
}
int t, w;
int ans;
int ec[maxn], dis[maxn];
inline bool cmp (int a, int b)
{
if (x[a] != x[b]) return x[a] < x[b];
return y[a] < y[b];
}
int nu[maxn];
inline void remove ()
{
for (int i = 1; i <= m; i++)
{
nu[i] = i;
x[i] = co[x[i]];
y[i] = co[y[i]];
}
sort (nu + 1, nu + m + 1, cmp);
}
inline void new_map ()
{
cnt = 0;
memset (hd, 0, sizeof hd);
memset (e, 0, sizeof e);
for (int i = 1; i <= m; i++)
{
int op = nu[i];
if ((x[op] != y[op]) and (x[op] != x[nu[i - 1]] or y[op] != y[nu[i - 1]]))
{
de[y[op]]++;
add (x[op], y[op]);
}
}
}
inline void init ()
{
for (int i = 1; i <= col; i++)
{
if (!de[i])
{
ue[++w] = i;
dis[i] = si[i];
ec[i] = 1;
if (dis[ans] < dis[i]) ans = i;
}
}
}
inline void topo ()
{
while (t < w)
{
int u = ue[++t];
for (int i = hd[u]; i; i = e[i].nxt)
{
int v = e[i].to;
de[v]--;
if (dis[v] < dis[u] + si[v])
{
dis[v] = dis[u] + si[v];
ec[v] = 0;
if (dis[ans] < dis[v]) ans = v;
}
if (dis[v] == dis[u] + si[v]) ec[v] = (ec[v] + ec[u]) % mod;
if (!de[v]) ue[++w] = v;
}
}
}
int anss;
inline void ask ()
{
for (int i = 1; i <= n; i++)
{
if (dis[i] == dis[ans])
{
anss = (anss + ec[i]) % mod;
// cout << anss << " " << ec[i] << endl;
}
}
}
int main ()
{
n = read (), m = read (), mod = read ();
for (int i = 1; i <= m; i++)
{
x[i] = read (), y[i] = read ();
add (x[i], y[i]);
}
for (int i = 1; i <= n; i++)
{
if (!dfn[i]) tarjan (i);
}
remove ();
new_map ();
init ();
topo ();
ask ();
printf ("%d\n%d", dis[ans], anss);
return 0;
}
终于写完了…
—— E n d End End——