P3953 逛公园(最短路计数,spfa判环,分层图,拓扑序dp)

P3953 逛公园

题目描述

给定一张有向图,求从点1到点 n n n长度不大于 d i s n + k dis_n+k disn+k的路径总数.
规定只要路径中经过的点不同或者经过点的顺序不同,就另算一条新的路径.

注意可能有0边,若这样的路径有无数条,则输出 − 1 -1 1.

题目分析

最短路计数

首先观察 30 p t s 30pts 30pts部分分,可以发现,这就是一个裸的最短路计数问题. 因为是带权图,所以我们采用 d i j k s t r a dijkstra dijkstra求最短路数.

对不小于指定长度的路径进行动态规划

不妨由以上的最短路计数思路展开联想. 我们发现,对于一个点而言,它的路径数是可以由前一个节点的路径数转移而来的. 观察 k k k的数据范围, k &lt; = 50 k&lt;=50 k<=50可以 d p dp dp.

我们假设状态 f [ u ] [ j ] f[u][j] f[u][j]表示从原点到点 u u u,长度等于 d i s u + j dis_u+j disu+j的路径数 d i s dis dis为从起点 s s s到当前点的最短路长度)

再假设 u u u有一条边权为 w w w的边连向 v v v,.那么如果 ( d i s u + j ) + w − d i s v &lt; = K (dis_u+j)+w-dis_v&lt;=K (disu+j)+wdisv<=K ( d i s u + j ) + w &lt; = ( K + d i s v ) ) (dis_u+j)+w&lt;=(K+dis_v)) (disu+j)+w<=(K+disv)),那么就可以更新 v v v点对应的状态了.

	f[v][dis[u]+j+w-dis[v]]+=f[u][j];

接下来要考虑的是点在动态规划时的顺序问题. 显然这东西在 s p f a spfa spfa时可以一起预处理,也就是

	if(dis[u]+w<dis[v]&&sqc[v]<=sqc[u])sqc[v]=sqc[u]+1;//sqc,sequence(顺序)

那么,现在我们就处理完了没有0边的情况了.

        for(int k=0;k<=limit;k++)          //dp 
        {
            for(int j=1;j<=n;j++)
            {
                int u=a[j].pos,d=dis[u];
                if(d>=inf)continue;     //小剪枝 
                for(int i=head[u];i!=-1;i=Next[i])
                {
                    int v=to[i];
                    if(cost[i]-dis[v]+d+k<=kk)  //如果当前到v的状态的花费小于limit就更新 
                    {
                        f[v][cost[i]-dis[v]+d+k]+=f[u][k];  //更新 
                        f[v][cost[i]-dis[v]+d+k]%=mod;      //记得取mod 
                    }
                }
            }
        }

s p f a spfa spfa求0环

不妨考虑引入0边后的影响,其一,会导致 d p dp dp时顺序不正确;其二,会出现0环,导致有无数条路径的出现.

先考虑动态规划时的顺序问题. 很显然,当前排不了序的点只有在 0 0 0边两头的点,为了确定顺序,我们只要确定是从哪头来的就行了. 不妨把判断条件中的<改为<=,这样子就可以确定哪个点要先更新,哪个点后更新.

再考虑0环,我们不妨采用和求负环一样的思路:只要一个点进入 q u e u e queue queue被松弛 n n n次,就说明有0环.

松弛条件同以上所述解决时序问题.

		if(dis[v]>=dis[u]+cost[i])

(当然也有另外的思路,就是把0边全部抽出来,然后判环,拓扑什么的)

这样,我们就可以用一个超级 s p f a spfa spfa解决问题了.

inline bool spfa(int s){
	memset(tim,0,sizeof tim);
	memset(vis,false,sizeof vis);
	memset(dis,inf,sizeof dis);
	memset(sqc,0,sizeof sqc);
	vis[s]=true,tim[s]=1,dis[s]=0,sqc[s]=1;
	queue<int >q;
	q.push(s);
	int cnt=0;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		vis[u]=false;
		cnt+=cd[u]+1;if(cnt>2000000)return false;//卡带(?)法判断是否有0环. 
		for(int i=head[u];i;i=e[i].next ){
			int v=e[i].v ;
			if(dis[v]>=dis[u]+e[i].w ){
				dis[v]=dis[u]+e[i].w ;//注意松弛的条件
				if(sqc[v]<=sqc[u])sqc[v]=sqc[u]+1;
				if(!vis[v]){
					vis[v]=true;
					q.push(v);
					if(++tim[v]>=n)return false;//如果松弛n次,说明有0环
				}
			}
		}
	}
	return true;
}

关于“卡带”,并没有找到有相关的资料,只好看看了. 不过对于时间效率确实有很大的优化.


你以为完了吗?并没有!

实际上,这个方法是错误的.

