【图论 动态规划拆点】luoguP3953 逛公园
经典的动态规划拆点问题。
题目描述
策策同学特别喜欢逛公园。公园可以看成一张 NN 个点 MM 条边构成的有向图,且没有 自环和重边。其中1号点是公园的入口, NN 号点是公园的出口,每条边有一个非负权值, 代表策策经过这条边所要花的时间。
策策每天都会去逛公园,他总是从1号点进去,从 NN 号点出来。
策策喜欢新鲜的事物,它不希望有两天逛公园的路线完全一样,同时策策还是一个 特别热爱学习的好孩子,它不希望每天在逛公园这件事上花费太多的时间。如果1号点 到 NN 号点的最短路长为 dd ,那么策策只会喜欢长度不超过 d + Kd+K 的路线。
策策同学想知道总共有多少条满足条件的路线,你能帮帮它吗?
为避免输出过大,答案对 PP 取模。
如果有无穷多条合法的路线,请输出 -1−1 。
输入输出格式
输入格式:
第一行包含一个整数 TT , 代表数据组数。
接下来 TT 组数据,对于每组数据: 第一行包含四个整数 N,M,K,PN,M,K,P ,每两个整数之间用一个空格隔开。
接下来 MM 行,每行三个整数 a_i,b_i,c_iai,bi,ci ,代表编号为 a_i,b_iai,bi 的点之间有一条权值为 c_ici 的有向边,每两个整数之间用一个空格隔开。
输出格式:
输出文件包含 TT 行,每行一个整数代表答案。
说明
【样例解释1】
对于第一组数据,最短路为 33 。 $1 – 5, 1 – 2 – 4 – 5, 1 – 2 – 3 – 5$ 为 33 条合法路径。
【测试数据与约定】
对于不同的测试点,我们约定各种参数的规模不会超过如下
| 测试点编号 | TT | NN | MM | KK | 是否有0边 |
|---|---|---|---|---|---|
| 1 | 5 | 5 | 10 | 0 | 否 |
| 2 | 5 | 1000 | 2000 | 0 | 否 |
| 3 | 5 | 1000 | 2000 | 50 | 否 |
| 4 | 5 | 1000 | 2000 | 50 | 否 |
| 5 | 5 | 1000 | 2000 | 50 | 否 |
| 6 | 5 | 1000 | 2000 | 50 | 是 |
| 7 | 5 | 100000 | 200000 | 0 | 否 |
| 8 | 3 | 100000 | 200000 | 50 | 否 |
| 9 | 3 | 100000 | 200000 | 50 | 是 |
| 10 | 3 | 100000 | 200000 | 50 | 是 |
对于 100%的数据, 1 \le P \le 10^9,1 \le a_i,b_i \le N ,0 \le c_i \le 10001≤P≤109,1≤ai,bi≤N,0≤ci≤1000 。
数据保证:至少存在一条合法的路线。
题目分析
计数题那么当然首先考虑dp啊。这是一个经典的拆点模型。由于k非常小,所以可以把每一个点拆成$k$个状态。
以上就是拆点的核心。
dp状态怎么设①
“拆点”听上去很高端,但实际上应该大家都在写题时不知不觉应用过。这里可以用$f[i][j]$表示$dis[1][i]=mnDis[1][i]+j$的方案数,其中$mnDis[1][i]$表示1到i的最短路径长度。
之所以$j$这一维代表的路径长度是$mnDis[1][i]+j$,是因为1..i的最短路长度是固定的,可以预先处理,而不用在状态里枚举。(这个和所谓的“dp套dp”非常像,本质就是通过预处理节省时间复杂度)
那么有了状态就很容易想到大概的转移思路了。用$dis[i]$表示1...i的最短路长度,那么通过一条边$(u,v,w)$存在$f[u][d]->f[v][dis[u]+d+w-dis[v]]$。
假设现在已经判断完是否存在零环了(这个对边排序或者怎么搞都行),那么具体的转移方程应该是怎么样的?
dp转移怎么搞①ⅰ
最初我是想:既然$f[u][d]->f[v][dis[u]+d+w-dis[v]]$,并且必定有$dis[u]+w≥dis[v]$,那么每一次转移时$f[i][j]$这个状态的$j$必定是单调增的啊,那么从$f[1][0]$开始对图进行记忆化搜索不就好了吗?
麻烦就麻烦在这样dp转移还受到多条最短路的干扰。

