[九省联考2018]-Day2-劈配-林克卡特树-制胡窜

本文深入解析了竞赛编程中网络流、最优匹配等算法的应用场景及实现细节,通过具体题目介绍了如何利用二分答案、网络流解决最优匹配问题,并探讨了复杂路径选择问题的高效求解策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说在前面

模拟考,只考了125,这题难的可以= =
被T2折磨致死
T3感觉复杂…懒得写


题目

LOJ传送门


T1

连题目名字都提示了!!这就是一个最优匹配问题
像这样的肯定和网络流(或者匈牙利)有关系,稍微思考一下就能出来,二分答案+网络流就好了

比如第一问,当前的图是上一个人跑完之后的,然后考虑当前这个人可以满足的最小志愿是什么。二分答案,把这个前缀的边直接加进图里面去,然后跑一遍网络流看流量有没有变大,第一个变大的位置就是最小志愿
第二问类似,把每个人跑完之后的图都存下来,然后二分在哪个人之后加边流量可以变大,就ok了

下面是代码

#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int ttt , C , N , M , b[205] , ss[205] ;
int id_c , stu[205] , tea[205] , S , T ;
vector<int> a[202][202] ;
struct Path{
    short pre , to ;
    int flow ;
} ;

int dis[405] , que[405] , fr , ba ;
struct Gragh{
    Path p[5205] ;
    short tp , head[405] ;
    int flow ;
    void init(){
        tp = 1 ; flow = 0 ;
        memset( head , 0 , sizeof( head ) ) ;
    }
    void In( short t1 , short t2 , int t3 ){
        p[++tp] = ( Path ){ head[t1] , t2 , t3 } ; head[t1] = tp ;
        p[++tp] = ( Path ){ head[t2] , t1 ,  0 } ; head[t2] = tp ;
    }
    bool BFS(){
        memset( dis , -1 , sizeof( dis ) ) ;
        fr = 1 , ba = 0 ; dis[S] = 0 ;
        que[++ba] = S ;
        while( ba >= fr ){
            int u = que[fr++] ;
            for( int i = head[u] ; i ; i = p[i].pre ){
                int v = p[i].to ;
                if( !p[i].flow || dis[v] != -1 ) continue ;
                dis[v] = dis[u] + 1 ;
                que[++ba] = v ;
            }
        } return dis[T] != -1 ;
    }
    int dfs( int u , int flow ){
        if( u == T ) return flow ;
        int rt = 0 , nowf ;
        for( int i = head[u] ; i ; i = p[i].pre ){
            int v = p[i].to ;
            if( dis[v] != dis[u] + 1 || !p[i].flow ) continue ;
            if( ( nowf = dfs( v , min( flow , p[i].flow ) ) ) ){
                rt += nowf ;
                flow -= nowf ;
                p[i].flow -= nowf ;
                p[i^1].flow += nowf ;
                if( !flow ) break ;
            }
        } if( flow ) dis[u] = -1 ;
        return rt ;
    }
    int Dinic(){
        while( BFS() ){
            flow += dfs( S , 205 ) ;
        }
        return flow ;
    }
} G[202] , Gt ;

void clear(){
    for( int i = 1 ; i <= N ; i ++ )
        for( int j = 1 ; j <= M ; j ++ )
            a[i][j].clear() ;
    for( int i = 0 ; i <= N ; i ++ )
        G[i].init() ;
}

int getAns1( int x ){
    int lf = 1 , rg = M , rt = M + 1 ;
    while( lf <= rg ){
        int mid = ( lf + rg ) >> 1 ;
    //  printf( "x(%d) spe : %d %d %d\n" , x , lf , mid , rg ) ;
        Gt = G[x-1] ;
        for( int i = lf ; i <= mid ; i ++ ){
        //  printf( "siz = %d\n" , a[x][i].size() ) ;
            for( int siz = a[x][i].size() , j = 0 ; j < siz ; j ++ )
                Gt.In( stu[x] , tea[ a[x][i][j] ] , 1 ) ;
        }
        Gt.Dinic() ;
        if( Gt.flow - G[x-1].flow ) rt = mid , rg = mid - 1 ;
        else lf = mid + 1 ;
    } return rt ;
}

int getAns2( int x ){
    int lf = 1 , rg = x - 1 , rt = 0 ;
    while( lf <= rg ){
        int mid = ( lf + rg ) >> 1 ;
    //  if( x == 2 ) printf( "spe : %d %d %d\n" , lf , mid , rg ) ;
        Gt = G[mid-1] ;
        for( int i = ss[x] ; i ; i -- )
            for( int siz = a[x][i].size() , j = 0 ; j < siz ; j ++ )
                Gt.In( stu[x] , tea[ a[x][i][j] ] , 1 ) ;
        Gt.Dinic() ;
        if( Gt.flow - G[mid-1].flow ) rt = mid , lf = mid + 1 ;
        else rg = mid - 1 ;
    } return rt ;
}

