点分小专题-[POJ1741][BZOJ2152][BZOJ2599]

UPD 2018.2.20 21:20 把BZOJ2599的第一种写法的坑填了,第二种写法现已无法通过

说在前面

没有什么好说的,但是要保持格式=w=


概述

说一下me对点分的拙见
点分呢,大概是用于处理树上的树链计数问题

其实简单的想,统计树上路径的时候,假设当前处理到了点u
对于当前点u,先dfs一遍统计出半条路径(从u到子树内的某个点),然后利用一些技巧,在 O(siz) O ( s i z ) 内把路过点u的所有路径(包括v->u->v这种路径)全部累计答案,然后再对于每个v,减去那些多计算的路径

但是如果直接这样处理的话,每到一个点u就要dfs一遍,万一这棵树是一条链,这个时间复杂度就是 O(N2) O ( N 2 ) 的了,明显是不能接受的,所以这个时候点分就出来了=w=

对于一条路径x->u->v->y,我们既可以在u点的时候累计这条路径,也可以在v点的时候累计这条路径。利用这个,我们每次处理的时候就不一定按着顺序,一层一层的处理。而是每次在子树里找出重心,然后把那个重心当作下一个u去处理。由于重心的性质,可以保证最后这棵”重心树”是比较平衡的,树高最多 log(N) l o g ( N ) 层,于是也就可以保证复杂度了


POJ1741

POJ1741传送门

题意

给你由N个点组成的树,有边权。边权为正整数且不超过1e3。
现在给你一个数字K,询问树中有多少条简单路径的长度小于等于K

输入输出格式

输入格式:
对于每组数据
第一行包含两个整数N,K,含义如题
接下来N-1行,每行三个整数 u , v , w ,表示u到v有一条权值为w的边
当N和K都是0时,输入结束

输出格式:
对于每组数据,输出一行一个整数表示答案

解法

很明显的点分题,树上路径统计问题
每次找到一个子树的重心,先dfs一遍把该子树内的点到重心的距离算出来,排序之后用two pointer计算出所有经过重心且长度小于K的路径数(包含复杂路径),然后再减去类似v->u->v这样的复杂路径即可。减去复杂路径的实现也是类似的
点分入门题的话,题解应该不少,就不详细写了…

自带大常数的代码

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

int N , K , head[100005] , tp , root , tot ;
struct Path{
    int pre , to , len ;
}p[200005] ;

void In( int t1 , int t2 , int t3 ){
    p[++tp].pre = head[t1] ;
    p[ head[t1] = tp ].to = t2 ;
    p[tp].len = t3 ;
}

long long ans ;
bool vis[100005] ;
int siz[100005] , maxs[100005] , dis[100005] , sta[100005] , topp ;

void dfsroot( int u , int f ){
    siz[u] = 1 , maxs[u] = 0 ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( v == f || vis[v] ) continue ;
        dfsroot( v , u ) ;
        siz[u] += siz[v] ;
        maxs[u] = max( maxs[u] , siz[v] ) ;
    }
    maxs[u] = max( maxs[u] , tot - siz[u] ) ;
    if( maxs[u] < maxs[root] )
        root = u ;
}

void dfsdis( int u , int f ){
    sta[++topp] = dis[u] ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( v == f || vis[v] ) continue ;
        dis[v] = dis[u] + p[i].len ;
        dfsdis( v , u ) ;
    }
}

long long cnt( int u , int len ){
    dis[u] = len ;
    topp = 0 ;
    dfsdis( u , 0 ) ;

    sort( sta + 1 , sta + topp + 1 ) ;
    long long rt = 0 ;
    int lf = 1 , rg = topp ;
    while( lf <= rg ){
        while( sta[lf] + sta[rg] > K && lf < rg ) rg -- ;
        rt += rg - lf;
        lf ++ ;
    }
    return rt ;
}

void work( int u ){
    vis[u] = true ;
    ans += cnt( u , 0 ) ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( vis[v] ) continue ;
        ans -= cnt( v , p[i].len ) ;
        root = 0 , tot = siz[v] ;
        dfsroot( v , u ) ;
        work( root ) ;
    }
}

void solve(){
    maxs[0] = 0x3f3f3f3f ;
    root = 0 , tot = N ;
    dfsroot( 1 , 0 ) ;
    work( root ) ;
}

