【题目链接】
ybt 1514:【例 2】最大半连通子图
洛谷 P2272 [ZJOI2007] 最大半连通子图
【题目考点】
1. 图论:强连通分量 缩点
2. 图论:拓扑排序 有向无环图动规
【解题思路】
对于图中任意两顶点u、v,满足u到v或 v到u有路径,该图就是单向连通图 。本题中的半连通图,指的就是单向连通图。
导出图,指的是选择顶点之间的所有边也都必须选择。
该题求图中最大的半连通子图,而且该图必须是导出图,也就是选择顶点数最多的单向连通子图,而且要同时选择选出顶点之间所有的边。
强连通图一定是单向连通图,因此可以不用考虑各个强连通分量内部的情况,可以将每个强连通分量视为一个顶点,进行缩点。缩点后,每个顶点(强连通分量)的权值是该强连通分量中顶点数量。
有向无环图中的单向连通子图中的顶点一定是图中一条路径上的顶点。
反证法:一条路径上的顶点是 A 1 , A 2 , . . . , A m A_1,A_2,...,A_m A1,A2,...,Am,假设存在顶点 B B B,顶点 B B B和顶点 A 1 , A 2 , . . . , A m A_1,A_2,...,A_m A1,A2,...,Am不会构成一条路径, A 1 , A 2 , . . . , A m , B A_1,A_2,...,A_m,B A1,A2,...,Am,B的导出图是一个单向连通图。
- 如果顶点 B B B到顶点 A 1 A_1 A1有边,则 B , A 1 , A 2 , . . . , A m B,A_1,A_2,...,A_m B,A1,A2,...,Am是一条路径,不符合假设。而该图是单向连通图,如果顶点 B B B到顶点 A 1 A_1 A1没有路径,那么必然存在顶点 A 1 A_1 A1到顶点 B B B的路径。
- 如果顶点 B B B到顶点 A 2 A_2 A2有边,则 A 1 , B , A 2 , . . . , A m A_1,B,A_2,...,A_m A1,B,A2,...,Am是一条路径,不符合假设。而该图是单向连通图,如果顶点 B B B到顶点 A 2 A_2 A2没有路径,那么必然存在顶点 A 2 A_2 A2到顶点 B B B的路径。
...- 依此类推,顶点 A 1 A_1 A1、 A 2 A_2 A2、...、 A m − 1 A_{m-1} Am−1到顶点B都有路径。
如果顶点 A m A_m Am到顶点 B B B有边,那么 A 1 , A 2 , . . . , A m , B A_1,A_2,...,A_m,B A1,A2,...,Am,B是一条路径,不符合假设。
如果顶点 B B B到顶点 A m A_m Am有边,那么 A 1 , A 2 , . . . , A m − 1 , B , A m A_1,A_2,...,A_{m-1},B,A_m A1,A2,...,Am−1,B,Am是一条路径,不符合假设。
因此假设不成立,原命题得证。
在缩点后的图中,找到点权加和最大的路径,选择该路径上的顶点(强连通分量)在原图中对应的顶点,以及这些顶点之间的边构成的子图,就是原图中的最大半连通子图。
缩点后图中点权加和最大路径的点权加和,就是原图中最大半连通子图包含的顶点数量。
而后还需要求最大半连通子图的数量,这里可以通过统计点权和为最大路径点权和的路径数量,来统计半连通子图的数量。
两个连通分量中可能有多条边相连,缩点后的图中两个顶点之间就可能有多条边,即为重边。如果缩点后的图中保留重边,假设顶点A到顶点B有两条重边,那么顶点A到顶点B会被认为有两条路径。而本题实际求的是半连通子图的数量,半连通子图必须是导出图,这里选择了顶点A和顶点B,那么顶点A、B之间的所有边都必须被选择,实际只有一个半连通子图。为了使路径数量和半连通子图一致,本题必须
在缩点后的图中去掉所有重边 ,此处可以使用与离散化类似的方法完成对重边去重。
接下来就是在缩点后的图中,求有向无环图中点权加和最大的路径的点权加和,具体方法见该题:
信息学奥赛一本通 1262:【例9.6】挖地雷 | 洛谷 P2196 [NOIP1996 提高组] 挖地雷
d p [ i ] dp[i] dp[i]表示以顶点i为终点的点权加和最大的路径的点权加和,在拓扑排序过程中可以求出 d p dp dp数组的值。求 d p dp dp数组最大值的下标为 m x i mxi mxi,顶点 m x i mxi mxi就是点权加和最大的路径的终点。 d p [ m x i ] dp[mxi] dp[mxi]就是第一个要输出的解。
为了求出最大半连通子图的数量,在拓扑排序时还需要进行另一个动规状态求解:
状态定义 c n t [ i ] cnt[i] cnt[i]:以顶点i为终点的点权加和为最大值 d p [ m x i ] dp[mxi] dp[mxi]的路径的数量。
在拓扑排序过程中,在访问u的邻接点v时:
- 如果 d p [ v ] < d p [ u ] + w [ v ] dp[v] < dp[u]+w[v] dp[v]<dp[u]+w[v],( w [ v ] w[v] w[v]是顶点v的点权)。那么要更新 d p [ v ] = d p [ u ] + w [ v ] dp[v]=dp[u]+w[v] dp[v]=dp[u]+w[v],经过顶点u再到顶点v的路径的点权加和最大,到顶点v点权加和为 d p [ v ] dp[v] dp[v]的路径数量就是到顶点u点权加为 d p [ u ] dp[u] dp[u]的路径数量,也就是 c n t [ v ] = c n t [ u ] cnt[v] = cnt[u] cnt[v]=cnt[u]
- 如果 d p [ v ] = = d p [ u ] + w [ v ] dp[v] == dp[u]+w[v] dp[v]==dp[u]+w[v],那么 d p [ v ] dp[v] dp[v]无需更新,但到达顶点v的点权加和为 d p [ v ] dp[v] dp[v]的路径增多了,增加了到达顶点u点权加为 d p [ u ] dp[u] dp[u]的路径数量,即 c n t [ v ] + = c n t [ u ] cnt[v] += cnt[u] cnt[v]+=cnt[u],该题路径数量需要对 X X X取模,所以增加后应该再模X,写为
cnt[v] = (cnt[v]+cnt[u])%x
【题解代码】
解法1:图论 tarjan求强连通分量 缩点 拓扑排序DP
cpp
#include<bits/stdc++.h>
using namespace std;
#define N 100005
int n, m, x, dp[N], w[N], cnt[N], cntAns, dfn[N], low[N], ts, scc[N], sn, degIn[N], mxi = 1;
stack<int> stk;
bool inStk[N];
vector<int> edge[N], g[N];//edge:原图 g:缩点后的图
void tarjan(int u)
{
int t;
dfn[u] = low[u] = ++ts;
stk.push(u);
inStk[u] = true;
for(int v : edge[u])
{
if(dfn[v] == 0)
{
tarjan(v);
low[u] = min(low[u], low[v]);
}
else if(inStk[v])
low[u] = min(low[u], dfn[v]);
}
if(dfn[u] == low[u])
{
++sn;
do
{
t = stk.top();
stk.pop();
inStk[t] = false;
scc[t] = sn;
w[sn]++;//连通分量sn中的顶点数,也就是缩点后顶点sn的权值w[sn]增加1
} while(t != u);
}
}
void topoSort()
{
queue<int> que;
for(int i = 1; i <= sn; ++i)
{
dp[i] = w[i];//dp[i]:缩点后以强连通分量i为终点的点权加和最大的路径的点权加和
cnt[i] = 1;
if(degIn[i] == 0)
que.push(i);
}
while(!que.empty())
{
int u = que.front();
que.pop();
if(dp[u] > dp[mxi])
mxi = u;//mxi:某个以mxi为终点的路径的点权加和最大
for(int v : g[u])
{
if(dp[v] < dp[u]+w[v])
{
dp[v] = dp[u]+w[v];
cnt[v] = cnt[u];
}
else if(dp[v] == dp[u]+w[v])
cnt[v] = (cnt[v]+cnt[u])%x;
if(--degIn[v] == 0)
que.push(v);
}
}
}
int main()
{
int a, b;
cin >> n >> m >> x;
for(int i = 1; i <= m; ++i)
{
cin >> a >> b;
edge[a].push_back(b);
}
for(int i = 1; i <= n; ++i) if(dfn[i] == 0)
tarjan(i);
for(int u = 1; u <= n; ++u)
for(int v : edge[u]) if(scc[v] != scc[u])
g[scc[u]].push_back(scc[v]);
for(int i = 1; i <= sn; ++i)
{
sort(g[i].begin(), g[i].end());
g[i].erase(unique(g[i].begin(), g[i].end()), g[i].end());//g[i]去重
}
for(int u = 1; u <= sn; ++u)
for(int v : g[u])
degIn[v]++;//degIn[i]:缩点后强连通分量i的入度
topoSort();
cout << dp[mxi] << '\n';
for(int i = 1; i <= sn; ++i) if(dp[i] == dp[mxi])//所有以i为终点的,点权加和为dp[mxi]的路径数量加和
cntAns = (cntAns+cnt[i])%x;
cout << cntAns;
return 0;
}