题目描述
给定一张有向图,求从点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 < = 50 k<=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 < = K (dis_u+j)+w-dis_v<=K (disu+j)+w−disv<=K( ( d i s u + j ) + w < = ( K + d i s v ) ) (dis_u+j)+w<=(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的点的上界判断,详见程序