0x60 图论

巧了我就是萌 提交于 2019-12-04 13:19:40

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]表示1n的路径上最大值,g[i]表示in的路径上的最小值

跑两遍\(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] )

但是为了方便我们统计我们保证kikj之间一定有一条边直接连接,这样就变成了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所能到达的最远距离,yx的儿子

显然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\)表示树的直径

修建第二条道路时,又会形成一个环,那么如果两条新道路所成的环不重叠,则答案继续减小,若重叠,则两个环重叠部分需经历两次, 那么我们得到如下算法 :

  1. 求树的直径\(L_1\),将\(L_1\)上的边权赋值为\(-1\)
  2. 再求树的直径\(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;
}
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!