树上dp 难题

USACO 2008 Jan G
最小点覆盖板子 3种情况讨论

P3574 推式子变形贪心
暴力推dp方程,对式子分析,贪心考虑先后顺序的影响,化简式子排序

Tree
裸换根dp+逆元

CF1101D GCD Counting
首先对题目数据分析, a i a_i ai最多包含7个不同的质因子,若一条路径 x − > y x->y x>y g c d ( x , y ) > 1 gcd(x,y)>1 gcd(x,y)>1,那么路径上的点至少共同包含一个质因子

d p i , j dp_{i,j} dpi,j表示以 i i i为根的子树中,由 i i i到子树中的某个点 j j j的路径中,经过的点都包含 a i a_i ai中第 j j j大的质因子,这种路径的最长长度

状态设计好后就很好转移了,预处理一下每个点权值的质因子有哪些

CF735E Ostap and Tree
最初的想法是 d p u , j dp_{u,j} dpu,j表示为 u u u为根节点,离 u u u最近的染色点距离为 j j j的合法染色方案的有多少种。这种就时合法子树去转移,所以少了一些情况。
对于一些不合法的以 v 1 v1 v1为根子树,可能它树内的不合法点会因为其它子树 v 2 v2 v2中的合法点,在合并到 u u u后合法

为此我们既要知道离 u u u最近的染色点距离为 j j j时的方案数,又要知道离 u u u最远的不合法点的距离为 k k k时的方案数。然后加法乘法原理合并即可。
1.合法树 u u u与合法子树 v v v,状态取min
2.合法树 u u u与非合法子树 v v v,看非合法点与染色点距离是否 < = k <=k <=k,再转移到 u u u的合法状态或非合法状态
3.非合法树 u u u与非合法子树 v v v ,状态取max

CF1153D Serval and Rooted Tree
将题目抽象为排名, d p u = x dp_{u}=x dpu=x表示节点u最大为第x大,叶子结点dp[u]=1
1.如果当前节点u操作为min, u u u有一些儿子 v 1 , v 2 , v 3 . . . . v m v_1,v_2,v_3....v_m v1v2v3....vm,那么每个儿子的最大排名都会对u产生影响, d p u = ∑ i = 0 m d p v i dp_{u}=\sum_{i=0}^{m} {dp_{v_i}} dpu=i=0mdpvi
2.如果当前节点 u u u操作为max,只有最大的儿子对u产生影响,其它儿子为根的子树内的数字没有影响,随意安排叶子结点, d p u = m i n ( d p v 1 , d p v 2 . . . d p v m ) dp_u=min(dp_{v_1},dp_{v_2}...dp_{v_m}) dpu=min(dpv1,dpv2...dpvm)
最终答案为 k − d p 1 + 1 k-dp_1+1 kdp1+1

CF1097G Vladislav and a Great Legend
首先对于 f ( S ) k f(S)^k f(S)k,化为第二类斯特林数形式:
∑ S ∑ i = 0 k S 2 ( k , i ) i ! ( f ( S ) i ) \sum_{S}\sum_{i=0}^{k} {S_2(k,i) i!{f(S) \choose i}} Si=0kS2(k,i)i!(if(S))
对于 ∑ i = 0 k S 2 ( k , i ) i ! \sum_{i=0}^{k} {S_2(k,i)i!} i=0kS2(k,i)i! 是不变的,所以式子变为:
∑ i = 0 k S 2 ( k , i ) i ! × ∑ S ∑ i = 0 k ( f ( S ) i ) \sum_{i=0}^{k} {S_2(k,i)i!} \times \sum_{S}\sum_{i=0}^{k} {f(S) \choose i} i=0kS2(k,i)i!×Si=0k(if(S))
对第二部分加法结合律
∑ i = 0 k S 2 ( k , i ) i ! × ∑ i = 0 k ∑ S ( f ( S ) i ) \sum_{i=0}^{k} {S_2(k,i)i!} \times \sum_{i=0}^{k}\sum_{S} {f(S) \choose i} i=0kS2(k,i)i!×i=0kS(if(S))
∑ i = 0 k S 2 ( k , i ) i ! ∑ S ( f ( S ) i ) \sum_{i=0}^{k} {S_2(k,i)i!} \sum_{S} {f(S) \choose i} i=0kS2(k,i)i!S(if(S))
前一部分很好求,那么对于第二部分,考虑它的意义:因为 f ( S ) f(S) f(S)是集合 S S S形成虚树的边数,所以可以理解为选 i i i条边。
考虑dp做法,因为是虚树,假设当前以 u u u为根,点集中包括两个以上儿子,那么u这个点就会自动加入进去,这就是另一种点集的情况。
定义 d p u , j dp_{u,j} dpu,j表示当前根节点为 u u u u u u和它的父亲连边,选了 j j j条边方案有多少。这种边转移边更新答案,然后再合并完儿子后,主动处理 u u u f a [ u ] fa[u] fa[u]连边是否选的情况

