/* 总体思路见小白书 P173 的讲解,不过小白书非常言简意赅,有点不好理解,所以我把自己的一些理解,加到了代码注释里 收获: 1. C++ 中,bool 类型的 true 等价于 int 类型的 1, bool 类型的 false 等价于 int 类型的 0 参见 ( https://stackoverflow.com/questions/5369770/bool-to-int-conversion ) 摘取: If the source type is bool, the value false is converted to zero and the value true is converted to one. 说明: 1. 凡是加上了 (int) 强制类型转换的,基本都是因为,STL中的size()函数,返回的并不一定是int类型,按专业的说法,返回的应该是 size_t类型,所以还是应该转换的,虽然大部分时候,不强制转换好像也不会有错,但是,既然知道了这个知识点,最好还是严谨点 */
#include <iostream> #include <cstring> #include <vector> #include <queue> #define rep(i, n) for ( int i = 0; i < (n); i++ ) using namespace std; const int N = 1e5 + 5; const int INF = 0x3f3f3f3f; struct edge { int u, v, c; edge ( int u = 0, int v = 0, int c = 0 ) : u(u), v(v), c(c) { } }; vector<edge> edges; vector<int> G[N]; vector<int> ans; int n, vis[N]; int d[N]; void addEdge (int u, int v, int c) { edges.push_back ( edge(u, v, c) ); int idx = (int)edges.size() - 1; G[u].push_back(idx); // G[u] 记录了所有与 u 邻接的边,在 edges 中的下标 } // 反向 bfs, 以找到每个结点 i 到终点的最短距离 d[i] void rev_bfs() { memset( vis, 0, sizeof(vis) ); d[n - 1] = 0; vis[n - 1] = true; queue<int> q; q.push(n - 1); while ( !q.empty() ) { int v = q.front(); q.pop(); rep( i, (int)G[v].size() ) { int e = G[v][i]; int u = edges[e].v; /* u 是所有与 v 相邻的结点,u 结点如果已经被访问过,说明最短距离 d[u] 已被赋值,且肯定比 v 想要赋给 u 的 d[v] + 1 要小 (这个是通过遍历的规律能推出来的,不过我不知道如何严谨证明),所以如果已经被赋值,我们就不去更改了; 只对没有赋值的相邻结点作如下处理:更改其d[u],并设置标记 vis[u] (使其的 d[u]不会被 u 的其他相邻结点更改,因为 d[u] 不可能比当前更小了),并把 u 结点的标号压栈 */ if ( !vis[u] ) { vis[u] = true; d[u] = d[v] + 1; q.push(u); } } } } /* *从起点开始走,但是每次达到一个新结点时,要保证d值恰好减少 1, 如果有多种走法,选颜色字典序最小的走, *相当于再做了一次 BFS */ void bfs() { memset( vis, 0, sizeof(vis) ); vis[0] = true; ans.clear(); vector<int> next; next.push_back(0); rep ( i, d[0] ) //要选取的路线,使得 d 值恰好减少1,而起点到终点的最短距离又是 d[0],故而循环次数就为 d[0] { int min_color = INF; rep ( j, (int)next.size() ) // next中存放了上一步的所有可能解 ( 可能有多条边颜色字典序最小,那么走下一步时,要把所有这些可能的解,都当作起点考虑一遍 ) { int u = next[j]; rep ( k, (int)G[u].size() ) //找到这些可能的“起点”的临边,逐一分析 { int e = G[u][k]; int v = edges[e].v; if ( d[u] == d[v] + 1 ) //如果有恰好能走一步的情况,则看能否更新当前的 min_color,可以则更新 min_color = min ( min_color, edges[e].c ); } } ans.push_back(min_color); //这是从所有可能的“起点”开始,考虑了其临边以后,我们得到的,这一步真正该走的颜色,压入该颜色 /* 因为最初可能有多个字典序最小,在 rep ( i, d[0] ) 循环中的 rep ( j, (int)next.size() )里,其实就是以这些点为起点,考虑其所走的下一步,该取哪个颜色; 但是,走完下一步以后,其实还是有可能有多解,下面的这些代码,就是找出再走一步时的多解 (这些解的起点必定来自 next,且它们选取的颜色,肯定和要走的下一步,所选取的 min_color 颜色相等,且在走的过程中,必定也是 d 每次递减 1 的),把这些多解都压入 next2 这个 vector 中,并将其赋给 next 这个 vector,在 i 循环的下一轮循环里,我们就又从 next里的多解来考虑 ( 此时next 已经变成,走下一步时,保存的多解了 ),如此循环往复,每次往ans里面压入所选取的一个颜色,并再次从多解中,挑出真正满足“最小颜色”、“最小字典序”的解,这样不断地进行剔除,知道选出了从起点走向终点的,共 d[0] 步的走法 */ vector<int> next2; rep( j, (int)next.size() ) { int u = next[j]; rep( k, (int)G[u].size() ) { int e = G[u][k]; int v = edges[e].v; if ( d[u] == d[v] + 1 && !vis[v] && edges[e].c == min_color ) { vis[v] = true; next2.push_back(v); } } } next = next2; //话说,这个的思路,好像有点像,DP 里的滚动数组呢!~就是永远是两个容器来保存,进行下一轮时,要以另一个容器的值作为新的启示值 //虽然好像和 DP 的滚动数组,还是有不少差别的,但我觉得思想上,可以类比帮助理解,因为这题比滚动数组还抽象,比喻一下以后,感觉更好理解了一些 } cout << (int)ans.size() << endl; cout << ans[0]; for ( int i = 1; i < (int)ans.size(); i++) cout << " " << ans[i]; cout << endl; } int main () { int u, v, c, m; while (cin >> n >> m) { edges.clear(); rep(i, n) G[i].clear(); while (m--) { cin >> u >> v >> c; addEdge(u - 1, v - 1, c); addEdge(v - 1, u - 1, c); //注意,该题是无向图问题,所以要将起点终点颠倒,再建一次边 } rev_bfs(); // reverse-bfs 反向bfs bfs(); } return 0; }