举个最简单的例子,这个做法先$1->4$遍历到了$f[4][0]$一次,然后用这个$f[4][0]=1$向外贡献答案。然而过了一会儿从1开始的路径$1->2->3->4$又遍历到了$f[4][0]$,于是又用$f[4][0]=2$向外贡献答案。这样不就重复计算了吗!
当然可以每次遍历到的时候先减去上一次的贡献再处理。但是这样便既不是我们最初想要达到的,又变得十分冗长,况且并不是没有改进的方法。
dp转移怎么搞①ⅱ ——70pts dp
在没有零边的情况下,注意到dp的顺序一定是先做$dis[]$小的,再做$dis[]$大的。那么就可以以端点到原点距离对于边排序,再做一次稳定的$O(mk)$转移。
//由于是按照另一种写法改编而来,所以这份代码有点丑
#include<bits/stdc++.h>
const int maxn = ;
const int maxm = ; int n,m,T,k,p,deal,DIS,mnDis,ans;
int initNxt[maxm],initHead[maxn],initEdgeTot;
int nxt[maxm],head[maxm],edgeTot;
int judge4cir0[maxn];
int dis[maxn][];
int f[maxn][];
bool bad[maxn],visQ[maxn];
struct Edge
{
int u,v,val;
long long w;
Edge(int b=, int c=):v(b),val(c) {}
}edgesSv[maxm],initEdges[maxm],edges[maxm];
struct cmp
{
bool operator()(int a, int b)
{
return dis[a][DIS] > dis[b][DIS];
}
};
std::priority_queue<int, std::vector<int>, cmp> disQ; int read()
{
char ch = getchar();
int num = ;
bool fl = ;
for (; !isdigit(ch); ch = getchar())
if (ch=='-') fl = ;
for (; isdigit(ch); ch = getchar())
num = (num<<)+(num<<)+ch-;
if (fl) num = -num;
return num;
}
bool cmpEdge1(Edge b, Edge a)
{
if (b.val!=a.val) return b.val < a.val;
return b.w < a.w;
}
bool cmpEdge2(Edge a, Edge b)
{
if (dis[a.u][]!=dis[b.u][]) return dis[a.u][] < dis[b.u][];
return dis[a.v][] < dis[b.v][];
}
void initAddedge(int u, int v, int w)
{
initEdges[++initEdgeTot] = Edge(v, w), initNxt[initEdgeTot] = initHead[u], initHead[u] = initEdgeTot;
}
void addedge(int u, int v, int w)
{
edges[++edgeTot] = Edge(v, w), nxt[edgeTot] = head[u], head[u] = edgeTot;
}
bool legal()
{
memset(judge4cir0, -, sizeof judge4cir0);
while (edgesSv[deal+].val==&&deal<m)
{
deal++;
int u = edgesSv[deal].u, v = edgesSv[deal].v;
if (judge4cir0[u]==v) return ;
initAddedge(u, v, );
judge4cir0[v] = u;
}
return ;
}
void dijsktra(int s)
{
disQ.push(s), dis[s][DIS] = ;
while (disQ.size())
{
int tt = disQ.top();
disQ.pop(), visQ[tt] = ;
for (int i=initHead[tt]; i!=-; i=initNxt[i])
{
int v = initEdges[i].v, w = initEdges[i].val;
if (dis[tt][DIS]+w < dis[v][DIS]){
dis[v][DIS] = dis[tt][DIS]+w;
if (!visQ[v]){
visQ[v] = ;
disQ.push(v);
}
}
}
}
}
void dp(int x, int y)
{
printf("f[%d][%d]:%d\n",x,y,f[x][y]);
for (int i=head[x]; i!=-; i=nxt[i])
{
int v = edges[i].v, w = edges[i].val;
printf("f[%d][%d], v:%d, w:%d\n",x,y,v,w);
if (y+dis[x][]+w-dis[v][] > k) continue;
(f[v][y+dis[x][]+w-dis[v][]] += f[x][y]) %= p;
dp(v, y+dis[x][]+w-dis[v][]);
}
}
int main()
{
// freopen("lg3953.in","r",stdin);
// freopen("lg3953.out","w",stdout);
T = read();
while (T--)
{
memset(initHead, -, sizeof initHead);
memset(dis, 0x3f3f3f3f, sizeof dis);
memset(head, -, sizeof head);
memset(bad, , sizeof bad);
memset(f, , sizeof f);
n = read(), m = read(), k = read(), p = read();
ans = mnDis = deal = edgeTot = initEdgeTot = ;
for (int i=; i<=m; i++)
edgesSv[i].u = read(), edgesSv[i].v = read(),
edgesSv[i].w = 1ll*edgesSv[i].u*edgesSv[i].v, edgesSv[i].val = read();
std::sort(edgesSv+, edgesSv+m+, cmpEdge1);
if (!legal()){
puts("-1");
continue;
}
for (int i=deal+; i<=m; i++)
initAddedge(edgesSv[i].u, edgesSv[i].v, edgesSv[i].val);
DIS = , dijsktra();
memset(initHead, -, sizeof initHead);
initEdgeTot = ;
for (int i=; i<=m; i++)
initAddedge(edgesSv[i].v, edgesSv[i].u, edgesSv[i].val);
DIS = , dijsktra(n);
mnDis = dis[n][], f[][] = ;
for (int i=; i<=n; i++)
if (dis[i][]==dis[][]||dis[i][]==dis[][]||dis[i][]+dis[i][] > mnDis+k)
bad[i] = ;
std::sort(edgesSv+, edgesSv+m+, cmpEdge2);
for (int d=; d<=k; d++)
for (int i=; i<=m; i++)
{
int u = edgesSv[i].u, v = edgesSv[i].v, w = edgesSv[i].val;
if (!bad[u]&&!bad[v])
{
int tt = d+dis[u][]+w-dis[v][];
if (tt <= k){
(f[v][tt] += f[u][d]) %= p;
}
}
}
for (int i=; i<=k; i++) (ans += f[n][i]) %= p;
printf("%d\n",ans);
}
return ;
}
无零边-70pts
dp转移怎么搞①ⅲ ——100pts dp
那么有零边意味着什么呢?意味着仅仅需要对于零边单独拓扑序处理即可。