CF835F Roads in the Kingdom
在这里插入图片描述
CF592D Super M
首先一定易证一定从某个关键点出发,然后从这个关键点向外延伸路径去遍历其它关键点,观察到延伸的路径只有一条路径不用回来,只用走一遍,其它路径都要过去再回来,为使路程最小,显然走一遍的路径就是关键点构成虚树的直径
首先找出虚树的直径,用类似求树的直径的方法,从某个关键点出发dfs一遍求出一个端点,再dfs一遍求出另外一个端点并记录下直径。然后遍历直径,dp处理出直径上的端点遍历到其它关键点来回的长度,最终统计答案即可

CF1779F Xorcerer’s Stones
树形dp记录路径
首先我们分析一下操作。
对于奇树,进行一次操作后,其异或和不变;对于偶树,进行一次操作后,其异或和为0。
如果我们能让所有点异或和为0,只要在根节点再进行一次操作,就可以得到一种合法操作方案,考虑树形dp出是否存在一种方案, d p u , j dp_{u,j} dpu,j表示以 u u u为根节点的子树内,是否存在异或和为 j j j的操作方案。合并很简单,但是对于 s z u sz_u szu为偶数时,不论其子树内操作如何,都可以进行一次操作,使得 d p u , 0 = 1 dp_{u,0}=1 dpu,0=1
对于方案的记录,我们开一个 p r e v , j x o r k = j pre_{v,jxork}=j prev,jxork=j,表示 d p u , j x o r k = 1 dp_{u,jxork}=1 dpu,jxork=1这种方案是在儿子异或和为 k k k的情况下,根节点异或和为 j j j的情况下合并转移而来的,因为dp时 u u u的儿子由前往后遍历转移,那么输出路径时就应该由后往前遍历

CF1312G Autocompletion
按题意建出一颗字典树,我们要求出的是每一个询问的答案,对于某一个结点 x x x,其实就是一个从根节点到 x x x路径最小步数的问题
很显然我们只能离线去求,我们发现题目问的是最小,并且父亲的最小步数是可以传递给儿子的,所以我们考虑dp

那么状态就不难设计了,重点是转移,假设我们现在遍历到点 u u u,两种转移:第一种直接从父亲到 u u u,花费步数1;第二种,从 u u u的某个直接或间接父亲自动补全转移过来,假设从 f a fa fa转移过来, d p u = m a x ( d p u , d p f a + d f n u − d f n f a − 1 ) dp_u=max(dp_u,dp_{fa}+dfn_u-dfn_{fa}-1) dpu=max(dpu,dpfa+dfnudfnfa1),对于 d p f a + d f n f a − 1 dp_{fa}+dfn_{fa}-1 dpfa+dfnfa1,前缀最大值优化即可做到O(1)转移,当 x x x在集合 S S S中的才进行 d f n x dfn_x dfnx++操作,否则只是传承父亲节点的 d f n dfn dfn

CF804D Expected diameter of a tree
对于两颗树 x , y x,y x,y,期望=任意两点连后直径和 / ( s z x ⋅ s z y ) /(sz_x\cdot sz_y) /(szxszy)
考虑任意两点 x , y x,y x,y(上面是树根,这里定义不一样),要求直径,易证为 m a x ( l e n f i n d ( x ) , l e n f i n d ( y ) , d p x + d p y + 1 ) max(len_{find(x)},len_{find(y)},dp_x+dp_y+1) max(lenfind(x),lenfind(y),dpx+dpy+1)
l e n f i n d ( x ) len_{find(x)} lenfind(x)表示 x x x所在树的直径长度, d p x dp_x dpx表示 x x x所在树,离 x x x最远点的距离(易证最远点为树直径端点)

首先最朴素的做法就是预处理出树上的信息,然后每次查询,特判两点是否在同一颗树上,然后分别枚举两个树上的点,求得贡献,时间复杂度 O ( q n 2 ) O(qn^2) O(qn2)

m a x n = m a x ( l e n f i n d ( x ) , l e n f i n d ( y ) ) maxn=max(len_{find(x)},len_{find(y)}) maxn=max(lenfind(x),lenfind(y))
如果 d p x + d p y + 1 < = m a x n dp_x+dp_y+1<=maxn dpx+dpy+1<=maxn,那么这两个点是哪两个已经不重要了,直接求得贡献,因此可以优化:枚举包含 x x x树的点,然后二分另一棵树到第pos小距离时大于maxn,并前缀和优化, O ( 1 ) O(1) O(1)算出 ∑ d p y \sum dp_y dpy m a x n maxn maxn所产生的贡献,最后贡献加上 d p x ∗ s z f i n d ( y ) dp_x*sz_{find(y)} dpxszfind(y),细节具体想
O ( q n l o g n ) O(qnlogn) O(qnlogn)

