MYOJ_7788:(洛谷P3387)【模板】缩点(有关强连通分量)

题目描述

给定一个 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;
}
运行结果
结语

文章的最后,送每位读者一句话:

只要你愿意,没有什么是能够难住你的思考的。

感谢阅读,我们下期再会。

如果您喜欢本文,请您点赞、收藏加关注,以防你找不到回来的路,谢谢!

相关推荐
枫叶丹41 小时前
【Qt开发】Qt界面优化(六)-> Qt样式表(QSS) 伪类选择器
c语言·开发语言·c++·qt
小O的算法实验室1 小时前
2026年IEEE TCYB SCI1区TOP,少即是多:一种用于大规模优化的小规模学习粒子群算法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
0 0 02 小时前
CCF-CSP 34-2 矩阵重塑(其二)(reshape2)【C++】考点:矩阵转置模拟
开发语言·c++·算法·矩阵
二年级程序员2 小时前
一篇文章掌握“队列”
c语言·数据结构·算法
ArturiaZ2 小时前
【day33】
算法
Drifter_yh2 小时前
「JVM」Java 垃圾回收机制全解析:回收算法、分代流转与 G1 收集器底层拆解
java·jvm·算法
载数而行5202 小时前
算法系列3之拓扑排序
c语言·数据结构·c++·算法·排序算法
!停2 小时前
数据结构排序算法—插入排序
数据结构·算法·排序算法
s砚山s2 小时前
代码随想录刷题——二叉树篇(二十一)
算法