无向图问题(树形dp)

文章来自:https://www.cnblogs.com/seaupnice/p/9471700.html

例一:
一个树,每个点有一个“快乐”值,父子结点不能同时快乐,问这个结构的最大快乐值。

分析:
dp[i][0]:表示不邀请i员工其子树达到的最大快乐值,dp[i][1]则表示邀请。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
#define mst(s, t) memset(s, t, sizeof(s))
const int INF = 0x3f3f3f3f;
const int maxn = 6010;
vector<int> G[maxn];
int father[maxn], dp[maxn][2];
void dfs(int root){
    for(int i=0; i<G[root].size(); i++){
        dfs(G[root][i]);
    }
    for(int i=0; i<G[root].size(); i++){
        dp[root][0] += max(dp[G[root][i]][0], dp[G[root][i]][1]);
        dp[root][1] += dp[G[root][i]][0];
    }
}
int main() 
{
    freopen("in.txt", "r", stdin);
    mst(dp, 0); mst(father, -1);
    int n;
    scanf("%d", &n);
    for(int i=1; i<=n; i++){
        scanf("%d", &dp[i][1]);
        G[i].clear();
    }
    int fa, so;
    while(scanf("%d%d", &so, &fa) && fa && so){
        G[fa].push_back(so);
        father[so] = fa;
    }
    int root = 1;
    while(father[root] != -1)    root=father[root];
    dfs(root);
    printf("%d\n", max(dp[root][0], dp[root][1]));
    return 0;
}

例二
现在要在一棵树上布置士兵,每个士兵在结点上,每个士兵可以守护其结点直接相连的全部边,问最少需要布置多少个士兵。

分析:
1 dp[root][0] += dp[G[root][i]][1];
2 dp[root][1] += min(dp[G[root][i]][0], dp[G[root][i]][1]);

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
#define mst(s, t) memset(s, t, sizeof(s))
const int INF = 0x3f3f3f3f;
const int maxn = 1510;
int dp[maxn][2], father[maxn];
vector<int> G[maxn];
void dfs(int root){
    for(int i=0; i<G[root].size(); i++){
        dfs(G[root][i]);
    }
    for(int i=0; i<G[root].size(); i++){
        dp[root][0] += dp[G[root][i]][1];
        dp[root][1] += min(dp[G[root][i]][0], dp[G[root][i]][1]);
    }
}
int main() 
{
    //freopen("in.txt", "r", stdin);
    int n;
    while( scanf("%d", &n) != EOF){
        for(int i=0; i<=n; i++){
            G[i].clear();
            dp[i][1] = 1,  dp[i][0] = 0;
            father[i] = -1;
        }
        for(int i=0; i<n; i++){
            int root, node, cnt;
            scanf("%d:(%d)",&root, &cnt);
            for(int i=0; i<cnt; i++){
                scanf("%d", &node);
                G[root].push_back(node);
                father[node] = root;
            }
        }
        int root = 1;
        while(father[root] != -1) root=father[root];
        dfs(root);
        printf("%d\n", min(dp[root][0], dp[root][1]));
    }
    return 0;
}

例三:
一棵无向树,结点为n(<=10,000),删除哪些结点可以使得新图中每一棵树结点小于n/2。

分析:
在遍历树的过程中,访问每个node,维护两个值:

所有子树的结点数的最大值childmax
所有子树(这里包括node)的结点数之和sum。
递归过程中用上一层的sum,不断更新这一层的childmax。

而childmax和sum则共同用来判断这个node是否可以删除。

下面再分析判断条件: childmax<=n/2 && n-sum<=n/2

childmax<=n/2 :去掉node后,原先node的子树均满足条件。