复杂度还是不够,根号分治,当两颗树大小都大于 n \sqrt n n 时,直接离线预处理一下,因为这样的数不超过 n \sqrt n n 颗,所以时间复杂度 O ( n n l o g n ) O(n\sqrt n logn) O(nn logn),在线查询部分每次枚举大小较小的树, O ( q n l o g n ) O(q\sqrt n logn) O(qn logn) 综合 O ( n n l o g n ) O(n\sqrt n logn) O(nn logn)

在线离线差不多,在线部分:

int maxlen=std::max(len[x],len[y]);
ll ans=0;
for (int i=0;i<use[x].size();i++){
	int pos=lower_bound(use[y].begin(),use[y].end(),maxlen-use[x][i])-use[y].begin();
	ans+=1LL*pos*maxlen;
	if (pos<use[y].size()){
	    ans+=1LL*dp[p[x][i]]*(use[y].size()-pos)+sum[y][use[y].size()-1]-(pos-1>=0?sum[y][pos-1]:0)+(use[y].size()-pos);
	 }
}
std::cout<<std::fixed<<std::setprecision(8)<<1.0*ans/sz[x]/sz[y]<<"\n";

这道题贡献计算并不难,难点在于优化,根号分治和排序后前缀和优化

2022济南C
先考虑兄弟节点,设 f i , j f_{i,j} fi,j表示除自己外,兄弟节点选了 i i i 个,遍历到自己前还有 j j j个点(不考虑父亲及之前)的方案数
实现:先不考虑除谁,一次性背包求所有方案数,然后回退背包+组合求出

然后考虑上父亲及之前的点,树上遍历合并背包即可

#include <bits/stdc++.h>
#define ll long long
#define ull unsigned long long
constexpr ll mod=998244353;
ll fac[505];
ll ksm(ll x,ll p){
    ll ans=1;
    x%=mod;
    while (p){
        if (p&1) ans=ans*x%mod;
        x=x*x%mod;
        p>>=1;
    }
    return ans;
}
ll inv(ll x){
    return ksm(x,mod-2);
}
void yrzr(){
    int n;
    std::cin>>n;
    std::vector<std::vector<int>> e(n+1);
    for (int i=1;i<n;i++){
        int x,y;
        std::cin>>x>>y;
        e[x].push_back(y);
        e[y].push_back(x);
    }


    std::vector<std::vector<ll>> dp(n+2,std::vector<ll>(n+2));
    std::vector<int> sz(n+2),tol(n+2);
    dp[1][1]=1;

    auto dfs1=[&](auto self,int u,int fa)->void{
        sz[u]=1;
        for (auto v:e[u]){
            if (v==fa) continue;
            self(self,v,u);
            sz[u]+=sz[v];
            tol[u]++;
        }
        dp[1][1]=dp[1][1]*fac[tol[u]]%mod;
    };
    dfs1(dfs1,1,0);

    auto dfs2=[&](auto self,int u,int fa)->void{
        std::vector<std::vector<ll>> f(e[u].size()+2,std::vector<ll>(n+2));
        f[0][0]=1;
        for (auto v:e[u]){
            if (v==fa) continue;

            for (int i=tol[u];i>=1;i--){
                for (int j=sz[u];j>=sz[v];j--){
                    f[i][j]=(f[i][j]+f[i-1][j-sz[v]])%mod;
                }
            }
        }

        for (auto v:e[u]){
            if (v==fa) continue;

            std::vector<ll> g(n+2);
            for (int i=1;i<=tol[u];i++){
                for (int j=sz[v];j<=sz[u];j++){
                    f[i][j]=(f[i][j]-f[i-1][j-sz[v]]+mod)%mod;
                }
            }

            for (int i=0;i<=tol[u];i++){
                for (int j=0;j<=sz[u];j++){
                    g[j+1]=(g[j+1]+f[i][j]*fac[i]%mod*fac[tol[u]-1-i]%mod)%mod;
                }
            }

            for (int j=0;j<=n;j++){
                for (int k=0;k<=sz[u]+1;k++){
                    if (j+k<=n){
                        dp[v][j+k]=(dp[v][j+k]+dp[u][j]*g[k]%mod)%mod;
                    }
                }
            }

            for (int i=tol[u];i>=1;i--){
                for (int j=sz[u];j>=sz[v];j--){
                    f[i][j]=(f[i][j]+f[i-1][j-sz[v]])%mod;
                }
            }
        }

        for (auto v:e[u]){
            if (v==fa) continue;
            self(self,v,u);
        }
    };
    dfs2(dfs2,1,0);

    for (int i=1;i<=n;i++){
        ll sum=0;
        for (int j=1;j<=n;j++){
            sum=(sum+dp[i][j])%mod;
            // std::cout<<dp[i][j]<<" ";
        }
        for (int j=1;j<=n;j++){
            std::cout<<dp[1][1]*dp[i][j]%mod*inv(sum)%mod<<" ";
        }
        std::cout<<"\n";
    }
}
int main(){
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);


    fac[0]=1;
    for (int i=1;i<=500;i++){
        fac[i]=fac[i-1]*i%mod;
    }

    int T=1;
    // std::cin>>T;
    while (T--){
        yrzr();
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值