• 【浮*光】#树形DP# 树形DP的习题集


    T1:【p2996】拜访奶牛

    • 树的相邻节点不能选择,求最多选择的节点数。

    【0/1型树形dp】← 也只有我这样叫... 这题是真的很模板...

    f[x] 即 拜访x时最大数量,g[x] 即 不拜访x时最大数量。

    转移方程:f[x]=1+∑g[son[i]],g[x]=∑max(f[son[i]],g[son[i]])。

    不妨假设从1号点出发,那么答案即为max(f[1],g[1])。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p2996】拜访奶牛
    N(1<=N<=50000)个朋友构成一棵树。求:可以拜访的朋友的最大数目。
    限制:对于由一条路直接相连的两个奶牛,只能拜访其中的一个。 */
    
    /*【0/1型树形dp】← 也只有我这样叫...
    f[x] 即 拜访x时最大数量,g[x] 即 不拜访x时最大数量。
    转移方程:f[x]=1+∑g[son[i]],g[x]=∑max(f[son[i]],g[son[i]])。
    不妨假设从1号点出发,那么答案即为max(f[1],g[1])。 */
    
    void reads(int &x){ //读入优化(正负整数)
        int fa=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fa; //正负号
    }
    
    const int N=100019;
    
    int n,f[N],g[N],head[N],tot=0;
    
    struct node{ int ver,nextt; }e[N*2];
    
    void add(int x,int y)
     { e[++tot].ver=y,e[tot].nextt=head[x],head[x]=tot; }
    
    void dp(int x,int fa_){
        f[x]=1; //选择自己
        for(int i=head[x],y;i;i=e[i].nextt){
            if(e[i].ver==fa_) continue; y=e[i].ver;
            dp(y,x); f[x]+=g[y]; //(1),不能选儿子
            g[x]+=max(f[y],g[y]); //(2),不选x,下方随意
        }
    }
    
    int main(){
        reads(n); for(int i=1,x,y;i<n;i++)
            reads(x),reads(y),add(x,y),add(y,x);
        dp(1,0); cout<<max(f[1],g[1])<<endl;
    }
    【p2996】拜访奶牛

    T2:【p2585】三色二叉树

    • 递归给出一棵二叉树。每个节点的颜色可以是0,1,2。
    • 要求:父子、兄弟节点的颜色不同。求最多/最少的0色点个数。

    【0/1型树形dp】因为只用考虑0色点的个数,所以每个点可以分为:0色/非0色。

    那么就和上一题很相似了。f[i]/g[i] 表示 i 为/不为 0色时,以i为根的子树中 0色点的最值个数。

    二叉树则:f[i]=1+g[ls[i]]+g[rs[i]],g[i]=min/max(f[ls[i]]+g[rs[i]],g[ls[i]]+f[rs[i]])。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p2585】三色二叉树
    递归给出一棵二叉树。每个节点的颜色可以是0,1,2。
    要求:父子、兄弟节点的颜色不同。求最多/最少的0色点个数。 */
    
    /*【0/1型树形dp】因为只用考虑0色点的个数,所以每个点可以分为:0色/非0色。
    那么就和 模板题p2996 很相似了。f[i]/g[i] 表示 i 为/不为 0色时,以i为根的子树中 0色点的最值个数。
    二叉树则:f[i]=1+g[ls[i]]+g[rs[i]],g[i]=min/max(f[ls[i]]+g[rs[i]],g[ls[i]]+f[rs[i]])。 */
    
    void reads(int &x){ //读入优化(正负整数)
        int fa=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fa; //正负号
    }
    
    const int N=500019; char ss[N]; int f[N],g[N],ls[N],rs[N],p,n;
    
    void init(){ p++,n++; int now=n;
      if(ss[p]=='2') ls[now]=n+1,init(),rs[now]=n+1,init();
      if(ss[p]=='1') ls[now]=n+1,init(); }
    
    void dp_max(int x){
      if(!x) return; dp_max(ls[x]),dp_max(rs[x]);
      f[x]=1+g[ls[x]]+g[rs[x]], //↓↓ 三个点必须只选一个 
      g[x]=max(f[ls[x]]+g[rs[x]],g[ls[x]]+f[rs[x]]); }
    
    void dp_min(int x){
      if(!x) return; dp_min(ls[x]),dp_min(rs[x]);
      f[x]=1+g[ls[x]]+g[rs[x]], //↓↓ 三个点必须只选一个 
      g[x]=min(f[ls[x]]+g[rs[x]],g[ls[x]]+f[rs[x]]); }
    
    int main(){
        scanf("%s",ss+1),init(); //递归输入
        dp_max(1),printf("%d ",max(f[1],g[1]));
        memset(f,0,sizeof(f)),memset(g,0,sizeof(g));
        dp_min(1),printf("%d
    ",min(f[1],g[1]));
    }
    【p2585】三色二叉树

    T3:【p4107】兔子与樱花

    • n个节点的樱花树,0号节点是根节点。第i个节点有c_i朵樱花。
    • 每一个节点都有载重量m。对于节点i,要求:儿子个数son_i<=m-c_i。
    • 要删除一些节点。每次删除之后儿子节点会向上连接。求最多能删除多少节点。

    【树形dp+贪心】每次删除选定节点i后,它父亲fa_增加的重量为(son_i+c_i)-1。

    那么就可以把每个节点的权值看成子节点数目+樱花数(son_i+c_i)。

    于是就有贪心策略:优先选择权值小的,判断能否去掉,若能去掉则更新当前节点的重量。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p4107】兔子与樱花 
    n个节点的樱花树,0号节点是根节点。第i个节点有c_i朵樱花。
    每一个节点都有载重量m。对于节点i,要求:儿子个数son_i<=m-c_i。
    要删除一些节点。每次删除之后儿子节点会向上连接。求最多能删除多少节点。*/
    
    /*【树形dp+贪心】每次删除选定节点i后,它父亲fa_增加的重量为(son_i+c_i)-1。
    那么就可以把每个节点的权值看成子节点数目+樱花数(son_i+c_i)。
    于是就有贪心策略:优先选择权值小的,判断能否去掉,若能去掉则更新当前节点的重量。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fa=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fa; //正负号
    }
    
    const int N=2000019; int n,m,ans=0,c[N]; 
    
    vector<int> son[N]; //存儿子节点 
    
    bool cmp(int a,int b){ return c[a]<c[b]; }
    
    void dp(int x){ //↓↓先递归到最底部,再自底而上一步步贪心
        for(int i=0;i<(int)son[x].size();i++) dp(son[x][i]);
        sort(son[x].begin(),son[x].end(),cmp); //每层都要重新排序
        c[x]+=son[x].size(); //当前节点的权值改成son_i+c_i
        //↑↑因为自底而上,所以不会有冲突和重复,每个节点只会遍历一次
        for(int i=0;i<(int)son[x].size();i++)
         { if(c[x]+c[son[x][i]]-1>m) break; //不能再删除了
           c[x]+=c[son[x][i]]-1,ans++; } //按照贪心策略...
    }
    
    int main(){
        reads(n),reads(m); for(int i=1;i<=n;i++) reads(c[i]);
        for(int i=1,k,x;i<=n;i++){ reads(k); //↓↓转化为根节点为1
            while(k--) reads(x),son[i].push_back(x+1);
        } dp(1); printf("%d
    ",ans); return 0;
    }
    【p4107】兔子与樱花

    T4:【p3698】小Q的棋盘

    • 求 ‘从0号点出发,移动p步’ 最多能经过多少节点。

    【树形dp+贪心】dfs求出以0为起点的最长一条链,此链上的点只经过一次,消耗1步;

    其它的点经过后需要返回这条链上,消耗2步。然后分类讨论:是否能走完链、走完树。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p3698】小Q的棋盘
    求 ‘从0号点出发,移动p步’ 最多能经过多少节点。*/
    
    /*【树形dp+贪心】dfs求出以0为起点的最长一条链,此链上的点只经过一次,消耗1步;
    其它的点经过后需要返回这条链上,消耗2步。然后分类讨论:是否能走完链、走完树。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fa=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fa; //正负号
    }
    
    const int N=519; int n,p,max_l=0,x,y;
    
    int head[N],ver[N<<1],nextt[N<<1],tot=0,dep[N];
    
    void add(int x,int y){ ver[++tot]=y,nextt[tot]=head[x],head[x]=tot; }
    
    void dfs(int x,int fa){ for(int i=head[x];i;i=nextt[i])
      if(ver[i]!=fa) dep[ver[i]]=dep[x]+1,dfs(ver[i],x); }
    
    int main(){
        reads(n),reads(p); for(int i=1;i<n;i++) reads(x),reads(y),add(x,y),add(y,x);
        dfs(0,-1); for(int i=1;i<n;i++) if(max_l<dep[i]) max_l=dep[i];
        if(p<=max_l) printf("%d
    ",p+1); //比最长链短
        else if(p>=max_l+2*(n-max_l-1)) printf("%d
    ",n); //比所有长度还长
        else printf("%d
    ",max_l+(p-max_l)/2+1); //只能走除最长链之外的一部分
    }
    【p3698】小Q的棋盘

    T5:【p3478】Station

    • 给出一个N个点的树,找出一个点来,以这个点为根的树时,所有点的深度之和最大。

    【树形dp】f[x]表示子树x中所有点到点x的距离之和,g[x]表示整个树中所有点到点x的距离之和。

    x的子树的对应路径中,有si[to[i]]个点到x的距离比to[i]多1:f[x]=∑(f[to[i]]+si[to[i]])。

    可知g[1]=f[1]。因为有n-si[to[i]]个点到to[i]的距离比到x多1,加n-si[to[i]];

    有si[to[i]]个点到to[i]的距离比到x少1,所以再减si[to[i]];所以g[to[i]]=g[x]+n-2*si[to[i]]。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p3478】Station
    给出一个N个点的树,找出一个点来,以这个点为根的树时,所有点的深度之和最大。*/
    
    /*【树形dp】f[x]表示子树x中所有点到点x的距离之和,g[x]表示整个树中所有点到点x的距离之和。
    x的子树的对应路径中,有si[to[i]]个点到x的距离比to[i]多1:f[x]=∑(f[to[i]]+si[to[i]])。
    可知g[1]=f[1]。因为有n-si[to[i]]个点到to[i]的距离比到x多1,加n-si[to[i]];
    有si[to[i]]个点到to[i]的距离比到x少1,所以再减si[to[i]];所以g[to[i]]=g[x]+n-2*si[to[i]]。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fa=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fa; //正负号
    }
    
    const int N=1000019; int n,ans=0,f[N],g[N];
    
    int head[N],ver[N<<1],nextt[N<<1],tot=0,siz[N];
    
    void add(int x,int y){ ver[++tot]=y,nextt[tot]=head[x],head[x]=tot; }
    
    void dfs1(int x,int fa){ siz[x]=1; for(int i=head[x];i;i=nextt[i])
      if(ver[i]!=fa) dfs1(ver[i],x),siz[x]+=siz[ver[i]],f[x]+=f[ver[i]]+siz[ver[i]]; }
    
    void dfs2(int x,int fa){ for(int i=head[x];i;i=nextt[i])
      if(ver[i]!=fa) g[ver[i]]=g[x]+n-2*siz[ver[i]],dfs2(ver[i],x); }
    
    int main(){
        reads(n); for(int i=1,x,y;i<n;i++) 
            reads(x),reads(y),add(x,y),add(y,x);
        dfs1(1,0); g[1]=f[1]; dfs2(1,0); //分别求出f[],g[]
        for(int i=1;i<=n;i++) if(g[ans]<g[i]) ans=i; 
        cout<<ans<<endl; return 0; //g[]中最大的就是ans
    }
    【p3478】Station //没开LL,wa2点的代码...
    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p3478】Station
    给出一个N个点的树,找出一个点来,以这个点为根的树时,所有点的深度之和最大。*/
    
    /*【树形dp】f[x]表示子树x中所有点到点x的距离之和,g[x]表示整个树中所有点到点x的距离之和。
    x的子树的对应路径中,有si[to[i]]个点到x的距离比to[i]多1:f[x]=∑(f[to[i]]+si[to[i]])。
    可知g[1]=f[1]。因为有n-si[to[i]]个点到to[i]的距离比到x多1,加n-si[to[i]];
    有si[to[i]]个点到to[i]的距离比到x少1,所以再减si[to[i]];所以g[to[i]]=g[x]+n-2*si[to[i]]。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fa=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fa; //正负号
    }
    
    const int N=1000019; int n,ans=0; ll f[N],g[N],siz[N];
    
    int head[N],ver[N<<1],nextt[N<<1],tot=0;
    
    void add(int x,int y){ ver[++tot]=y,nextt[tot]=head[x],head[x]=tot; }
    
    void dfs1(int x,int fa){ siz[x]=1; for(int i=head[x];i;i=nextt[i])
      if(ver[i]!=fa) dfs1(ver[i],x),siz[x]+=siz[ver[i]],f[x]+=f[ver[i]]+siz[ver[i]]; }
    
    void dfs2(int x,int fa){ for(int i=head[x];i;i=nextt[i])
      if(ver[i]!=fa) g[ver[i]]=g[x]+n-2*siz[ver[i]],dfs2(ver[i],x); }
    
    int main(){
        reads(n); for(int i=1,x,y;i<n;i++) 
            reads(x),reads(y),add(x,y),add(y,x);
        dfs1(1,0); g[1]=f[1]; dfs2(1,0); //分别求出f[],g[]
        for(int i=1;i<=n;i++) if(g[ans]<g[i]) ans=i; 
        cout<<ans<<endl; return 0; //g[]中最大的就是ans
    }
    【p3478】Station //AC代码

    T6:【p4253】小凸玩密室

    • 一棵有n个节点的完全二叉树,每个节点有一个灯泡。
    • 点亮所有灯泡即可逃出密室。点值ai​,边权bi。
    • 点亮第一个灯泡不需要花费,之后每点亮一个新的灯泡v的花费,
    • 等于上一个被点亮的灯泡u到这个点v的距离Du,v,乘以这个点的权值av​。
    • 在点亮一个灯泡后必须先点亮其子树所有灯泡。求最少花费。

    点亮灯泡k,点亮它的一个子树,再点亮它另外的子树,

    然后回到k的父节点,点亮fa之后再点亮fa的其他子树……

    对于一个节点u,有这样两种情况:

       1.u还没有被点亮,则下一个被点亮的一定是它的儿子

       2.u是下方(叶子)节点,在下一个被点亮的一定是它的某一级祖先,或者是它某一级祖先的儿子

    • f[i][j]表示点亮i之后回到i的第j个祖先的最小花费。
    • g[i][j]表示点亮i之后回到i的第j个祖先的另一个儿子的最小花费。

    倒序推导,注意讨论当前节点的儿子个数。统计答案时,根据点亮的过程累加即可。

    ps:由于这是一棵完全二叉树,所以可以不用递归的方式dfs。

    直接预处理出每个节点的儿子和它到各级祖先的距离,用循环转移即可。

    #include <cmath>
    #include <iostream>
    #include <cstdio>
    #include <string>
    #include <cstring>
    #include <vector>
    #include <algorithm>
    #include <queue>
    #include <stack>
    #include <map>
    using namespace std;
    typedef long long ll;
    typedef unsigned int uint;
    typedef unsigned long long ull;
    
    /*【p4253】小凸玩密室 // 树形DP
    一棵有n个节点的完全二叉树,每个节点有一个灯泡。点亮所有灯泡即可逃出密室。
    点值ai​,边权bi。点亮第一个灯泡不需要花费,之后每点亮一个新的灯泡v的花费,
    等于上一个被点亮的灯泡u到这个点v的距离Du,v,乘以这个点的权值av​。
    在点亮一个灯泡后必须先点亮其子树所有灯泡。求最少花费。 */
    
    /*【分析】点亮灯泡k,点亮它的一个子树,再点亮它另外的子树,
    然后回到k的父节点,点亮fa之后再点亮fa的其他子树……
    
    所以对于一个节点u,有这样两种情况:
    
    1.u还没有被点亮,则下一个被点亮的一定是它的儿子
    2.u是下方(叶子)节点,在下一个被点亮的一定是它的某一级祖先,或者是它某一级祖先的儿子
    
    f[i][j]表示点亮i之后回到i的第j个祖先的最小花费。
    
    g[i][j]表示点亮i之后回到i的第j个祖先的另一个儿子的最小花费。
    
    倒序推导,注意讨论当前节点的儿子个数。统计答案时,根据点亮的过程累加即可。
    
    ps:由于这是一棵完全二叉树,所以可以不用递归的方式dfs。
    直接预处理出每个节点的儿子和它到各级祖先的距离,用循环转移即可。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fx_=1;x=0;char ch_=getchar();
        while(ch_<'0'||ch_>'9'){if(ch_=='-')fx_=-1;ch_=getchar();}
        while(ch_>='0'&&ch_<='9'){x=x*10+ch_-'0';ch_=getchar();}
        x*=fx_; //正负号
    }
    
    const int N=200019;
    
    int n,w[N],fa[N],ls[N],rs[N]; ll ans=(ll)1e17;
    
    ll f[N][20]; //f[][]:i是亮的,回到i的第j个祖先的最小花费
    
    ll g[N][20]; //g[][]:i是亮的,回到i的第j个祖先的另一个儿子的最小花费 
    
    ll dis[N][20]; //dis[][]:从i到i的第j个祖先的距离 
    
    int brother(int k,int x){ return k>>(x-1)^1; }
    
    ll work(){
        for(int k=n;k>=1;k--){ 
            if(!ls[k]) for(int i=1;k>>(i-1);i++) //叶子节点
                g[k][i]=(dis[k][i]+dis[brother(k,i)][1])*w[brother(k,i)];
            else if(!rs[k]) for(int i=1;k>>(i-1);i++)
                g[k][i]=dis[ls[k]][1]*w[ls[k]]+g[ls[k]][i+1];
            else for(int i=1;k>>(i-1);i++)
              g[k][i]=min(dis[ls[k]][1]*w[ls[k]]+g[ls[k]][1]+g[rs[k]][i+1],
                dis[rs[k]][1]*w[rs[k]]+g[rs[k]][1]+g[ls[k]][i+1]);
        } for(int k=n;k>=1;k--){
            if(!ls[k]) for(int i=1;k>>(i-1);i++)
                f[k][i]=dis[k][i]*w[k>>i];
            else if(!rs[k]) for(int i=1;k>>(i-1);i++)
                f[k][i]=f[ls[k]][i+1]+dis[ls[k]][1]*w[ls[k]];
            else for(int i=1;k>>(i-1);i++)
              f[k][i]=min(dis[ls[k]][1]*w[ls[k]]+g[ls[k]][1]+f[rs[k]][i+1],
                dis[rs[k]][1]*w[rs[k]]+g[rs[k]][1]+f[ls[k]][i+1]);
        } for(int k=1;k<=n;k++){
            ll sum=f[k][1];
            for(int i=1,fa=k>>1;fa;i++,fa>>=1){
                int bro=brother(k,i);
                if (bro>n) sum+=dis[fa][1]*w[fa>>1];
                else sum+=dis[bro][1]*w[bro]+f[bro][2];
        } ans=min(ans,sum); } return ans;
    }
    
    int main(){
        reads(n); for(int i=1;i<=n;i++) reads(w[i]);
        for(int i=2;i<=n;i++) scanf("%lld",&dis[i][1]); //边权
        for(int i=1;i<=(n>>1)+1;i++){ //完全二叉树
            if((i<<1)<=n) ls[i]=(i<<1); else break;
            if((i<<1|1)<=n) rs[i]=(i<<1|1); //记录完全二叉树对应的左右儿子
        } for(int i=2;i<=18;i++) for(int k=n;k>>i;k--)
            dis[k][i]=dis[k][i-1]+dis[k>>(i-1)][1];
        printf("%lld
    ",work()); return 0;
    }
    【p4253】小凸玩密室

    T7:【p3237】米特运输

    • 给定一棵以1为根的树,每个节点又有一个权值。
    • 问最少要改变多少个节点的权值,使得:
    •  (1)每个节点的子节点的权值相同。
    •  (2)每个节点的子节点权值之和等于该点的权值。

    【树形dp+log( )压缩】改变后每个节点与根节点权值有固定的倍数关系。

    对于树上任一个点,其权值一旦确定,整棵树的权值即可确定。

    先初始化一下,确定改变后每个节点与根节点权值的倍数关系,

    将原权值乘上这个倍数关系,最后求出最多的‘相等权值’即可。

    由于数据特别大,所以使用log将乘法压缩为加法来保存(主要是让数变小)。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p3237】米特运输
    给定一棵以1为根的树,每个节点又有一个权值。
    问:最少要改变多少个节点的权值,使得:
     (1)每个节点的子节点的权值相同。
     (2)每个节点的子节点权值之和等于该点的权值。*/
    
    /*【树形dp】改变后每个节点与根节点权值有固定的倍数关系。
    对于树上任一个点,其权值一旦确定,整棵树的权值即可确定。
    先初始化一下,确定改变后每个节点与根节点权值的倍数关系,
    将原权值乘上这个倍数关系,最后求出最多的‘相等权值’即可。
    由于数据特别大,所以使用log将乘法压缩为加法来保存。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fa=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fa; //正负号
    }
    
    const int N=500019; double a[N],s[N],val[N]; 
    
    int siz[N],ver[N*2],nextt[N*2],head[N],tot=0;
    
    void add(int x,int y){ ver[++tot]=y,nextt[tot]=head[x],head[x]=tot; }
    
    void pre_dfs(int x,int fa){
        for(int i=head[x],y;i;i=nextt[i]){
            y=ver[i]; if(y==fa) continue; //用log压缩
            s[y]=s[x]+log(siz[x]),pre_dfs(y,x); //s保存倍数关系
        }
    }
    
    int main(){
        int n,tmp=1,ans=0; reads(n); 
        for(int i=1;i<=n;i++) scanf("%lf",&a[i]); //初始权值
        for(int i=1,x,y;i<n;i++) reads(x),reads(y),
            add(x,y),add(y,x),siz[x]++,siz[y]++;
        for(int i=2;i<=n;i++) siz[i]--; //计算子树大小
        s[1]=log(1); pre_dfs(1,0);
        for(int i=1;i<=n;i++) val[i]=s[i]+log(a[i]);
        sort(val+1,val+n+1);
        for(int i=2;i<=n;i++){ //最多的相同个数
            if(val[i]-val[i-1]<=1e-5) tmp++;
            else ans=max(ans,tmp),tmp=1;
        } printf("%d
    ",n-max(tmp,ans));
    }
    【p3237】米特运输

    T8:【p3174】毛毛虫

    • 毛毛虫:树上某条链和与该链相连的边构成的图。求最多的点数。

    【树形dp】记a[i]为该点入度,某条链对应的毛毛虫点数为:∑a[i]−(s−1)+1。

    简化式子:∑a[i]−(s−1)+1 = ∑a[i]−s+2 = ∑(a[i]−1)+2;设定点权之后找直径即可。

     // 关于这题的数据...  真心无语..  无法承受的数据之水...

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p3174】毛毛虫
    毛毛虫:树上某条链和与该链相连的边构成的图。求最多的点数。*/
    
    /*【树形dp】记a[i]为该点入度,某条链对应的毛毛虫点数为:∑a[i]−(s−1)+1。
    简化式子:∑a[i]−(s−1)+1 = ∑a[i]−s+2 = ∑(a[i]−1)+2;设定点权之后找直径即可。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fa=1;x=0;char s=getchar();
        while(s<'0'||s>'9'){if(s=='-')fa=-1;s=getchar();}
        while(s>='0'&&s<='9'){x=(x<<3)+(x<<1)+s-'0';s=getchar();}
        x*=fa; //正负号
    }
    
    const int N=500019; int a[N],ver[N*2],nextt[N*2],head[N],tot=0,now,mx;
    
    void add(int x,int y){ ver[++tot]=y,nextt[tot]=head[x],head[x]=tot; }
    
    void dfs(int x,int fa_,int dis){
        if(dis>mx) mx=dis,now=x;
        for(int i=head[x];i;i=nextt[i]){
            if(ver[i]==fa_) continue;
            dfs(ver[i],x,dis+a[ver[i]]);
        }
    }
    
    int main(){
        int n,m; reads(n),reads(m); //m=n-1
        for(int i=1,x,y;i<=m;i++) reads(x),reads(y),
            add(x,y),add(y,x),a[x]++,a[y]++;
        for(int i=1;i<=n;i++) a[i]--; //ans=∑(a[i]−1)+2
        dfs(1,0,a[1]),mx=0,dfs(now,0,a[now]),cout<<mx+2<<endl;
    }
    【p3174】毛毛虫

    T9:【p3565】Hotel

    • 在边权为1的树上选三个点使得两两距离相等,求方案数。

    【树形dp】如果树上三个点两两距离相同,

    那么这三条路径的中点重合,且长度一定是偶数。

    可以枚举这个中点,那么题意转化为:

    • 求 ‘ 选出三个点到这个中点距离相同 ’ 的方案数。

    设 f 1/2/3 [i] 表示选出 1/2/3 个深度为 i 的点的方案数。

    枚举每个点之后,记得要清空数组(动态清空 g [ ] )。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p3565】Hotel
    在边权为1的树上选三个点使得两两距离相等,求方案数。*/
    
    /*【树形dp】如果树上三个点两两距离相同,那么这三条路径的中点重合,且长度一定是偶数。
    可以枚举这个中点,那么题意转化为:求 ‘ 选出三个点到这个中点距离相同 ’ 的方案数。
    设f1/2/3[i]表示选出1/2/3个深度为i的点的方案数。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fx=1;x=0;char ch_=getchar();
        while(ch_<'0'||ch_>'9'){if(ch_=='-')fx=-1;ch_=getchar();}
        while(ch_>='0'&&ch_<='9'){x=x*10+ch_-'0';ch_=getchar();}
        x*=fx;  /* hs_love_wjy */  }
    
    const int N=500019;
    
    ll ans,f1[N],f2[N],f3[N],g[N];
    
    int n,dep[N],ver[N*2],nextt[N*2],head[N],tot=0,max_dep;
    
    void add(int x,int y){ ver[++tot]=y,nextt[tot]=head[x],head[x]=tot; }
    
    void dfs(int x,int fa_){
        max_dep=max(max_dep,dep[x]),g[dep[x]]++;
        for(int i=head[x];i;i=nextt[i])
            if(ver[i]!=fa_) dep[ver[i]]=dep[x]+1,dfs(ver[i],x);
    }
    
    ll query(int x){
        memset(f1,0,sizeof(f1)),memset(f2,0,sizeof(f2)),memset(f3,0,sizeof(f3));
        for(int i=head[x];i;i=nextt[i]){
            dep[ver[i]]=max_dep=1,dfs(ver[i],x); //以此点为根,求dep
            for(int j=max_dep;j;j--) // 注意 : g[]直接在这里清空了↓↓
                f3[j]+=f2[j]*g[j],f2[j]+=f1[j]*g[j],f1[j]+=g[j],g[j]=0;
        } ll sum=0; for(int i=1;i<=n;i++) sum+=f3[i]; return sum;
    }
    
    int main(){
        reads(n); for(int i=1,x,y;i<n;i++)
            reads(x),reads(y),add(x,y),add(y,x);
        for(int i=1;i<=n;i++) ans+=query(i); //此点作为中点
        printf("%lld
    ",ans); return 0; //记得用LL
    }
    【p3565】Hotel //数据开大了所以tle了...改成5000就行

    T10:【p3574】Farmcraft

    • 一棵树,从root=1出发,边权都为1,
    • 每个点有点权,求最小的max(点权+到达时间)。

    【树形dp】f[i]代表以i为根的子树上的‘最大到达时间’。

    于是目的就是最小化f[1]。用siz[i]代表遍历此点子树的用时,

    因为要遍历完整个子树才可以去另一棵,得到转移方程:

    • dp[u] = max{ dp[a] , dp[b] + siz[a] + 2 };
    • dp[u] = max{ dp[b] , dp[a] + siz[b] + 2 };

    那么就是max{ dp[b] + siz[a] + 2 , dp[a] + siz[b] + 2}。

    贪心按 ‘ dp[b] + siz[a] + 2 < dp[a] + siz[b] + 2 ’ 排序即可。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p3574】Farmcraft
    一棵树,从root=1出发,边权都为1,
    每个点有点权,求最小的max(点权+到达时间)。*/
    
    /*【树形dp】f[i]代表以i为根的子树上的‘最大到达时间’。
    于是目的就是最小化f[1]。用siz[i]代表遍历此点子树的用时,
    因为要遍历完整个子树才可以去另一棵,得到转移方程:
    dp[u] = max{ dp[a] , dp[b] + siz[a] + 2 };
    dp[u] = max{ dp[b] , dp[a] + siz[b] + 2 }; 
    那么就是max{ dp[b] + siz[a] + 2 , dp[a] + siz[b] + 2}。
    贪心按‘ dp[b] + siz[a] + 2 < dp[a] + siz[b] + 2 ’排序即可。*/
    
    void reads(ll &x){ //读入优化(正负整数)
        ll fx=1;x=0;char ch_=getchar();
        while(ch_<'0'||ch_>'9'){if(ch_=='-')fx=-1;ch_=getchar();}
        while(ch_>='0'&&ch_<='9'){x=x*10+ch_-'0';ch_=getchar();}
        x*=fx;  /* hs_love_wjy */  }
    
    const int N=600019;
    
    ll n,x,y,a[N],siz[N],dp[N],ver[N*2],nextt[N*2],head[N],tot=0;
    
    void add(ll x,ll y){ ver[++tot]=y,nextt[tot]=head[x],head[x]=tot; }
    
    struct Node{ ll id,dp_,siz_; }P[N];
    
    bool cmp(Node a,Node b)
     { ll p=max(a.dp_,b.dp_+a.siz_+2),q=max(b.dp_,a.dp_+b.siz_+2); return p<q; }
    
    void DP(ll u, ll fa){
        if(u!=1) dp[u]=a[u]; ll cnt=0;
        for(ll i=head[u];i;i=nextt[i]) if(ver[i]!=fa) DP(ver[i],u);  
        for(ll i=head[u];i;i=nextt[i]) if(ver[i]!=fa)
            P[++cnt]=(Node){ver[i],dp[ver[i]],siz[ver[i]]};
        if(cnt){ sort(P+1,P+1+cnt,cmp);
          for(ll i=1;i<=cnt;i++){ ll v=P[i].id;
            dp[u]=max(dp[u],dp[v]+(siz[u]+1)),siz[u]+=siz[v]+2; } 
        }
    }
    
    int main(){
        reads(n); for(ll i=1;i<=n;i++) reads(a[i]);
        for(ll i=1;i<n;i++) reads(x),reads(y),add(x,y),add(y,x);
        DP(1,0); printf("%lld
    ",max(dp[1],(n-1)*2+a[1]));
    }
    【p3574】Farmcraft

    T11:【p2052】道路修建

    • 连一条边的代价 = 此边边权 * 两端节点个数之差的绝对值。求总费用。

    【树形dp】初始化每个非根节点与其父节点连线的权值,

    递推出每个点的子树siz,那么两端个数差为:siz-(n-siz)=2*siz-n。

    由于栈的限制,普通的dfs树形dp会爆栈,所以采用bfs。

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p2052】道路修建
    连一条边的代价 = 此边边权 * 两端节点个数之差的绝对值。求总费用。*/
    
    /*【树形dp】初始化每个非根节点与其父节点连线的权值,
    递推出每个点的子树siz,那么两端个数差为:siz-(n-siz)=2*siz-n。
    由于栈的限制,普通的dfs树形dp会爆栈,所以采用bfs。*/
    
    void reads(int &x){ //读入优化(正负整数)
        int fx=1;x=0;char ch_=getchar();
        while(ch_<'0'||ch_>'9'){if(ch_=='-')fx=-1;ch_=getchar();}
        while(ch_>='0'&&ch_<='9'){x=x*10+ch_-'0';ch_=getchar();}
        x*=fx;  /* hs_love_wjy */  }
    
    const int N=1000019; ll z,ans=0,val[N*2],v[N];
    
    int x,y,q[N],qh=1,qt=1; //bfs队列
    
    int n,fa[N],siz[N],ver[N*2],nextt[N*2],head[N],tot=0;
    
    void add(int x,int y,ll z)
     { ver[++tot]=y,nextt[tot]=head[x],val[tot]=z,head[x]=tot; }
    
    int main(){
        reads(n); for(int i=1;i<n;i++) 
            reads(x),reads(y),cin>>z,add(x,y,z),add(y,x,z);
        fa[1]=-1,q[1]=1; while(qh<=qt){ //广搜,head<=tail
            x=q[qh++]; for(int i=head[x];i;i=nextt[i]){
                if(fa[ver[i]]) continue; fa[ver[i]]=x;
                //↑↑注意,要把fa[1]设为-1,在此处就会标记为访问过
                q[++qt]=ver[i],siz[ver[i]]=1,v[ver[i]]=val[i]; }
        } for(int i=qt;i>=2;i--) x=q[i],siz[fa[x]]+=siz[x],
            ans+=(ll)v[x]*abs(2*siz[x]-n); //‘此点与父亲节点连边’两端个数差
        printf("%lld
    ",ans); return 0;
    }
    【p2052】道路修建

    T12:【p4657】Chase

    • 给出一棵树,求一条路径:选择路上的V个点,
    • 使得被选择的点的相邻且不在路径上的点的权值和最大。

    【树形dp】多方向 多层次dfs 预处理

    c[i][j]为从i点的子树中走到i,选择j个点的权值和。

    b[i][j]为从i点开始,向子树中走,选择j个点的权值和。

    a[i]为i的点权。g[i]为点i的相邻节点的权值和。fa[i]为i的父节点。

    初始值:c[x][0]=0,c[x][i]=g[x];b[x][0]=0,b[x][i]=g[x]−a[fa[x]]。

    转移方程:c[x][i]=max(c[y][i],c[y][i−1]+g[x]−a[y]);

              b[x][i]=max(b[y][i],b[y][i−1]+g[x]−a[fa[x]])。

    这题思维难度还是有点大的,而且细节处理有点难想全...

    真心就是那种,随便错一点,根本调不出来...还会爆零的...

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<map>
    #include<vector>
    #include<cmath>
    using namespace std;
    typedef long long ll;
    
    /*【p4657】chase //复杂の树形DP
    给出一棵树,求一条路径:选择路上的V个点,
    使得被选择的点的相邻且不在路径上的点的权值和最大。 */
    
    /*【树形dp】多方向 多层次dfs 预处理
    c[i][j]为从i点的子树中走到i,选择j个点的权值和。
    b[i][j]为从i点开始,向子树中走,选择j个点的权值和。
    a[i]为i的点权。g[i]为点i的相邻节点的权值和。fa[i]为i的父节点。
    初始值:c[x][0]=0,c[x][i]=g[x];b[x][0]=0,b[x][i]=g[x]−a[fa[x]]。
    转移方程:c[x][i]=max(c[y][i],c[y][i−1]+g[x]−a[y]);
              b[x][i]=max(b[y][i],b[y][i−1]+g[x]−a[fa[x]])。 */
    
    void reads(int &x){ //读入优化(正负整数)
        int fx=1;x=0;char ch_=getchar();
        while(ch_<'0'||ch_>'9'){if(ch_=='-')fx=-1;ch_=getchar();}
        while(ch_>='0'&&ch_<='9'){x=x*10+ch_-'0';ch_=getchar();}
        x*=fx;  /* hs_love_wjy */  }
    
    const int N=100019;
    
    int n,v,x,y,tot,top,a[N],sta[N];
    
    int ver[N<<1],nextt[N<<1],head[N];
    
    long long g[N],c[N][105],b[N][105],ans;
    
    inline void add(int x,int y)
     { ver[++tot]=y,nextt[tot]=head[x],head[x]=tot;
       ver[++tot]=x,nextt[tot]=head[y],head[y]=tot; }
    
    inline void DP(int x,int y,int fa_){
        for(int i=1;i<=v;i++) //提前赋值
            ans=max(ans,c[x][i]+b[y][v-i]);
        for(int i=1;i<=v;i++) //向下走 / 向上走
            c[x][i]=max(c[x][i],max(c[y][i],c[y][i-1]+g[x]-a[y])),
            b[x][i]=max(b[x][i],max(b[y][i],b[y][i-1]+g[x]-a[fa_]));
    }
    
    void dfs(int x,int fa_){
    
        for(int i=1;i<=v;i++) c[x][i]=g[x],b[x][i]=g[x]-a[fa_];
        for(int i=head[x];i;i=nextt[i]) //↓↓更新此节点在每一种状态下的dp值
            if(ver[i]!=fa_) dfs(ver[i],x),DP(x,ver[i],fa_);
    
        for(int i=1;i<=v;i++) c[x][i]=g[x],b[x][i]=g[x]-a[fa_]; //要倒着做一遍
        top=0; for(int i=head[x];i;i=nextt[i]) if(ver[i]!=fa_) sta[++top]=ver[i];
        for(int i=top;i;i--) DP(x,sta[i],fa_); //反着走,dp更新反向路径最优值
    
        ans=max(ans,max(c[x][v],b[x][v])); //以x为根节点的最优答案
    }
    
    int main(){
        reads(n),reads(v); for(int i=1;i<=n;i++) reads(a[i]);
        for(int i=1,x,y;i<n;i++) reads(x),reads(y),
            add(x,y),g[x]+=a[y],g[y]+=a[x];
        dfs(1,0); printf("%lld
    ",ans); return 0;
    }
    【p4657】chase //复杂の树形DP

    T13:【p4284】概率充电器

    • 一棵树,每个点/每条边都有充电的概率,求进入充电状态的元件个数的期望。

    首先把可以充电-->1-不能充电。然后用两个数组分别记录两次dp的状态。

    • f[x]:在子树中,节点x不能工作的概率。那么下方节点不能有相通的连边。
    • g[x]:统计上方节点之后,计算得到的每个点不能工作的概率。

    第一次dfs求f[x],要自下而上;第二次dfs求g[x],要自上而下。当然,还要推式子...

    #include<iostream>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<string>
    #include<queue>
    #include<vector>
    #include<cmath>
    #include<map>
    using namespace std;
    typedef long long ll;
    
    /*【p4284】概率充电器 // 多方向树形dp
    一棵树,每个点/每条边都有充电的概率,求进入充电状态的元件个数的期望。*/
    
    //分析见:https://www.cnblogs.com/GXZlegend/p/7392084.html
    
    const int N=500019; const double eps=1e-7;
    
    int head[N],ver[N<<1],nextt[N<<1],tot;
    
    double w[N<<1],a[N],f[N],g[N];
    
    void add(int x,int y,double z)
     { ver[++tot]=y,w[tot]=z,nextt[tot]=head[x],head[x]=tot; }
    
    void dfs1(int x,int fa){ f[x]=1-a[x]; // f[x]:(在子树中)节点x不能工作的概率
      for(int i=head[x];i;i=nextt[i]) //那么下方所有节点不能有相通的连边
        if(ver[i]!=fa) dfs1(ver[i],x),f[x]*=1-(1-f[ver[i]])*w[i]; }
    
    void dfs2(int x,int fa){ //g[x]:统计上方节点之后,每个点不能工作的概率
        for(int i=head[x];i;i=nextt[i]) if(ver[i]!=fa){
            if(1-(1-f[ver[i]])*w[i]<eps) g[ver[i]]=f[ver[i]]*w[i]; 
            //↑↑特判分母(1-(1-f[ver[i]])*w[i])=0的情况
            else g[ver[i]]=f[ver[i]]*(1-(1-g[x]/(1-(1-f[ver[i]])*w[i]))*w[i]);
            dfs2(ver[i],x); //自上而下,继续递归,更新所有节点的g[]
        }
    }
    
    int main(){
        int n; double z,ans=0; scanf("%d",&n); 
        for(int i=1,x,y;i<n;i++) // 去除百分号 ↓↓
            scanf("%d%d%lf",&x,&y,&z),add(x,y,z/100),add(y,x,z/100);
        for(int i=1;i<=n;i++) scanf("%lf",&a[i]),a[i]/=100;
        dfs1(1,0),g[1]=f[1],dfs2(1,0); //自下而上求f[x],自上而下求g[x]
        for(int i=1;i<=n;i++) ans+=1-g[i]; printf("%.6lf
    ",ans);
    }
    /*【p4284】概率充电器 // 思维 + 多方向树形dp

                                                              ——时间划过风的轨迹,那个少年,还在等你

  • 相关阅读:
    没有什么,开发ASP.NET时随便写写,想到什么写什么
    MS SQL Server带有时间的记录怎样查询
    给RadioButtonList绑定Selected的值
    在GridView控件内文本框实现TextChanged事件
    MS SQL Server递归查询
    ASP.NET MVC使用jQuery无刷新上传
    MySQL 慢查询操作梳理
    ubuntu系统下防火墙简单使用
    crontab日常使用梳理
    ubuntu下nginx+php5的部署
  • 原文地址:https://www.cnblogs.com/FloraLOVERyuuji/p/10560021.html
Copyright © 2020-2023  润新知