这是一个仅用$dis[]$排序而不够的例子。
dp状态怎么设②
但其实dp题的状态是一个很玄妙的东西。
用$mnDis[i]$表示i到n的最短路,那么这一次$f[i][j]$表示的是$dis[i][n]+j≤mnDis[i]$的方案数。
注意前一种状态是严格=的方案数,而这里利用了前缀和的方法,表示了所有≤的方案数。
dp转移怎么搞②
这样表示的好处在于:从$f[1][k]$直接开始,并不用考虑拓扑序,可以记忆化搜索,并且遇到处理过状态的直接return即可。原因便是这种状态下,答案的贡献是被动的;而不是答案从自身这个状态转移出去。因此一旦搜索到终点$i==n$,$f[i][d]$就可以+1,同时在这个状态路径上的其他所有状态,都会且仅会收到一次这个合法状态的反馈。
或许有点绕口……?不过细想也就是这个理。
还注意到在这种遍历方式之下,如果访问到了尚未出栈的节点,就意味着出现了零环。因此可以省去预判断零环的过程。
那么就可以愉快地记忆化搜索啦。
#include<bits/stdc++.h>
const int maxn = ;
const int maxm = ; int n,m,T,k,p,ans;
int nxt[maxm],head[maxm],edgeTot;
int dis[maxn];
int f[maxn][];
bool visQ[maxn],stk[maxn][];
struct Edge
{
int u,v,val;
Edge(int b=, int c=):v(b),val(c) {}
}edgesSv[maxm],edges[maxm];
struct cmp
{
bool operator()(int a, int b)
{
return dis[a] > dis[b];
}
};
std::priority_queue<int, std::vector<int>, cmp> disQ; int read()
{
char ch = getchar();
int num = ;
bool fl = ;
for (; !isdigit(ch); ch = getchar())
if (ch=='-') fl = ;
for (; isdigit(ch); ch = getchar())
num = (num<<)+(num<<)+ch-;
if (fl) num = -num;
return num;
}
void addedge(int u, int v, int w)
{
edges[++edgeTot] = Edge(v, w), nxt[edgeTot] = head[u], head[u] = edgeTot;
}
void dijsktra(int s)
{
disQ.push(s), dis[s] = ;
while (disQ.size())
{
int tt = disQ.top();
disQ.pop(), visQ[tt] = ;
for (int i=head[tt]; i!=-; i=nxt[i])
{
int v = edges[i].v, w = edges[i].val;
if (dis[tt]+w < dis[v]){
dis[v] = dis[tt]+w;
if (!visQ[v])
visQ[v] = , disQ.push(v);
}
}
}
}
int dp(int x, int y)
{
if (f[x][y]) return f[x][y];
if (stk[x][y]) return -;
if (x==n) f[x][y]++;
stk[x][y] = ;
for (int i=head[x]; i!=-; i=nxt[i])
{
int v = edges[i].v, w = edges[i].val;
int tt = y-dis[v]+dis[x]-w;
if (tt>=){
int tmp = dp(v, tt);
if (tmp==-){
stk[x][y] = ;
return -;
}
(f[x][y] += f[v][tt]) %= p;
}
}
stk[x][y] = ;
return f[x][y];
}
int main()
{
// freopen("lg3953.in","r",stdin);
T = read();
while (T--)
{
memset(dis, 0x3f3f3f3f, sizeof dis);
memset(head, -, sizeof head);
memset(f, , sizeof f);
n = read(), m = read(), k = read(), p = read(), edgeTot = ;
for (int i=; i<=m; i++)
edgesSv[i].u = read(), edgesSv[i].v = read(), edgesSv[i].val = read();
memset(head, -, sizeof head);
edgeTot = ;
for (int i=; i<=m; i++)
addedge(edgesSv[i].v, edgesSv[i].u, edgesSv[i].val);
dijsktra(n);
memset(head, -, sizeof head);
edgeTot = ;
for (int i=; i<=m; i++)
addedge(edgesSv[i].u, edgesSv[i].v, edgesSv[i].val);
printf("%d\n",dp(, k));
}
return ;
}
推荐相关
题解 P3953 【逛公园】https://kelin.blog.luogu.org/solution-p3953
END
【图论 动态规划拆点】luoguP3953 逛公园的更多相关文章
- [luoguP3953] 逛公园(DP + spfa)
传送门 看到求方案数,应该很容易想到dp f[u][i]表示到点u,且比到u的最短距离多i的方案数 那么需要先预处理dis数组,spfa或者堆优化的dijk 因为考虑到dp的顺序,f[u][i]转移到 ...
- 【NOIP2017】逛公园 拆点最短路+拓扑(记忆化搜索
题目描述 策策同学特别喜欢逛公园.公园可以看成一张N个点M条边构成的有向图,且没有 自环和重边.其中1号点是公园的入口,N号点是公园的出口,每条边有一个非负权值, 代表策策经过这条边所要花的时间. 策 ...
- [NOIp 2017]逛公园
Description 策策同学特别喜欢逛公园.公园可以看成一张$N$个点$M$条边构成的有向图,且没有 自环和重边.其中1号点是公园的入口,$N$号点是公园的出口,每条边有一个非负权值, 代表策策经 ...
- 【NOIP2017 D1T3】逛公园
NOIP2017 D1T3 逛公园 题意:给一个有向图,每条边有权值,问从\(1\)到\(N\)的长度不超过最短路长度\(+K\)的路径条数.如果有无数条则输出\(-1\). 思路:我们首先扔掉\(- ...
- 逛公园[NOIP2017 D2 T3](dp+spfa)
题目描述 策策同学特别喜欢逛公园. 公园可以看成一张 \(N\)个点\(M\) 条边构成的有向图,且没有自环和重边.其中 1号点是公园的入口,N号点是公园的出口,每条边有一个非负权值,代表策策经过这条 ...
- NOIP2017 Day1 T3 逛公园
NOIP2017 Day1 T3 更好的阅读体验 题目描述 策策同学特别喜欢逛公园.公园可以看成一张\(N\)个点\(M\)条边构成的有向图,且没有 自环和重边.其中1号点是公园的入口,\(N\)号点 ...
- [vijos P1083] 小白逛公园
不知怎地竟有种错觉此题最近做过= =目测是类似的?那道题貌似是纯动归? 本来今晚想做两道题的,一道是本题,一道是P1653疯狂的方格取数或NOI08 Employee,看看现在的时间目测这个目标又达不 ...
- Bzoj 1756: Vijos1083 小白逛公园 线段树
1756: Vijos1083 小白逛公园 Time Limit: 10 Sec Memory Limit: 64 MBSubmit: 1021 Solved: 326[Submit][Statu ...
- BZOJ 1756: Vijos1083 小白逛公园
题目 1756: Vijos1083 小白逛公园 Time Limit: 10 Sec Memory Limit: 64 MBSubmit: 856 Solved: 264[Submit][Sta ...
随机推荐
- 解决tomcat一闪而过(转)
转自 http://blog.csdn.net/znn626/article/details/7893555 遇到很多次运行startup.bat后,一个窗口一闪而过的问题,但是从来没去纠正怎样修改配 ...
- mysql8必知必会7 连接 内连接 外连接 交叉连接
/* 2 需求:建立产品和分类表 1.查询每种分类的产品数量,没有产品的分类也要统计.(cname,quantity) 2.根据分类名称查询分类中的所有产品 */ -- --------------- ...
- log日志中不打印异常栈的具体信息
问题与分析 最近在查项目的log时发现报了大量的NPE(NullPointerException),诡异的是只log了Exception的类名,却没有具体的堆栈信息,以致于无法对该NPE异常进行准确定 ...
- 关于JS中的call()方法和apply() 暂时只接触到call() 等接触到apply()再回头来看
1. 每个函数都包含两个非继承而来的方法:call()方法和apply()方法. 2. 相同点:这两个方法的作用是一样的. 都是在特定的作用域中调用函数,等于设置函数体内this对象的值,以扩充函数赖 ...
- STP-5-STP配置及分析
拓扑: root id列出了根的网桥id为两部分,前边是优先级,后边跟着mac地址,cost 0 暗示sw1就是根: 下边的命令确认sw1就是vlan1的根: 下边,sw2配置了一个比sw1更低的优先 ...
- G-华华对月月的忠诚
链接:https://ac.nowcoder.com/acm/contest/392/G 题意: 月月要参加学校的信息学集训,晚上不能陪华华聊天了.不过为了防止华华去和别的小姐姐聊天,浪费时间影响学习 ...
- morphia(5)-删除
@Test public void delete() throws Exception { final Query<Employee> query = datastore.createQu ...
- 基于.net core微服务(Consul、Ocelot、Docker、App.Metrics+InfluxDB+Grafana、Exceptionless、数据一致性、Jenkins)
1.微服务简介 一种架构模式,提倡将单一应用程序划分成一组小的服务,服务之间互相协调.互相配合,为用户提供最终价值.每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相沟通(RESTfu ...
- Unity EditorWindow知识记录
1.创建EditorWindow using UnityEditor; using UnityEngine; public class ZZEditorWindow : EditorWindow { ...
- Unity Shader入门精要学习笔记 - 第11章 让画面动起来
转自 冯乐乐的 <Unity Shader入门精要> Unity Shader 中的内置变量 动画效果往往都是把时间添加到一些变量的计算中,以便在时间变化时画面也可以随之变化.Unity ...