0x61 最短路
Dijkstra
最短路的经典做法之一,相比后面的\(SPFA\),\(Dij\)的复杂度更低,更加的稳定
但是如果出现负环时,\(Dij\)是不能用的
#include <bits/stdc++.h> #define PII pair< int , int > #define F first #define S second using namespace std; const int N = 10005 , M = 500005 , INF = 0x7f7f7f7f; int n , m , s ; int dis[N]; vector< PII > e[N]; bool vis[N]; set<PII> q; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline void add( int x , int y , int w ) { e[x].push_back( { y , w } ); } inline void dijkstra() { memset( dis , INF , sizeof( dis ) ) , dis[s] = 0; q.insert( { 0 , s } ); for( register int u ; q.size() ; ) { u = q.begin()->S , q.erase( q.begin() ); if( vis[u] ) continue; vis[u] = 1; for( register auto it : e[u] ) { if( dis[ it.F ] <= dis[u] + it.S ) continue; dis[it.F] = dis[u] + it.S; q.insert( { dis[it.F] , it.F } ); } } return ; } int main() { n = read() , m = read() , s = read(); for( register int i = 1 , x , y , z ; i <= m ; x = read() , y = read() , z = read() , add( x , y , z ) , i ++ ); dijkstra(); for( register int i = 1 ; i <= n ; printf( "%d " , ( vis[i] ? dis[i] : 0x7fffffff ) ) , i ++ ); puts(""); return 0; }
为什么\(Dijkstra\)不能处理带负权边
不能处理负环很简单,有负环的情况下不存在最短路,可以一直刷呢一个负环
为什么不能处理负权边呢?看下面这个图
1
是起点,我们先把1
加入堆中,然后开始扩展可以得到dis[2] = 1 , dis[3] = 2
然后取出2
可以扩展出dis[3] = -1
然后取出3
可以扩展出dis[2] = -3
,此时就出现错误了,因为dij
每次从堆顶取出的点一定是最短路已知的点
但是如果出现负权边我们可以通过这条边扩展一条已经出队的点,与dij
算法的前提要求冲突,所以dij
不能处理带负权边的图
SPFA
\(SPFA\)这个名字貌似只有中国人在用,国外一般叫“队列优化的Bellman-Ford
算法”
\(SPFA\)算法一般在判断有无负环的时候用,如果是网格图的话,不推荐使用\(SPFA\),会非常的慢
#include <bits/stdc++.h>s #define PII pair< int , int > #define F first #define S second using namespace std; const int N = 1e4+5 , M = 5e5 + 5 ; const int INF = 0x7fffffff; int n , m , s ; int dis[N]; vector< PII > e[N]; queue<int> q; bool vis[N]; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch >'9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline void spfa() { for( register int i = 1 ; i <= n ; i ++ ) dis[i] = INF; q.push( s ) , vis[s] = 1 , dis[s] = 0; for( register int u ; q.size() ; ) { u = q.front() , q.pop() , vis[u] = 0; for( register auto it : e[u] ) { if( dis[u] + it.S >= dis[ it.F ] ) continue; dis[ it.F ] = dis[u] + it.S ; if( vis[it.F] ) continue; q.push( it.F ) , vis[ it.F ] = 1; } } return ; } int main() { n = read() , m = read() , s = read(); for( register int i = 1 , x , y , z ; i <= m ; x = read() , y = read() , z = read() , e[x].push_back( { y , z } ) , i ++ ); spfa(); for( register int i = 1 ; i <= n ; printf( "%d " , dis[i] ) , i ++ ); return 0; }
判负环
说道\(SPFA\)判断负环,常用地做法是判断每个点被松弛的次数,如果有一个点被松弛了\(N\)次,图中就会有负环
对比Dij
其实就是看数据选择了,如果没有负权边的话,肯定是\(Dijkstra\),出现负权边在考虑是否用\(SPFA\)
这里在讨论一个问题,\(SPFA\)和\(Dij\)有什么本质区别,或者说原理上有什么区别
\(Dij\)是按照点来扩展,\(SPFA\)是按照边来扩展
\(Dij\)按照点扩展很好理解,因为他每次都是去找距离最小的点在向外扩展,所以也很好理解为什么要用堆来维护
那么\(SPFA\)按照边来扩展,看似没有道理吗,确实如果你单看\(SPFA\)你可能会觉得这玩意有什么用,很暴力的样子
其实并不是,你可以先了解下\(Bellman-ford\)算法,枚举每一条边,扩展这边所连接的点
然后根据后人的研究发现,只有经过松弛的点,所连的边才有可能会松弛其他的点,所以就有人队列优化
来储存松弛过的点
在说一点,\(Dij\)算法中堆最大是多少
可以根据\(Dij\)是按照点连扩展的,所以一个点最多可以扩展他连接的所有的点,故堆最大就是所有点的出度之和
AcWing 340. 通信线路
二分答案,加\(Dij\)判断
二分答案,二分第\(k+1\)条边的权值,小于\(k+1\)的边不用考虑费用,大于\(k+1\)的边消耗优惠活动的一个条边
所以可以令大于\(k+1\)的边权值为\(1\),其他的边权值为\(0\)这样跑一遍\(Dij\)就可以统计出有多少条边大于\(k+1\)条边这样就可判断
其实这道题数据较水\(SPFA\)也可以直接过
#include <bits/stdc++.h> #define PII pair< int , int > #define F first #define S second using namespace std; const int N = 1005 , maxline = 1e6 + 5 , INF = 0x3f3f3f3f; int n , m , k , l , r = maxline , mid , ans = -1 , dis[N]; bool vis[N]; vector< PII > e[N]; set< PII > s; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch -'0'; ch = getchar(); } return x; } inline void add( int x , int y , int z ) { e[x].push_back( { y , z } ) , e[y].push_back( { x , z } ); return ; } inline bool panding() { memset( dis , INF , sizeof( dis ) ) , memset( vis , 0 , sizeof( vis ) ) , dis[1] = 0 , s.clear() , s.insert( { 0 , 1 } ); for( register int u , cur ; s.size() ; ) { u = s.begin() -> S , s.erase( s.begin() ); if( vis[u] ) continue; vis[u] = 1; for( register auto it : e[u] ) { cur = dis[u] + ( ( it.S > mid ) ? 1 : 0 ); if( dis[it.F] <= cur ) continue; dis[it.F] = cur; s.insert( { cur , it.F } ); } } return dis[n] <= k; } int main() { n = read() , m = read() , k = read(); for( register int i = 1 , x , y , z ; i <= m ; x = read() , y = read() , z = read() , add( x , y , z ) , i ++ ); while( l <= r ) { mid = ( l + r ) >> 1; if( panding() ) ans = mid , r = mid - 1; else l = mid + 1; } cout << ans << endl; return 0; }
Luogu P2296 寻找道路
\(Noip\)原题,首先考虑如何满足第一个条件,反向建图,充目标点开始做\(DFS\),把所有到达的点打上标记,这样如果一个点的子节点全部都被打上标记,那么这个点就满足第一条件
第二个条件就是裸的最短路,直接在被标记的点的中跑一遍最短路
#include <bits/stdc++.h> #define PII pair< int, int > #define F first #define S second using namespace std; const int N = 10005 , INF = 0x7f7f7f7f; int n , m , st , ed , dis[N]; bool vis[N] , can[N]; set< PII > s; vector<int> e[N] , t[N] ; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline void add( int x , int y ) { e[x].push_back(y) , t[y].push_back(x); } inline void dfs( int x ) { can[x] = 1; for( register auto v : t[x] ) { if( can[v] ) continue; dfs( v ); } return ; } inline bool panding( int x ) { for( register auto v : e[x] ) { if( can[v] ) continue; return 1; } return 0; } inline void Dijkstra() { for( register int i = 1 ; i <= n ; i ++ ) dis[i] = INF; s.insert( { 0 , st } ) , dis[st] = 0; for( register int u , w; s.size() ; ) { u = s.begin() -> S , s.erase( s.begin() ); if( vis[u] ) continue; vis[u] = 1; if( panding(u) ) continue; if( u == ed ) return ; for( register auto v : e[u] ) { if( dis[v] <= dis[u] + 1 ) continue; dis[v] = dis[u] + 1; s.insert( { dis[v] , v } ); } } } int main() { n = read() , m = read(); for( register int x , y ; m >= 1 ; x = read() , y = read() , add( x , y ) , m -- ); st = read() , ed = read(); dfs(ed); Dijkstra(); cout << ( dis[ed] == INF ? -1 : dis[ed] ) << endl; return 0; }
Loj #2590.最优贸易
f[i]
表示1
到n
的路径上最大值,g[i]
表示i
到n
的路径上的最小值
跑两遍\(Dij\),更新的时候把f[v]= f[u] + w
变为f[v] = max( f[u] , w )
即可
最后找到max( f[i] - g[i] )
即可
#include<bits/stdc++.h> #define PII pair< int , int > #define F first #define S second using namespace std; const int N = 1e5 + 5 , INF = 0x7f7f7f7f; int n , m , a[N] , f[N] , g[N] , ans; bool vis[N]; vector< int > e[N] , t[N]; set< PII > s ; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } inline void add( int x , int y , int z ) { e[x].push_back(y) , t[y].push_back(x); if( !( z & 1 ) ) e[y].push_back(x) , t[x].push_back(y); return ; } inline void solove() { memset( g , INF , sizeof( g ) ) , g[1] = a[1]; s.insert( { g[1] , 1 } ); for( register int u ; s.size() ; ) { u = s.begin() -> S , s.erase( s.begin() ); if( vis[u] ) continue; vis[u] = 1; for( register auto v : e[u] ) { g[v] = min( g[u] , a[v] ); s.insert( { g[v] , v } ); } } memset( vis , 0 , sizeof( vis ) ) , memset( f , -INF , sizeof( f ) ) , f[n] = a[n]; s.insert( { f[n] , n } ); for( register int u ; s.size() ; ) { u = s.begin() -> S , s.erase( s.begin() ); if( vis[u] ) continue; vis[u] = 1; for( register auto v : t[u] ) { f[v] = max( f[u] , a[v] ); s.insert( { f[v] , v } ); } } return ; } int main() { n = read() , m = read(); for( register int i = 1 ; i <= n ; a[i] = read() , i ++ ); for( register int i = 1 , x , y , z ; i <= m ; x = read() , y = read() , z = read() , add( x , y , z ) , i ++ ); solove(); for( register int i = 1 ; i <= n ; i ++ ) ans = max( ans , f[i] - g[i] ); cout << ans << endl; return 0; }
AcWing 342. 道路与航线
因为有负权边,不能直接用\(Dij\),考虑\(SPFA\)会\(T\)
观察题面的性质,只有单向变会出现负值,先不考虑单向变
会发现图变成了不连通了,但是每个连通块内没有负值了,块内可以直接跑\(Dij\)求最短路
然后我们根据拓扑序逐个跑每个联通块内的最短路,因为拓扑序的性质,可以避免重复的松弛,来保证\(Dij\)的性质
#include<bits/stdc++.h> #define pb( x ) push_back( x ) #define PII pair< int , int > #define F first #define S second using namespace std; const int N = 50005 , INF = 0x7f7f7f7f;; int n , r , p , st , tot , bl[N] , deg[N] , dis[N]; bool vis[N]; queue< int >q ; vector< int > group[N]; vector< PII > e[N]; set< PII >heap; inline int read() { register int x = 0 , f = 1; register char ch = getchar(); for( ; ch < '0' || ch > '9' ; ( ch == '-' ? f = - 1 : f ) , ch = getchar() ); for( ; ch >= '0' && ch <= '9' ; x = ( x << 3 ) + ( x << 1 ) + ch - '0' , ch = getchar() ); return x * f; } inline void add( int x , int y , int z , bool f ) { e[x].push_back( { y , z } ); if( f ) e[y].push_back( { x , z } ); return ; } inline void dfs( int x , int id ) { bl[x] = id; group[id].pb( x ); for( auto it : e[x] ) { if( bl[it.F] ) continue; dfs( it.F , id ); } return ; } inline void Dijkstra() { for( register int u ; heap.size() ; ) { u = heap.begin() -> S , heap.erase( heap.begin() ); if( vis[u] ) continue; vis[u] = 1; for( register auto it : e[u] ) { if( dis[it.F] > dis[u] + it.S ) { dis[it.F] = dis[u] + it.S; if( bl[it.F] == bl[u] ) heap.insert( { dis[it.F] , it.F } ); } if( bl[it.F] != bl[u] ) { deg[ bl[it.F] ] --; if( !deg[ bl[it.F] ] ) q.push( bl[it.F] ); } } } return ; } int main() { n = read() , r = read() , p = read() , st = read(); for( register int x , y , z ; r >= 1 ; x = read() , y = read() , z = read() , add( x , y , z , 1 ) , r -- ); for( register int i = 1 ; i <= n ; i ++ ) { if( bl[i] ) continue; dfs( i , ++ tot ); } for( register int x , y , z ; p >= 1 ; x = read() , y = read() , z = read() , add( x , y , z , 0 ) , deg[ bl[y] ] ++ , p -- ); q.push( bl[st] ); for( register int i = 1 ; i <= tot ; i ++ ) { if( deg[i] ) continue; q.push(i); } for( register int i = 1 ; i <= n ; dis[i] = INF , i ++ ); dis[st] = 0; for( register int t ; q.size() ; ) { t = q.front() , q.pop(); for( auto i : group[t] ) heap.insert( { dis[i] , i } ); Dijkstra(); } for( register int i = 1 ; i <= n ; ( dis[i] >= INF ? puts("NO PATH") : printf( "%d\n" , dis[i] ) ) , i ++ ); return 0; }
Floyd
上面给的两种算法,都只是但源多汇最短路,如果要求多源多汇最短路当然可以用n
次\(Dij\)或\(SPFA\)来做
但这样的效果并不好,这时就可以采用\(Floyd\)算法
for( int i = 1 ; i <= n ; i ++ ) { for( int j = 1 ; j <= n ; j ++ ) { for( int k = 1 ; k <= n ; k ++ ) { dis[i][j] = min( dis[i][j] , dis[i][k] + dis[k][j] ); } } }
Luogu P2910 Clear And Present Danger
因为这题给定的了访问点的顺序,所以我们不许要考虑顺序的问题
跑一边最短路,统计下给定路径的权值和即可
#include<bits/stdc++.h> using namespace std; const int N = 105 , M = 10005; int n , m , a[M] , dis[N][N] , sum; inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } int main() { n = read() , m = read(); for( register int i = 1 ; i <= m ; a[i] = read() , i ++ ); a[0] = 1; for( register int i = 1 ; i <= n ; i ++ ) { for( register int j = 1 ; j <= n ; dis[i][j] = read() , j ++ ); } for( register int i = 1 ; i <= n ; i ++ ) { for( register int j = 1 ; j <= n ; j ++ ) { if( i == j ) continue; for( register int k = 1 ; k <= n ; k ++ ) { if( k == i || k == j ) continue; dis[i][j] = min( dis[i][j] , dis[i][k] + dis[k][j] ); } } } for( register int i = 1 ; i <= m ; sum += dis[ a[ i - 1 ] ][ a[i] ] , i ++ ); cout << sum << endl; return 0; }
Loj #10072.Sightseeing Trip
求最小环的经典问题,一边做\(Floyd\),一边求min( d[i][j] + d[j][k] + d[k][i] )
但是为了方便我们统计我们保证k
和i
、k
和j
之间一定有一条边直接连接,这样就变成了min( d[i][j] + a[j][k] + a[k][i] )
,其中a[i][j]
表示的是原图
统计路径只需要递归的去做即可
#include <bits/stdc++.h> #define LL long long using namespace std; const int N = 310 , INF = 0x3f3f3f3f; int a[N][N] , d[N][N] , pos[N][N] , n , m , ans = INF + 1 ; vector< int > path; inline void get_path( int x , int y ) { if( pos[x][y] == 0 ) return ; get_path( x , pos[x][y] ); path.push_back( pos[x][y] ); get_path( pos[x][y] , y ); } inline int read() { register int x = 0; register char ch = getchar(); while( ch < '0' || ch > '9' ) ch = getchar(); while( ch >= '0' && ch <= '9' ) { x = ( x << 3 ) + ( x << 1 ) + ch - '0'; ch = getchar(); } return x; } int main() { n = read() , m = read(); memset( a , 0x3f , sizeof( a ) ); for( register int i = 1 ; i <= n ; i ++ ) a[i][i] = 0; for( register int i = 1 , x , y , z ; i <= m ; i ++ ) { x = read() , y = read() , z = read(); a[x][y] = a[y][x] = min( a[x][y] , z ); } memcpy( d , a , sizeof( d ) ); for( register int k = 1 ; k <= n ; k ++ ) { for( register int i = 1 ; i < k ; i ++ ) { for( register int j = i + 1 ; j < k ; j ++ ) { if( (LL)d[i][j] + a[j][k] + a[k][i] >= ans ) continue; ans = d[i][j] + a[j][k] + a[k][i]; path.clear(); path.push_back(i); get_path( i , j ); path.push_back(j); path.push_back(k); } } for( register int i = 1 ; i <= n ; i ++ ) { for( register int j = 1 ; j <= n ; j ++ ) { if( d[i][j] <= d[i][k] + d[k][j] ) continue; d[i][j] = d[i][k] + d[k][j]; pos[i][j] = k; } } } if( ans == 0x3f3f3f3f + 1 ) { puts("No solution."); return 0; } for( register int i = 0 ; i < path.size() ; i ++ ) printf("%d " , path[i] ); puts(""); return 0; }
0x62 最小生成树
Kruskal
起初每个点的都是一个独立的集合,把边权从小到达排序,按照边权枚举边,用并查集判断两个是否在同一个集合,如果在一个集合就跳过当前边,反之就联通这两个集合
下面是Luogu P3366的代码
#include <bits/stdc++.h> #define PIII pair< int , pair< int , int > > #define S second #define F first using namespace std; const int N = 5005; int n , m , fa[N] , cnt , sum ; vector< PIII > e; inline int read() { register int x = 0; register char ch = getchar(); for( ; ch < '0' || ch > '9' ; ch = getchar() ); for( ; ch >= '0' && ch <= '9' ; x = ( x << 3 ) + ( x << 1 ) + ch - '0' , ch = getchar() ); return x; } inline int getfa( int x ) { if( fa[x] == x ) return x; return fa[x] = getfa( fa[x] ); } inline void Kruskal() { for( register int i = 1 ; i <= n ; fa[i] = i , i ++ ); sort( e.begin() , e.end() ); register int fx , fy ; for( auto it : e ) { fx = getfa( it.S.F ) , fy = getfa( it.S.S ); if( fx == fy ) continue; fa[fx] = fy , cnt ++ , sum += it.F; if( cnt == n - 1 ) return ; } return ; } int main() { n = read() , m = read(); for( register int i = 1 , x , y , z ; i <= m ; x = read() , y = read() , z = read() , e.push_back( { z , { x , y } } ) , i ++ ); Kruskal(); ( cnt == n - 1 ? printf( "%d\n" , sum ) : puts( "orz" ) ); return 0; }
Luogu P2212 浇地Watering the Fields
首先读入所有的点,\(O(n^2)\)枚举所有的点,在枚举点的过程中直接判断掉小于\(C\)的点
然后跑一遍\(Kruskal\)即可
#include <bits/stdc++.h> #define PII pair< int , int > #define edge pair< int , PII > #define F first #define S second using namespace std; const int N = 2005; int n , c , fa[N] , sum , cnt; vector< edge > e; PII node[N]; inline int read() { register int x = 0; register char ch = getchar(); for( ; ch < '0' || ch > '9' ; ch = getchar() ); for( ; ch >= '0' && ch <= '9' ; x = ( x << 3 ) + ( x << 1 ) + ch - '0' , ch = getchar() ); return x; } inline int getlen( int x , int y ) { return ( node[x].F - node[y].F ) * ( node[x].F - node[y].F ) + ( node[x].S - node[y].S ) * ( node[x].S - node[y].S );} inline int getfa( int x ) { if( fa[x] == x ) return x; return fa[x] = getfa( fa[x] ); } inline void Kruskal() { for( register int i = 1 ; i <= n ; fa[i] = i , i ++ ); sort( e.begin() , e.end() ); register int fx , fy; for(auto it : e ) { fx = getfa( it.S.F ) , fy = getfa( it.S.S ); if( fx == fy ) continue; fa[fx] = fy , sum += it.F , cnt ++; if( cnt == n - 1 ) return ; } return ; } int main() { n = read() , c = read(); for( register int i = 1 ; i <= n ; node[i].F = read() , node[i].S = read() , i ++ ); for( register int i = 1 , d ; i <= n ; i ++) { for( register int j = i + 1 ; j <= n ; j ++ ) { d = getlen( i , j ); if( d < c ) continue; e.push_back( { d , { i , j } } ); } } Kruskal(); cout << ( cnt == n - 1 ? sum : -1 ) << endl; return 0; }
AcWing 346. 走廊泼水节
这道题用了\(Kruskal\)的思想和带权并查集
首先我们把说有的边权进行排序,然后从小到大排序
对于一条边\((x,y,z)\)
首先这条边要在完全图的最小生成树上,其次对于点\(x,y\)所在的连通块中每个点都要直接有边相连
\(s(i)\)表示节点\(i\)所在连通块点的个数,所以对于一条边\((x,y,z)\)的贡献就是\([ s(x)\times s(y) + 1 ] \times ( z + 1 )\)
所以用带权并差集维护一下即可
#include <bits/stdc++.h> #define LL long long #define PIII pair< int , pair< int , int > > #define S second #define F first using namespace std; const int N = 6005; int n , fa[N] , s[N] , fx , fy; LL ans; PIII e[N]; inline int read() { register int x = 0; register char ch = getchar(); for( ; ch < '0' || ch > '9' ; ch = getchar() ); for( ; ch >= '0' && ch <= '9' ; x = ( x << 3 ) + ( x << 1) + ch - '0' , ch = getchar() ); return x; } inline int getfa( int x ) { if( x == fa[x] ) return x; return fa[x] = getfa( fa[x] ); } inline void work() { n = read(); for( register int i = 1 , x , y ,z ; i < n ; x = read() , y = read() , z = read() , e[i] = { z , { x , y } } , i ++ ); sort( e + 1 , e + n ); ans = 0; for( register int i = 1 ; i <= n ; fa[i] = i , s[i] = 1 , i ++ ); for( register int i = 1 ; i < n ; i ++ ) { fx = getfa( e[i].S.F ) , fy = getfa( e[i].S.S ); ans += (LL)( s[fx] * s[fy] - 1 ) * ( e[i].F + 1 ); fa[fx] = fy; s[fy] += s[fx]; } printf( "%d\n" , ans ); return ; } int main() { for( register int T = read() ; T >= 1 ; work() , T -- ); }
0x63 树的直径与最近公共祖先
树的直径
树上两点的距离定义为,从树上一点到另一点所经过的权值
当树上两点距离最大时,就称作树的直径,树的直径既可以指这个权值,也可以指这个路径
两遍DFS求树的直径
我们可以先从任意一点开始\(DFS\),记录下当前点所能到达的最远距离,这个点为\(P\)
在从\(P\)开始\(DFS\)记录下所能达到的最远点的距离,这个点为\(Q\)
\(P,Q\)就是直径的端点,\(dis(P,Q)\)就是直径
void dfs( int x ) { if( dis[x] > ans ) ans = dis[x] , p = x; vis[x] = 1; for( int i = head[x] ; i ; i = e[i].next ) { if( e[i].v ) continue; dis[ e[i].v ] + e[i].w; dfs( e[i].v ); } return ; } void dfs2( int x ) { if( dis[x] > ans ) ans = dis[x] , q = x; vis[x] = 1; for( int i = head[x] ; i ; i = e[i].next ) { if( e[i].v ) continue; dis[ e[i].v ] + e[i].w; dfs( e[i].v ); } return ; } void solove() { dfs(1); ans = dis[p] = 0; memset( vis , 0 , sizeof( dis ) ); dfs2(p); cout << ans << endl }
两遍\(DFS\)基本相同,其实两遍\(BFS\)也可以做到同样的效果,这里就不在赘述
树形DP求树的直径
设d[x]
表示x
所能到达的最远距离,y
是x
的儿子
显然d[x] = max( d[y] + w( x , y ) )
那么x
所在点的最长链自然就是x
所到的最长距离加次长距离,在转移的过程中记录最大值即可
void dp( int x ) { v[x] = 1; for( int i = head[x] ; i ; i = e[i].next ) { if( v[ e[i].v ] ) continue; dp( e[i].v ); ans = max( ans , d[x] + d[y] + e[i].w ); d[x] = max( d[x] , d[y] + e[i].w ); } return ; }
Luogu P3629 巡逻
在不建立道路时,我们需要把每条边都经过一遍,那么我们要走的路程显然是边数二倍, 即\(2\times ( n - 1)\)。
只修建一条道路时,这条路应建在直径的两个端点处,那么我们要走的路径长度即为\(2\times(n−1)−L\), 其中\(L\)表示树的直径
修建第二条道路时,又会形成一个环,那么如果两条新道路所成的环不重叠,则答案继续减小,若重叠,则两个环重叠部分需经历两次, 那么我们得到如下算法 :
- 求树的直径\(L_1\),将\(L_1\)上的边权赋值为\(-1\)
- 再求树的直径\(L_2\),答案即为\(2\times n-L_1-L_2\)
#include <bits/stdc++.h> using namespace std; const int SIZE = 2e5 + 100; template<typename _T> inline void read(_T &s) { s = 0; _T w = 1, ch = getchar(); while (!isdigit(ch)) { if (ch == '-') w = -1; ch = getchar(); } while (isdigit(ch)) { s = (s << 1) + (s << 3) + (ch ^ 48); ch = getchar(); } s *= w; } template<typename _T> inline void write(_T s) { if (s < 0) putchar('-'), s = -s; if (s > 9) write(s / 10); putchar(s % 10 + '0'); } int n, k, cnt, tot = 1, ans, st, ed; int nex[SIZE << 1], lin[SIZE], ver[SIZE << 1], dis[SIZE], edge[SIZE], fa[SIZE], f[SIZE]; bool vis[SIZE]; queue <int> q; inline void add(int from, int to, int dis) { ver[++tot] = to; nex[tot] = lin[from]; edge[tot] = dis; lin[from] = tot; } inline void init() { read(n); read(k); for (int i = 1, x, y; i < n; ++i) { read(x), read(y); add(x, y, 1), add(y, x, 1); } } int bfs(int s) { memset(vis, false, sizeof(vis)); int loc = s; fa[s] = 0; vis[s] = true; dis[s] = 0; q.push(s); while (!q.empty()) { int u = q.front(); q.pop(); for (int i = lin[u]; i; i = nex[i]) { int v = ver[i]; if (!vis[v]) { vis[v] = true; q.push(v); dis[v] = dis[u] + 1; fa[v] = u; if (dis[loc] < dis[v]) loc = v; } } } return loc; } void pre_work() { st = bfs(1); ed = bfs(st); bfs(1); memset(vis, false, sizeof(vis)); if (dis[st] < dis[ed]) swap(st, ed); vis[st] = vis[ed] = true; while (dis[st] > dis[ed]) { st = fa[st]; vis[st] = true; ++cnt; } while (st != ed) { st = fa[st]; ed = fa[ed]; vis[st] = vis[ed] = true; cnt += 2; } } void sign(int u) { for (int i = lin[u]; i; i = nex[i]) { int v = ver[i]; if (v != fa[u]) { if (vis[v] && vis[u]) { edge[i] = edge[i ^ 1] = -1; } sign(v); } } } void dp(int u) { int _max = 0; for (int i = lin[u]; i; i = nex[i]) { int v = ver[i]; if (v != fa[u]) { dp(v); ans = max(ans, _max + f[v] + edge[i]); _max = max(_max, f[v] + edge[i]); } } ans = max(ans, _max); f[u] = _max; } inline void output() { if (k == 1) { write(2 * (n - 1) - cnt + 1), putchar('\n'); exit(0); } if (cnt == n - 1) { write(n + 1), putchar('\n'); exit(0); } sign(1); dp(1); write(2 * n - cnt - ans), putchar('\n'); } int main() { init(); pre_work(); output(); return 0; }