void clear(){
    tp = 0 ; ans = 0 ;
    memset( vis , 0 , sizeof( vis ) ) ;
    memset( head , 0 , sizeof( head ) ) ;
}

int main(){
    while( 1 ){
        scanf( "%d%d" , &N , &K ) ;
        if( !N && !K ) exit(0) ;
        clear() ;
        for( int i = 1 , u , v , len ; i < N ; i ++ ){
            scanf( "%d%d%d" , &u , &v , &len ) ;
            In( u , v , len ) ;
            In( v , u , len ) ;
        }
        solve() ;
        printf( "%lld\n" , ans ) ;
    }
}


BZOJ2152

BZOJ2152传送门

题面

聪聪和可可是兄弟俩,他们俩经常为了一些琐事打起来,例如家中只剩下最后一根冰棍而两人都想吃、两个人都想玩儿电脑(可是他们家只有一台电脑)……遇到这种问题,一般情况下石头剪刀布就好了,可是他们已经玩儿腻了这种低智商的游戏。他们的爸爸快被他们的争吵烦死了,所以他发明了一个新游戏:由爸爸在纸上画n个“点”,并用n-1条“边”把这n个“点”恰好连通(其实这就是一棵树)。并且每条“边”上都有一个数。接下来由聪聪和可可分别随即选一个点(当然他们选点时是看不到这棵树的),如果两个点之间所有边上数的和加起来恰好是3的倍数,则判聪聪赢,否则可可赢。聪聪非常爱思考问题,在每次游戏后都会仔细研究这棵树,希望知道对于这张图自己的获胜概率是多少。现请你帮忙求出这个值以验证聪聪的答案是否正确。

输入输出格式

输入格式:
输入的第1行包含1个正整数n。后面n-1行,每行3个整数x、y、w,表示x号点和y号点之间有一条边,上面的数是w。

输出格式:
以即约分数形式输出这个概率(即“a/b”的形式,其中a和b必须互质。如果概率为1,输出“1/1”)。

解法

和POJ1741差不多的,只是这里的统计条件不一样

把每条边取模3后得到的数字作为新的边权建图
然后每次找出重心后,dfs维护三个值,到重心距离模3,余数分别是0,1,2的个数
那么贡献就是cnt0 * cnt0 + cnt1 * cnt2 * 2,最后再减去重复计算的即可

自带大常数的代码

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

int N , tp , head[200005] , root , tot ;
struct Path{
    int pre , to , len ;
}p[400005] ;

void In( int t1 , int t2 , int t3 ){
    p[++tp].pre = head[t1] ;
    p[ head[t1] = tp ].to = t2 ;
    p[tp].len = t3 ;
}

long long ans , fra ;
bool vis[200005] ;
int siz[200005] , maxs[200005] , cnt[3] ;
void dfsroot( int u , int f ){
    siz[u] = 1 , maxs[u] = 0 ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( v == f || vis[v] ) continue ;
        dfsroot( v , u ) ;
        siz[u] += siz[v] ;
        maxs[u] = max( maxs[u] , siz[v] ) ;
    }
    maxs[u] = max( maxs[u] , tot - siz[u] ) ;
    if( maxs[u] < maxs[root] )
        root = u ;
}

int dis[200005] ;
void dfsdis( int u , int f ){
    cnt[ dis[u]%3 ] ++ ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( v == f || vis[v] ) continue ;
        dis[v] = dis[u] + p[i].len ;
        dfsdis( v , u ) ;
    }
}

long long cntAns( int u , int inival ){
    dis[u] = inival ;
    cnt[0] = cnt[1] = cnt[2] = 0 ;
    dfsdis( u , 0 ) ;
    return 1LL * cnt[0] * cnt[0] + 2LL * cnt[1] * cnt[2] ;
}

void work( int u ){
    vis[u] = true ;
    ans += cntAns( u , 0 ) ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( vis[v] ) continue ;
        ans -= cntAns( v , p[i].len ) ;
        root = 0 , tot = siz[v] ;
        dfsroot( v , u ) ;
        work( root ) ;
    }
}

long long gcd( long long a , long long b ){
    return !b ? a : gcd( b , a%b ) ;
}

void solve(){
    maxs[root] = 0x3f3f3f3f ;
    root = 0 , tot = N ;
    dfsroot( 1 , 0 ) ;
    work( root ) ;
    printf( "%lld/%lld" , ans/gcd(ans,fra) , fra/gcd(ans,fra) ) ;
}

