这题是我最先开始做的一批题之一,糖哥tql,您带的榜可卡死我咯。一开始想到一个很单纯的套路,就是搜,最短路,最少代价最先到达,BFS走起冲冲冲,每次到下一个层次的新点就延申出一条边,一次性更新全部同色的边上点的深度。每次广搜扩展就有了很多跳过条件,能保证每条边最多遍历一次,这么听起来好像算是O(e)的算法?但是由于每次在一个点上判断次数过多,所以这么做基本上是必t的,加了算是优秀的剪枝也过不了19点。糖哥先过的E嘛,我问他前面的点跑的多快,发现自己前面的速度快到起飞,正解13点150ms我能跑出35ms,所以甚至有了这个爆搜有可能的念头,糖哥还陪着我一起调了好久hhhh。(在最后这几天糖哥把这个方法调教出来了,用了multiset去重和排序etc,当然最后不能算我的东西代码我就不贴了,但是也学到了点东西)
虽然一直在TLE的样子很狼狈,但是不懈调参爆破的样子真的很靓仔
贴一下自己的一个版本,纪念一下:
//这个版本不是最终的版本没有加删边的优化过不了18点,但是比我瞎删边的那个版本思路清晰很多
void update(int now, int d, int c, int father) //同色更新
{
if (now == n)
{
printf("%d\n", d - 1);
exit(0);
}
for (int i = head[now]; i; i = edge[i].nex)
{
int to = edge[i].to, color = edge[i].col; //颜色是边上的性质
if (father == to) continue;
if (color != c) continue; //同色边即可不花费深度更新
if (depth[to] < d) continue;
if (edge[i].vis) continue;
if (depth[to] != d)
{
depth[to] = d;
v[d].push_back(to);
}
//printf("now = %d to = %d depth = %d color = %d\n", now, to, d, color);
edge[i].vis = true; //一条边真的只遍历一次
update(to, d, c, now);
}
}
void sou_soutmd()
{
for (int i = 1; i <= n; i ++)
depth[i] = inf / 10;
depth[1] = 1; v[1].push_back(1);
for (int i = 1; i <= n; i++)
{
int num = v[i].size(); //vector v[i]代表当前深度i中各种元素
for (int j = 0; j < num; j++)
{
int now = v[i][j];
for (int k = head[now]; k; k = edge[k].nex)
{
int to = edge[k].to;
if (edge[k].vis) continue;
if (depth[to] <= i) continue; //这样保证了点to一定不同色且深度一定是最优
if (depth[to] != i + 1)
{
depth[to] = i + 1;
v[i + 1].push_back(to);
}
edge[k].vis = true; //真的只遍历一次
update(to, i + 1, edge[k].col, now);
}
}
}
}
最后想想有那时间爆破为啥不去学学真正的图论算法,好傻,就弃了这样的做法。
后来才知道这题的正解,拆点(我喜欢叫拆边),可以说是很巧妙了。
我们首先知道有两点一边u → v(边上颜色为c),我们这么认为这个过程,
对于一个点,我们只关心,它进入了什么颜色,以及它一下步要变成什么颜色。
所以从u点经过颜色c到v点我们可以看成分解成这样的
几个过程:
1.从u这个入口走进颜色c
2.在颜色c中走到出口v
3.从这个出口走到v
把这条边拆成u → uc → vc → v,u到一个点uc走了1个单位距离,从uc到vc是同色无代价,再从vc出v也需要付出1的代价。这条边上的代价就是总共的2除以2为1 。
这样也许你看不出来啥,如果两条边同色边相连呢?
eg. u → v → w(c):
拆成 u → uc → vc → v → vc → wc → w,我们注意到出现了两个vc,根据最短路的松弛规则,u与w之间,uc到vc的最短距离就是0,所以我们可以说u → w的最短路是,u → uc → vc → wc → w,而这个付出的代价仅为2除以2,就可以巧妙的过滤掉了中间同色边上的节点v。不同色就无法松弛,所以能构成正解。
同理就是全图上所有的节点,拆点后最短路就完事了。
拆点时注意可以用map存:
int getid(int x, int y){
if (!Map[x][y]) //没有出现过,就赋予新点id
Map[x][y] = ++n;
return Map[x][y];
}
然后上ac代码吧,感觉也没啥好说的了,图的构建才是关键,思路牛逼震惊我:
#include<bits/stdc++.h>
#define FOR(a, b, c) for(int a=b; a<=c; a++)
#define maxn 5000005 //因为拆了后有很多边,数组一定要开大
#define maxm 55
#define hrdg 1000000007
#define zh 16711680
#define inf 2147483647
#define llinf 9223372036854775807
#define ll long long
#define pi acos(-1.0)
#define ls p<<1
#define rs p<<1|1
using namespace std;
int n, m, u, v, c;
int dis[maxn], goal;
bool vis[maxn];
struct Edge{
int to, nex, val;
bool operator < (const Edge & rhs) const //优先队列内排序规则
{return val > rhs.val;}
}edge[maxn];
int head[maxn], tot=0;
void add_edge(int u, int v, int c){edge[tot]={v, head[u], c}; head[u]=tot++;}
map<int, int> Map[maxn];
priority_queue<Edge> q;
inline int read(){
char c=getchar();long long x=0,f=1;
while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
while(c>='0'&&c<='9'){x=x*10+c-'0';c=getchar();}
return x*f;
}
int getid(int x, int y){
if (!Map[x][y])
Map[x][y] = ++n;
return Map[x][y];
}
void dijsktra(int start, int goal) //最短路
{
for (int i = 1; i <= 10 * n; i++)
dis[i] = inf;
dis[start] = 0;
q.push((Edge){start, 0, 0});
while (!q.empty())
{
Edge k = q.top(); q.pop();
int now = k.to;
if (vis[now]) continue;
vis[now] = 1;
for (int i= head[now]; ~i; i = edge[i].nex)
{
int to = edge[i].to;
if (dis[to] > dis[now] + edge[i].val)
{
dis[to] = dis[now] + edge[i].val;
q.push((Edge){to, 0, dis[to]});
}
}
}
if (dis[goal] == inf) //证明没届到
puts("-1");
else
printf("%d", dis[goal] / 2);
}
int main()
{
n = read(); m = read();
goal = n;
for (int i = 1; i <= n; i++) Map[i].clear(); //好像这步地图清空没有多少必要?
memset(head, -1, sizeof(head));
for (int i = 1; i <= m; i++)
{
u = read(); v = read(); c = read();
int uc = getid(u, c);
int vc = getid(v, c);
add_edge(uc, vc, 0); add_edge(vc, uc, 0); //在有了uc和vc的编号的前提下建边,规则已经说过了
add_edge(u, uc, 1); add_edge(v, vc, 1);
add_edge(uc, u, 1); add_edge(vc, v, 1);
}
dijsktra(1, goal); //最短路
return 0;
}