1. 程式人生 > >[九省聯考2018]-Day2-劈配-林克卡特樹-制胡竄

[九省聯考2018]-Day2-劈配-林克卡特樹-制胡竄

說在前面

模擬考,只考了125,這題難的可以= =
被T2折磨致死
T3感覺複雜…懶得寫

題目

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 條邊把這 K+1 條路徑串起來了。刪掉的邊,就可以是這些路徑之間的邊,這樣一定可行
然後我們就有了一種部分分的做法

但是如果 K 的限制存在,就只能做類似揹包的dp,複雜度是GG的

我們可以發現(其實不一定能發現,但是可以感性的猜測),隨著K變大,答案會先增大後減小
也就是說g[K]g[K1]這個值隨著K變大,不增
感性理解一下,一開始肯定是 K 越大 g[K] 越大,因為點不相交路徑可以選更多條
但是K大到某個值的時候,我們就需要肢解一些路徑,這就導致有些正權邊無法產生貢獻,所以 g[K] 變小

然後考慮運用一類二分方法,這類二分方法可以稱作wqs二分
就是說,因為答案的斜率不增,所以形成了一個上凸殼,我們用一條線去切這個凸包,肯定能切到一個點上
那麼這條線的斜率,我們可以看作是,每多選一條路徑,就需要付出斜率這麼多的代價。於是按照這種方式來做dp,dp選任意條路徑最大獲利是多少。這樣dp出來的值就是這條線與凸包相切時的縱截距

因為我們需要K處的答案,所以我們在dp的時候順便記錄一下,當前最優的方案到底是選了多少條路徑。如果選擇的路徑條數大於了K+1(刪k條等於選k+1條路徑),就相當於是這條線切在了K以後的某個位置,另一種情況類似
按照這種方式來二分答案,就能找到剛好切K的那一條線,從而根據縱截距和斜率,算出K處的答案

類似方法的題,相信各位大佬在學MST的時候,都應該做過這個題:BZOJ2654

關於一些需要注意的地方:

  1. 答案形成的凸包可能有多個點在同一條線上,這種情況可能無論如何都不能恰好取到我們想要的值。我們可以這樣,當權值相同的時候,邊數較少的那個優先順序高。這樣如果在二分結束時,仍然取不到我們想要的K+1,而是比K+1小,那麼就可以斷定,當前的dp值也就是剛好選K+1條路徑時的dp值,從而得出答案
  2. 在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),正著做不好做,於是我們倒著來,考慮不滿足條件的有哪些
一次選取,相當於是把這個字串切成三段,三段裡都不含有目標串,也就是說切割位置正好切斷了:所有能和目標串匹配上的串
可以發現如果說存在三個匹配串,它們倆倆無交,那麼無論怎麼切都滿足條件
所以只用考慮:只有一個匹配串;可以把匹配串分成兩部分,並且分別並集不為空;這兩種情況
然後按照這個思路想下去就ok了

感覺比較複雜,於是偷懶沒有寫,可以去看看Hugh的cnblog裡的題解,比me這裡詳細很多