ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

树形DP

2020-10-21 20:00:25  阅读:158  来源: 互联网

标签:min int LL fa 树形 节点 DP mod


题目一 包含每个点连通块计数

https://ac.nowcoder.com/acm/problem/19782

题目描述

修修去年种下了一棵树,现在它已经有n个结点了。
修修非常擅长数数,他很快就数出了包含每个点的连通点集的数量。
澜澜也想知道答案,但他不会数数,于是他把问题交给了你。

输入描述:

第一行一个整数n (1≤ n ≤ 106),接下来n-1行每行两个整数ai,bi表示一条边 (1≤ ai,bi≤ n)。

输出描述:

输出n行,每行一个非负整数。第i行表示包含第i个点的连通点集的数量对109+7取模的结果。
示例1
输入

6
1 2
1 3
2 4
4 5
4 6
输出

12
15
7
16
9
9

思路

f[i]:i的子树的连通点集的数量(一定包含i)。
往上转移时:\(f[x]=f[x]*(f[y]+1)\) y是x的子节点,+1是可以不选这棵子树
当然我们只统计的子树,我们没有统计父节点往上的方案。
换根时:fsz:代表父节点来的方案数。
ans[i]:i节点的方案数。\(ans[i]=f[u]*(fsz+1)\)
当去x子树时。往下传时:\(fsz=ans[i]/(f[x]+1)\)。

因为有取mod,所以\(f[x]+1%mod==0\)时,0是没有逆元的。我们要重新计算贡献。

#include <bits/stdc++.h>
#define LL long long
const int mod=1e9+7;
using namespace std;

vector<vector<int> > G(1000005);
LL f[1000005], ans[1000005];

