题目链接

(模板)缩点

题目背景

缩点+DP

题目描述

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

输入格式

第一行,$n$,$m$
第二行,$n$个整数,依次代表点权
第三至$m+2$行,每行两个整数$u$,$v$,表示$u$->$v$有一条有向边

输出格式

共一行,最大的点权之和。

输入输出样例

输入 #1

2 2
1 1
1 2
2 1

输出 #1

2

说明/提示

$n<=10^4$,$m<=10^5$,$0<=点权<=1000$
算法:Tarjan缩点+DAGdp

题解

缩点的模板题。
题目中因为没有限制走过的边不能走,所以如果在一个强连通分量里,这个强连通分量里的每个点都可以加到$ans$里,所以我们只要把每个强连通分量缩成一个点,再搜索就行了。
如果不缩点直接搜索会TLE哦。
缩点其实就是在原图上找强联通分量,然后把每一个强连通分量作为一个点建一个新图。
那么重点就在搜索强联通分量上了,这里我们可以用tarjan来搜索,时间复杂度是线性的。(如果用dfs暴搜强联通分量会TLE,数据范围很能卡)
博主在这里略讲一下tarjan吧。(博主只是略讲,根据博主自己的理解,如果无法理解的可以先自行学习tarjan)
tarjan:
这里设$dfn[j]$为搜索到点$j$时的时间,$low[j]$表示在当前强联通分量里最小的$dfn$值。
最开始时每个点的$low$值和$dfn$值相等。
这里我们用一个栈维护在当前强连通分量里的点。
tarjan就是用dfs把图搜索一遍,遇到没有搜过的点就去搜它,然后用它的$low$值更新当前点的$low$值。
如果遇到在栈里的点时就用这个点的$dfn$值更新当前点的$low$值。
这里用$dfn$值更新而不用$low$值更新其实在求强联通分量的时候是没有影响的,因为$low$值更新后只会变小,不管变成多少都不再会是当前点的$dfn$值,而求强联通分量时只要区分这个点是不是所在强联通分量的根(dfs搜索树上子树的根)就行了。
但是在求割点时用$dfn$值更新$low$值就会出bug。(有关割点的知识在这里不做详解)
在搜完回溯时如果当前节点的$low$值和$dfn$值是一样的,那么这个点就是子树的根了,我们在这里开始弹出再栈中当前强联通分量的点。
因为我们的目的是缩点,所以在弹出的时候要把所有强联通分量中的点指向它们的根。
建新图:
tarjan结束后我们开始建一个新图。
把每个原图的边枚举一遍,新建一条从旧边起始节点所在的强连通分量的根到这条边所到达的节点所在的强连通分量的根的边。(这句话读着比较拗口,大家可以感性理解一下)
注意当新边的两个节点相同时(也就是两个原节点在同一个强联通分量里)就不要建这条边了。
这里可以用在dfs时枚举的方法枚举,而不是从1到m枚举,这样可以确定当前边的起始节点。
当然你也可以在建原图的边时增加一个$from$变量。
因为我们把所有的强联通分量缩成了一个点,所以剩下的图将不会有后向边(也就是环)。
显而易见,新图是一个拓扑图。
因为题目只是求最大权值,没有起点和终点,所以我们可以用每次删除入度为0的点来更新最大的权值。(只是觉得这么做跑得比较快)
注意:不要把新图的点/边和旧图的点/边弄混了,博主因为手残打错了导致debug耗费了好长时间。
上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
#include<bits/stdc++.h>
using namespace std;
int n,m,a[20009],u,v;
int dfn[20009],low[20009];
struct aa{
int to,nxt,from;
}p[200009],p2[200009];
int h[20009],len;
int h2[20009],len2;
void add(int x,int y){
len++;
p[len].to=y;
p[len].from=x;
p[len].nxt=h[x];
h[x]=len;
}
bool k[10009];
int uu[10009],lu;
int t;
int is[10009];
void tj(int x){
uu[++lu]=x;
k[x]=1;
dfn[x]=low[x]=++t;
for(int j=h[x];j;j=p[j].nxt){
if(dfn[p[j].to]==0){
tj(p[j].to);
low[x]=min(low[x],low[p[j].to]);
}else if(k[p[j].to]) low[x]=min(low[x],dfn[p[j].to]);
}
if(dfn[x]==low[x]){//退栈
while(uu[lu]!=x){
a[x]+=a[uu[lu]];
is[uu[lu]]=x;
k[uu[lu--]]=0;
}
is[uu[lu]]=x;
k[uu[lu--]]=0;
}
}
int dis[10009];
int in[10009];
int up[100009],l=1,r=0;
int ans=0;
int main(){
scanf("%d%d",&n,&m);
for(int j=1;j<=n;j++)
scanf("%d",&a[j]);
for(int j=1;j<=m;j++){
scanf("%d%d",&u,&v);
add(u,v);
}
for(int j=1;j<=n;j++)
if(!dfn[j]) tj(j);//原图可能是不连通的
for(int j=1;j<=n;j++){
for(int i=h[j];i;i=p[i].nxt){//建新图
u=is[j];
v=is[p[i].to];
if(v!=u){
len2++;
p2[len2].to=v;
p2[len2].from=u;
in[v]++;
p2[len2].nxt=h2[u];
h2[u]=len2;
}
}
}
for(int j=1;j<=n;j++)//查找入度为0的点
if(in[is[j]]==0 && is[j]==j){
up[++r]=is[j];
dis[is[j]]=a[is[j]];
}
while(l<=r){
for(int j=h2[up[l]];j;j=p2[j].nxt){
dis[p2[j].to]=max(dis[p2[j].to],dis[up[l]]+a[p2[j].to]);
in[p2[j].to]--;
if(in[p2[j].to]==0) up[++r]=p2[j].to;
}
l++;
}
for(int j=1;j<=n;j++)
ans=max(ans,dis[j]);
printf("%d",ans);
return 0;
}

最后更新: 2019年09月27日 20:14

原始链接: http://oierlin.cf/2019/08/05/【洛谷P3387】(模板)缩点/

× 请我吃糖~
打赏二维码