nyoj-895How many ways【图上dp+拓扑序】

How many ways?

时间限制: 1000 ms  |  内存限制: 65535 KB
难度: 4
描述

给一个 n 个点 m 条边的有向无环图,问从点 1 到点 n 一共有多少条路径?(结果对 10007 取模)

输入
多组测试数据。
第一行两个数,n 和 m(2<=n<=100,2<=m<=10000)。
输出
每组测试数据输出一行,表示从点 1 到点 n 的方案数对 10007 取模后的值。

首先,我分析这个问题是在图上的dp。

用dp[i][j]表示 从i点->j点的最大路径数

dp[0][j]记录到j点总的最大路径数

g[i][j]表示i点到j点的的边数

状态转移方程为:

dp[u][v]=Max(dp[0][u]*g[u][v]);

dp[0][v]=∑(Max(dp[u][v]));u为所有能连接到v的点

总体意思就是一个点的最大路径数为 sum v= ∑(  sum u *  count_e ); v的最大路径数 = 所有的 (v的最大路径数 * v到u的边数)之和

接下来就是枚举所有边,更新dp值。一个点的dp值被改变了,那么它连接的点及其以后的dp值都是可变的。所以不断的枚举所有边更新dp值,直到再没有点更新为止。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define Max_V 110
long long int dp[Max_V][Max_V];
long long int g[Max_V][Max_V];
int n,m;
void init()
{
	memset(dp,0,sizeof(dp));
	memset(g,0,sizeof(g));	
}
int main()
{
	int i;
	while(~scanf("%d%d",&n,&m))
	{
		init();
		int u,v;
		for(i=0;i<m;i++)
		{
			scanf("%d%d",&u,&v);
			g[u][v]++;
		}
		dp[0][1]=1;//从1点出发
		bool flag=true;
		while(flag)
		{
			flag=false;
			//枚举所有边
			for(u=1;u<=n;u++)
				for(v=1;v<=n;v++)
				{
					if(dp[u][v]<g[u][v]*dp[0][u])
					{
						flag=true;
						//dp[0][v]记录最大路径数,
						//更新前的dp[u][v]无效后就得减去
						//再加上更新后的dp[u][v]
						dp[0][v]-=dp[u][v];
						dp[u][v]=g[u][v]*dp[0][u];
						dp[0][v]+=dp[u][v];
					}
				}
		}
		printf("%lld\n",dp[0][n]%10007);
	}
	return 0;
}

但是,问题就来了。数据范围十分的大,远超过long long int。因为求模之后比较大小是无效的。那么怎么解决这个问题呢?


关键字:有向无环图

联想:DAG(拓扑排序)

由AOV网构造拓扑序列的拓扑排序算法主要是循环执行以下两步,直到不存在入度为0的顶点为止。

重复执行
(1) 选择一个入度为0的顶点;
(2) 从网中删除此顶点及所有出边。

这样就不断列举出入度为0的点。

承接上面的思路。一个点的入度为0,是不是就说明这个点就不会再被其它点更新了?是不是就说明这个点已经更新到底了?

那么 dp[0][u]就不会再改变了。dp[u][v]也随之固定。这样问题就变成了一个单纯的递推式,并且dp[]数组可以从二维优化到一维:dp[v]+=dp[u]*g[u][v];因为dp[u]固定(u入度为0,所以dp[u]不会再被更改,所以dp[u]当前为最大路径数),g[u][v]固定。

就不需要再判断大小了。就可以利用取模运算了。

所以在此点更新了其它点之后,此点及其出边就可以删除了,这样又会创造出入度为0的点。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define Max_V 110
#define mod 10007
int n,m;
int indeg[Max_V];
int g[Max_V][Max_V];
int dp[Max_V];
bool vis[Max_V];
void init()
{
	memset(indeg,0,sizeof(indeg));
	memset(g,0,sizeof(g));
	memset(dp,0,sizeof(dp));
	memset(vis,0,sizeof(vis));
}
int main()
{
	int i;
	while(~scanf("%d%d",&n,&m))
	{
		init();
		int u,v;
		for(i=0;i<m;i++)
		{
			scanf("%d%d",&u,&v);
			g[u][v]++;
			indeg[v]++;
		}
		dp[1]=1;
		while(true)
		{
			for(u=1;u<=n;u++)//寻找未删除的0度顶点
				if(!indeg[u]&&!vis[u])
					break;
			if(u>=n)
				break;
			vis[u]=1;//标记数组表示删除该点
			for(v=1;v<=n;v++)
				if(g[u][v])
				{
					dp[v]+=dp[u]*g[u][v];//(a+b)%m=(a%m+b%m)%m
					dp[v]%=mod;
					indeg[v]-=g[u][v];//减少入度表示删除此点的出边
				}
		}
		printf("%d\n",dp[n]);
	}
	return 0;
}


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值