n-sum<=n/2 :去掉node后,原先除node和node的所有子树外的树(就当是node的祖先树吧)均满足条件。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
#define mst(s, t) memset(s, t, sizeof(s))
const int INF = 0x3f3f3f3f;
const int maxn = 10010;
vector<int> G[maxn];
int ans[maxn], num, n;
int dfs(int node, int father){
    int sum = 1, childmax = 0;   //若是叶子结点则return sum=1,否则求其子树(包括自己)的总结点数
    for(int i=0; i<G[node].size(); i++){
        if(G[node][i] == father)continue; //因为是树结构,这里可以在无向时避免遍历成环 
        int sum_son = dfs(G[node][i], node);
        childmax = max(sum_son, childmax);//所有子树的结点数的最大值
        sum += sum_son;//sum:node的子树的结点数和
    }
    childmax = max(childmax, n-sum);
    if(childmax <= n/2){
        /*
         * 当node结点的孩子结点的结点数最大为Sum,若Sum<=n/2,则该点符合条件
         * 因为去掉node后,任意子树结点数<=n/2, max()保证其非子树结点和仍<=n/2
         * 故该点满足条件
        */
        ans[num++] = node;
    }
    return sum;
}
int main() 
{
    //freopen("in.txt", "r", stdin);
    scanf("%d", &n);
    for(int i=0; i<n-1; i++){
        int a, b;
        scanf("%d%d", &a, &b);
        G[a].push_back(b);
        G[b].push_back(a);
    }
    num = 0;
    int tmp = dfs(1, 0);
    //cout << n << "==" << tmp << endl; //验证
    sort(ans, ans+num);
    if(num){
        for(int i=0; i<num; i++){
            printf("%d\n", ans[i]);
        }
    }else{
        printf("NONE\n");
    }
    return 0;
}

例四:

题意:一棵结点带权树,大小(结点数)为k的子树的权值和最大为多少
分析:
这里对每个子节点进行背包dp, dp[j] = max(dp[j], dp[j-w[i]]+v[i]) ,从后往前dp是因为若从后往前会使v的某一个t被重复选取。

这道题整体思路还不清晰,要再多看看。

#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
#include <vector>
using namespace std;
typedef long long LL;
#define mst(s, t) memset(s, t, sizeof(s))
const int INF = 0x3f3f3f3f;
const int maxn = 110;
vector<int> G[maxn];
int dp[maxn][maxn];  //dp[i][j]:node[i]结点数为j的子树的最大权值
int k, ans, cnt[maxn], weight[maxn];
int dfs(int node, int father){
    cnt[node] = 1;
    for(int i=0; i<G[node].size(); i++){
        if(G[node][i] == father) continue;
        cnt[node] += dfs(G[node][i], node);
    }
    dp[node][1] = weight[node];  
    //这里初始化不能在main()内  ?? 
/*
 *  dp[node][j-t]是之前的子节点为根更新的子树产生的
 *  dp[v][t]是以当前子节点为根的子树产生的
 *  j如果顺序遍历,前面dp[node][j]的更新会影响后面的dp[node][j-t],导致后面
 *更新dp[node][j]时是一当前子节点为根的子树产生的
*/
    for(int i = 0; i < G[node].size(); i++){
        int v = G[node][i];
        for(int j = cnt[node]; j >= 1; j--){
            for(int t = 0; t<j && t<=cnt[v]; t++){
                dp[node][j] = max(dp[node][j], dp[node][j-t]+dp[v][t]);
            }
        }
    }
    ans = max(ans, dp[node][k]);
    return cnt[node];
}
int main() 
{
    freopen("in.txt", "r", stdin);
    int n;
    while(scanf("%d%d",&n, &k) != EOF){
        mst(dp, 0);  ans = 0;
        for(int i=0; i<maxn; i++){
            G[i].clear();
        }
        for(int i=0; i<n; i++){
            scanf("%d", &weight[i]);
        }
        int a, b;
        for(int i = 1; i < n; i++){
            scanf("%d%d", &a, &b);
            G[a].push_back(b);
            G[b].push_back(a);
        }
        dfs(0, -1);
        printf("%d\n", ans);
    }
    return 0;
}

例五:E - Cell Phone Network

