POJ 3249 Test for Job (DAG图上的单源最短路径) 最详细的图解

传送门POJ 3249


题目大意:给出有n个点,m条边的图,每个点有一个值(点权),走到这个点就可以得到这个值。其中入度为0的点为源点,出度为0的点为汇点,源点和汇点可能有多个,问从源点走到汇点可以得到的最大值是多少。


Sample Input

6 5
1
2
2
3
3
4
1 2
1 3
2 4
3 4
5 6

Sample Output

7

前置技能:

1.链式前向星,这是存储图的一种方式,对于复杂图来说可以说是最好的存储方式,没有之一。

2.DAG上求单源最短路径算法,具体思想就是先得到DAG图的拓扑序列,然后按照拓扑排序的顺序处理每个点。

以上都为基础知识,希望还没有掌握的读者自行百度,当然也可以结合我的代码理解,我会给出详细的注释。


思路:我们注意到,n和m的值都很大(1 ≤ n ≤ 100000, 0 ≤m ≤ 1000000),直接用无向图的求最短路算法肯定会超时,又注意到我们能构造的图其实是个有向无环图,所以就可以用求DAG图上的单源最短路径算法来解决了,其时间复杂度为O(m)。

这时,你可能会说了,这个算法只能求单源最短路啊,但是题目中明明说了可能有多个源点,并且告诉我们的不是边权而是点权。所以我们就得自己转换一下思路,重新构图。



上图是根据题目给出的数据形成的图,可以很明显的得出结果为7。下面我们来重新构造一下图。



首先,我们加入一个超级源点(0点)和超级汇点(n+1点),让超级源点连接原来的所有源点,让原来的所有汇点连接超级汇点。然后还需要把点权转化为边权,具体做法是点 v 的点权转化为以 v 为入点的边上的边权,至于点到超级汇点之间的边权设为0。这样从某个源点走到某个汇点的最大距离就是从超级源点走到超级汇点的最大距离了。


具体实现:前面提到过,由于数据量很大,应该选用链式前向星的方式存储图结构,vector理论上也可以,但是肯定没有链式前向星好。首先根据给出的边和点权构图,超级源点连接原来的所有源点,将原来的所有汇点连接超级汇点接着跑一边拓扑排序,然后再根据拓扑序列求得最短路。


注意

1.数据很大,结果应该用long long型存储。

2.求的是最大距离,所以要在最短路的基础上改一下。

3.传统的拓扑排序算法时间复杂度为O(n^2)会超时,我这里用了队列来优化,因为一个节点只有在删除与它相连的边的时候它的入度才可能变为0,只判断这些点并将入度为0的点入队会节省很多时间。


#include<stdio.h>
#include<string.h>
#include<iostream>
#include<queue>
#define inf 0x3f3f3f3f
using namespace std;
typedef long long LL;

int n,m;
int tol,cnt,head[100010]; //链式前向星 
//w记录点权,chu记录点的出度,ru记录点的入度,que记录拓扑序列 
int w[100010],chu[100010],ru[100010],que[100010];
//dis记录点到源点的最大距离 
LL dis[100010];
queue<int> Q;

struct node
{ //链式前向星 
	int w,to,next; //分别为边权、终点和以u为起点的下一条边在的位置 
} edge[2000010];

void init()
{ //初始化函数 
	tol=0;
	memset(head,-1,sizeof(head));
	memset(chu,0,sizeof(chu));
	memset(ru,0,sizeof(ru));
}

void add(int u,int v,int w)
{ //加边函数 
	edge[tol].w=w;
	edge[tol].to=v;
	edge[tol].next=head[u];
	head[u]=tol++;
}

void tuopu()
{ //获得拓扑排序 
	int i,j,top;
	cnt=0;
	dis[0]=0;  
	Q.push(0); //0点入队
	for(i=1;i<=n+1;i++) dis[i]=-inf; //初始化最长距离为最小值 
	while(!Q.empty())
	{ //只有减边的点才有可能入度为0 
		top=Q.front();
		Q.pop();
		que[cnt++]=top; //保存拓扑序列 
		ru[top]--;      //当前节点入度-1
		for(j=head[top];j!=-1;j=edge[j].next)
		{ //遍历与top相连的节点 
			ru[edge[j].to]--; //入度-1 
			if(ru[edge[j].to]==0) Q.push(edge[j].to); //如果入度为0则入队 
		}
	}
}

void solve()
{
	int i,j,v;
	for(i=0;i<cnt;i++) //依照拓扑序列处理 
		for(j=head[que[i]];j!=-1;j=edge[j].next)
		{ //遍历i的相邻节点 
			v=edge[j].to;
			if(dis[v]<dis[que[i]]+edge[j].w) //更新距离 
				dis[v]=dis[que[i]]+edge[j].w;
		}
	printf("%lld\n",dis[n+1]);
}

int main()
{
	int i,j,u,v;
	while(~scanf("%d%d",&n,&m))
	{ //点数和边数 
		init();
		for(i=1;i<=n;i++) scanf("%d",&w[i]); //输入点权 
		for(i=0;i<m;i++)
		{
			scanf("%d%d",&u,&v);
			add(u,v,w[v]);
			chu[u]++;
			ru[v]++;
		}
		for(i=1;i<=n;i++)
		{
			if(ru[i]==0)
			{ //在0点和入度为0的点之间加边 
				add(0,i,w[i]);				
				ru[i]++;
			}
			if(chu[i]==0)
			{ //在出度为0的点和点n+1之间加边 
				add(i,n+1,0);
				ru[n+1]++;
			}
		}
		tuopu();
		solve();
	}
	return 0;
}

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值