算法:Tarjan & DP
显然缩点后的图是一个DAG
DAG上搞DP是基本操作啊
按拓扑序搞一波DP就好了
设 f[ i ] 表示从起点抢劫到 DAG 上的点 i 时能得到的最多的钱
那么 f[ i ] = max( f [ i ] , f[ j ] + sval[ i ]) ( j 有一条边指向 i , sval[ i ] 表示DAG上点 i 的钱数)
至于 Tarjan 的具体操作看代码,这里不再赘述
测了一波,自己的DP比SPFA快个100ms(都开了O2)
一些与本题无关的话:
其实真正意义的缩点还要把重复的边给去掉(缩点后可能两个块之间有多条边)
可以把边给离散化,然后去重
具体操作:
![](https://i-blog.csdnimg.cn/blog_migrate/8f900a89c6347c561fdf2122f13be562.gif)
![](https://i-blog.csdnimg.cn/blog_migrate/961ddebeb323a10fe0623af514929fc1.gif)
struct edge { int a,b; }e[N];//存边 int cnt3,du[N];//cnt存总边数,du存入度 int firr[N],fromm[N],too[N],cntt;//链式前向星存DAG inline void add2(int &a,int &b)//给DAG连边 { fromm[++cntt]=firr[a]; firr[a]=cntt; too[cntt]=b; } inline bool cmp(const edge &x,const edge &y) { return x.a==y.a ? x.a<y.a : x.b<y.b; }//排序函数,把边按 a,b 双关键字排序 void build()//构造缩点后的图 { for(int i=1;i<=n;i++) { if(!be[i]) continue;//因为我的主函数只缩了从起点能到的点,所以要判断一波 //这里be存每个点所在的联通块 for(int j=fir[i];j;j=from[j])//遍历原图的边 { if(be[i]==be[to[j]) continue;//同一个块的边不要连 e[++cnt3].a=be[i],e[cnt3].b=be[to[j]];//把边先统一存到e里面 } } sort(e+1,e+cnt3+1,cmp);//排序 for(int i=1;i<=cnt3;i++) if(e[i].b!=e[i-1].b||e[i].a!=e[i-1].a)//因为排好序了,所以可以这样判断重复的边 add2(e[i].a,e[i].b),du[e[i].b]++;//如果没重复就加入DAG }
但是对于本题根本不需要去除重复边
要也可以,但是sort多一个log,反而更慢...
废话结束上代码
#include<iostream> #include<cstring> #include<cstdio> #include<cmath> #include<algorithm> #include<queue> using namespace std; inline int read() { int x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9') { if(ch=='-') f=-1; ch=getchar(); } while(ch>='0'&&ch<='9') { x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*f; } const int N=5e5+7; int n,m,k,sta; int val[N]; int fir[N],from[N],to[N],cnt; inline void add(int &a,int &b)//给原图连边 { from[++cnt]=fir[a]; fir[a]=cnt; to[cnt]=b; } //几乎就是Tarjan的模板 int dfn[N],low[N],be[N],sval[N],st[N],cnt2,dfs_clock,_top; bool bar[N],Bar[N];//bar表示原图的酒吧,Bar表示DAG上的酒吧 void Tarjan(int x) { dfn[x]=low[x]=++dfs_clock; st[++_top]=x; for(int i=fir[x];i;i=from[i]) { int &v=to[i]; if(!dfn[v]) Tarjan(v),low[x]=min(low[x],low[v]); else if(!be[v]) low[x]=min(low[x],dfn[v]); } if(dfn[x]==low[x]) { cnt2++; while(st[_top]!=x) { if(bar[st[_top]]) Bar[cnt2]=1; sval[cnt2]+=val[st[_top]]; be[st[_top--]]=cnt2; } if(bar[st[_top]]) Bar[cnt2]=1; sval[cnt2]+=val[st[_top]]; be[st[_top--]]=cnt2; } } int du[N];//存入度,DP要用 int firr[N],fromm[N],too[N],cntt;//存DAG inline void add2(int &a,int &b)//连DAG的边 { fromm[++cntt]=firr[a]; firr[a]=cntt; too[cntt]=b; } void build()//没有去重边的DAG构造函数 { for(int i=1;i<=n;i++) { if(!be[i]) continue;//我的主函数只缩了从起点能到的点,所以要判断一波 for(int j=fir[i];j;j=from[j]) { if(be[i]==be[to[j]]) continue;//同块内的边就不要连了 add2(be[i],be[to[j]]),du[be[to[j]]]++;//不然就连起来,更新入度 } } } int f[N],ans; queue <int> q; void DP()//按拓扑序的DP { q.push(be[sta]); f[be[sta]]=sval[be[sta]]; while(!q.empty()) { int u=q.front(); q.pop(); for(int i=firr[u];i;i=fromm[i]) { int &v=too[i]; f[v]=max(f[v],f[u]+sval[v]);//尝试更新f[v] if(!(--du[v])) q.push(v);//更新入度 } } for(int i=1;i<=cnt2;i++) if(Bar[i]) ans=max(ans,f[i]);//在有酒吧的块内找一个钱最多的块 } int main() { int a,b; n=read(); m=read(); for(int i=1;i<=m;i++) { a=read(); b=read(); add(a,b); } for(int i=1;i<=n;i++) a=read(),val[i]=a; sta=read() ;k=read(); for(int i=1;i<=k;i++) a=read(),bar[a]=1; Tarjan(sta);//只要从起点Tarjan就好了,其他的反正也到不了 build();//构造DAG DP(); printf("%d",ans); return 0; }