[HDU 6643] Ridiculous Netizens(点分治+根号分治+dp)

HDU 6643 Ridiculous Netizens

problem

hdu6643

题目大意:给定一棵无根树,以及每个点的点权 w i w_i wi

定义一个连通块的价值为连通块内点的点权之积。

求有多少个连通块价值 ≤ m \le m m

n ≤ 2 e 3 , m ≤ 1 e 6 n\le 2e3,m\le 1e6 n2e3,m1e6

solution

取一个点作根,将无根树转化为有根树。

统计连通块包含根节点的情况,不包含根就分裂成若干个互不相同的子树,变成子问题,重复以上操作。

选取重心作根,点分治。

这是大致框架,问题就在于怎么快速计算包含根的符合要求的连通块个数。

observation:因为是连通块,如果父亲不选,那么其所有子孙都不可能入选。

所以我们考虑使用 dfn 序重编号,从后往前做。

d p i , j : d f n dp_{i,j}:dfn dpi,j:dfn 序编号到为 i i i 的点为止,价值为 j j j 的连通块个数。

  • 如果要选当前点,子孙是可选可不选的,从 d f n dfn dfn 序后一个直接转移。

    d f n [ i ] : d f n dfn[i]:dfn dfn[i]:dfn 序为 i i i 的原对应点】

    d p ( i , j × w d f n [ i ] ) ← d p ( i + 1 , j ) dp(i,j\times w_{dfn[i]})\leftarrow dp(i+1,j) dp(i,j×wdfn[i])dp(i+1,j)

  • 如果不选,就必须跳过其所有子孙。【 s i z [ i ] : i siz[i]:i siz[i]:i 子树的大小】

    d p ( i , j ) ← d p ( i + s i z i , j ) dp(i,j)\leftarrow dp(i+siz_i,j) dp(i,j)dp(i+sizi,j)

注意到 n m nm nm 的范围,根本开不下 2 e 9 2e9 2e9 的数组。

那就——分块!

按照连通块价值的大小分块, ≤ m \le\sqrt{m} m > m >\sqrt{m} >m

  • ≤ m \le \sqrt{m} m

    f i , j : f_{i,j}: fi,j: i i i 为止,价值为 j j j 的连通块个数。

    正常地向上面一样进行背包转移。

  • > m >\sqrt{m} >m

    f i , j : f_{i,j}: fi,j: i i i 为止,价值还能装下 j j j 的连通块个数。即价值已经为 m j \frac{m}{j} jm 的联通块个数。

具体可见代码实现。

code

#include <cmath>
#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
#define maxn 2005
#define maxm 1005
#define int long long
#define mod 1000000007
#define inf 0x7f7f7f7f
struct node { int to, nxt; }E[maxn << 1];
int cnt, tot, n, m, Max, N, M, T, root, ans;
bool vis[maxn];
int w[maxn], siz[maxn], dfn[maxn], head[maxn];
int f[maxn][maxm], g[maxn][maxm];

void addedge( int u, int v ) {
    E[tot] = { v, head[u] }, head[u] = tot ++;
    E[tot] = { u, head[v] }, head[v] = tot ++;
}

void get_root( int u, int fa ) {
    int maxson = 0; siz[u] = 1;
    for( int i = head[u];~ i;i = E[i].nxt ) {
        int v = E[i].to;
        if( vis[v] or v == fa ) continue;
        get_root( v, u );
        siz[u] += siz[v];
        maxson = max( maxson, siz[v] );
    }
    maxson = max( maxson, N - siz[u] );
    if( maxson < Max ) Max = maxson, root = u;
}

void dfs( int u, int fa ) {
    dfn[++ cnt] = u, siz[u] = 1;
    for( int i = head[u];~ i;i = E[i].nxt ) {
        int v = E[i].to;
        if( v == fa or vis[v] ) continue;
        else dfs( v, u ), siz[u] += siz[v];
    }
}

void calc() {
    cnt = 0, dfs( root, 0 );
    for( int i = 1;i <= cnt + 1;i ++ ) {
        memset( f[i], 0, sizeof( f[i] ) );
        memset( g[i], 0, sizeof( g[i] ) );
    } 
    f[cnt + 1][1] = 1;
    for( int i = cnt;i;i -- ) {
        int x = w[dfn[i]];
        //要选dfn[i]
        for( int j = 1;j <= min( M, m / x );j ++ ) { 
            //枚举后i+1个一共使用了j空间 如果后i个乘积不超过M 做普通背包
            int k = j * x;
            if( k <= M ) f[i][k] = ( f[i][k] + f[i + 1][j] ) % mod;
            else g[i][m / k] = ( g[i][m / k] + f[i + 1][j] ) % mod;
            //否则就是剩下了m/(j*x)的贡献
            //这里是用的f[i+1][j]在更新 只代表了后i+1乘积不超过M的情况
        }
        for( int j = x;j <= M;j ++ )
        //这里使用的g[i+1][j]在更新 只代表了后i+1乘积超过M的情况
            g[i][j / x] = ( g[i][j / x] + g[i + 1][j] ) % mod;
        //不选
        for( int j = 1;j <= M;j ++ ) {
            f[i][j] = ( f[i][j] + f[i + siz[dfn[i]]][j] ) % mod;
            g[i][j] = ( g[i][j] + g[i + siz[dfn[i]]][j] ) % mod;
        }
    }
    for( int i = 1;i <= M;i ++ )
        ans = ( ans + f[1][i] + g[1][i] ) % mod;
    ans = ( ans - 1 + mod ) % mod; //减去空连通块的贡献
}

void dfs( int u ) {
    vis[u] = 1;
    calc();
    for( int i = head[u];~ i;i = E[i].nxt ) {
        int v = E[i].to;
        if( vis[v] ) continue;
        Max = inf, N = siz[v];
        get_root( v, u );
        dfs( root );
    }
}

signed main() {
    scanf( "%lld", &T );
    while( T -- ) {
        tot = 0; memset( head, -1, sizeof( head ) );
        scanf( "%lld %lld", &n, &m );
        for( int i = 1;i <= n;i ++ ) vis[i] = 0, scanf( "%lld", &w[i] );
        for( int i = 1, u, v;i < n;i ++ ) {
            scanf( "%lld %lld", &u, &v );
            addedge( u, v );
        }
        M = sqrt( m );
        Max = inf, N = n;
        get_root( 1, 0 );
        dfs( root );
        printf( "%lld\n", ans );
        ans = 0;
    }
    return 0;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值