int ans1[205] ;
void solve(){
    for( int i = 1 ; i <= M ; i ++ ) G[0].In( tea[i] , T , b[i] ) ;
    for( int i = 1 ; i <= N ; i ++ ) G[0].In( S , stu[i] , 1 ) ;

    for( int i = 1 , t ; i <= N ; i ++ ){
        t = ans1[i] = getAns1( i ) ;
        printf( "%d " , t ) ;
        G[i] = G[i-1] ;
        if( t == M + 1 ) continue ;
        for( int siz = a[i][t].size() , j = 0 ; j < siz ; j ++ ){
            G[i].In( stu[i] , tea[ a[i][t][j] ] , 1 ) ;
        }
        G[i].Dinic() ;
    } puts( "" ) ;

    for( int i = 1 ; i <= N ; i ++ ){
        if( ans1[i] <= ss[i] ) printf( "%d " , 0 ) ;
        else printf( "%d " , i - getAns2( i ) ) ;
    } puts( "" ) ;
}

int main(){
    scanf( "%d%d" , &ttt , &C ) ;
    for( int i = 1 ; i <= 200 ; i ++ ) stu[i] = ++id_c ;
    for( int i = 1 ; i <= 200 ; i ++ ) tea[i] = ++id_c ;
    S = ++id_c , T = ++id_c ;

    while( ttt -- ){
        clear() ;
        scanf( "%d%d" , &N , &M ) ;
        for( int i = 1 ; i <= M ; i ++ ) scanf( "%d" , &b[i] ) ;
        for( int i = 1 , tmp ; i <= N ; i ++ ){
            for( int j = 1 ; j <= M ; j ++ ){
                scanf( "%d" , &tmp ) ;
                if( tmp ) a[i][tmp].push_back( j ) ;
            }
        } for( int i = 1 ; i <= N ; i ++ ) scanf( "%d" , &ss[i] ) ;
        solve() ;
    }
}

T2

首先这个题,能想到的就是,选出 K+1 K + 1 条点不相交的路径,使其权值和最大
相当于是新增的 K K 条边把这 K+1 条路径串起来了。删掉的边,就可以是这些路径之间的边,这样一定可行
然后我们就有了一种部分分的做法

但是如果 K K 的限制存在,就只能做类似背包的dp,复杂度是GG的

我们可以发现(其实不一定能发现,但是可以感性的猜测),随着K变大,答案会先增大后减小
也就是说g[K]g[K1]这个值随着 K K 变大,不增
感性理解一下,一开始肯定是 K 越大 g[K] g [ K ] 越大,因为点不相交路径可以选更多条
但是 K K 大到某个值的时候,我们就需要肢解一些路径,这就导致有些正权边无法产生贡献,所以 g[K] 变小

然后考虑运用一类二分方法,这类二分方法可以称作wqs二分
就是说,因为答案的斜率不增,所以形成了一个上凸壳,我们用一条线去切这个凸包,肯定能切到一个点上
那么这条线的斜率,我们可以看作是,每多选一条路径,就需要付出斜率这么多的代价。于是按照这种方式来做dp,dp选任意条路径最大获利是多少。这样dp出来的值就是这条线与凸包相切时的纵截距

因为我们需要 K K 处的答案,所以我们在dp的时候顺便记录一下,当前最优的方案到底是选了多少条路径。如果选择的路径条数大于了K+1(删 k k 条等于选k+1条路径),就相当于是这条线切在了 K K 以后的某个位置,另一种情况类似
按照这种方式来二分答案,就能找到刚好切K的那一条线,从而根据纵截距和斜率,算出 K K 处的答案

类似方法的题,相信各位大佬在学MST的时候,都应该做过这个题:BZOJ2654