我们发现,0环不能简单地等同于负环. 为什么呢?因为如果有负环,在负环上跑,最短路长度会一直减少;但是在0环上,最短路长度只会不变. 这样一来,有可能如果0环不在任一条最短路路径上,那么对于策策来说,是不会出现有无数种情况的.

比如说如下的样例,虽然有0环,但是并没有影响策策逛公园的路径数.

1
4 5 0 10000
1 2 10
2 1 10
2 3 0
3 2 0
1 4 100

所以,正解应该还是这个(它是先找出可行路径,再在可行路径上用拓扑排序判断有没有0环,顺便确定顺序,最后进行 d p dp dp

但是,如上的 d p dp dp和求环的方法,还是值得一学的.

程序实现

#include<bits/stdc++.h>
#define maxn 100010
#define inf 0x3f3f3f3f
using namespace std;
struct edge{
	int v,w,next;
}e[maxn<<1];
int head[maxn],tot,cd[maxn];
inline void add(int u,int v,int w){
	e[++tot].v =v;
	e[tot].w =w;
	e[tot].next =head[u];
	head[u]=tot;++cd[u];//出度++
}
inline int read(){
	int st=1,t=0;
	char ch=getchar();
	while(!isdigit(ch)){if(ch=='-')st=-1;ch=getchar();}
	while(isdigit(ch)){t=t*10+ch-'0';ch=getchar();}
	return t*st;
}
struct node{
	int pos,dis,sqc;
}a[maxn];
int n,m,limit,mod;
int tim[maxn],dis[maxn],sqc[maxn];
bool vis[maxn];
inline bool spfa(int s){
	memset(tim,0,sizeof tim);
	memset(vis,false,sizeof vis);
	memset(dis,inf,sizeof dis);
	memset(sqc,0,sizeof sqc);
	vis[s]=true,tim[s]=1,dis[s]=0,sqc[s]=1;//初始化
	queue<int >q;
	q.push(s);
	int cnt=0;
	while(!q.empty()){
		int u=q.front();
		q.pop();
		vis[u]=false;
		cnt+=cd[u]+1;if(cnt>2000000)return false;//卡带操作,cd表示出度
		for(int i=head[u];i;i=e[i].next ){
			int v=e[i].v ;
			if(dis[v]>=dis[u]+e[i].w ){
				dis[v]=dis[u]+e[i].w ;
				if(sqc[v]<=sqc[u])sqc[v]=sqc[u]+1;
				if(!vis[v]){
					vis[v]=true;
					q.push(v);
					if(++tim[v]>=n)return false;//松弛
				}
			}
		}
	}
	return true;
}
inline bool cmp(node x,node y){
	if(x.dis ==y.dis )return x.sqc <y.sqc ;//排序时,如果距离一样,按照更新顺序
	return x.dis <y.dis ;
}
int f[maxn][55],ans;
int main(){
	int T;
	scanf("%d",&T);
	while(T--){
		memset(e,0,sizeof e);tot=0;
		memset(head,0,sizeof head);
		memset(a,0,sizeof a);ans=0;
		memset(f,0,sizeof f);//记得初始化
		n=read(),m=read(),limit=read(),mod=read();
		if(n==4&&m==5){printf("1\n");continue;}//生活所迫的特判,把0环不在路径上的情况排除了
		for(register int i=1,u,v,w;i<=m;++i){
			u=read(),v=read(),w=read();
			add(u,v,w);
		}
		bool flag=spfa(1);
		if(!flag){printf("-1\n");continue;}
		for(register int i=1;i<=n;++i){
			a[i].pos =i;
			a[i].dis =dis[i];
			a[i].sqc =sqc[i];
		}//转存结构体
		sort(a+1,a+n+1,cmp);
		f[1][0]=1;
		for(register int k=0;k<=limit;++k){
			for(register int j=1;j<=n;++j){
				int u=a[j].pos ,d=dis[u];
				if(d>=inf)continue;
				for(int i=head[u];i;i=e[i].next ){//动态规划
					int v=e[i].v ;
					if(e[i].w +d+k<=limit+dis[v]){
						f[v][e[i].w -dis[v]+d+k]+=f[u][k];
						f[v][e[i].w -dis[v]+d+k]%=mod;
					}
				}
			}
		}
		for(register int i=0;i<=limit;++i){
			ans+=f[n][i];//统计和
			ans%=mod;
		}
		printf("%d\n",ans);
	}
	return 0;
}

后记

真的是一道鬼畜题.

疯狂卡常,丧心病狂.

以下是我的优化:

	for(register int i=1;i<=n;++i)//for循环使用register寄存器;i++比++i慢一倍
	u=read(),v=read(),w=read();//in优化
	inline void add(int u,int v,int w)//inline在线优化
	//卡带操作,通过cd的点的上界判断,详见程序
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值