洛谷 P3953 [NOIP2017]逛公园
简析
看到题第一反应用最短路计数的做法骗分(2333333)
总之我们来看看部分分的做法。
\(30pts\)
\(K = 0\) ,可以狂喜了。
由于 \(spfa\) 比较好写,所以在这里仅仅给出需要在模板上进行修改的地方,其他细节请读者自行思考:
if(u == n) continue;
......
if(d[v] == d[u] + w){
f[v] += f[u];
}else if(d[v] > d[u] + w){
d[v] = d[u] + w;
f[v] = f[u];
}if(!vis[v] && f[v]) q.push(v),vis[v] = 1;
......
f[u] = 0;//出队时清掉f
我们依次分析一下以上代码:
当扩展到终点时,我们应当直接跳过他(注意不是终止函数),用终点再去扩展终点的出边可能会导致答案重复统计。
显然,当从 \(u\) 到 \(v\) 的权与 \(u\) 当前最短路的和与当前 \(v\) 的最短路相等时,我们将 \(v\) 点的方案数加上 \(u\) 点的方案数。
如果最短路被更新,那么 \(v\) 原有的方案全部作废,新方案数与 \(u\) 方案数相同。
为什么入队有两个判断条件呢?第一个条件是显然的,本来就在队列中的点不应重复入队。对于第二个条件,显然只有进入了以上两个分支, \(f[v]\) 才会被更新。该条件保证了不扩展冗余的节点。
由于 \(spfa\) 的原理,每个节点可能入队多次,如果不清空 \(f\) 数组可能导致答案的重复统计,因此在离队时要清掉。
一个小小的题外话
关于 \(spfa\) ,我们发现由于其有着节点可能会多次进队的特殊性质,因此在运用它不论是进行什么样的操作,尤其是递推式更新时,一定要注意重复扩展的问题,或者换用 \(dijkstra\) 算法司普发早死几百年了。
对于本题 \(30pts\),由于 \(dijkstra\) 算法改动部分大同小异,因此这里不再赘述,仅保留以上添加代码的第二部分并将其改为 \(dijkstra\) 风格即可。
\(70pts\)
考虑没有 \(0\) 边的情况,由于 \(K = 50\) 我们来考虑一下 \(dp\) 做法。
如何设计状态呢?一个显而易见的思路如下:
我们现在已经计算出了到 \(n\) 点的最短路 \(d[n]\),显然题设要求路径长度不得超过 \(d[n] + K\)。不妨令 \(f[i][j]\) 表示在 \(i\) 号点,剩下可以走的路的长度是 \(j\)。
初态 : \(f[1][d[n] + k] = 1;\)
末态 : \(ans += f[n][j];\) \(j\) :\(d[n] -> d[n] + k\)
转移 : \(E(u,v,w)\),\(f[v][j - w] += f[u][j];\)
但 \(d[n]\) 可能很大,开不下,怎么办?
注意到我们有从 \(1\) 到每个点的最短路径长。同时观察终态我们可以发现统计答案时只用到了 \(d[n] -> d[n] + k\)。那么考虑在第二维只记录 \(K\)。\(f[i][j]\) 表示在 \(i\) 号点,剩下可以多走的路的长度是 \(j\)。很显然,我们只需要在走的过程中逐步计算相对于最短路多走的路径长,保证这个多出来的长小于 \(K\) 即可。
不过,这个做法的正确性似乎有待商榷。我们知道全局最短路并不完全是局部最短路,从 \(u\) 到 \(v\) 的最优路径也不一定是 \(d[v] + d[u]\) ,但这种思路处理本题足矣。
希望有想法的读者能在评论区留下您的看法,对此法证明或证伪,鄙人感激不尽。
\(100pts\)
考虑满分做法。当 \(0\) 边存在时,显然只有出现 \(0\) 环时无解。我们对递归中的状态打上一个标记,一旦发现尚在递归中的状态访问到自身时(访问同一节点且剩余路长相等)则返回无解。
AC代码
#include<iostream>
#include<cctype>
#include<cstdio>
#include<cstring>
#include<queue>
using namespace std;
const int maxn = 100005;
int read(){
int re = 0,ch = getchar();
while(!isdigit(ch)) ch = getchar();
while(isdigit(ch)) re = (re<<1) + (re<<3) + ch - '0',ch = getchar();
return re;
}
struct edge{
int v,w,nxt;
}e[maxn<<1];
int h[maxn],cnt;
void add(int u,int v,int w){
e[++cnt].w = w;
e[cnt].v = v;
e[cnt].nxt = h[u];
h[u] = cnt;
}
int n,m,k,p,t;
bool vis[maxn];
int dis[maxn];
int f[maxn][55];
bool v[maxn][55];
void spfa(){
memset(dis,0x3f,sizeof(dis));
queue<int> q;
vis[1] = 1;
dis[1] = 0;
q.push(1);
while(q.size()){
int u = q.front();q.pop();
vis[u] = 0;
for(int i = h[u];i;i = e[i].nxt){
int v = e[i].v,w = e[i].w;
if(dis[v] > dis[u] + w){
dis[v] = dis[u] + w;
if(!vis[v]) q.push(v),vis[v] = 1;
}
}
}
}
int dfs(int u,int left){
if(left < 0) return 0;
if(v[u][left]) return -1;// 0 ring
if(f[u][left] != -1) return f[u][left];
v[u][left] = 1;
int ans = 0;
if(u == n) ans++;
for(int i = h[u];i;i = e[i].nxt){
int v = e[i].v,w = e[i].w;
int ansc = dfs(v,left - (dis[u] + w - dis[v]));
if(ansc == -1) return -1;
ans = (ansc + ans) % p;
}
f[u][left] = ans % p;
v[u][left] = 0;
return ans;
}
int main(){
t = read();
while(t--){
n = read(),m = read(),k = read(),p = read();
memset(h,0,sizeof(h));
memset(f,-1,sizeof(f));
memset(v,0,sizeof(v));
cnt = 0;
for(int i = 1;i <= m;i++){
int u = read(),v = read(),w = read();
add(u,v,w);
}
spfa();
printf("%d\n",dfs(1,k));
}
return 0;
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 智能桌面机器人:用.NET IoT库控制舵机并多方法播放表情
· Linux glibc自带哈希表的用例及性能测试
· 深入理解 Mybatis 分库分表执行原理
· 如何打造一个高并发系统?
· .NET Core GC压缩(compact_phase)底层原理浅谈
· 手把手教你在本地部署DeepSeek R1,搭建web-ui ,建议收藏!
· 新年开篇:在本地部署DeepSeek大模型实现联网增强的AI应用
· 程序员常用高效实用工具推荐,办公效率提升利器!
· Janus Pro:DeepSeek 开源革新,多模态 AI 的未来
· 【译】WinForms:分析一下(我用 Visual Basic 写的)