关于一些需要注意的地方:

  1. 答案形成的凸包可能有多个点在同一条线上,这种情况可能无论如何都不能恰好取到我们想要的值。我们可以这样,当权值相同的时候,边数较少的那个优先级高。这样如果在二分结束时,仍然取不到我们想要的K+1,而是比 K+1 K + 1 小,那么就可以断定,当前的dp值也就是刚好选 K+1 K + 1 条路径时的dp值,从而得出答案

    • 在dp的时候注意细节,有可能存在点不属于任何一条路径

    • 下面是代码

      #include <cstdio>
      #include <cstring>
      #include <algorithm>
      using namespace std ;
      
      int N , K , tp , head[300005] ;
      long long lf , rg , mid , inf = 1LL << 56 ;
      struct Path{
          int pre , to , val ;
      }p[600005] ;
      
      struct Data{
          long long sum ;
          int cnt ;
          Data( long long _ = 0 , int __ = 0 ):sum(_) , cnt(__){} ;
          //here to write func & operat
          Data operator - ( const long long &D ) const { return Data( sum - D , cnt ) ; }
          Data operator + ( const Data &D )      const { return Data( sum + D.sum , cnt + D.cnt ) ; }
          Data operator + ( const long long &D ) const { return Data( sum + D , cnt ) ; }
          bool operator > ( const Data &A )      const { return sum > A.sum || ( sum == A.sum && cnt < A.cnt ) ; }
      } dp[300005][3] , val[300005] ;
      
      void In( int t1 , int t2 , int t3 ){
          p[++tp] = ( Path ){ head[t1] , t2 , t3 } ; head[t1] = tp ;
          p[++tp] = ( Path ){ head[t2] , t1 , t3 } ; head[t2] = tp ;
      }
      
      Data choose( Data A , Data B ){ return A > B ? A : B ; }
      Data choose( Data A , Data B , Data C ){ return choose( choose( A , B ) , C ) ; }
      void UPD( Data &A , Data B ){ if( B > A ) A = B ; }
      
      void dfs( int u , int f ){
          dp[u][1] = dp[u][2] = Data( -inf , 0 ) ;
          long long sum = 0 ; int c = 0 ;
          for( int i = head[u] ; i ; i = p[i].pre ){
              int v = p[i].to ;
              if( v == f ) continue ;
              dfs( v , u ) ;
              val[v] = choose( dp[v][0] , dp[v][1] , dp[v][2] ) - mid ;
              val[v].cnt ++ ; UPD( val[v] , dp[v][0] ) ;
              sum += val[v].sum , c += val[v].cnt ; 
          } dp[u][0] = Data( sum , c ) ;//直接结算儿子,在父亲处结算自己
      
          for( int i = head[u] ; i ; i = p[i].pre ){
              if( p[i].to == f ) continue ;
              int v = p[i].to ; long long x = p[i].val - val[v].sum ;
              UPD( dp[u][2] , dp[u][1] + Data( dp[v][0].sum + x , -val[v].cnt + dp[v][0].cnt ) ) ;
              UPD( dp[u][2] , dp[u][1] + Data( dp[v][1].sum + x , -val[v].cnt + dp[v][1].cnt ) ) ;
              UPD( dp[u][1] , dp[u][0] + Data( dp[v][0].sum + x , -val[v].cnt + dp[v][0].cnt ) ) ;
              UPD( dp[u][1] , dp[u][0] + Data( dp[v][1].sum + x , -val[v].cnt + dp[v][1].cnt ) ) ;
          } 
      }
      
      void solve(){
          long long ans = 0 ;
          while( lf <= rg ){
              mid = ( lf + rg ) >> 1 ;
              dfs( 1 , 1 ) ;//在外面结算根
              Data tmp = choose( dp[1][0] , dp[1][1] , dp[1][2] ) - mid ;
              tmp.cnt ++ ; UPD( tmp , dp[1][0] ) ;
      
              if( tmp.cnt == K + 1 ){ ans = tmp.sum + mid * ( K + 1 ) ; break ; }
              else if( tmp.cnt > K + 1 ) lf = mid + 1 ;
              else rg = mid - 1 , ans = tmp.sum + mid * ( K + 1 ) ;
          } printf( "%lld" , ans ) ;
      }
      
      int main(){
          scanf( "%d%d" , &N , &K ) ;
          for( int i = 1 , u , v , val ; i < N ; i ++ ){
              scanf( "%d%d%d" , &u , &v , &val ) ;
              In( u , v , val ) ;
              if( val > 0 ) lf = min( (long long)-val , lf ) , rg += val ;
          } solve() ;
      }
      

      T3

      对于题面上所说的,要选取满足条件的 (i,j) ( i , j ) ,正着做不好做,于是我们倒着来,考虑不满足条件的有哪些
      一次选取,相当于是把这个字符串切成三段,三段里都不含有目标串,也就是说切割位置正好切断了:所有能和目标串匹配上的串
      可以发现如果说存在三个匹配串,它们俩俩无交,那么无论怎么切都满足条件
      所以只用考虑:只有一个匹配串;可以把匹配串分成两部分,并且分别并集不为空;这两种情况
      然后按照这个思路想下去就ok了

      感觉比较复杂,于是偷懒没有写,可以去看看Hugh的cnblog里的题解,比me这里详细很多

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值