题意:给n[1,10000]个点,n-1条边,树形结构,从n个点中取尽量少的点构成一个集合,使剩下所有点都能与这个集合中的部分点相连。

(这个概念叫最小支配集)

分析:
dp[u][]:以点u为根的被染色的点的个数

dp[u][0]:u不染色,父节点染色覆盖u min(1, 2)u不染色,不能覆盖子节点v,故要不v染色覆盖自己,要不v被v染色的子节点覆盖

dp[u][1]:u不染色,子节点存在被染色的覆盖u min(1,2)u不染色,所以子节点v不存在被染色的父亲;若所有v均不染色,此时u未被覆盖,故需要有一个v来染色,选择min(dp[v][2]-dp[v][1])即可。

dp[u][2]:u染色 min(0,1,2)+1 子节点v染不染色都可以;自己染色故需+1

/*
 * poj3659
 * 最小支配集:从所有顶点中取尽量少的点组成一个集合,
 *            使剩下的所有点都与取出来的所有点相连。
 * dp[u]:以点u为根的被染色的点的个数
 *  
 * dp[u][0]:u不染色,父节点染色覆盖u 
 * dp[u][1]:u不染色,子节点存在被染色的覆盖u 
 * dp[u][2]:u染色 
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
using namespace std;
const int inf = 0x3f3f3f3f;
const int maxn = 10010;
vector<int> G[maxn];
int dp[maxn][3], n;

void dfs(int u, int f){ 
    //叶子结点
    if(G[u].size()==1 && G[u][0]==f){
        dp[u][0] = 0; 
        dp[u][1] = inf; 
        dp[u][2] = 1;
        return;
    }
    int mini = inf, flag = 1;
    for(int i=0; i<G[u].size(); i++){
        int v = G[u][i];
        if(v == f) continue;
        dfs(v, u);
        dp[u][0] += min(dp[v][1], dp[v][2]);
        dp[u][2] += min(min(dp[v][0], dp[v][1]), dp[v][2]);
        if(dp[v][1] < dp[v][2]){
            dp[u][1] += dp[v][1];
            mini = min(mini, (dp[v][2] - dp[v][1]));
        }else{
            flag = 0;
            dp[u][1] += dp[v][2];
        } 

    }
    dp[u][2]++;   //u点需要染色 
    if(flag){
/*
 * 如果所有子节点dp[v][1]<dp[v][2],则所有子节点点不放,这时必须有一个孩子结点放才可以保证
 * u被覆盖 
*/ 
        dp[u][1] += mini;
    }     
}

int main(){
    //freopen("in.txt", "r", stdin);
    int n; scanf("%d", &n);
    for(int i=0; i<=n; i++)G[i].clear();
    for(int i=1; i<n; i++){
        int a, b; scanf("%d%d", &a, &b);
        G[a].push_back(b); G[b].push_back(a);
    }
    dfs(1, -1); 
    printf("%d\n", min(dp[1][1], dp[1][2]));
    //1是根,无父节点 
    return 0;
}

例六:F - Computer

题意:一棵边带权值的树,求每个点在树上的最远距离。

解题过程

1、dp:计算点v在树上的最远距离,通过dfs()寻找。v通过v的子树可以找到最远距离,v也可以通过v的父节点找到最远距离。通过子树找,向下遍历更新即可(即向下寻找)。通过父节点找,需要知道父节点的最远距离,父节点可以通过找自己的父节点获得最远距离(即一直向上寻找),也可以通过寻找子树获得最远距离(但不能包含以v为根的子树(否则会重复))(即先向上后向下寻找),这里就需要结点的次远距离(即该距离是不包含结点最远距离上的第一个结点的最远距离,故称为次远距离)。

