Caution! This problem turned out to be NP-hard. But since there were no rules against writing an NP-hard problem, we decided to leave this problem here.
There is a bidirectional graph consisting of n vertices and m edges. The vertices and edges are numbered from 1 to n and 1 to m respectively, and the weight of edge iis wi. (1 ≤ i ≤ m) Given a natural number k, find the length of the shortest simple path that starts from vertex 1 and ends at vertex n, and consists of k edges. A simple path is a path that does not visit same vertex twice, and length of a path is the sum of weight of edges that consists the path.
In the first line, three space-separated integers n, m, k are given. (2 ≤ n < 106, 1 ≤ m, k < 106, min(n, m, k) ≤ 5)
In the next m lines, three space-separated integers xi, yi, wi are given. They denote that edge i is connecting vertex xi and vertex yi, and has weight wi. (1 ≤ xi, yi ≤ n, 1 ≤ wi ≤ 108)
No loops or multiple edges are given.
Print the length of the shortest simple path that starts from vertex 1 and ends at vertex n, and consists of k edges. If there is no such path, print -1.
给一个无向图,没有自环和多重边,让你找出一条从1-n的包含K条边的路径,要求这条路径最短,并且不重复经过同一个点。
然后输出这条路径的最小值,如果没有,就输出 -1 。
因为 min(n,m,k) <= 5,因此不难发现,k一定小于等于5,由此可以考虑各种暴力做法。
一开始看到这题,以为是最短路水题,给每个点打个标记,标记到达这个点经过了多少条边,然后直接跑 dijstra,跑到了第 27 个点,然后wa了......
后来想想不对,中间某一个点可能会被一条最短路更新,但这条路不是最优解,那么这个算法就错了。
在这之后,队友觉得K比较小,貌似可以直接搜索,就打了个dfs上去,然后无情T在了第20个点......
然后我又想了想,换了个算法,改成分层图去跑 dijstra ,然后给每个点记录一个前驱,当你要更新一个新的点的时候,用前驱来验证一下你接下来要跑的那个点是否在你之前的路径上,避免重复跑到同一个点,用这种算法去跑的话,能跑到第31个点...
在这之后,比赛就结束了......在这场比赛中,我全场像个zz一样怼一个没人A的题,这种zz行为简直可以登榜我的zz记录本......
赛后再仔细想了想,还是一开始dij那个问题,就算你进行了分层,但你一旦更新了一个点的最短路,而且这个最短距离所在的层次正好是正确答案所在的层次,那么跑正确答案的时候就跑不动了,因此虽然分层可以多过一些点,但是算法依然是错误的。
之后就去查题解了,然后研究题解C++11上的 e xin 代码。算是明白了思路。
本题的思路就是,如果 k >= n || k > m || k > 5,直接输出-1,因此本题k的最大值就是5。
然后我们来思考当k为5的时候我们应该如何求解。
【这里用的方法有点类似队友那个直接dfs了,此处其实类似于双向bfs,大大减少计算量】
对于每个点都求出到 1号点 和 n号点 到该点的最短距离,次短距离,次次短距离,然后枚举所有边,并且记录每个点最短距离、次短距离、次次短距离的前驱,验证这条路径上的五个点是否互不相同,然后更新 ans 即可。
这里为什么要记录3个最短距离呢,原因是满足最短距离的时候可能并不合法,即 1-i 的最短路径上经过了 j-n 上的点,而不合法的时候就需要再去验证次短距离了。
由于 j-n 上最多只有3个点,所以记录3个距离即可,一开始我记录了四个最短距离,A了之后,改成三个发现也可以过。
然后就是一个大型搜索现场,对着所有边进行枚举,对着每个点的三个距离进行 3*3 的组合验证,然后再一一对ans进行更新。
所以对于K = 5的情况我们就解决了,K < 5的情况,就直接在n点后面补虚点,然后将K加到5即可。
本题打了一天......一共wa了34发......各种算法换来换去,现在就感觉自己是个zz。
本题在题干中就已经提醒过我们这是一个 NP-hard 问题,虽然对于P与NP问题了解的很浅,但是至少也应该知道,P问题的时间复杂度是由多项式表达的,例如 O(n),O(nlogn)等。但NP问题的时间复杂度目前没有多项式表达的算法可以实现,既然如此,那尝试dijstra、分层图dijstra,怕不是石乐志,妄图想成为一名计算机理论学家吗?!
自己简直就像是一个弟弟,zz整场,没被队友干掉,我真是感激涕零......
而对于NP问题,大部分的解决方法是直接进行搜索,时间复杂度是指数级的,当然并不是所有的NP问题都是用搜索进行解决,比如背包问题,就是用动态规划进行解决,但是动态规划算法的复杂度依然是指数级别的。
因此正常的思路应该就是看到NP,然后想到,噢!搜索问题!那该怎么搜呢?我们先看看K的范围好了,噢!K最大是5,那对K == 5该如何解决呢?
直接搜五步?不对,首先时间复杂度就不行,那就双向bfs搜?好像时间复杂度可行。
那就从起点跑两个点,终点跑两个点,枚举所有边好了。记得验证这条路径上不能有重复点。
还是有问题,如果最短路不符合题意,而次短路确是答案该怎么办?那就给每个点记录是三个距离好了!
然后3*3暴力匹配,这样应该就行了!嗯,那既然K == 5可以解决了,那对于K < 5的时候,直接补虚点好了!
......
题目虽然做完了,但是还是需要总结一下自己学到了什么。
首先是NP问题的概念,这次应该印象很深刻了......没想到题意这么简单的一个问题,解决起来却如此麻烦......
其次是搜索的问题,一定要注意观察数据范围,当K很小的时候,可以从起点跑几个点,再从终点跑几个点,然后枚举边或者枚举点,这里需要注意。
再然后就是虚点的问题,通过加虚点这个方式,可以将大量的情况都归纳在最大的那一种情况之中,这个技巧也需要掌握!
#include
#include
#include
#include
#include
#define rep(i,a,b) for(int i = a;i <= b;i++)
using namespace std;
const int N = 1e6+1000;
const int M = 2*1e6+1000;
typedef long long ll;
const int inf = 1e9;
struct Edge{
int to,next;
int w;
}e[M];
int head[N],tot,pre1[N][5],pre2[N][5];
int n,m,k,tl;
int dis1[N][5],dis2[N][5],ans;
struct Line{
int u,v,w;
}line[M];
void init()
{
tl = 0;
tot = 1;
rep(i,1,n) head[i] = 0;
}
void add(int x,int y,ll w)
{
e[++tot].to = y, e[tot].next = head[x], e[tot].w = w, head[x] = tot;
}
int judge(int a,int b,int id1,int id2)
{
if(dis1[a][id1] == inf || dis2[b][id2] == inf) return 0;
int a1 = pre1[a][id1], b1 = pre2[b][id2];
if(a == 1 || a == n || a == b || a == b1 || a == a1 || a1 == -1 || b1 == -1 || a == -1 || b == -1) return 0;
if(a1 == 1 || a1 == n || a1 == b || a1 == b1) return 0;
if(b == 1 || b == n || b == b1) return 0;
if(b1 == 1 || b1 == n ) return 0;
return 1;
}
void cal(int a, int b, int id1, int id2,int w)
{
if(judge(a,b,id1,id2))
ans = min(ans,dis1[a][id1]+dis2[b][id2]+w);
}
void bfs()
{
rep(i,1,n)
rep(j,0,3)
dis1[i][j] = inf,dis2[i][j] = inf;
rep(i,1,n)
rep(j,0,3)
pre1[i][j] = -1, pre2[i][j] = -1;
for(int i = head[1]; i ;i = e[i].next)
{
int x = e[i].to;
int v1 = e[i].w;
if(x == 1 || x == n) continue;
for(int j = head[x]; j ; j = e[j].next)
{
int y = e[j].to;
int v2 = e[j].w;
if(y == 1 || y == n || y == x) continue;
if(dis1[y][0] > v1+v2)
{
dis1[y][2] = dis1[y][1], pre1[y][2] = pre1[y][1];
dis1[y][1] = dis1[y][0], pre1[y][1] = pre1[y][0];
dis1[y][0] = v1+v2, pre1[y][0] = x;
}
else if(dis1[y][1] > v1+v2)
{
dis1[y][2] = dis1[y][1], pre1[y][2] = pre1[y][1];
dis1[y][1] = v1+v2, pre1[y][1] = x;
}
else if(dis1[y][2] > v1+v2)
{
dis1[y][2] = v1+v2, pre1[y][2] = x;
}
}
}
for(int i = head[n]; i ;i = e[i].next)
{
int x = e[i].to;
int v1 = e[i].w;
if(x == 1 || x == n) continue;
for(int j = head[x]; j ; j = e[j].next)
{
int y = e[j].to;
int v2 = e[j].w;
if(y == n || y == 1 || y == x) continue;
if(dis2[y][0] > v1+v2)
{
dis2[y][2] = dis2[y][1], pre2[y][2] = pre2[y][1];
dis2[y][1] = dis2[y][0], pre2[y][1] = pre2[y][0];
dis2[y][0] = v1+v2, pre2[y][0] = x;
}
else if(dis2[y][1] > v1+v2)
{
dis2[y][2] = dis2[y][1], pre2[y][2] = pre2[y][1];
dis2[y][1] = v1+v2, pre2[y][1] = x;
}
else if(dis2[y][2] > v1+v2)
{
dis2[y][2] = v1+v2, pre2[y][2] = x;
}
}
}
rep(i,1,tl)
{
int u = line[i].u;
int v = line[i].v;
int w = line[i].w;
rep(k1,0,2)
rep(k2,0,2)
cal(u,v,k1,k2,w);
rep(k1,0,2)
rep(k2,0,2)
cal(v,u,k1,k2,w);
}
}
int main()
{
scanf("%d%d%d",&n,&m,&k);
init();
rep(i,1,m)
{
int x,y,z;
scanf("%d%d%d",&x,&y,&z);
add(x,y,z), add(y,x,z);
line[++tl].u = x, line[tl].v = y, line[tl].w = z;
}
if(k > m || k >= n || k > 5){
printf("-1\n");
return 0;
}
ans = inf;
while(k < 5){
add(n,n+1,0), add(n+1,n,0);
line[++tl].u = n, line[tl].v = n+1, line[tl].w = 0;
n++, k++;
}
bfs();
if(ans == inf)
printf("-1\n");
else printf("%d\n",ans);
return 0;
}
/*
4 6 3
1 2 10
2 3 1
3 4 1
2 4 10
1 3 1
1 4 1
*/