//好久不见。上上周忙于加试,上周又步行因病住院,我也很无奈啊。
首先我们注意到k的值非常小,最大也只能达到50,复杂度一定与它有关。
然后又是需要取模的计数问题。考虑dp。
首先是本人写的很丑的非常慢的解法:
首先我们跑dijkstra处理出两个数组,分别维护从起点和终点到达该点的距离。
dp状态:表示到第个顶点路径长度比从起点到该点最短路长的方案数。
转移很简单,对于每一个枚举以为起点的边,令该边的另一端点为,权值为。那么状态转移方程为:
其中就是对应的距离。显然,,因此把放在外层转移就可以了
吗?
nonono 显然不行,dp值可能是由第二个维度相同的另一个dp值转换过来的。对于这种情况,我们需要对第一个维度进行拓扑排序。
我们需要对全图进行拓扑排序吗?显然不需要。因为有些点根本不会出现在结果方案的路径上(如果一个点到起点的最短路径和到终点的最短路径之和与起点到终点最短路径的差都大于k,即,那么这个点一定不会被用到)。再者图是可以存在环的,且并不是所有的环都会导致-1的情况。我们可以反复走环。考虑到只有终点与起点距离差等于边权的情况是不能从更小的转移过来的,我们只需要对这些边的端点进行拓扑排序即可,其他可用的点随意放。而如果拓扑排序无法完成即代表存在0环。这也就是题目中所说的-1的情况。
第一遍做的时候只考虑到了根据到起点最短距离进行排序,但这种做法是无法处理环上的0边的。因为环起点处的一些dp值是可以从终点转移过来的。但终点到起点的距离一定是大于起点的,无法正确转移。
空间是的,时间因为是通过边转移,可以达到,大概是,如果常数写大点还是非常危险的。但仔细想想,首先对于的情况显然是不用转移的,可以continue掉。其次我们只需要跑可用的点,因此是跑不到1e7的。再加上题目的时限给到了3s,还是可以基本放心的。
上丑陋无比的代码。因为一开始思路并不是很明确写的有点乱。
#include<bits/stdc++.h>
using namespace std;
#define pb push_back
#define fi first
#define se second
#define ll long long
#define pq priority_queue
#define mp make_pair
#define pii pair<int,int>
#define mod 998244353
#define debug(x) cerr<<#x<<"="<<x<<'\n'
const int maxn=1e6+10;
int lowbit(int x) {return x&(-x);}
int T,n,m,k,p;
vector <pii> edge[2][maxn];
int dis[2][maxn];
int f[maxn][55];
void dijkstra(int x) {
pq <pii,vector<pii>,greater<pii> > q;
int sta;
if (x==0) sta=1;
else sta=n;
for (int i=1;i<=n;i++) dis[x][i]=2e9+1119;
dis[x][sta]=0;
q.push(mp(0,sta));
while (!q.empty()) {
int u=q.top().se,d=q.top().fi;
q.pop();
if (dis[x][u]!=d) continue;
for (int i=0;i<edge[x][u].size();i++) {
int v=edge[x][u][i].fi,dd=edge[x][u][i].se;
if (dis[x][v]>dd+d) {
dis[x][v]=dd+d;
q.push(mp(dis[x][v],v));
}
}
}
return;
}
int deg[maxn];
int tmp[maxn];
bool chosen[maxn];
int bg,ed;
bool check() {
memset(deg,0,sizeof(deg));
memset(tmp,0,sizeof(tmp));
memset(chosen,0,sizeof(chosen));
for (int i=1;i<=n;i++)
for (int j=0;j<edge[0][i].size();j++){
int x=edge[0][i][j].fi,y=edge[0][i][j].se;
if (dis[0][i]+dis[1][x]+y-dis[0][n]<=k) {
if (y+dis[0][i]==dis[0][x]) deg[x]++;
chosen[x]=chosen[i]=true;
}
}
bg=0,ed=0;
for (int i=1;i<=n;i++) {
if (chosen[i]) {
if (deg[i]==0) tmp[ed++]=i;
}
}
while (bg<ed) {
int cur=tmp[bg++];
chosen[cur]=false;
for (int i=0;i<edge[0][cur].size();i++) {
int v=edge[0][cur][i].fi;
if (dis[0][cur]+edge[0][cur][i].se!=dis[0][v]) continue;
if (!chosen[v]) continue;
deg[v]--;
if (deg[v]==0) tmp[ed++]=v;
}
}
for (int i=1;i<=n;i++)
if (chosen[i]) return true;
return false;
}
void solve() {
memset(f,0,sizeof(f));
scanf("%d%d%d%d",&n,&m,&k,&p);
for (int i=1;i<=n;i++) edge[0][i].clear(),edge[1][i].clear();
for (int i=0;i<m;i++) {
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
edge[0][a].pb(mp(b,c));
edge[1][b].pb(mp(a,c));
}
dijkstra(0);
dijkstra(1);
if (check()) {
puts("-1");
return;
}
f[1][0]=1;
for (int i=0;i<=k;i++)
for (int j=0;j<ed;j++){
int cur=tmp[j];
if (!f[cur][i]) continue;
for (int t=0;t<edge[0][cur].size();t++) {
int v=edge[0][cur][t].fi,d=edge[0][cur][t].se;
int tmp=dis[0][cur]+i+d-dis[0][v];
if (tmp>k) continue;
(f[v][tmp]+=f[cur][i])%=p;
}
}
int ans=0;
for (int i=0;i<=k;i++) {
(ans+=f[n][i])%=p;
}
printf("%d\n",ans);
return;
}
int main(){
scanf("%d",&T);
while (T--) solve();
return 0;
}
然后介绍另一种做法。大差不差,只不过是那个做法直接把各边的权值改成了经过该边对结果(路径长与最短路的差值)的贡献,即对于原来从到权值为的边把其权值修改为。这样无论是dp还是拓扑排序写起来都会清爽很多,不容易出错。