LL ksm(LL a, LL b){
    LL ans=1;
    while(b){
        if(b&1) ans=ans*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return ans;
}

void DFS(int u, int fa){
    for(auto x: G[u]){
        if(x!=fa){
            DFS(x, u);
            f[u]=f[u]*(f[x]+1)%mod;
        }
    }
}

void dfs(int u, int fa, LL fsz){
    ans[u]=f[u]*(fsz+1)%mod;
    for(auto x: G[u]){
        if(x!=fa){
            if((f[x]+1)%mod){//f[x]+1!=0时
                dfs(x, u, ans[u]*ksm(f[x]+1, mod-2)%mod);
            }
            else{
                LL t=(fsz+1);
                for(auto y: G[u]){
                    if(x!=y){
                        t=t*(f[y]+1)%mod;
                    }
                }
                dfs(x, u, t);
            }
        }
    }
}

int main() {
    int n; scanf("%d", &n);
    for(int i=1; i<=n; i++) f[i]=1;
    for(int i=2; i<=n; i++){
        int x, y; scanf("%d%d", &x, &y);
        G[x].push_back(y);
        G[y].push_back(x);
    }
    DFS(1, 0);
    dfs(1, 0, 0);
    for(int i=1; i<=n; i++){
        printf("%lld\n", ans[i]);
    }

    return 0;
}

题目二 和树重心相同的连通块计数

https://ac.nowcoder.com/acm/problem/19953

题目描述

给定一个n个点的树,每个点的编号从1至n,问这个树有多少不同的连通子树,和这个树有相同的重心。
其中n个点的树指的是n个点的最小连通图,显然n个点的树有n-1条边,去掉这n-1条边中的任何一条,原图都不再联通,任意两个点之间由唯一一条路径相连。 对于一个树,树的重心定义为:
删掉某点i后,若剩余k个连通分量,那么定义d(i)为这些连通分量中点的个数的最大值,所谓重心,就是使得d(i)最小的点i。
基于以上定义,一个树的重心可能会有一个或者两个,题中所要求的联通子树,其重心编号和个数必须和原树的完全一样。
找出给定的树中有多少联通的子树和这个树有相同的重心。

输入描述:

第1行中给出正整数Q,表示该组数据中有多少组测试样例。
每组样例首先输入一个整数n (0 < n ≤ 200),表示该组样例中输入的树包含n个点,之后n-1行,每行输入两整数数x,y(1 ≤ x, y ≤ n),表示编号为x的点和编号为y的点之间存在一条边,所有点的编号从1-n

输出描述:

首先输出样例编号,之后输出满足条件的子树的个数,由于这个数字较大,你只需要输出这个数字对10007取模后的结果,即mod 10007,详见输出示例,请严格按照输出实例中的格式输出

示例1

输入
3
2
1 2
3
1 2
2 3
5
1 2
1 3
2 4
2 5

输出

Case 1: 1
Case 2: 2
Case 3: 6

思路

主要是重心性质:
根据重心的性质我们知道:如果重心的所有子树s1 s2 s3 ... sk,假设s1为最大
1.若\(2*s1<n\),那么重心唯一
2.若\(2*s1=n\),那么重心有两个且相邻

所以我们求出所有重心后,分情况讨论

1.如果重心是唯一的,我们把重心提根
现在我们需要选取子树使得重心唯一且为根节点
我们枚举一下选取子树大小,设为Size,那么需要根节点所有子树的大小s满足2*s1<Size,且s1+...+sk=Size-1
树上背包就可以了。

2.如果重心有两个,那么他们一定相邻,我们将两个重心之间边切去,那么变成两棵树

我们需要从两个树中选取大小相同的两颗子树(注意一定要连着重心)

对每个重心跑一个树上背包就可以了。

然后枚举两棵子树大小乘起来即可
时间复杂度O(N^3*Q)

#include <bits/stdc++.h>
#define LL long long
const int mod=10007;
using namespace std;

vector<vector<int> > G(205);
int sz[205], g[205], n;
int dfs(int u, int fa) {
    int res=1<<30;
    sz[u]=1, g[u]=0;
    for(auto x: G[u]) {
        if(x!=fa) {
            res=min(res, dfs(x, u));
            sz[u]+=sz[x], g[u]=max(g[u], sz[x]);
        }
    }
    g[u]=max(g[u], n-sz[u]);
    res=min(res, g[u]);
    return res;
}
vector<int> root;//保存重心

int f[205][205], f1[205][205];
void DP(int u, int fa, int n) {
    f[u][1]=1;
    sz[u]=1;
    for(auto x: G[u]) {
        if(x==fa) continue;
        DP(x, u, n);
        for(int p=sz[u]; p>=1; p--) {
            for(int q=min(sz[x], (n+1)/2-1); q>=1; q--) {
                f[u][p+q]+=(f[u][p]*f[x][q])%mod;
                f[u][p+q]%=mod;
            }
        }
        sz[u]+=sz[x];
    }
}

int main() {
    int t, cas=1; scanf("%d", &t);
    while(t--) {
        scanf("%d", &n);
        for(int i=1; i<=n; i++){
            G[i].clear(); g[i]=0, sz[i]=0;
        }
        root.clear();
        for(int i=2; i<=n; i++) {
            int x, y;
            scanf("%d%d", &x, &y);
            G[x].push_back(y);
            G[y].push_back(x);
        }
        int mi=dfs(1, 0);
        for(int i=1; i<=n; i++) {//得到所有的重心,最多只有2个
            if(g[i]==mi) {
                root.push_back(i);
            }
        }
        LL ans=0;
        if(root.size()==1) {
            memset(sz, 0, sizeof(sz));
            for(int sz=1; sz<=n; sz++) { //枚举siz的大小
                memset(f, 0, sizeof(f));
                DP(root[0], 0, sz);
                ans+=f[root[0]][sz];
                ans%=mod;
            }
            printf("Case %d: %lld\n", cas++, ans);
        } else if(root.size()==2) {
            memset(sz, 0, sizeof(sz));
            memset(f, 0, sizeof(f));
            DP(root[0], root[1], n);
            memcpy(f1, f, sizeof(f));

            memset(sz, 0, sizeof(sz));
            memset(f, 0, sizeof(f));
            DP(root[1], root[0], n);

            for(int i=1; i<=n; i++){
                ans=(ans+f[root[1]][i]*f1[root[0]][i]%mod)%mod;
            }
            printf("Case %d: %lld\n", cas++, ans);
        }
    }

    return 0;
}

题目三 树上点集染色求两两距离和

https://ac.nowcoder.com/acm/problem/19996

题目描述

有一棵点数为N的树,树边有边权。给你一个在0~N之内的正整数K,你要在这棵树中选择K个点,将其染成黑色,并将其他的N-K个点染成白色。
将所有点染色后,你会获得黑点两两之间的距离加上白点两两之间距离的和的收益。问收益最大值是多少。

输入描述:

第一行两个整数N,K。
接下来N-1行每行三个正整数fr,to,dis,表示该树中存在一条长度为dis的边(fr,to)。
输入保证所有点之间是联通的。N ≤ 2000,0 ≤ K ≤ N

输出描述:

输出一个正整数,表示收益的最大值。

示例1

输入
5 2
1 2 3
1 5 1
2 3 1
2 4 2
输出
17

说明

【样例解释】
将点1,2染黑就能获得最大收益。

思路

树上背包就可以了。计算每条边的经过次数
如果在x这棵子数染了q个黑点。那么黑色点之间经过u-x这条边的次数为\(q*(k-q)\)。
那么x棵子树有siz[x]-q个白点,那么白色点之间经过u-x这条边的次数为\((n-k-(siz[x]-q))*(siz[x]-q)\)。

#include <bits/stdc++.h>
#define LL long long
#define pii pair<int, int>
using namespace std;

vector<vector<pii> > G(2005);
LL f[2005][2005], siz[2005];
LL n, k;
void DFS(int u, int fa){
    f[u][1]=0; siz[u]=1;
    for(auto t: G[u]){
        int x=t.first, w=t.second;
        if(x!=fa){
            DFS(x, u);
            for(int p=min(siz[u], k); p>=0; p--){
                for(int q=min(siz[x], k-p); q>=0; q--){
                    f[u][p+q]=max(f[u][p+q], f[u][p]+f[x][q]+q*(k-q)*w+(siz[x]-q)*(n-k-(siz[x]-q))*w);
                }
            }
            siz[u]+=siz[x];
        }
    }
}

int main() {

    scanf("%lld%lld", &n, &k);
    for(int i=2; i<=n; i++){
        int x, y, z; scanf("%d%d%d", &x, &y, &z);
        G[x].push_back({y, z}); G[y].push_back({x, z});
    }
    DFS(1, 0);
    printf("%lld\n", f[1][k]);

    return 0;
}

题目四 树上有限制的染色

https://ac.nowcoder.com/acm/problem/19914

题目描述

给一棵m个结点的无根树,你可以选择一个度数大于1的结点作为根,然后给一些结点(根、内部结点和叶子均可)着以黑色或白色。你的着色方案应该保证根结点到每个叶子的简单路径上都至少包含一个有色结点(哪怕是这个叶子本身)。
对于每个叶结点u,定义c[u]为从根结点从U的简单路径上最后一个有色结点的颜色。给出每个c[u]的值,设计着色方案,使得着色结点的个数尽量少。
输入描述:
第一行包含两个正整数m, n,其中n是叶子的个数,m是结点总数。结点编号为1,2,…,m,其中编号1,2,… ,n是叶子。
以下n行每行一个0或1的整数(0表示黑色,1表示白色),依次为c[1],c[2],…,c[n]。
以下m-1行每行两个整数a,b(1 ≤ a < b ≤ m),表示结点a和b 有边相连。
输出描述:
仅一个数,即着色结点数的最小值。

示例1

输入
5 3
0
1
0
1 4
2 5
4 5
3 5

输出

2

思路

有个小结论:选不同的根节点没有影响
用f[i][0/1/2]:表示i节点染(白/黑/不染色)的方案数,并且满足子树所有的叶子要求的最小染色节点数
叶子节点:
f[u][c[u]]=1;
f[u][!c[u]]=1<<30;
f[u][2]=1<<30;
非叶子节点:
f[u][0]+=min(f[x][0]-1, min(f[x][1], f[x][2]));
f[u][1]+=min(f[x][1]-1, min(f[x][0], f[x][2]));
f[u][2]+=min(f[x][0], min(f[x][1], f[x][2]));

#include <bits/stdc++.h>
#define LL long long
using namespace std;

vector<vector<int> > G(10005);
int f[10005][3], c[10005];
int n, m;
void DFS(int u, int fa){
    f[u][0]=f[u][1]=1;
    for(auto x: G[u]){
        if(x!=fa){
            DFS(x, u);
            f[u][0]+=min(f[x][0]-1, min(f[x][1], f[x][2]));
            f[u][1]+=min(f[x][1]-1, min(f[x][0], f[x][2]));
            f[u][2]+=min(f[x][0], min(f[x][1], f[x][2]));
        }
    }
    if(G[u].size()==1){
        f[u][c[u]]=1;
        f[u][!c[u]]=1<<30;
        f[u][2]=1<<30;
    }
}

int main() {

    scanf("%d%d", &n, &m);
    for(int i=1; i<=m; i++){
        scanf("%d", &c[i]);
    }
    for(int i=2; i<=n; i++){
        int x, y; scanf("%d%d", &x, &y);
        G[x].push_back(y); G[y].push_back(x);
    }
    DFS(m+1, 0);
    printf("%d\n", min(f[m+1][0], min(f[m+1][1], f[m+1][2])));

    return 0;
}

题目五 树上贪心距离为2的覆盖

https://ac.nowcoder.com/acm/problem/20031

题目描述

2020年,人类在火星上建立了一个庞大的基地群,总共有n个基地。起初为了节约材料,人类只修建了n-1条道路来连接这些基地,并且每两个基地都能够通过道路到达,所以所有的基地形成了一个巨大的树状结构。如果基地A到基地B至少要经过d条道路的话,我们称基地A到基地B的距离为d。
由于火星上非常干燥,经常引发火灾,人类决定在火星上修建若干个消防局。消防局只能修建在基地里,每个消防局有能力扑灭与它距离不超过2的基地的火灾。你的任务是计算至少要修建多少个消防局才能够确保火星上所有的基地在发生火灾时,消防队有能力及时扑灭火灾。

输入描述:

输入文件的第一行为n,表示火星上基地的数目。
接下来的n-1行每行有一个正整数,其中文件第i行的正整数为a[i],表示从编号为i的基地到编号为a[i]的基地之间有一条道路,为了更加简洁的描述树状结构的基地群,有a[i]

输出描述:

输出文件仅有一个正整数,表示至少要设立多少个消防局才有能力及时扑灭任何基地发生的火灾。

示例1

输入
6
1
2
3
4
5
输出
2

思路

我们贪心的选择:先选择最深的节点,因为它不可以由子节点覆盖。那么他可以被兄弟节点,父节点,爷节点覆盖。肯定被爷节点覆盖是最优的。
如果我们把爷节点可以覆盖的节点全部覆盖,继续选择没有被覆盖的最深的节点。用优先队列就可以了。

#include <bits/stdc++.h>
#define LL long long
#define pii pair<int, int>
using namespace std;

vector<vector<int> > G(1005);
int d[1005], f[1005], vis[1005];
priority_queue<pii> q;
void DFS(int u, int fa){
    d[u]=d[fa]+1; f[u]=fa;
    q.push({d[u], u});
    for(auto x: G[u]){
        if(x!=fa){
            DFS(x, u);
        }
    }
}

void dfs(int u, int d){
    if(d<0) return ;
    vis[u]=1;
    for(auto x: G[u]){
        dfs(x, d-1);
    }
}

int main() {

    int n; scanf("%d", &n);
    for(int i=2; i<=n; i++){
        int x; scanf("%d", &x);
        G[i].push_back(x); G[x].push_back(i);
    }
    DFS(1, 0);
    int ans=0;
    while(!q.empty()){
        pii t=q.top(); q.pop();
        int x=t.second;
        if(vis[x]) continue;
        ans++;
        if(f[f[x]]==0) break;
        dfs(f[f[x]], 2);//把f[f[x]]可以覆盖到的点全部处理
    }
    printf("%d\n", ans);

    return 0;
}

标签:min,int,LL,fa,树形,节点,DP,mod
来源: https://www.cnblogs.com/liweihang/p/13854324.html

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有