int main(){
    scanf( "%d" , &N ) ; fra = N * N ;
    for( int i = 1 , u , v , len ; i < N ; i ++ ){
        scanf( "%d%d%d" , &u , &v , &len ) ;
        len %= 3 ;
        In( u , v , len ) ;
        In( v , u , len ) ;
    }
    solve() ;
}

BZOJ2599

BZOJ2599传送门

题面

给一棵树,每条边有权.求一条简单路径,权值和等于K,且边的数量最小.N <= 200000, K <= 1000000

输入输出格式

输入格式:
第一行包含两个整数N, K,含义如题
接下来N-1行,每行三个整数,表示一条无向边的两端和权值 (注意点的编号从0开始)

输出格式:
一个整数,表示最小边数量,如果不存在这样的路径则输出-1

解法

可以去看一看Candy?的博客园,写得比较清晰
传送门 for Candy?`s blog

有两种解法
一种解法是:对于当前重心u可以到的点v,都跑一遍dfs计算对于当前重心的f[i],表示在已经遍历过的点中,路径长度为i的最小边数,顺便处理出dep[i]。计算答案的时候再次dfs,用ans = min( ans , f[ K-dis[u] ]+dep[u] )来更新答案。因为f[i]存的是之前已经遍历过的点的信息,不包含当前子树信息,经过同一个点的路径就不会被计算,所以是正确的。更新完之后再dfs一遍把f[i]全部清零,然后递归处理下一层重心

另外一种解法是:因为min值无法相减,于是统计长度为K的路径中,每种边数的出现次数,这样就可以用经过重心的减去重复的,思想和前两道题差不多

然而,第二种解法的时间复杂度是无法保证的,在统计长度为K的路径的时候,由于需要枚举到所有长度为K的路径,因此时间复杂度完全取决于长度为K的路径的条数,因此是可以卡的,虽然BZOJ上可以跑过

自带大常数的代码

由于me比较懒,所以只写了第二种=w=

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

int N , K , tp , head[200005] , root , tot , x ;
struct Path{
    int pre , to , len ;
}p[400005] ;

void In( int t1 , int t2 , int t3 ){
    p[++tp].pre = head[t1] ;
    p[ head[t1] = tp ].to = t2 ;
    p[tp].len = t3 ;
}

int ans[200005] ;
bool vis[200005] ;
int siz[200005] , maxs[200005] , dis[200005] , sta[200005] , dep[200005] , topp ;

void dfsroot( int u , int f ){
    siz[u] = 1 , maxs[u] = 0 ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( v == f || vis[v] ) continue ;
        dfsroot( v , u ) ;
        siz[u] += siz[v] ;
        maxs[u] = max( maxs[u] , siz[v] ) ;
    }
    maxs[u] = max( maxs[u] , tot - siz[u] ) ;
    if( maxs[u] < maxs[root] )
        root = u ;
}

void dfsdis( int u , int f ){
    sta[++topp] = u ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( v == f || vis[v] ) continue ;
        dis[v] = dis[u] + p[i].len ;
        dep[v] = dep[u] + 1 ;
        dfsdis( v , u ) ;
    }
}

bool cmpdis( const int &a , const int &b ){
    return dis[a] < dis[b] ;
}

long long cnt( int u , int inidis , int add , int inidep ){
    dis[u] = inidis ; dep[u] = inidep ;
    topp = 0 ;
    dfsdis( u , 0 ) ;

    sort( sta + 1 , sta + topp + 1 , cmpdis ) ;
    long long rt = 0 ;
    int lf = 1 , rg = topp ;
    while( lf < rg ){
        while( dis[ sta[lf] ] + dis[ sta[rg] ] > K && lf < rg ) rg -- ;
        for( int i = rg ; i > lf ; i -- )
            if( dis[ sta[i] ] + dis[ sta[lf] ] == K )
                ans[ dep[sta[i]]+dep[sta[lf]] ] += add , x++ ;
            else break ;
        lf ++ ;
    }
    return rt ;
}

void work( int u ){
    vis[u] = true ;
    cnt( u , 0 , 1 , 0 ) ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( vis[v] ) continue ;
        cnt( v , p[i].len , -1 , 1 ) ;
        root = 0 , tot = siz[v] ;
        dfsroot( v , u ) ;
        work( root ) ;
    }
}


void solve(){
    maxs[0] = 0x3f3f3f3f ;
    root = 0 ; tot = N ;
    dfsroot( 1 , 0 ) ;
    work( root ) ;
    printf( "test : x(%d)\n" , x ) ;
    for( int i = 1 ; i < N ; i ++ )
        if( ans[i] ){
            printf( "%d" , i ) ;
            exit( 0 ) ;
        }
    puts( "-1" ) ;
}

int main(){
    freopen( "2599.in" , "r" , stdin ) ;
    scanf( "%d%d" , &N , &K ) ;
    for( int i = 1 , u , v , len ; i < N ; i ++ ){
        scanf( "%d%d%d" , &u , &v , &len ) ;
        u ++ , v ++ ;
        In( u , v , len ) ;
        In( v , u , len ) ;
    }
    solve() ;
}
数据生成器
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int main(){
    freopen( "2599.in" , "w" , stdout) ;
    puts( "131072 32") ;
    for( int i = 0 ; i < 131072 ; i ++ )
        printf( "%d %d %d\n" , i+1 , (i+1)/2 , 1 ) ;
}
第一种写法的代码
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std ;

int N , K , tp , head[200005] , ans ;
struct Path{
    int pre , to , len ;
}p[400005] ;

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 ;
}

int Gr , siz[200005] , vis[200005] , maxs[200005] , SIZ ;
void dfs_Gr( int u , int f ){
    siz[u] = 1 , maxs[u] = 0 ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( vis[v] || v == f ) continue ;
        dfs_Gr( v , u ) ; siz[u] += siz[v] ;
        if( siz[v] > maxs[u] ) maxs[u] = siz[v] ;
    }
    maxs[u] = max( maxs[u] , SIZ - siz[u] ) ;
    if( maxs[Gr] > maxs[u] ) Gr = u ;
}

struct Data{
    bool exis[1000005] ;
    int sta[1000005] , mind[1000005] , topp ;
    void clear(){
        while( topp ) exis[ sta[topp] ] = false , topp -- ;
    }
    void update( int val , int x ){
        if( !exis[x] ){
            exis[x] = true ;
            sta[++topp] = x ;
            mind[x] = val ;
        } else if( val < mind[x] ) mind[x] = val ;
    }
    int query( int x ){
        if( !exis[x] ) return 0x3f3f3f3f ;
        else return mind[x] ;
    }
    void update( pair<int,int> p ){ update( p.first , p.second ) ; }
    pair<int,int> pop_top(){
        exis[ sta[topp] ] = false ;
        topp -- ;
        return make_pair( mind[ sta[topp+1] ] , sta[topp+1] ) ;
    }
}now , upd ;

void dfs_solve( int u , int f , int dep , int dis ){
    if( dis > K ) return ;
    upd.update( dep , dis ) ;
    int tmp = min( dep + now.query( K - dis ) , ( dis != K ) * 0x3f3f3f3f + dep ) ;
    if( tmp < ans ) ans = tmp ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( vis[v] || v == f ) continue ;
        dfs_solve( v , u , dep + 1 , dis + p[i].len ) ;
    }
}

void dfs_div( int u ){
    vis[u] = 1 ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( vis[v] ) continue ;
        dfs_solve( v , u , 1 , p[i].len ) ;
        while( upd.topp ) now.update( upd.pop_top() ) ;
    }
    now.clear() ;
    for( int i = head[u] ; i ; i = p[i].pre ){
        int v = p[i].to ;
        if( vis[v] ) continue ;
        Gr = 0 , SIZ = siz[v] ;
        dfs_Gr( v , u ) ;
        dfs_div( Gr ) ;
    }
}

void solve(){
    ans = maxs[0] = 0x3f3f3f3f ;
    Gr = 0 , SIZ = N ;
    dfs_Gr( 1 , 1 ) ;
    dfs_div( Gr ) ;
    printf( "%d" , ans == 0x3f3f3f3f ? -1 : ans ) ;
}

inline void read_( int &x ){
    x = 0 ;
    char ch = getchar() ;
    while( ch < '0' || ch > '9' ) ch = getchar() ;
    while( ch >='0' && ch <='9' ) x = (x<<1) + (x<<3) + ch - '0' , ch = getchar() ;
}

int main(){
    scanf( "%d%d" , &N , &K ) ;
    for( int i = 1 , u , v , len ; i < N ; i ++ ){
        read_( u ) , read_( v ) , read_( len ) ;
        In( u + 1 , v + 1 , len ) ;
    }
    solve() ;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值