luogu P3387强连通缩点模板(tarjan+拓扑排序+DP)

题目描述

给定一个 nn 个点 mm 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。

思路

强连通模板题。

对于一个强连通分量,我们的选择就是把里面的点都走一遍,这肯定是最优的,也就是说一个强连通分量就相当于一个点,点权相当于这个分量里面所有点的权重和。
缩点后根据每个强连通分量的父节点重新建图,之后在DAG上跑DP。

如果有一条边从当前所在的点 u u u通向与其相连的点 v v v,那么转移方程如下,其中 w [ v ] w[v] w[v]是v点的点权。
d p [ v ] = m a x ( d p [ v ] , d p [ u ] + w [ v ] ) dp[v]=max(dp[v],dp[u]+w[v]) dp[v]=max(dp[v],dp[u]+w[v])

我们可以分别以每个点为起点跑一次,统计以此最大值,但是一个更好的方法是通过拓扑排序。

因为是DAG,我们可以通过拓扑排序确定这个图的起点,对于前一种方法,我们以每个点为起点,但是如果这个点的入度不是0,那么他肯定不是最优的,因为比他更优的是从这个点的父节点出发。所以用拓扑排序就可以避免这种情况。

链式前向星存图。数据大的情况下vector效率比较低。
#include <iostream>
#include <cstring>
#include <queue>
#include <cstdio>
using namespace std;
const int maxn = 1e4+10;
const int maxm = 1e5+10;
struct E {
	int from, to, nxt;
	E(){}
	E(int from_, int to_, int nxt_):from(from_), to(to_), nxt(nxt_){}
} Edge[maxm], Edge2[maxm]; //要存两个图。
int tot, tot2, head[maxn], head2[maxn];

int n, m, tp, id;
int st[maxn], dfn[maxn], low[maxn], w[maxn], scc[maxn], inD[maxn], dp[maxn];
bool vis[maxn];

void tarjan(int now) {
	// 基本操作
	dfn[now] = low[now] = ++id;
	st[tp++] = now; //节点入栈
	vis[now] = 1;
	for (int i = head[now]; ~i; i = Edge[i].nxt) {
        int v = Edge[i].to;
		if (!dfn[v]) {
			 tarjan(v);
			 low[now] = min(low[now], low[v]);
		}
		else if (vis[v]) {
			low[now] = min(low[now], dfn[v]);
		}
	}
	if (low[now] == dfn[now]) {
	    int y;
    	while (y = st[--tp]) {
        	vis[y] = 0; //别忘了出栈
            scc[y] = now;
            if (y == now) break;
            w[now] += w[y];
        }
	}
}
void getNewG() {
	//根据缩点的结果建立新的图
	memset(head2, -1, sizeof head2);
	for (int i = 0; i < m; i++) {
		int u = scc[Edge[i].from], v = scc[Edge[i].to];
		if (u != v) {
			// 不在一个分量里,需要连边
			Edge2[tot2] = E(u, v, head2[u]);
			head2[u] = tot2++;
			inD[v]++; 
		}
	}
}

int topSortAndDp() {
	queue<int> q;
	//拓扑排序的同时DP
	for (int i = 1; i <= n; i++) {
		if (scc[i] == i && !inD[i]) {
			q.push(i);
			dp[i] = w[i];
		}
	}
	while (!q.empty()) {
		int v = q.front(); q.pop();
		for (int i = head2[v]; ~i; i = Edge2[i].nxt){
			int to = Edge2[i].to;
			dp[to] = max(dp[to], dp[v] + w[to]);
			if(--inD[to] == 0) {
				q.push(to);
			}
		}
	}
    
	int ans = 0;
	for (int i = 1; i <= n; i++) ans = max(ans, dp[i]);
	return ans;
}
int main() {
	memset(head, -1, sizeof head);
	scanf("%d%d", &n, &m);
	for (int i = 1; i <= n; i++) scanf("%d", &w[i]);
	for (int i = 1; i <= m; i++) {
		int u, v;
		scanf("%d%d", &u,&v);
		Edge[tot]=E(u, v, head[u]);
		head[u] = tot++;	
	}
	for (int i = 1; i <= n; i++)
		if (!dfn[i]) tarjan(i);
	getNewG();
	printf("%d\n", topSortAndDp());
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值