/*
 * hdu2196
 * 
*/
#include <bits/stdc++.h>
#define LL long long
using namespace std;
const int maxn =  1e4+10;
struct edge{
    int to, val;
    edge(int a, int b) : to(a), val(b) {}
};
vector<edge> G[maxn];
int dp[3][maxn], id[maxn];
//树只有一个父亲,会有多个儿子 
//id[v]: v在v的子树中可以得到的最大距离,所经过的第一个孩子结点 
//dp[v][0]: v在v的所有子树中获得的最长距离 
//dp[v][1]: v的孩子的第二长距离
//dp[v][2]: v通过父亲获得的最长距离
void dfs1(int x, int f){
    for(int i=0; i<G[x].size(); i++){
        int to = G[x][i].to, w = G[x][i].val;
        if(to == f) continue;
        dfs1(to, x); 
        if(dp[0][x] < dp[0][to] + w){
            dp[0][x] = dp[0][to] + w;
            id[x] = to;   //记录点x的最大距离经过的第一个孩子结点 
        }
    }
    
    for(int i=0; i<G[x].size(); i++){
        int to = G[x][i].to, w = G[x][i].val;
        if(to == f) continue;
        if(id[x] == to) continue;  //找次大的 
        dp[1][x] = max(dp[1][x], w + dp[0][to]);
    }
}
void dfs2(int x, int f){
    for(int i=0; i<G[x].size(); i++){
        int to = G[x][i].to, w = G[x][i].val;
        if(to == f) continue;
        if(to == id[x]){
            dp[2][to] = max(dp[2][x], dp[1][x]) + w;
            //to是x的孩子:to的最大距离是 x不经过to的最大距离(即次大距离)[向下的]和
            //x向上的最大距离  的最大值 + dist(x,to) (画图理解)
            //这里的转移也是dp[v][1]和id[x]存在的意义 
        }else{ 
            dp[2][to] = max(dp[2][x], dp[0][x]) + w;
            //to不是x最大距离经过的点
            //则to的最大距离是dist(x,to)和x向上或向下的最大距离的最大值
        } 
        dfs2(to, x);
        //0和1子树的信息可以直接用,2也是步步更新,一直到最优 
    }
}
int main(){
    //freopen("in.txt", "r", stdin);
    int n;
    while(scanf("%d", &n) != EOF){
        memset(dp, 0, sizeof(dp)); 
        for(int i=1;i<=n;i++)G[i].clear();
        for(int i=2; i<=n; i++){
            int a, b; scanf("%d%d", &a, &b);
            G[i].push_back(edge(a, b));
            G[a].push_back(edge(i, b));
        }
        dfs1(1, -1);
        dfs2(1, -1); 
        for(int i=1; i<=n; i++){
            printf("%d\n", max(dp[0][i], dp[2][i]));
        }
    }
    return 0;
}

2、用树的直径求解:3次dfs()。前两次求树的直径,后两次求得所有点距离直径端点的最远距离。

/*
 * 树中的最长路径, 
*/ 
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e4+10;
struct edge{
    int to, val;
    edge(int a, int b) : to(a), val(b) {}
};
vector<edge> G[maxn];
int dp[maxn], max_len, s;

void dfs(int x, int f, int len){
//len:起点到当前点的距离
    if(max_len <= len){
        s = x;
        max_len = len;
    }
    for(int i=0; i<G[x].size(); i++){
        int to = G[x][i].to, w = G[x][i].val;
        if(f == to) continue;
        dfs(to, x, len+w);
        dp[to] = max(dp[to], len+w);
        //更新起点到当前点的距离 
    }
}
int main(){
    freopen("in.txt", "r", stdin);
    int n;
    while(scanf("%d", &n) != EOF){
        memset(dp, 0, sizeof(dp));
        for(int i=1;i<=n;i++) G[i].clear();
        for(int i=2; i<=n; i++){
            int a,b; scanf("%d%d",&a, &b);
            G[i].push_back(edge(a, b));
            G[a].push_back(edge(i, b));
        }
        s=0, max_len=0;
        dfs(1, -1, 0);
        dfs(s, -1, 0);
        dfs(s, -1, 0);
        for(int i=1; i<=n; i++){
            printf("%d\n", dp[i]);
        }
    }
    return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值