题目描述
给定一个 n 个点 m 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。
输入
第一行两个正整数 n,m。
第二行 n 个整数,其中第 i 个数 ai 表示点 i 的点权。
第三至 m+2 行,每行两个整数 u,v,表示一条 u→v 的有向边。
输出
共一行,最大的点权之和。
样例输入输出
输入
2 2
1 1
1 2
2 1
输出
2
思路
缩点的概念是把一张图中所有的强连通分量缩成点,形成一个有向无环图(DAG)。
由于我之前的博客没有讲过强连通分量,这里一并讲一下。
强连通分量是"极大"的连通子图(即加入任何其他已有顶点或边都会破坏这个q
因此,强连通分量中,子图中任意两个顶点都是强连通的。
解决强连通分量问题,需要用到tarjan算法。这是一种基于深度优先搜索(DFS)的图论算法,主要用于求解有向图的强连通分量、无向图的割点与桥。
tarjan实现的时间复杂度为O(n+m),其中n,m分别是顶点和边。
实现本算法,一般需要以下数据结构:
cpp
int dfn[N]; // 时间戳:节点被访问的顺序
int low[N]; // 追溯值:节点能回溯到的最早时间戳
int ts; // 全局时间计数器
stack<int>stk; // 栈:存储正在探索的节点
bool instk[N]; // 标记节点是否在栈中
int scc[N]; // 节点所属的强连通分量编号
int sn; // 强连通分量数量
tarjan有不同的形式,在这里只介绍求强连通分量的写法:
cpp
void tarjan(int u)//u:起始节点
{
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])//找到了一个强连通分量
{
int t;
sn++;
do
{
t=stk.top();
stk.pop();
instk[t]=false;
scc[t]=sn;
id2[sn]+=id[t];
}while(t!=u);
}
}
现在,scc数组就保存了每个缩点包含的原来的节点的信息,为下一步拓扑排序打好了基础。
由于缩点后的图是无环的,因此可以用DP求最长路径求解。
因为DP是无后效性的,所以本题需要用到拓扑排序。拓扑排序之前已经讲过,这里再次提一下。
拓扑排序两要素:
1.有向无环图
2.如果存在边 u→v,则u一定排在v前面
现在来看DP。本题中,dp[i]表示以第i个强连通分量为起点的最大权值和。
状态转移方程如下:
初始为dp[i] = id2[i],其中id2[i]表示缩点后顶点i的权值。
如果u到v有边,则dp[v] = max(dp[v], dp[u] + id2[v])。
具体步骤如下:
STEP 1:定义变量,除了输入变量和tarjan所用外,还包括:
1.deg[N]:缩点后每个分量的入度
2.id2[N]:缩点后每个分量的权值和
3.dp[N](dp[i]:以分量i为起点的最大权值和)
4.vector<int>edge[N]:原始图
5.vector<int>dag[N]:缩点后的DAG图
STEP 2:tarjan,基本与上面模板一样,但是找到强连通分量后要对id2数组更新。
STEP 3:写拓扑排序+DP,逻辑与上面完全一致。
STEP 4:按要求输入,建有向图,然后调用tarjan算法(注意:没保证这个图连通,所以要枚举每一个顶点调用tarjan!!!)
STEP 5:tarjan调用完毕后,根据scc数组建缩点图并统计每个所点的入度。
STEP 6:调用拓扑排序,对dp取max,然后输出ans即可。
代码
cpp#include<bits/stdc++.h> #define int long long #define N 10005 #define Letian 14 using namespace std; int n,m,u,v; int id[N];//id[i]:i的点权值; int dfn[N];//dfn[i];顶点i的时间戳; int low[N],ts;//low[N]:顶点i的追溯值,ts:时间戳; int scc[N];//scc[i]:顶点i所属的强连通分量编号 int sn;//sn:数量; int deg[N];//deg[i]:缩点后i顶点的入读 int ans,id2[N]; //id2[i]:缩点后i顶点的权 int dp[N];//dp[i]:从scc[i]开始的最大权值和 vector<int>edge[N];//原始图 vector<int>dag[N];//建缩点图 stack<int>stk;//算法栈 bool instk[N];//instk[i]:顶点i是否在站内 void tarjan(int u) { 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]) { int t; sn++; do { t=stk.top(); stk.pop(); instk[t]=false; scc[t]=sn; id2[sn]+=id[t]; }while(t!=u); } } void topodp()//对缩点后的图拓扑+dp { queue<int>q; for(int i=1;i<=sn;i++) { dp[i]=id2[i];//自己 if(deg[i]==0) { q.push(i); } } while(!q.empty()) { int u=q.front(); q.pop(); for(int v:dag[u])//遍历缩点以后的图 { dp[v]=max(dp[v],dp[u]+id2[v]); if(--deg[v]==0) { q.push(v); } } } } signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); cin>>n>>m; for(int i=1;i<=n;i++) { cin>>id[i]; } for(int i=1;i<=m;i++) { cin>>u>>v; edge[u].push_back(v); } //调用tarjan算法 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]) { deg[scc[v]]++; dag[scc[u]].push_back(scc[v]);; } } } topodp(); for(int i=1;i<=sn;i++) { ans=max(dp[i],ans); } cout<<ans; return 0; }运行结果

结语
文章的最后,送每位读者一句话:
只要你愿意,没有什么是能够难住你的思考的。
感谢阅读,我们下期再会。
如果您喜欢本文,请您点赞、收藏加关注,以防你找不到回来的路,谢谢!